Merge pull request #8414 from thingsboard/feature/singleton-rule-node

Feature/singleton rule node
This commit is contained in:
Andrew Shvayka 2023-04-25 14:34:08 +03:00 committed by GitHub
commit 4dd5280191
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 253 additions and 61 deletions

View File

@ -614,3 +614,16 @@ END
$$; $$;
-- TTL DROP PARTITIONS FUNCTIONS UPDATE END -- TTL DROP PARTITIONS FUNCTIONS UPDATE END
-- RULE NODE SINGLETON MODE SUPPORT
ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS singleton_mode bool DEFAULT false;
UPDATE rule_node SET singleton_mode = true WHERE type IN ('org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode', 'org.thingsboard.rule.engine.mqtt.TbMqttNode');
ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS clustering_mode varchar(255) DEFAULT 'ENABLED';
UPDATE component_descriptor SET clustering_mode = 'USER_PREFERENCE' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.TbMqttNode';
UPDATE component_descriptor SET clustering_mode = 'SINGLETON' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode';

View File

@ -87,6 +87,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
@ -179,6 +180,10 @@ public class ActorSystemContext {
@Setter @Setter
private ComponentDiscoveryService componentService; private ComponentDiscoveryService componentService;
@Autowired
@Getter
private DiscoveryService discoveryService;
@Autowired @Autowired
@Getter @Getter
private DataDecodingEncodingService encodingService; private DataDecodingEncodingService encodingService;

View File

@ -31,7 +31,10 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.gen.transport.TransportProtos;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -39,11 +42,10 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient;
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> { public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private final String ruleChainName; private final String ruleChainName;
private final TbActorRef self;
private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private final DefaultTbContext defaultCtx;
private RuleNode ruleNode; private RuleNode ruleNode;
private TbNode tbNode; private TbNode tbNode;
private DefaultTbContext defaultCtx;
private RuleNodeInfo info; private RuleNodeInfo info;
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
@ -51,7 +53,6 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
super(systemContext, tenantId, ruleNodeId); super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient(); this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName; this.ruleChainName = ruleChainName;
this.self = self;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode)); this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode));
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown"); this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown");
@ -59,14 +60,17 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void start(TbActorCtx context) throws Exception { public void start(TbActorCtx context) throws Exception {
if (isMyNodePartition()) {
tbNode = initComponent(ruleNode); tbNode = initComponent(ruleNode);
if (tbNode != null) { if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE; state = ComponentLifecycleState.ACTIVE;
} }
} }
}
@Override @Override
public void onUpdate(TbActorCtx context) throws Exception { public void onUpdate(TbActorCtx context) throws Exception {
if (isMyNodePartition()) {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown"); this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
boolean restartRequired = state != ComponentLifecycleState.ACTIVE || boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
@ -83,6 +87,10 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
throw new TbRuleNodeUpdateException("Failed to update rule node", e); throw new TbRuleNodeUpdateException("Failed to update rule node", e);
} }
} }
} else if (tbNode != null) {
stop(null);
tbNode = null;
}
} }
@Override @Override
@ -94,10 +102,17 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
@Override @Override
public void onPartitionChangeMsg(PartitionChangeMsg msg) { public void onPartitionChangeMsg(PartitionChangeMsg msg) throws Exception {
if (tbNode != null) { if (tbNode != null) {
if (!isMyNodePartition()) {
stop(null);
tbNode = null;
} else {
tbNode.onPartitionChangeMsg(defaultCtx, msg); tbNode.onPartitionChangeMsg(defaultCtx, msg);
} }
} else if (isMyNodePartition()) {
start(null);
}
} }
public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception { public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
@ -121,6 +136,9 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
if (!isMyNodePartition()) {
putToNodePartition(msg.getMsg());
} else {
msg.getMsg().getCallback().onProcessingStart(info); msg.getMsg().getCallback().onProcessingStart(info);
checkComponentStateActive(msg.getMsg()); checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg(); TbMsg tbMsg = msg.getMsg();
@ -140,6 +158,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode)); tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
} }
} }
}
@Override @Override
public String getComponentName() { public String getComponentName() {
@ -160,4 +179,23 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
protected RuleNodeException getInactiveException() { protected RuleNodeException getInactiveException() {
return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode); return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode);
} }
private boolean isMyNodePartition() {
return !ruleNode.isSingletonMode()
|| systemContext.getDiscoveryService().isMonolith()
|| defaultCtx.isLocalEntity(ruleNode.getId());
}
//Message will return after processing. See RuleChainActorMessageProcessor.pushToTarget.
private void putToNodePartition(TbMsg source) {
TbMsg tbMsg = TbMsg.newMsg(source, source.getQueueName(), source.getRuleChainId(), entityId);
TopicPartitionInfo tpi = systemContext.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, ruleNode.getId());
TransportProtos.ToRuleEngineMsg toQueueMsg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg))
.build();
systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null);
defaultCtx.ack(source);
}
} }

