Add External Rule Node types. Rule Chain UI improvements.

This commit is contained in:
Igor Kulikov 2018-05-10 12:15:55 +03:00
parent 5e88b02f1e
commit e7ae82b104
27 changed files with 102 additions and 34 deletions

View File

@ -118,6 +118,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
case FILTER: case FILTER:
case TRANSFORMATION: case TRANSFORMATION:
case ACTION: case ACTION:
case EXTERNAL:
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setScope(ruleNodeAnnotation.scope());
@ -194,6 +195,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
nodeDefinition.setUiResources(nodeAnnotation.uiResources()); nodeDefinition.setUiResources(nodeAnnotation.uiResources());
nodeDefinition.setConfigDirective(nodeAnnotation.configDirective()); nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
nodeDefinition.setIcon(nodeAnnotation.icon());
nodeDefinition.setIconUrl(nodeAnnotation.iconUrl());
return nodeDefinition; return nodeDefinition;
} }

View File

@ -20,6 +20,6 @@ package org.thingsboard.server.common.data.plugin;
*/ */
public enum ComponentType { public enum ComponentType {
ENRICHMENT, FILTER, TRANSFORMATION, ACTION, OLD_ACTION, PLUGIN ENRICHMENT, FILTER, TRANSFORMATION, ACTION, EXTERNAL, OLD_ACTION, PLUGIN
} }

View File

@ -31,5 +31,7 @@ public class NodeDefinition {
JsonNode defaultConfiguration; JsonNode defaultConfiguration;
String[] uiResources; String[] uiResources;
String configDirective; String configDirective;
String icon;
String iconUrl;
} }

View File

@ -49,6 +49,10 @@ public @interface RuleNode {
String configDirective() default ""; String configDirective() default "";
String icon() default "";
String iconUrl() default "";
boolean customRelations() default false; boolean customRelations() default false;
} }

View File

