Merge pull request #3152 from node-red/link-call

Add Link Call node
This commit is contained in:
Nick O'Leary
2021-10-01 15:59:43 +01:00
committed by GitHub
14 changed files with 349 additions and 44 deletions

View File

@@ -12,6 +12,26 @@
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-mode"><span data-i18n="link.outMode"></span></label>
<select id="node-input-mode" style="width: 70%">
<option value="link" selected data-i18n="link.sendToAll"></option>
<option value="return" data-i18n="link.returnToCaller"></option>
</select>
</div>
<div class="node-input-link-rows" style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
<div class="form-row node-input-link-row node-input-link-rows"></div>
</script>
<script type="text/html" data-template-name="link call">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<label for="node-input-timeout"><span data-i18n="exec.label.timeout"></span></label>
<input type="text" id="node-input-timeout" placeholder="30" style="width: 70px; margin-right: 5px;"><span data-i18n="inject.seconds"></span>
</div>
<div style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
<div class="form-row node-input-link-row"></div>
</script>
@@ -48,7 +68,6 @@
}
});
var candidateNodes = RED.nodes.filterNodes({type:targetType});
var search = $("#node-input-link-target-filter").searchBox({
style: "compact",
delay: 300,
@@ -104,7 +123,8 @@
node: n,
label: n.name||n.id,
selected: isChecked,
checkbox: true
checkbox: node.type !== "link call",
radio: node.type === "link call"
})
}
});
@@ -129,15 +149,22 @@
function onEditSave(node) {
var flows = treeList.treeList('data');
node.links = [];
flows.forEach(function(f) {
f.children.forEach(function(n) {
if (n.selected) {
node.links.push(n.id);
}
if (node.type !== "link out" || $("node-input-mode").val() === 'link') {
flows.forEach(function(f) {
f.children.forEach(function(n) {
if (n.selected) {
node.links.push(n.id);
}
})
})
})
}
node.oldLinks.sort();
node.links.sort();
if (node.type === "link call") {
return
}
var nodeMap = {};
var length = Math.max(node.oldLinks.length,node.links.length);
for (var i=0;i<length;i++) {
@@ -195,6 +222,7 @@
outputLabels: function(i) {
return this.name||this._("link.linkIn");
},
showLabel: false,
label: function() {
return this.name||this._("link.linkIn");
},
@@ -211,25 +239,32 @@
oneditresize: resizeNodeList
});
RED.nodes.registerType('link out',{
RED.nodes.registerType('link call',{
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
links: { value: [], type:"link in[]"}
links: { value: [], type:"link in[]"},
timeout: { value: "30", validate:RED.validators.number(true) }
},
align:"right",
inputs:1,
outputs:0,
icon: "link-out.svg",
inputs: 1,
outputs: 1,
icon: "link-call.svg",
inputLabels: function(i) {
return this.name||this._("link.linkOut");
return this.name||this._("link.linkCall");
},
label: function() {
return this.name||this._("link.linkOut");
if (this.name) {
return this.name;
}
if (this.links.length > 0) {
var targetNode = RED.nodes.node(this.links[0]);
return targetNode && (targetNode.name || targetNode.id);
}
return this._("link.linkCall");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
return (this.name || this.links.length > 0)?"node_label_italic":"";
},
oneditprepare: function() {
onEditPrepare(this,"link in");
@@ -237,8 +272,56 @@
oneditsave: function() {
onEditSave(this);
},
oneditresize: resizeNodeList
});
RED.nodes.registerType('link out',{
category: 'common',
color:"#ddd",//"#87D8CF",
defaults: {
name: {value:""},
mode: { value: "link" },// link || return
links: { value: [], type:"link in[]"}
},
align:"right",
inputs:1,
outputs:0,
icon: function() {
if (this.mode === "return") {
return "link-return.svg";
} else {
return "link-out.svg";
}
},
inputLabels: function(i) {
return this.name||(this.mode === "return" ?this._("link.linkOutReturn"):this._("link.linkOut"));
},
showLabel: false,
label: function() {
return this.name||(this.mode === "return" ?this._("link.linkOutReturn"):this._("link.linkOut"));
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
onEditPrepare(this,"link in");
$("#node-input-mode").on("change", function() {
$(".node-input-link-rows").toggle(this.value === "link")
})
if (!this.mode) {
$("#node-input-mode").val('link').trigger("change");
}
},
oneditsave: function() {
onEditSave(this);
},
onadd: onAdd,
oneditresize: resizeNodeList
});
})();
</script>

View File

@@ -17,6 +17,8 @@
module.exports = function(RED) {
"use strict";
const crypto = require("crypto");
function LinkInNode(n) {
RED.nodes.createNode(this,n);
var node = this;
@@ -40,13 +42,91 @@ module.exports = function(RED) {
function LinkOutNode(n) {
RED.nodes.createNode(this,n);
var node = this;
var mode = n.mode || "link";
var event = "node:"+n.id;
this.on("input", function(msg, send, done) {
msg._event = event;
RED.events.emit(event,msg)
send(msg);
done();
if (mode === "return") {
if (Array.isArray(msg._linkSource) && msg._linkSource.length > 0) {
var messageEvent = msg._linkSource.pop();
var returnNode = RED.nodes.getNode(messageEvent.node);
if (returnNode && returnNode.returnLinkMessage) {
returnNode.returnLinkMessage(messageEvent.id, msg);
} else {
node.warn(RED._("link.error.missingReturn"))
}
} else {
node.warn(RED._("link.error.missingReturn"))
}
done();
} else if (mode === "link") {
send(msg);
done();
}
});
}
RED.nodes.registerType("link out",LinkOutNode);
function LinkCallNode(n) {
RED.nodes.createNode(this,n);
const node = this;
const target = n.links[0];
const messageEvents = {};
let timeout = parseFloat(n.timeout || "30")*1000;
if (isNaN(timeout)) {
timeout = 30000;
}
this.on("input", function(msg, send, done) {
msg._linkSource = msg._linkSource || [];
const messageEvent = {
id: crypto.randomBytes(14).toString('hex'),
node: node.id,
}
messageEvents[messageEvent.id] = {
msg: RED.util.cloneMessage(msg),
send,
done,
ts: setTimeout(function() {
timeoutMessage(messageEvent.id)
}, timeout )
};
msg._linkSource.push(messageEvent);
var targetNode = RED.nodes.getNode(target);
if (targetNode) {
targetNode.receive(msg);
}
});
this.returnLinkMessage = function(eventId, msg) {
if (Array.isArray(msg._linkSource) && msg._linkSource.length === 0) {
delete msg._linkSource;
}
const messageEvent = messageEvents[eventId];
if (messageEvent) {
clearTimeout(messageEvent.ts);
delete messageEvents[eventId];
messageEvent.send(msg);
messageEvent.done();
} else {
node.send(msg);
}
}
function timeoutMessage(eventId) {
const messageEvent = messageEvents[eventId];
if (messageEvent) {
delete messageEvents[eventId];
node.error("timeout",messageEvent.msg);
}
}
}
RED.nodes.registerType("link call",LinkCallNode);
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="10.583mm" height="15.875mm" version="1.1" viewBox="0 0 10.583 15.875" xmlns="http://www.w3.org/2000/svg">
<path d="m8.2021 2.3812-4.8922 0.53612 1.604 0.92604-1.0395 1.8004c0.73719-0.37402 1.6437-0.38227 2.4095 0.059892 0.76511 0.44174 1.2118 1.2301 1.2577 2.055l1.0384-1.7986 1.604 0.92604zm-2.3813 4.1244c-0.77016-0.44465-1.7402-0.18474-2.1848 0.58542-0.44465 0.77016-0.185 1.7406 0.58516 2.1853 0.77016 0.44465 1.7422 0.18533 2.1869-0.58483 0.44465-0.77016 0.18295-1.7412-0.58721-2.1858zm-3.3193 1.5159-1.8211 3.1542 3.6662 2.1167 1.82-3.1524c-0.73731 0.37266-1.6431 0.37961-2.4082-0.062129-0.76585-0.44216-1.2122-1.2309-1.2569-2.0563z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="10.583mm" height="15.875mm" version="1.1" viewBox="0 0 10.583 15.875" xmlns="http://www.w3.org/2000/svg">
<path d="m2.6623 13.292 4.8922-0.53612-1.604-0.92604 1.0395-1.8004c-0.73719 0.37402-1.6437 0.38227-2.4095-0.059892-0.76511-0.44174-1.2118-1.2301-1.2577-2.055l-1.0384 1.7986-1.604-0.92604zm2.3813-4.1244c0.77016 0.44465 1.7402 0.18474 2.1848-0.58542 0.44465-0.77016 0.185-1.7406-0.58516-2.1853-0.77016-0.44465-1.7422-0.18533-2.1869 0.58483-0.44465 0.77016-0.18295 1.7412 0.58721 2.1858zm3.3193-1.5159 1.8211-3.1542-3.6662-2.1167-1.82 3.1524c0.73731-0.37266 1.6431-0.37961 2.4082 0.062129 0.76585 0.44216 1.2122 1.2309 1.2569 2.0563z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 769 B

View File

@@ -28,10 +28,23 @@
<script type="text/html" data-help-name="link out">
<p>Create virtual wires between flows.</p>
<h3>Details</h3>
<p>The node can be connected to any <code>link in</code> node that exists on any tab.
Once connected, they behave as if they were wired together.</p>
<p>The wires between link nodes are only displayed when a link node is selected.
If there are any wires to other tabs, a virtual node is show that can be clicked
on to jump to the appropriate tab.</p>
<p>This node can be configured to either send messages to all <code>link in</code>
nodes it is connected to, or to send a response back to the <code>link call</code>
node that triggered the flow.</p>
<p>When in 'send to all' mode, the wires between link nodes are only displayed when
the node is selected. If there are any wires to other tabs, a virtual node
is shown that can be clicked on to jump to the appropriate tab.</p>
<p><b>Note: </b>Links cannot be created going into, or out of, a subflow.</p>
</script>
<script type="text/html" data-help-name="link call">
<p>Calls a flow that starts with a <code>link in</code> node and passes on the response.</p>
<h3>Details</h3>
<p>This node can be connected to a <code>link in</code> node that exists on any tab.
The flow connected to that node must end with a <code>link out</code> node configured
in 'return' mode.</p>
<p>When this node receives a message, it is passed to the connected <code>link in</code> node.
It then waits for a response which it then sends on.</o>
<p>If no response is received within the configured timeout, default 30 seconds, the node
will log an error that can be caught using the <code>catch</code> node.</p>
</script>

View File

@@ -159,7 +159,15 @@
},
"link": {
"linkIn": "link in",
"linkOut": "link out"
"linkOut": "link out",
"linkCall": "link call",
"linkOutReturn": "link return",
"outMode": "Mode",
"sendToAll": "Send to all connected link nodes",
"returnToCaller": "Return to calling link node",
"error": {
"missingReturn": "Missing return node information"
}
},
"tls": {
"tls": "TLS configuration",