View File

@ -155,6 +155,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
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());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
ObjectNode configurationDescriptor = mapper.createObjectNode(); ObjectNode configurationDescriptor = mapper.createObjectNode();
JsonNode node = mapper.valueToTree(nodeDefinition); JsonNode node = mapper.valueToTree(nodeDefinition);

View File

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 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.server.common.data.plugin;
/**
* The main idea to use this - it's adding the ability to start rule nodes in singleton mode in cluster setup
* (singleton rule node will start in only one Rule Engine instance)
* USER_PREFERENCE - user has ability to configure clustering mode (enable/disable singleton mode in rule node config)
* ENABLE - user doesn't have ability to configure clustering mode (singleton mode is always FALSE in rule node config)
* SINGLETON - user doesn't have ability to configure clustering mode (singleton mode is always TRUE in rule node config)
*/
public enum ComponentClusteringMode {
USER_PREFERENCE,
ENABLED,
SINGLETON
}

View File

@ -36,15 +36,17 @@ public class ComponentDescriptor extends SearchTextBased<ComponentDescriptorId>
@Getter @Setter private ComponentType type; @Getter @Setter private ComponentType type;
@ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT") @ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT")
@Getter @Setter private ComponentScope scope; @Getter @Setter private ComponentScope scope;
@ApiModelProperty(position = 5, value = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "USER_PREFERENCE, ENABLED, SINGLETON", example = "ENABLED")
@Getter @Setter private ComponentClusteringMode clusteringMode;
@Length(fieldName = "name") @Length(fieldName = "name")
@ApiModelProperty(position = 5, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "Custom Rule Node") @ApiModelProperty(position = 6, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "Custom Rule Node")
@Getter @Setter private String name; @Getter @Setter private String name;
@ApiModelProperty(position = 6, value = "Full name of the Java class that implements the Rule Engine Node interface.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "com.mycompany.CustomRuleNode") @ApiModelProperty(position = 7, value = "Full name of the Java class that implements the Rule Engine Node interface.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "com.mycompany.CustomRuleNode")
@Getter @Setter private String clazz; @Getter @Setter private String clazz;
@ApiModelProperty(position = 7, value = "Complex JSON object that represents the Rule Node configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 8, value = "Complex JSON object that represents the Rule Node configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private transient JsonNode configurationDescriptor; @Getter @Setter private transient JsonNode configurationDescriptor;
@Length(fieldName = "actions") @Length(fieldName = "actions")
@ApiModelProperty(position = 8, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 9, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private String actions; @Getter @Setter private String actions;
public ComponentDescriptor() { public ComponentDescriptor() {

View File

@ -48,7 +48,9 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
private String name; private String name;
@ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false") @ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false")
private boolean debugMode; private boolean debugMode;
@ApiModelProperty(position = 7, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode") @ApiModelProperty(position = 7, value = "Enable/disable singleton mode. ", example = "false")
private boolean singletonMode;
@ApiModelProperty(position = 8, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode")
private transient JsonNode configuration; private transient JsonNode configuration;
@JsonIgnore @JsonIgnore
private byte[] configurationBytes; private byte[] configurationBytes;
@ -69,6 +71,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
this.type = ruleNode.getType(); this.type = ruleNode.getType();
this.name = ruleNode.getName(); this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode(); this.debugMode = ruleNode.isDebugMode();
this.singletonMode = ruleNode.isSingletonMode();
this.setConfiguration(ruleNode.getConfiguration()); this.setConfiguration(ruleNode.getConfiguration());
this.externalId = ruleNode.getExternalId(); this.externalId = ruleNode.getExternalId();
} }

View File

@ -403,6 +403,7 @@ public class ModelConstants {
public static final String COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME = "component_descriptor"; public static final String COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME = "component_descriptor";
public static final String COMPONENT_DESCRIPTOR_TYPE_PROPERTY = "type"; public static final String COMPONENT_DESCRIPTOR_TYPE_PROPERTY = "type";
public static final String COMPONENT_DESCRIPTOR_SCOPE_PROPERTY = "scope"; public static final String COMPONENT_DESCRIPTOR_SCOPE_PROPERTY = "scope";
public static final String COMPONENT_DESCRIPTOR_CLUSTERING_MODE_PROPERTY = "clustering_mode";
public static final String COMPONENT_DESCRIPTOR_NAME_PROPERTY = "name"; public static final String COMPONENT_DESCRIPTOR_NAME_PROPERTY = "name";
public static final String COMPONENT_DESCRIPTOR_CLASS_PROPERTY = "clazz"; public static final String COMPONENT_DESCRIPTOR_CLASS_PROPERTY = "clazz";
public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY = "configuration_descriptor"; public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY = "configuration_descriptor";
@ -446,6 +447,7 @@ public class ModelConstants {
public static final String EVENT_MESSAGE_COLUMN_NAME = "e_message"; public static final String EVENT_MESSAGE_COLUMN_NAME = "e_message";
public static final String DEBUG_MODE = "debug_mode"; public static final String DEBUG_MODE = "debug_mode";
public static final String SINGLETON_MODE = "singleton_mode";
/** /**
* Cassandra rule chain constants. * Cassandra rule chain constants.

View File

@ -23,6 +23,7 @@ import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.id.ComponentDescriptorId; import org.thingsboard.server.common.data.id.ComponentDescriptorId;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentScope;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
@ -50,6 +51,10 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY) @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY)
private ComponentScope scope; private ComponentScope scope;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CLUSTERING_MODE_PROPERTY)
private ComponentClusteringMode clusteringMode;
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY) @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY)
private String name; private String name;
@ -77,6 +82,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
this.actions = component.getActions(); this.actions = component.getActions();
this.type = component.getType(); this.type = component.getType();
this.scope = component.getScope(); this.scope = component.getScope();
this.clusteringMode = component.getClusteringMode();
this.name = component.getName(); this.name = component.getName();
this.clazz = component.getClazz(); this.clazz = component.getClazz();
this.configurationDescriptor = component.getConfigurationDescriptor(); this.configurationDescriptor = component.getConfigurationDescriptor();
@ -89,6 +95,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
data.setCreatedTime(createdTime); data.setCreatedTime(createdTime);
data.setType(type); data.setType(type);
data.setScope(scope); data.setScope(scope);
data.setClusteringMode(clusteringMode);
data.setName(this.getName()); data.setName(this.getName());
data.setClazz(this.getClazz()); data.setClazz(this.getClazz());
data.setActions(this.getActions()); data.setActions(this.getActions());

View File

@ -64,6 +64,9 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
@Column(name = ModelConstants.DEBUG_MODE) @Column(name = ModelConstants.DEBUG_MODE)
private boolean debugMode; private boolean debugMode;
@Column(name = ModelConstants.SINGLETON_MODE)
private boolean singletonMode;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId; private UUID externalId;
@ -81,6 +84,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
this.type = ruleNode.getType(); this.type = ruleNode.getType();
this.name = ruleNode.getName(); this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode(); this.debugMode = ruleNode.isDebugMode();
this.singletonMode = ruleNode.isSingletonMode();
this.searchText = ruleNode.getName(); this.searchText = ruleNode.getName();
this.configuration = ruleNode.getConfiguration(); this.configuration = ruleNode.getConfiguration();
this.additionalInfo = ruleNode.getAdditionalInfo(); this.additionalInfo = ruleNode.getAdditionalInfo();
@ -109,6 +113,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
ruleNode.setType(type); ruleNode.setType(type);
ruleNode.setName(name); ruleNode.setName(name);
ruleNode.setDebugMode(debugMode); ruleNode.setDebugMode(debugMode);
ruleNode.setSingletonMode(singletonMode);
ruleNode.setConfiguration(configuration); ruleNode.setConfiguration(configuration);
ruleNode.setAdditionalInfo(additionalInfo); ruleNode.setAdditionalInfo(additionalInfo);
if (externalId != null) { if (externalId != null) {

View File

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
@ -50,6 +51,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult; import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult;
import org.thingsboard.server.common.data.util.ReflectionUtils;
import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -154,6 +156,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>(); Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>();
if (nodes != null) { if (nodes != null) {
for (RuleNode node : nodes) { for (RuleNode node : nodes) {
setSingletonMode(node);
if (node.getId() != null) { if (node.getId() != null) {
ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node));
} else { } else {
@ -783,4 +786,30 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
} }
}; };
private void setSingletonMode(RuleNode ruleNode) {
boolean singletonMode;
try {
ComponentClusteringMode nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(),
"org.thingsboard.rule.engine.api.RuleNode", "clusteringMode");
switch (nodeConfigType) {
case ENABLED:
singletonMode = false;
break;
case SINGLETON:
singletonMode = true;
break;
case USER_PREFERENCE:
default:
singletonMode = ruleNode.isSingletonMode();
break;
}
} catch (Exception e) {
log.warn("Failed to get clustering mode: {}", ExceptionUtils.getRootCauseMessage(e));
singletonMode = false;
}
ruleNode.setSingletonMode(singletonMode);
}
} }

View File

@ -30,14 +30,12 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.ReflectionUtils; import org.thingsboard.server.common.data.util.ReflectionUtils;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import java.util.HashMap; import java.util.HashMap;

View File

@ -76,7 +76,8 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
.setParameter("name", entity.getName()) .setParameter("name", entity.getName())
.setParameter("scope", entity.getScope().name()) .setParameter("scope", entity.getScope().name())
.setParameter("search_text", entity.getSearchText()) .setParameter("search_text", entity.getSearchText())
.setParameter("type", entity.getType().name()); .setParameter("type", entity.getType().name())
.setParameter("clustering_mode", entity.getClusteringMode().name());
} }
private ComponentDescriptorEntity processSaveOrUpdate(ComponentDescriptorEntity entity, String query) { private ComponentDescriptorEntity processSaveOrUpdate(ComponentDescriptorEntity entity, String query) {

View File

@ -44,10 +44,10 @@ public class SqlComponentDescriptorInsertRepository extends AbstractComponentDes
} }
private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) { private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) {
return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *"; return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, name, scope, search_text, type, clustering_mode) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type, :clustering_mode) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *";
} }
private static String getUpdateStatement(String id) { private static String getUpdateStatement(String id) {
return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, name = :name, scope = :scope, search_text = :search_text, type = :type"; return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, name = :name, scope = :scope, search_text = :search_text, type = :type, clustering_mode = :clustering_mode";
} }
} }

View File

@ -125,7 +125,8 @@ CREATE TABLE IF NOT EXISTS component_descriptor (
name varchar(255), name varchar(255),
scope varchar(255), scope varchar(255),
search_text varchar(255), search_text varchar(255),
type varchar(255) type varchar(255),
clustering_mode varchar(255)
); );
CREATE TABLE IF NOT EXISTS customer ( CREATE TABLE IF NOT EXISTS customer (
@ -187,6 +188,7 @@ CREATE TABLE IF NOT EXISTS rule_node (
type varchar(255), type varchar(255),
name varchar(255), name varchar(255),
debug_mode boolean, debug_mode boolean,
singleton_mode boolean,
search_text varchar(255), search_text varchar(255),
external_id uuid external_id uuid
); );

View File

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.api; package org.thingsboard.rule.engine.api;
import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentScope;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
@ -38,6 +39,8 @@ public @interface RuleNode {
Class<? extends NodeConfiguration> configClazz(); Class<? extends NodeConfiguration> configClazz();
ComponentClusteringMode clusteringMode() default ComponentClusteringMode.ENABLED;
boolean inEnabled() default true; boolean inEnabled() default true;
boolean outEnabled() default true; boolean outEnabled() default true;

View File

@ -33,6 +33,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.BasicCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials;
import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.rule.engine.credentials.CredentialsType;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -47,6 +48,7 @@ import java.util.concurrent.TimeoutException;
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
name = "mqtt", name = "mqtt",
configClazz = TbMqttNodeConfiguration.class, configClazz = TbMqttNodeConfiguration.class,
clusteringMode = ComponentClusteringMode.USER_PREFERENCE,
nodeDescription = "Publish messages to the MQTT broker", nodeDescription = "Publish messages to the MQTT broker",
nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.", nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},

View File

@ -16,9 +16,7 @@
package org.thingsboard.rule.engine.mqtt.azure; package org.thingsboard.rule.engine.mqtt.azure;
import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.codec.mqtt.MqttVersion;
import io.netty.handler.ssl.SslContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.common.util.AzureIotHubUtil; import org.thingsboard.common.util.AzureIotHubUtil;
import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
@ -26,12 +24,12 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.credentials.BasicCredentials;
import org.thingsboard.rule.engine.credentials.CertPemCredentials; import org.thingsboard.rule.engine.credentials.CertPemCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials;
import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.rule.engine.credentials.CredentialsType;
import org.thingsboard.rule.engine.mqtt.TbMqttNode; import org.thingsboard.rule.engine.mqtt.TbMqttNode;
import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
@ -41,6 +39,7 @@ import javax.net.ssl.SSLException;
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
name = "azure iot hub", name = "azure iot hub",
configClazz = TbAzureIotHubNodeConfiguration.class, configClazz = TbAzureIotHubNodeConfiguration.class,
clusteringMode = ComponentClusteringMode.SINGLETON,
nodeDescription = "Publish messages to the Azure IoT Hub", nodeDescription = "Publish messages to the Azure IoT Hub",
nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS <b>AT_LEAST_ONCE</b>.", nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS <b>AT_LEAST_ONCE</b>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},

View File

@ -467,6 +467,7 @@ export class ImportExportService {
const ruleChainNode: RuleNode = { const ruleChainNode: RuleNode = {
name: '', name: '',
debugMode: false, debugMode: false,
singletonMode: false,
type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode', type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode',
configuration: { configuration: {
ruleChainId: ruleChainConnection.targetRuleChainId.id ruleChainId: ruleChainConnection.targetRuleChainId.id

View File

@ -24,7 +24,7 @@
<form [formGroup]="ruleNodeFormGroup" class="mat-padding"> <form [formGroup]="ruleNodeFormGroup" class="mat-padding">
<fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly"> <fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly">
<section> <section>
<section fxLayout="column" fxLayout.gt-sm="row"> <section class="title-row">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>rulenode.name</mat-label> <mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required> <input matInput formControlName="name" required>
@ -36,9 +36,14 @@
{{ 'rulenode.name-max-length' | translate }} {{ 'rulenode.name-max-length' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="debugMode"> <section class="node-setting">
<mat-slide-toggle formControlName="debugMode">
{{ 'rulenode.debug-mode' | translate }} {{ 'rulenode.debug-mode' | translate }}
</mat-checkbox> </mat-slide-toggle>
<mat-slide-toggle *ngIf="isSingletonEditAllowed()" formControlName="singletonMode">
{{ 'rulenode.singleton-mode' | translate }}
</mat-slide-toggle >
</section>
</section> </section>
<tb-rule-node-config #ruleNodeConfigComponent <tb-rule-node-config #ruleNodeConfigComponent
formControlName="configuration" formControlName="configuration"

View File

@ -13,8 +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.
*/ */
@import './../scss/constants';
:host { :host {
form { form {
overflow-x: hidden !important; overflow-x: hidden !important;
} }
.title-row {
display: flex;
flex-direction: column;
.node-setting {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 5px;
}
@media #{$mat-gt-sm} {
flex-direction: row;
gap: 8px;
.node-setting {
margin-top: 5px;
}
}
}
} }

View File

@ -26,6 +26,7 @@ import { RuleChainService } from '@core/http/rule-chain.service';
import { RuleNodeConfigComponent } from './rule-node-config.component'; import { RuleNodeConfigComponent } from './rule-node-config.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RuleChainType } from '@app/shared/models/rule-chain.models'; import { RuleChainType } from '@app/shared/models/rule-chain.models';
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
@Component({ @Component({
selector: 'tb-rule-node', selector: 'tb-rule-node',
@ -78,6 +79,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
this.ruleNodeFormGroup = this.fb.group({ this.ruleNodeFormGroup = this.fb.group({
name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]], name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]],
debugMode: [this.ruleNode.debugMode, []], debugMode: [this.ruleNode.debugMode, []],
singletonMode: [this.ruleNode.singletonMode, []],
configuration: [this.ruleNode.configuration, [Validators.required]], configuration: [this.ruleNode.configuration, [Validators.required]],
additionalInfo: this.fb.group( additionalInfo: this.fb.group(
{ {
@ -130,4 +132,8 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
} }
} }
} }
isSingletonEditAllowed() {
return this.ruleNode.component.clusteringMode === ComponentClusteringMode.USER_PREFERENCE;
}
} }

View File

@ -89,6 +89,7 @@ import { DebugEventType, EventType } from '@shared/models/event.models';
import { MatMiniFabButton } from '@angular/material/button'; import { MatMiniFabButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@Component({ @Component({
@ -469,6 +470,7 @@ export class RuleChainPageComponent extends PageComponent
component: ruleNodeComponent, component: ruleNodeComponent,
name: '', name: '',
nodeClass: desc.nodeClass, nodeClass: desc.nodeClass,
singletonMode: ruleNodeComponent.clusteringMode !== ComponentClusteringMode.ENABLED,
icon, icon,
iconUrl, iconUrl,
x: 30, x: 30,
@ -554,6 +556,7 @@ export class RuleChainPageComponent extends PageComponent
additionalInfo: ruleNode.additionalInfo, additionalInfo: ruleNode.additionalInfo,
configuration: ruleNode.configuration, configuration: ruleNode.configuration,
debugMode: ruleNode.debugMode, debugMode: ruleNode.debugMode,
singletonMode: ruleNode.singletonMode,
x: Math.round(ruleNode.additionalInfo.layoutX), x: Math.round(ruleNode.additionalInfo.layoutX),
y: Math.round(ruleNode.additionalInfo.layoutY), y: Math.round(ruleNode.additionalInfo.layoutY),
component, component,
@ -912,7 +915,8 @@ export class RuleChainPageComponent extends PageComponent
name: node.name, name: node.name,
configuration: deepClone(node.configuration), configuration: deepClone(node.configuration),
additionalInfo: node.additionalInfo ? deepClone(node.additionalInfo) : {}, additionalInfo: node.additionalInfo ? deepClone(node.additionalInfo) : {},
debugMode: node.debugMode debugMode: node.debugMode,
singletonMode: node.singletonMode
}; };
if (minX === null) { if (minX === null) {
minX = node.x; minX = node.x;
@ -983,7 +987,8 @@ export class RuleChainPageComponent extends PageComponent
name: outputEdge.label, name: outputEdge.label,
configuration: {}, configuration: {},
additionalInfo: {}, additionalInfo: {},
debugMode: false debugMode: false,
singletonMode: false
}; };
outputNode.additionalInfo.layoutX = Math.round(destNode.x); outputNode.additionalInfo.layoutX = Math.round(destNode.x);
outputNode.additionalInfo.layoutY = Math.round(destNode.y); outputNode.additionalInfo.layoutY = Math.round(destNode.y);
@ -1029,6 +1034,7 @@ export class RuleChainPageComponent extends PageComponent
ruleChainId: ruleChain.id.id ruleChainId: ruleChain.id.id
}, },
debugMode: false, debugMode: false,
singletonMode: false,
x: Math.round(ruleChainNodeX), x: Math.round(ruleChainNodeX),
y: Math.round(ruleChainNodeY), y: Math.round(ruleChainNodeY),
nodeClass: descriptor.nodeClass, nodeClass: descriptor.nodeClass,
@ -1420,7 +1426,8 @@ export class RuleChainPageComponent extends PageComponent
name: node.name, name: node.name,
configuration: node.configuration, configuration: node.configuration,
additionalInfo: node.additionalInfo ? node.additionalInfo : {}, additionalInfo: node.additionalInfo ? node.additionalInfo : {},
debugMode: node.debugMode debugMode: node.debugMode,
singletonMode: node.singletonMode
}; };
ruleNode.additionalInfo.layoutX = Math.round(node.x); ruleNode.additionalInfo.layoutX = Math.round(node.x);
ruleNode.additionalInfo.layoutY = Math.round(node.y); ruleNode.additionalInfo.layoutY = Math.round(node.y);

View File

@ -30,9 +30,16 @@ export enum ComponentScope {
TENANT = 'TENANT' TENANT = 'TENANT'
} }
export enum ComponentClusteringMode {
USER_PREFERENCE = 'USER_PREFERENCE',
ENABLED = 'ENABLED',
SINGLETON = 'SINGLETON'
}
export interface ComponentDescriptor { export interface ComponentDescriptor {
type: ComponentType | RuleNodeType; type: ComponentType | RuleNodeType;
scope?: ComponentScope; scope?: ComponentScope;
clusteringMode: ComponentClusteringMode;
name: string; name: string;
clazz: string; clazz: string;
configurationDescriptor?: any; configurationDescriptor?: any;

View File

@ -19,7 +19,7 @@ import { TenantId } from '@shared/models/id/tenant-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { RuleNodeId } from '@shared/models/id/rule-node-id'; import { RuleNodeId } from '@shared/models/id/rule-node-id';
import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models'; import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models';
import { ComponentType } from '@shared/models/component-descriptor.models'; import { ComponentClusteringMode, ComponentType } from '@shared/models/component-descriptor.models';
export interface RuleChain extends BaseData<RuleChainId>, ExportableEntity<RuleChainId> { export interface RuleChain extends BaseData<RuleChainId>, ExportableEntity<RuleChainId> {
tenantId: TenantId; tenantId: TenantId;
@ -64,6 +64,7 @@ export const ruleNodeTypeComponentTypes: ComponentType[] =
export const unknownNodeComponent: RuleNodeComponentDescriptor = { export const unknownNodeComponent: RuleNodeComponentDescriptor = {
type: RuleNodeType.UNKNOWN, type: RuleNodeType.UNKNOWN,
name: 'unknown', name: 'unknown',
clusteringMode: ComponentClusteringMode.ENABLED,
clazz: 'tb.internal.Unknown', clazz: 'tb.internal.Unknown',
configurationDescriptor: { configurationDescriptor: {
nodeDefinition: { nodeDefinition: {
@ -80,6 +81,7 @@ export const unknownNodeComponent: RuleNodeComponentDescriptor = {
export const inputNodeComponent: RuleNodeComponentDescriptor = { export const inputNodeComponent: RuleNodeComponentDescriptor = {
type: RuleNodeType.INPUT, type: RuleNodeType.INPUT,
clusteringMode: ComponentClusteringMode.ENABLED,
name: 'Input', name: 'Input',
clazz: 'tb.internal.Input' clazz: 'tb.internal.Input'
}; };

View File

@ -36,6 +36,7 @@ export interface RuleNode extends BaseData<RuleNodeId> {
type: string; type: string;
name: string; name: string;
debugMode: boolean; debugMode: boolean;
singletonMode: boolean;
configuration: RuleNodeConfiguration; configuration: RuleNodeConfiguration;
additionalInfo?: any; additionalInfo?: any;
} }
@ -308,6 +309,7 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
export interface FcRuleNodeType extends FcNode { export interface FcRuleNodeType extends FcNode {
component?: RuleNodeComponentDescriptor; component?: RuleNodeComponentDescriptor;
singletonMode?: boolean;
nodeClass?: string; nodeClass?: string;
icon?: string; icon?: string;
iconUrl?: string; iconUrl?: string;

View File

@ -3304,6 +3304,7 @@
"deselect-all": "Deselect all", "deselect-all": "Deselect all",
"rulenode-details": "Rule node details", "rulenode-details": "Rule node details",
"debug-mode": "Debug mode", "debug-mode": "Debug mode",
"singleton-mode": "Singleton mode",
"configuration": "Configuration", "configuration": "Configuration",
"link": "Link", "link": "Link",
"link-details": "Rule node link details", "link-details": "Rule node link details",