@ -47,7 +47,9 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" + "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>", "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeAlarmConfig") configDirective = "tbActionNodeAlarmConfig",
icon = "notifications_active"
)
public class TbAlarmNode implements TbNode { public class TbAlarmNode implements TbNode {

View File

@ -33,7 +33,9 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" + "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>", "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeLogConfig") configDirective = "tbActionNodeLogConfig",
icon = "menu"
)
public class TbLogNode implements TbNode { public class TbLogNode implements TbNode {

View File

@ -38,13 +38,14 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "aws sns", name = "aws sns",
configClazz = TbSnsNodeConfiguration.class, configClazz = TbSnsNodeConfiguration.class,
nodeDescription = "Publish messages to AWS SNS", nodeDescription = "Publish messages to AWS SNS",
nodeDetails = "Expects messages with any message type. Will publish message to AWS SNS topic.", nodeDetails = "Expects messages with any message type. Will publish message to AWS SNS topic.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeSnsConfig" configDirective = "tbActionNodeSnsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
) )
public class TbSnsNode implements TbNode { public class TbSnsNode implements TbNode {

View File

@ -41,13 +41,14 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "aws sqs", name = "aws sqs",
configClazz = TbSqsNodeConfiguration.class, configClazz = TbSqsNodeConfiguration.class,
nodeDescription = "Publish messages to AWS SQS", nodeDescription = "Publish messages to AWS SQS",
nodeDetails = "Expects messages with any message type. Will publish message to AWS SQS queue.", nodeDetails = "Expects messages with any message type. Will publish message to AWS SQS queue.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeSqsConfig" configDirective = "tbActionNodeSqsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
) )
public class TbSqsNode implements TbNode { public class TbSqsNode implements TbNode {

View File

@ -40,7 +40,8 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
nodeDetails = "Generates messages with configurable period. ", nodeDetails = "Generates messages with configurable period. ",
inEnabled = false, inEnabled = false,
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeGeneratorConfig" configDirective = "tbActionNodeGeneratorConfig",
icon = "repeat"
) )
public class TbMsgGeneratorNode implements TbNode { public class TbMsgGeneratorNode implements TbNode {

View File

@ -28,13 +28,14 @@ import java.util.concurrent.ExecutionException;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "kafka", name = "kafka",
configClazz = TbKafkaNodeConfiguration.class, configClazz = TbKafkaNodeConfiguration.class,
nodeDescription = "Publish messages to Kafka server", nodeDescription = "Publish messages to Kafka server",
nodeDetails = "Expects messages with any message type. Will send record via Kafka producer to Kafka server.", nodeDetails = "Expects messages with any message type. Will send record via Kafka producer to Kafka server.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeKafkaConfig" configDirective = "tbActionNodeKafkaConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg=="
) )
public class TbKafkaNode implements TbNode { public class TbKafkaNode implements TbNode {

View File

@ -38,7 +38,9 @@ import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE;
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ", "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeToEmailConfig") configDirective = "tbTransformationNodeToEmailConfig",
icon = "email"
)
public class TbMsgToEmailNode implements TbNode { public class TbMsgToEmailNode implements TbNode {
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();

View File

@ -35,7 +35,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "send email", name = "send email",
configClazz = TbSendEmailNodeConfiguration.class, configClazz = TbSendEmailNodeConfiguration.class,
nodeDescription = "Log incoming messages using JS script for transformation Message into String", nodeDescription = "Log incoming messages using JS script for transformation Message into String",
@ -43,7 +43,8 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" + "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>", "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeSendEmailConfig" configDirective = "tbActionNodeSendEmailConfig",
icon = "send"
) )
public class TbSendEmailNode implements TbNode { public class TbSendEmailNode implements TbNode {

View File

@ -43,13 +43,14 @@ import java.util.concurrent.TimeoutException;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "mqtt", name = "mqtt",
configClazz = TbMqttNodeConfiguration.class, configClazz = TbMqttNodeConfiguration.class,
nodeDescription = "Publish messages to MQTT broker", nodeDescription = "Publish messages to MQTT broker",
nodeDetails = "Expects messages with any message type. Will publish message to MQTT broker.", nodeDetails = "Expects messages with any message type. Will publish message to MQTT broker.",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeMqttConfig" configDirective = "tbActionNodeMqttConfig",
icon = "call_split"
) )
public class TbMqttNode implements TbNode { public class TbMqttNode implements TbNode {

View File

@ -33,13 +33,14 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "rabbitmq", name = "rabbitmq",
configClazz = TbRabbitMqNodeConfiguration.class, configClazz = TbRabbitMqNodeConfiguration.class,
nodeDescription = "Publish messages to RabbitMQ", nodeDescription = "Publish messages to RabbitMQ",
nodeDetails = "Expects messages with any message type. Will publish message to RabbitMQ queue.", nodeDetails = "Expects messages with any message type. Will publish message to RabbitMQ queue.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRabbitMqConfig" configDirective = "tbActionNodeRabbitMqConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4="
) )
public class TbRabbitMqNode implements TbNode { public class TbRabbitMqNode implements TbNode {

View File

@ -26,7 +26,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.Netty4ClientHttpRequestFactory; import org.springframework.http.client.Netty4ClientHttpRequestFactory;
import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.client.*; import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.HttpClientErrorException;
import org.thingsboard.rule.engine.TbNodeUtils; import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
@ -34,19 +35,19 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.EXTERNAL,
name = "rest api call", name = "rest api call",
configClazz = TbRestApiCallNodeConfiguration.class, configClazz = TbRestApiCallNodeConfiguration.class,
nodeDescription = "Invoke REST API calls to external REST server", nodeDescription = "Invoke REST API calls to external REST server",
nodeDetails = "Expects messages with any message type. Will invoke REST API call to external REST server.", nodeDetails = "Expects messages with any message type. Will invoke REST API call to external REST server.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRestApiCallConfig" configDirective = "tbActionNodeRestApiCallConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+"
) )
public class TbRestApiCallNode implements TbNode { public class TbRestApiCallNode implements TbNode {

View File

@ -36,7 +36,8 @@ import org.thingsboard.server.common.msg.TbMsg;
nodeDescription = "Sends reply to the RPC call from device", nodeDescription = "Sends reply to the RPC call from device",
nodeDetails = "Expects messages with any message type. Will forward message body to the device.", nodeDetails = "Expects messages with any message type. Will forward message body to the device.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRpcReplyConfig" configDirective = "tbActionNodeRpcReplyConfig",
icon = "call_merge"
) )
public class TbSendRPCReplyNode implements TbNode { public class TbSendRPCReplyNode implements TbNode {

View File

@ -43,7 +43,8 @@ import java.util.concurrent.TimeUnit;
nodeDescription = "Sends one-way RPC call to device", nodeDescription = "Sends one-way RPC call to device",
nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes.", nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeRpcRequestConfig" configDirective = "tbActionNodeRpcRequestConfig",
icon = "call_made"
) )
public class TbSendRPCRequestNode implements TbNode { public class TbSendRPCRequestNode implements TbNode {

View File

@ -48,7 +48,8 @@ import java.util.Set;
nodeDescription = "Saves attributes data", nodeDescription = "Saves attributes data",
nodeDetails = "Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type", nodeDetails = "Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeAttributesConfig" configDirective = "tbActionNodeAttributesConfig",
icon = "file_upload"
) )
public class TbMsgAttributesNode implements TbNode { public class TbMsgAttributesNode implements TbNode {

View File

@ -45,7 +45,8 @@ import java.util.Map;
nodeDescription = "Saves timeseries data", nodeDescription = "Saves timeseries data",
nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type", nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbActionNodeTimeseriesConfig" configDirective = "tbActionNodeTimeseriesConfig",
icon = "file_upload"
) )
public class TbMsgTimeseriesNode implements TbNode { public class TbMsgTimeseriesNode implements TbNode {

View File

@ -40,7 +40,9 @@ import java.util.HashSet;
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ", "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"}, uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
configDirective = "tbTransformationNodeChangeOriginatorConfig") configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace"
)
public class TbChangeOriginatorNode extends TbAbstractTransformNode { public class TbChangeOriginatorNode extends TbAbstractTransformNode {
protected static final String CUSTOMER_SOURCE = "CUSTOMER"; protected static final String CUSTOMER_SOURCE = "CUSTOMER";

View File

@ -485,7 +485,7 @@ export default angular.module('thingsboard.types', [])
clientSide: false clientSide: false
} }
}, },
ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION"], ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION", "EXTERNAL"],
ruleChainNodeComponent: { ruleChainNodeComponent: {
type: 'RULE_CHAIN', type: 'RULE_CHAIN',
name: 'rule chain', name: 'rule chain',
@ -536,6 +536,13 @@ export default angular.module('thingsboard.types', [])
nodeClass: "tb-action-type", nodeClass: "tb-action-type",
icon: "flash_on" icon: "flash_on"
}, },
EXTERNAL: {
value: "EXTERNAL",
name: "rulenode.type-external",
details: "rulenode.type-external-details",
nodeClass: "tb-external-type",
icon: "cloud_upload"
},
RULE_CHAIN: { RULE_CHAIN: {
value: "RULE_CHAIN", value: "RULE_CHAIN",
name: "rulenode.type-rule-chain", name: "rulenode.type-rule-chain",

View File

@ -1228,6 +1228,8 @@ export default angular.module('thingsboard.locale', [])
"type-transformation-details": "Change Message payload and Metadata", "type-transformation-details": "Change Message payload and Metadata",
"type-action": "Action", "type-action": "Action",
"type-action-details": "Perform special action", "type-action-details": "Perform special action",
"type-external": "External",
"type-external-details": "Interacts with external system",
"type-rule-chain": "Rule Chain", "type-rule-chain": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
"directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",

View File

@ -246,6 +246,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
var contextInfo = { var contextInfo = {
headerClass: node.nodeClass, headerClass: node.nodeClass,
icon: node.icon, icon: node.icon,
iconUrl: node.iconUrl,
title: node.name, title: node.name,
subtitle: node.component.name subtitle: node.component.name
}; };
@ -805,12 +806,21 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
var ruleNodeComponent = ruleNodeComponents[i]; var ruleNodeComponent = ruleNodeComponents[i];
componentType = ruleNodeComponent.type; componentType = ruleNodeComponent.type;
var model = vm.ruleNodeTypesModel[componentType].model; var model = vm.ruleNodeTypesModel[componentType].model;
var icon = vm.types.ruleNodeType[componentType].icon;
var iconUrl = null;
if (ruleNodeComponent.configurationDescriptor.nodeDefinition.icon) {
icon = ruleNodeComponent.configurationDescriptor.nodeDefinition.icon;
}
if (ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl) {
iconUrl = ruleNodeComponent.configurationDescriptor.nodeDefinition.iconUrl;
}
var node = { var node = {
id: 'node-lib-' + componentType + '-' + model.nodes.length, id: 'node-lib-' + componentType + '-' + model.nodes.length,
component: ruleNodeComponent, component: ruleNodeComponent,
name: '', name: '',
nodeClass: vm.types.ruleNodeType[componentType].nodeClass, nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
icon: vm.types.ruleNodeType[componentType].icon, icon: icon,
iconUrl: iconUrl,
x: 30, x: 30,
y: 10+50*model.nodes.length, y: 10+50*model.nodes.length,
connectors: [] connectors: []
@ -904,6 +914,14 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
var ruleNode = vm.ruleChainMetaData.nodes[i]; var ruleNode = vm.ruleChainMetaData.nodes[i];
var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type); var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
if (component) { if (component) {
var icon = vm.types.ruleNodeType[component.type].icon;
var iconUrl = null;
if (component.configurationDescriptor.nodeDefinition.icon) {
icon = component.configurationDescriptor.nodeDefinition.icon;
}
if (component.configurationDescriptor.nodeDefinition.iconUrl) {
iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl;
}
var node = { var node = {
id: 'rule-chain-node-' + vm.nextNodeID++, id: 'rule-chain-node-' + vm.nextNodeID++,
ruleNodeId: ruleNode.id, ruleNodeId: ruleNode.id,
@ -915,7 +933,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
component: component, component: component,
name: ruleNode.name, name: ruleNode.name,
nodeClass: vm.types.ruleNodeType[component.type].nodeClass, nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
icon: vm.types.ruleNodeType[component.type].icon, icon: icon,
iconUrl: iconUrl,
connectors: [] connectors: []
}; };
if (component.configurationDescriptor.nodeDefinition.inEnabled) { if (component.configurationDescriptor.nodeDefinition.inEnabled) {

View File

@ -76,7 +76,7 @@
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
min-width: 180px; min-width: 150px;
} }
.fc-canvas { .fc-canvas {
background: #f9f9f9; background: #f9f9f9;
@ -161,6 +161,9 @@
&.tb-action-type { &.tb-action-type {
background-color: #f1928f; background-color: #f1928f;
} }
&.tb-external-type {
background-color: #fbc766;
}
&.tb-rule-chain-type { &.tb-rule-chain-type {
background-color: #d6c4f1; background-color: #d6c4f1;
} }

View File

@ -75,6 +75,8 @@
<md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel"> <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
<md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)" <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
ng-mouseleave="vm.destroyTooltips()"> ng-mouseleave="vm.destroyTooltips()">
<md-icon aria-label="node-type-icon"
class="material-icons" style="margin-right: 8px;">{{vm.types.ruleNodeType[typeId].icon}}</md-icon>
<div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div> <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
<md-expansion-panel-icon></md-expansion-panel-icon> <md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-collapsed> </md-expansion-panel-collapsed>
@ -82,6 +84,8 @@
<md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)" <md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
ng-mouseleave="vm.destroyTooltips()" ng-mouseleave="vm.destroyTooltips()"
ng-click="vm.$mdExpansionPanel(typeId).collapse()"> ng-click="vm.$mdExpansionPanel(typeId).collapse()">
<md-icon aria-label="node-type-icon"
class="material-icons" style="margin-right: 8px;">{{vm.types.ruleNodeType[typeId].icon}}</md-icon>
<div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div> <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
<md-expansion-panel-icon></md-expansion-panel-icon> <md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-header> </md-expansion-panel-header>
@ -114,8 +118,10 @@
</div> </div>
<md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()"> <md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
<div class="tb-context-menu-header {{vm.contextInfo.headerClass}}"> <div class="tb-context-menu-header {{vm.contextInfo.headerClass}}">
<md-icon aria-label="node-type-icon" <md-icon ng-if="!vm.contextInfo.iconUrl" aria-label="node-type-icon"
class="material-icons">{{vm.contextInfo.icon}}</md-icon> class="material-icons">{{vm.contextInfo.icon}}</md-icon>
<md-icon ng-if="vm.contextInfo.iconUrl" aria-label="node-type-icon"
md-svg-icon="{{vm.contextInfo.iconUrl}}"></md-icon>
<div flex> <div flex>
<div class="tb-context-menu-title">{{vm.contextInfo.title}}</div> <div class="tb-context-menu-title">{{vm.contextInfo.title}}</div>
<div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div> <div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div>

View File

@ -24,8 +24,10 @@
ng-mouseleave="callbacks.mouseLeave($event, node)"> ng-mouseleave="callbacks.mouseLeave($event, node)">
<div class="{{flowchartConstants.nodeOverlayClass}}"></div> <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
<div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }"> <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
<md-icon aria-label="node-type-icon" flex="15" <md-icon ng-if="!node.iconUrl" aria-label="node-type-icon" flex="15"
class="material-icons">{{node.icon}}</md-icon> class="material-icons">{{node.icon}}</md-icon>
<md-icon ng-if="node.iconUrl" aria-label="node-type-icon" flex="15"
md-svg-icon="{{node.iconUrl}}"></md-icon>
<div layout="column" flex="85" layout-align="center"> <div layout="column" flex="85" layout-align="center">
<span class="tb-node-type">{{ node.component.name }}</span> <span class="tb-node-type">{{ node.component.name }}</span>
<span class="tb-node-title" ng-if="node.name">{{ node.name }}</span> <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>