Compare commits

...

54 Commits

Author SHA1 Message Date
Nick O'Leary
b1a706f811 Add comment on RED.nodes.getNodeLinkCount api 2022-01-12 18:04:53 +00:00
Nick O'Leary
c27dd336d9 Move ports behind node body and increase touch zone for each port 2022-01-12 18:01:11 +00:00
Nick O'Leary
5e9ff98c49 Add arrow-heads to links 2022-01-12 18:01:06 +00:00
Nick O'Leary
50cb074172 Visually distinguish ports with connected wires 2022-01-12 18:01:01 +00:00
Nick O'Leary
510a09ecba Add RED.nodes.getNodeLinkCount to provide O(1) lookup of link count on node port 2022-01-12 18:00:53 +00:00
Nick O'Leary
f86e743cce Merge branch 'master' into dev 2022-01-12 17:59:26 +00:00
Nick O'Leary
cb96fb735e Merge pull request #3289 from node-red/snap-grid
Snap nodes on grid using either edge as reference
2022-01-12 17:57:26 +00:00
Nick O'Leary
459a52d31d Merge pull request #3340 from node-red/cut-wires
Add wire-slice mode to delete wires with Ctrl-RHClick-Drag
2022-01-12 17:52:54 +00:00
Nick O'Leary
44ef9a13d6 Merge pull request #3338 from node-red/detach-action
Detach action
2022-01-12 17:52:32 +00:00
Nick O'Leary
0bb3652a63 Merge pull request #3323 from node-red/highlight-links
Highlight links when selecting nodes
2022-01-12 17:52:03 +00:00
Nick O'Leary
6a82d683a9 Merge pull request #3262 from node-red/search-history
Add search history to main search box
2022-01-12 17:51:46 +00:00
Nick O'Leary
fad708e8de Merge pull request #3294 from node-red/multi-select-links
Allow multiple links to be selected by ctrl-click
2022-01-12 17:51:15 +00:00
Nick O'Leary
c6a38b8355 Merge pull request #3300 from node-red/remove-use-of-verbose-flag-in-core-nodes
Remove use of verbose flag in core nodes - and use node.debug level instead
2022-01-12 17:38:34 +00:00
Nick O'Leary
ee84eb666b Merge pull request #3307 from node-red/add-tls-option-to-tcp-node
Add TLS option to tcp client nodes
2022-01-12 17:37:43 +00:00
Nick O'Leary
6d2793cac6 Merge pull request #3336 from node-red-hitachi/add-link-call-example
add link call example
2022-01-12 17:31:57 +00:00
Nick O'Leary
86d518fc2e Merge pull request #3343 from node-red/adminAuth-logging
Initialize passport when only adminAuth.tokens is set
2022-01-12 17:31:44 +00:00
Nick O'Leary
25dba1a6d5 Merge pull request #3342 from node-red/add-log-logging
Add log logging
2022-01-12 17:29:29 +00:00
Nick O'Leary
af949c62c2 Allow 'escape' to cancel wire-slicing 2022-01-12 17:24:25 +00:00
Nick O'Leary
ea20342d76 Handle non-error responses from token function 2022-01-12 13:48:06 +00:00
Nick O'Leary
7732d52583 Initialize passport when only adminAuth.tokens is set
Fixes #3341
2022-01-12 13:27:01 +00:00
Nick O'Leary
c801bc5e6b Add error logging if Node.log throws an error
Part of #3327
2022-01-12 11:13:29 +00:00
Nick O'Leary
ea43729063 Add _module to Node object to provide module info 2022-01-12 11:07:25 +00:00
Nick O'Leary
e26bae8027 Merge pull request #3333 from tobiasoort/dev
Implemented support for Websocket Subprotocols in WS Client Node.
2022-01-12 09:31:55 +00:00
Tobias Oort
555f155cad Added support for commaseparated subprotocols
Removed placeholder from html
2022-01-12 09:10:35 +01:00
Nick O'Leary
f8c47f59bc Merge pull request #3339 from node-red/fix-filter-node-start-condition
Fix for filter node narrrowbandEq mode start condition failure
2022-01-11 22:39:14 +00:00
Nick O'Leary
f3997128b9 Add wire-slice mode to delete wires with Ctrl-RHClick-Drag 2022-01-11 19:49:40 +00:00
Nick O'Leary
154a4e23dd Add delete-selection-and-reconnect action 2022-01-11 14:11:55 +00:00
Nick O'Leary
30f2b96c68 Pressing escape whilst DETACH_DRAGGING should revert change 2022-01-11 00:24:34 +00:00
Nick O'Leary
58c94b7773 Add core:detach-selected-nodes action 2022-01-11 00:16:24 +00:00
Tobias Oort
83203d5f5d Fixed property name in unit-test 2022-01-10 22:23:45 +01:00
Dave Conway-Jones
f77d161643 remove debug msg from inject close 2022-01-10 20:22:20 +00:00
Nick O'Leary
6580b139c0 Merge pull request #3288 from node-red/wiring-shortcut
Wiring shortcut
2022-01-10 18:34:06 +00:00
Nick O'Leary
2743c7c6ac Merge pull request #3304 from node-red-hitachi/dev-unknownconfig
Check availability of type of config node on deploy
2022-01-10 18:33:36 +00:00
Hiroyasu Nishiyama
3ce35a8a4b add link call example 2022-01-09 20:24:15 +09:00
Tobias Oort
bd77d7eec3 Implemented support for Websocket Subprotocols in WS Client Node. 2022-01-08 22:18:05 +01:00
Nick O'Leary
cb3fcb7bfa Ctrl-Shift-Click on node should add to existing selection 2022-01-03 23:29:42 +00:00
Nick O'Leary
bef641609e Select links when nodes on either side selected 2022-01-03 23:29:12 +00:00
Nick O'Leary
24b52f09df Add RED.nodes.getNodeLinks to help node-link lookup 2022-01-03 23:27:25 +00:00
Dave Conway-Jones
c9f03f1ac5 better tests
(and a small fix as a result)
2021-12-27 09:51:50 +00:00
Dave Conway-Jones
02bd292b8c fix and test 2021-12-26 16:12:47 +00:00
Dave Conway-Jones
e5f1029d0c fix variable names for test 2021-12-26 15:37:41 +00:00
Dave Conway-Jones
cae247160f Let tcprequest split incoming strings on delimiter (as per tcpin node)
and fixup i18n messages
2021-12-26 15:28:16 +00:00
Dave Conway-Jones
6692b1992c TCP add tls option to inbound nodes 2021-12-26 12:12:31 +00:00
Dave Conway-Jones
0937837b7f Add TLS config option to TCP client nodes
(not yet when in server mode)
2021-12-24 16:18:00 +00:00
Kunihiko Toumura
828888490a check availability of config node on deploy 2021-12-21 18:10:42 +09:00
Dave Conway-Jones
2cad42870e chaneg exec, file and inject node to use node.debug rather than -v flag 2021-12-17 09:58:10 +00:00
Nick O'Leary
43651135f3 Replace selectedLinks array with wrapped Set object 2021-12-13 21:02:55 +00:00
Nick O'Leary
ecaf866613 Fix serialisation of selection in view 2021-12-07 09:29:11 +00:00
Nick O'Leary
5ea3329b36 Allow multiple links to be selected by ctrl-click 2021-12-07 09:19:27 +00:00
Nick O'Leary
7bb7149f4c Snap nodes on grid using either edge as reference 2021-12-05 23:23:57 +00:00
Nick O'Leary
5856d043ca Add node wiring actions 2021-12-05 19:52:47 +00:00
Nick O'Leary
7cd3e49f04 Merge branch 'master' into dev 2021-12-05 19:32:13 +00:00
Nick O'Leary
720a163273 Bump dev version 2021-11-15 23:38:01 +00:00
Nick O'Leary
a9b12e5172 Add search history to main search box 2021-11-15 23:24:57 +00:00
43 changed files with 1475 additions and 274 deletions

View File

@@ -1,3 +1,7 @@
#### 2.2.0-beta.1: Beta Release
#### 2.1.4: Maintenance Release
Runtime

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",

View File

@@ -146,7 +146,7 @@ function authenticateUserToken(req) {
} else {
reject();
}
});
}).catch(reject);
} else {
reject();
}
@@ -163,6 +163,9 @@ TokensStrategy.prototype.authenticate = function(req) {
authenticateUserToken(req).then(user => {
this.success(user,{scope:user.permissions});
}).catch(err => {
if (err) {
log.trace("token authentication failure: "+err.stack?err.stack:err)
}
this.fail(401);
});
}

View File

@@ -90,6 +90,8 @@ function init(settings,_server,storage,runtimeAPI) {
auth.getToken,
auth.errorHandler
);
} else if (settings.adminAuth.tokens) {
adminApp.use(passport.initialize());
}
adminApp.post("/auth/revoke",auth.needsPermission(""),auth.revoke,apiUtil.errorHandler);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,8 +16,8 @@
}
],
"dependencies": {
"@node-red/util": "2.1.4",
"@node-red/editor-client": "2.1.4",
"@node-red/util": "2.2.0-beta.1",
"@node-red/editor-client": "2.2.0-beta.1",
"bcryptjs": "2.4.3",
"body-parser": "1.19.0",
"clone": "2.1.2",

View File

@@ -894,6 +894,8 @@
"addTitle": "add an item"
},
"search": {
"history": "Search history",
"clear": "clear all",
"empty": "No matches found",
"addNode": "add a node..."
},

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-client",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -684,6 +684,13 @@ RED.history = (function() {
peek: function() {
return undoHistory[undoHistory.length-1];
},
replace: function(ev) {
if (undoHistory.length === 0) {
RED.history.push(ev);
} else {
undoHistory[undoHistory.length-1] = ev;
}
},
clear: function() {
undoHistory = [];
redoHistory = [];

View File

@@ -38,7 +38,9 @@
},
"red-ui-workspace": {
"backspace": "core:delete-selection",
"ctrl-backspace": "core:delete-selection-and-reconnect",
"delete": "core:delete-selection",
"ctrl-delete": "core:delete-selection-and-reconnect",
"enter": "core:edit-selected-node",
"ctrl-enter": "core:go-to-selection",
"ctrl-c": "core:copy-selection-to-internal-clipboard",

View File

@@ -15,6 +15,9 @@
**/
RED.nodes = (function() {
var PORT_TYPE_INPUT = 1;
var PORT_TYPE_OUTPUT = 0;
var node_defs = {};
var linkTabMap = {};
@@ -591,7 +594,9 @@ RED.nodes = (function() {
}
allNodes.addNode(n);
if (!nodeLinks[n.id]) {
nodeLinks[n.id] = {in:[],out:[]};
nodeLinks[n.id] = {
inCount:[],outCount:[],in:[],out:[]
};
}
}
RED.events.emit('nodes:add',n);
@@ -601,15 +606,19 @@ RED.nodes = (function() {
if (l.source) {
// Possible the node hasn't been added yet
if (!nodeLinks[l.source.id]) {
nodeLinks[l.source.id] = {in:[],out:[]};
nodeLinks[l.source.id] = {inCount:[],outCount:[],in:[],out:[]};
}
nodeLinks[l.source.id].out.push(l);
nodeLinks[l.source.id].outCount[l.sourcePort] = (nodeLinks[l.source.id].outCount[l.sourcePort] || 0) + 1
l.source.dirty = true;
}
if (l.target) {
if (!nodeLinks[l.target.id]) {
nodeLinks[l.target.id] = {in:[],out:[]};
nodeLinks[l.target.id] = {inCount:[],outCount:[],in:[],out:[]};
}
nodeLinks[l.target.id].in.push(l);
nodeLinks[l.target.id].inCount[0] = (nodeLinks[l.target.id].inCount[0] || 0) + 1
l.target.dirty = true;
}
if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
linkTabMap[l.source.z].push(l);
@@ -758,15 +767,19 @@ RED.nodes = (function() {
if (index != -1) {
links.splice(index,1);
if (l.source && nodeLinks[l.source.id]) {
l.source.dirty = true;
var sIndex = nodeLinks[l.source.id].out.indexOf(l)
if (sIndex !== -1) {
nodeLinks[l.source.id].out.splice(sIndex,1)
nodeLinks[l.source.id].outCount[l.sourcePort]--
}
}
if (l.target && nodeLinks[l.target.id]) {
l.target.dirty = true;
var tIndex = nodeLinks[l.target.id].in.indexOf(l)
if (tIndex !== -1) {
nodeLinks[l.target.id].in.splice(tIndex,1)
nodeLinks[l.target.id].inCount[0]--
}
}
if (l.source.z === l.target.z && linkTabMap[l.source.z]) {
@@ -2458,6 +2471,144 @@ RED.nodes = (function() {
return helpContent;
}
function getNodeIslands(nodes) {
var selectedNodes = new Set(nodes);
// Maps node => island index
var nodeToIslandIndex = new Map();
// Maps island index => [nodes in island]
var islandIndexToNodes = new Map();
var internalLinks = new Set();
nodes.forEach((node, index) => {
nodeToIslandIndex.set(node,index);
islandIndexToNodes.set(index, [node]);
var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT);
var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
inboundLinks.forEach(l => {
if (selectedNodes.has(l.source)) {
internalLinks.add(l)
}
})
outboundLinks.forEach(l => {
if (selectedNodes.has(l.target)) {
internalLinks.add(l)
}
})
})
internalLinks.forEach(l => {
let source = l.source;
let target = l.target;
if (nodeToIslandIndex.get(source) !== nodeToIslandIndex.get(target)) {
let sourceIsland = nodeToIslandIndex.get(source);
let islandToMove = nodeToIslandIndex.get(target);
let nodesToMove = islandIndexToNodes.get(islandToMove);
nodesToMove.forEach(n => {
nodeToIslandIndex.set(n,sourceIsland);
islandIndexToNodes.get(sourceIsland).push(n);
})
islandIndexToNodes.delete(islandToMove);
}
})
const result = [];
islandIndexToNodes.forEach((nodes,index) => {
result.push(nodes);
})
return result;
}
function detachNodes(nodes) {
let allSelectedNodes = [];
nodes.forEach(node => {
if (node.type === 'group') {
let groupNodes = RED.group.getNodes(node,true,true);
allSelectedNodes = allSelectedNodes.concat(groupNodes);
} else {
allSelectedNodes.push(node);
}
})
if (allSelectedNodes.length > 0 ) {
const nodeIslands = RED.nodes.getNodeIslands(allSelectedNodes);
let removedLinks = [];
let newLinks = [];
let createdLinkIds = new Set();
nodeIslands.forEach(nodes => {
let selectedNodes = new Set(nodes);
let allInboundLinks = [];
let allOutboundLinks = [];
// Identify links that enter or exit this island of nodes
nodes.forEach(node => {
var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT);
var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
inboundLinks.forEach(l => {
if (!selectedNodes.has(l.source)) {
allInboundLinks.push(l)
}
})
outboundLinks.forEach(l => {
if (!selectedNodes.has(l.target)) {
allOutboundLinks.push(l)
}
})
});
// Identify the links to restore
allInboundLinks.forEach(inLink => {
// For Each inbound link,
// - get source node.
// - trace through to all outbound links
let sourceNode = inLink.source;
let targetNodes = new Set();
let visited = new Set();
let stack = [inLink.target];
while (stack.length > 0) {
let node = stack.pop(stack);
visited.add(node)
let links = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
links.forEach(l => {
if (visited.has(l.target)) {
return
}
visited.add(l.target);
if (selectedNodes.has(l.target)) {
// internal link
stack.push(l.target)
} else {
targetNodes.add(l.target)
}
})
}
targetNodes.forEach(target => {
let linkId = `${sourceNode.id}[${inLink.sourcePort}] -> ${target.id}`
if (!createdLinkIds.has(linkId)) {
createdLinkIds.add(linkId);
let link = {
source: sourceNode,
sourcePort: inLink.sourcePort,
target: target
}
let existingLinks = RED.nodes.filterLinks(link)
if (existingLinks.length === 0) {
newLinks.push(link);
}
}
})
})
// 2. delete all those links
allInboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)})
allOutboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)})
})
newLinks.forEach(l => RED.nodes.addLink(l));
return {
newLinks,
removedLinks
}
}
}
return {
init: function() {
RED.events.on("registry:node-type-added",function(type) {
@@ -2539,7 +2690,7 @@ RED.nodes = (function() {
add: addNode,
remove: removeNode,
clear: clear,
detachNodes: detachNodes,
moveNodesForwards: moveNodesForwards,
moveNodesBackwards: moveNodesBackwards,
moveNodesToFront: moveNodesToFront,
@@ -2551,7 +2702,34 @@ RED.nodes = (function() {
addLink: addLink,
removeLink: removeLink,
getNodeLinks: function(id, portType) {
if (typeof id !== 'string') {
id = id.id;
}
if (nodeLinks[id]) {
if (portType === 1) {
// Return cloned arrays so they can be safely modified by caller
return [].concat(nodeLinks[id].in)
} else {
return [].concat(nodeLinks[id].out)
}
}
return [];
},
getNodeLinkCount: function(id,portType,index) {
// We *could* just let callers use `getNodeLinks` and get the
// the length for themselves. However, that function creates
// a clone of the array - which is needless work if all you
// want is the length
if (nodeLinks[id]) {
if (portType === 1) {
return nodeLinks[id].inCount[index] || 0
} else {
return nodeLinks[id].outCount[index] || 0
}
}
return 0;
},
addWorkspace: addWorkspace,
removeWorkspace: removeWorkspace,
getWorkspaceOrder: function() { return workspacesOrder },
@@ -2625,6 +2803,7 @@ RED.nodes = (function() {
getAllFlowNodes: getAllFlowNodes,
getAllUpstreamNodes: getAllUpstreamNodes,
getAllDownstreamNodes: getAllDownstreamNodes,
getNodeIslands: getNodeIslands,
createExportableNodeSet: createExportableNodeSet,
createCompleteNodeSet: createCompleteNodeSet,
updateConfigNodeUsers: updateConfigNodeUsers,

View File

@@ -333,6 +333,16 @@ RED.deploy = (function() {
var unknownNodes = [];
var invalidNodes = [];
RED.nodes.eachConfig(function(node) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
if (unknownNodes.indexOf(node.name) == -1) {
unknownNodes.push(node.name);
}
}
});
RED.nodes.eachNode(function(node) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));

View File

@@ -590,12 +590,14 @@ RED.group = (function() {
markDirty(group);
}
function getNodes(group,recursive) {
function getNodes(group,recursive,excludeGroup) {
var nodes = [];
group.nodes.forEach(function(n) {
nodes.push(n);
if (n.type !== 'group' || !excludeGroup) {
nodes.push(n);
}
if (recursive && n.type === 'group') {
nodes = nodes.concat(getNodes(n,recursive))
nodes = nodes.concat(getNodes(n,recursive,excludeGroup))
}
})
return nodes;

View File

@@ -22,6 +22,7 @@ RED.search = (function() {
var selected = -1;
var visible = false;
var searchHistory = [];
var index = {};
var currentResults = [];
var previousActiveElement;
@@ -205,6 +206,20 @@ RED.search = (function() {
}
}
function populateSearchHistory() {
if (searchHistory.length > 0) {
searchResults.editableList('addItem',{
historyHeader: true
});
searchHistory.forEach(function(entry) {
searchResults.editableList('addItem',{
history: true,
value: entry
});
})
}
}
function createDialog() {
dialog = $("<div>",{id:"red-ui-search",class:"red-ui-search"}).appendTo("#red-ui-main-container");
var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(dialog);
@@ -213,7 +228,12 @@ RED.search = (function() {
change: function() {
searchResults.editableList('empty');
selected = -1;
currentResults = search($(this).val());
var value = $(this).val();
if (value === "") {
populateSearchHistory();
return;
}
currentResults = search(value);
if (currentResults.length > 0) {
for (i=0;i<Math.min(currentResults.length,25);i++) {
searchResults.editableList('addItem',currentResults[i])
@@ -285,7 +305,12 @@ RED.search = (function() {
})
}
}
} else {
} if ($(children[selected]).hasClass("red-ui-search-history")) {
var object = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
if (object) {
searchInput.searchBox('value',object.value)
}
} else if (!$(children[selected]).hasClass("red-ui-search-historyHeader")) {
if (currentResults.length > 0) {
reveal(currentResults[Math.max(0,selected)].node);
}
@@ -301,7 +326,32 @@ RED.search = (function() {
addItem: function(container,i,object) {
var node = object.node;
var div;
if (object.more) {
if (object.historyHeader) {
container.parent().addClass("red-ui-search-historyHeader")
$('<div>',{class:"red-ui-search-empty"}).text(RED._("search.history")).appendTo(container);
$('<button type="button" class="red-ui-button red-ui-button-small"></button>').text(RED._("search.clear")).appendTo(container).on("click", function(evt) {
evt.preventDefault();
searchHistory = [];
searchResults.editableList('empty');
});
} else if (object.history) {
container.parent().addClass("red-ui-search-history")
div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container);
div.text(object.value);
div.on("click", function(evt) {
evt.preventDefault();
searchInput.searchBox('value',object.value)
searchInput.focus();
})
$('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-remove"></i></button>').appendTo(container).on("click", function(evt) {
evt.preventDefault();
var index = searchHistory.indexOf(object.value);
searchHistory.splice(index,1);
searchResults.editableList('removeItem', object);
});
} else if (object.more) {
container.parent().addClass("red-ui-search-more")
div = $('<a>',{href:'#',class:"red-ui-search-result red-ui-search-empty"}).appendTo(container);
div.text(RED._("palette.editor.more",{count:object.more.results.length-object.more.start}));
@@ -356,6 +406,12 @@ RED.search = (function() {
}
function reveal(node) {
var searchVal = searchInput.val();
var existingIndex = searchHistory.indexOf(searchVal);
if (existingIndex > -1) {
searchHistory.splice(existingIndex,1);
}
searchHistory.unshift(searchInput.val());
hide();
RED.view.reveal(node.id);
}
@@ -374,9 +430,14 @@ RED.search = (function() {
if (dialog === null) {
createDialog();
} else {
searchResults.editableList('empty');
}
dialog.slideDown(300);
searchInput.searchBox('value',v)
if (!v || v === "") {
populateSearchHistory();
}
RED.events.emit("search:open");
visible = true;
}

View File

@@ -27,5 +27,7 @@ RED.state = {
PANNING: 10,
SELECTING_NODE: 11,
GROUP_DRAGGING: 12,
GROUP_RESIZE: 13
GROUP_RESIZE: 13,
DETACHED_DRAGGING: 14,
SLICING: 15
}

View File

@@ -725,6 +725,90 @@ RED.view.tools = (function() {
}
}
function wireSeriesOfNodes() {
var selection = RED.view.selection();
if (selection.nodes) {
if (selection.nodes.length > 1) {
var i = 0;
var newLinks = [];
while (i < selection.nodes.length - 1) {
var nodeA = selection.nodes[i];
var nodeB = selection.nodes[i+1];
if (nodeA.outputs > 0 && nodeB.inputs > 0) {
var existingLinks = RED.nodes.filterLinks({
source: nodeA,
target: nodeB,
sourcePort: 0
})
if (existingLinks.length === 0) {
var newLink = {
source: nodeA,
target: nodeB,
sourcePort: 0
}
RED.nodes.addLink(newLink);
newLinks.push(newLink);
}
}
i++;
}
if (newLinks.length > 0) {
RED.history.push({
t: 'add',
links: newLinks,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
function wireNodeToMultiple() {
var selection = RED.view.selection();
if (selection.nodes) {
if (selection.nodes.length > 1) {
var sourceNode = selection.nodes[0];
if (sourceNode.outputs === 0) {
return;
}
var i = 1;
var newLinks = [];
while (i < selection.nodes.length) {
var targetNode = selection.nodes[i];
if (targetNode.inputs > 0) {
var existingLinks = RED.nodes.filterLinks({
source: sourceNode,
target: targetNode,
sourcePort: Math.min(sourceNode.outputs-1,i-1)
})
if (existingLinks.length === 0) {
var newLink = {
source: sourceNode,
target: targetNode,
sourcePort: Math.min(sourceNode.outputs-1,i-1)
}
RED.nodes.addLink(newLink);
newLinks.push(newLink);
}
}
i++;
}
if (newLinks.length > 0) {
RED.history.push({
t: 'add',
links: newLinks,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
return {
init: function() {
RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); })
@@ -783,7 +867,8 @@ RED.view.tools = (function() {
RED.actions.add("core:distribute-selection-horizontally", function() { distributeSelection('h') })
RED.actions.add("core:distribute-selection-vertically", function() { distributeSelection('v') })
RED.actions.add("core:wire-series-of-nodes", function() { wireSeriesOfNodes() })
RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() })
// RED.actions.add("core:add-node", function() { addNode() })
},

View File

@@ -37,6 +37,8 @@ RED.view = (function() {
node_height = 30,
dblClickInterval = 650;
var ARROW_HEADS = false;
var touchLongPressTimeout = 1000,
startTouchDistance = 0,
startTouchCenter = [],
@@ -63,7 +65,6 @@ RED.view = (function() {
var activeGroups = [];
var dirtyGroups = {};
var selected_link = null;
var mousedown_link = null;
var mousedown_node = null;
var mousedown_group = null;
@@ -75,6 +76,8 @@ RED.view = (function() {
var mouse_mode = 0;
var mousedown_group_handle = null;
var lasso = null;
var slicePath = null;
var slicePathLast = null;
var ghostNode = null;
var showStatus = false;
var lastClickNode = null;
@@ -129,6 +132,14 @@ RED.view = (function() {
if (!setIds.has(node.id)) {
set.push({n:node});
setIds.add(node.id);
var links = RED.nodes.getNodeLinks(node.id,PORT_TYPE_INPUT).concat(RED.nodes.getNodeLinks(node.id,PORT_TYPE_OUTPUT))
for (var i=0,l=links.length;i<l;i++) {
var link = links[i]
if (link.source === node && setIds.has(link.target.id) ||
link.target === node && setIds.has(link.source.id)) {
selectedLinks.add(link)
}
}
}
}
},
@@ -145,6 +156,10 @@ RED.view = (function() {
}
}
}
var links = RED.nodes.getNodeLinks(node.id,PORT_TYPE_INPUT).concat(RED.nodes.getNodeLinks(node.id,PORT_TYPE_OUTPUT))
for (var i=0,l=links.length;i<l;i++) {
selectedLinks.remove(links[i]);
}
}
},
clear: function() {
@@ -159,6 +174,31 @@ RED.view = (function() {
return api;
})();
var selectedLinks = (function() {
var links = new Set();
return {
add: function(link) {
links.add(link);
link.selected = true;
},
remove: function(link) {
links.delete(link);
link.selected = false;
},
clear: function() {
links.forEach(function(link) { link.selected = false })
links.clear();
},
length: function() {
return links.size;
},
forEach: function(func) { links.forEach(func) },
has: function(link) { return links.has(link) },
toArray: function() { return Array.from(links) }
}
})();
function init() {
chart = $("#red-ui-workspace-chart");
@@ -193,6 +233,12 @@ RED.view = (function() {
}
} else if (mouse_mode === RED.state.PANNING && d3.event.buttons !== 4) {
resetMouseVars();
} else if (slicePath) {
if (d3.event.buttons !== 2) {
slicePath.remove();
slicePath = null;
resetMouseVars()
}
}
})
.on("touchend", function() {
@@ -412,26 +458,56 @@ RED.view = (function() {
var historyEvent = result.historyEvent;
var nn = result.node;
RED.nodes.add(nn);
var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
nn.l = showLabel;
}
var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
var helperWidth = ui.helper.width();
var helperHeight = ui.helper.height();
var mousePos = d3.touches(this)[0]||d3.mouse(this);
mousePos[1] += this.scrollTop + ((nn.h/2)-helperOffset[1]);
mousePos[0] += this.scrollLeft + ((nn.w/2)-helperOffset[0]);
try {
var isLink = (nn.type === "link in" || nn.type === "link out")
var hideLabel = nn.hasOwnProperty('l')?!nn.l : isLink;
var label = RED.utils.getNodeLabel(nn, nn.type);
var labelParts = getLabelParts(label, "red-ui-flow-node-label");
if (hideLabel) {
nn.w = node_height;
nn.h = Math.max(node_height,(nn.outputs || 0) * 15);
} else {
nn.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(nn._def.inputs>0?7:0))/20)) );
nn.h = Math.max(6+24*labelParts.lines.length,(nn.outputs || 0) * 15, 30);
}
} catch(err) {
}
mousePos[1] += this.scrollTop + ((helperHeight/2)-helperOffset[1]);
mousePos[0] += this.scrollLeft + ((helperWidth/2)-helperOffset[0]);
mousePos[1] /= scaleFactor;
mousePos[0] /= scaleFactor;
if (snapGrid) {
mousePos[0] = gridSize*(Math.ceil(mousePos[0]/gridSize));
mousePos[1] = gridSize*(Math.ceil(mousePos[1]/gridSize));
}
nn.x = mousePos[0];
nn.y = mousePos[1];
if (snapGrid) {
var gridOffset = [0,0];
var offsetLeft = nn.x-(gridSize*Math.round((nn.x-nn.w/2)/gridSize)+nn.w/2);
var offsetRight = nn.x-(gridSize*Math.round((nn.x+nn.w/2)/gridSize)-nn.w/2);
if (Math.abs(offsetLeft) < Math.abs(offsetRight)) {
gridOffset[0] = offsetLeft
} else {
gridOffset[0] = offsetRight
}
gridOffset[1] = nn.y-(gridSize*Math.round(nn.y/gridSize));
nn.x -= gridOffset[0];
nn.y -= gridOffset[1];
}
var spliceLink = $(ui.helper).data("splice");
if (spliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp/showQuickAddDialog
@@ -452,7 +528,6 @@ RED.view = (function() {
historyEvent.removedLinks = [spliceLink];
}
RED.nodes.add(nn);
var group = $(ui.helper).data("group");
if (group) {
@@ -502,6 +577,8 @@ RED.view = (function() {
RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();});
RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});});
RED.actions.add("core:detach-selected-nodes", function() { detachSelectedNodes() })
RED.events.on("view:selection-changed", function(selection) {
var hasSelection = (selection.nodes && selection.nodes.length > 0);
var hasMultipleSelection = hasSelection && selection.nodes.length > 1;
@@ -524,6 +601,7 @@ RED.view = (function() {
})
RED.actions.add("core:delete-selection",deleteSelection);
RED.actions.add("core:delete-selection-and-reconnect",function() { deleteSelection(true) });
RED.actions.add("core:edit-selected-node",editSelection);
RED.actions.add("core:go-to-selection",function() {
if (movingSet.length() > 0) {
@@ -759,8 +837,9 @@ RED.view = (function() {
} else {
scale = 0.4-0.2*(Math.max(0,(node_width-Math.min(Math.abs(dx),Math.abs(dy)))/node_width));
}
var result;
if (dx*sc > 0) {
return "M "+origX+" "+origY+
result = "M "+origX+" "+origY+
" C "+(origX+sc*(node_width*scale))+" "+(origY+scaleY*node_height)+" "+
(destX-sc*(scale)*node_width)+" "+(destY-scaleY*node_height)+" "+
destX+" "+destY
@@ -801,7 +880,7 @@ RED.view = (function() {
}
cp[2][0] = topX;
}
return "M "+origX+" "+origY+
result = "M "+origX+" "+origY+
" C "+
cp[0][0]+" "+cp[0][1]+" "+
cp[1][0]+" "+cp[1][1]+" "+
@@ -816,6 +895,7 @@ RED.view = (function() {
cp[4][0]+" "+cp[4][1]+" "+
destX+" "+destY
}
return result;
}
function addNode(type,x,y) {
@@ -909,7 +989,7 @@ RED.view = (function() {
return;
}
if (!mousedown_node && !mousedown_link && !mousedown_group) {
selected_link = null;
selectedLinks.clear();
updateSelection();
}
if (mouse_mode === 0) {
@@ -918,19 +998,18 @@ RED.view = (function() {
lasso = null;
}
}
if (mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) {
if (d3.event.metaKey || d3.event.ctrlKey) {
d3.event.stopPropagation();
clearSelection();
point = d3.mouse(this);
var clickedGroup = getGroupAt(point[0],point[1]);
if (drag_lines.length > 0) {
clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g)
}
showQuickAddDialog({position:point, group:clickedGroup});
if ((mouse_mode === 0 || mouse_mode === RED.state.QUICK_JOINING) && (d3.event.touches || d3.event.button === 0) && (d3.event.metaKey || d3.event.ctrlKey)) {
// Trigger quick add dialog
d3.event.stopPropagation();
clearSelection();
point = d3.mouse(this);
var clickedGroup = getGroupAt(point[0],point[1]);
if (drag_lines.length > 0) {
clickedGroup = clickedGroup || RED.nodes.group(drag_lines[0].node.g)
}
}
if (mouse_mode === 0 && !(d3.event.metaKey || d3.event.ctrlKey)) {
showQuickAddDialog({position:point, group:clickedGroup});
} else if (mouse_mode === 0 && (d3.event.touches || d3.event.button === 0) && !(d3.event.metaKey || d3.event.ctrlKey)) {
// Tigger lasso
if (!touchStartTime) {
point = d3.mouse(this);
lasso = eventLayer.append("rect")
@@ -945,6 +1024,13 @@ RED.view = (function() {
.attr("class","nr-ui-view-lasso");
d3.event.preventDefault();
}
} else if (mouse_mode === 0 && d3.event.button === 2 && (d3.event.metaKey || d3.event.ctrlKey)) {
clearSelection();
mouse_mode = RED.state.SLICING;
point = d3.mouse(this);
slicePath = eventLayer.append("path").attr("class","nr-ui-view-slice").attr("d",`M${point[0]} ${point[1]}`)
slicePathLast = point;
RED.view.redraw();
}
}
@@ -1341,6 +1427,17 @@ RED.view = (function() {
.attr("height",h)
;
return;
} else if (mouse_mode === RED.state.SLICING) {
if (slicePath) {
var delta = Math.max(1,Math.abs(slicePathLast[0]-mouse_position[0]))*Math.max(1,Math.abs(slicePathLast[1]-mouse_position[1]))
if (delta > 20) {
var currentPath = slicePath.attr("d")
currentPath += " L"+mouse_position[0]+" "+mouse_position[1]
slicePath.attr("d",currentPath);
slicePathLast = mouse_position
}
}
return
}
if (mouse_mode === RED.state.SELECTING_NODE) {
@@ -1348,7 +1445,7 @@ RED.view = (function() {
return;
}
if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && !mousedown_group && selected_link == null) {
if (mouse_mode != RED.state.QUICK_JOINING && mouse_mode != RED.state.IMPORT_DRAGGING && mouse_mode != RED.state.DETACHED_DRAGGING && !mousedown_node && !mousedown_group && selectedLinks.length() === 0) {
return;
}
@@ -1372,16 +1469,18 @@ RED.view = (function() {
// Get all the wires we need to detach.
var links = [];
var existingLinks = [];
if (selected_link &&
((mousedown_port_type === PORT_TYPE_OUTPUT &&
selected_link.source === mousedown_node &&
selected_link.sourcePort === mousedown_port_index
) ||
(mousedown_port_type === PORT_TYPE_INPUT &&
selected_link.target === mousedown_node
))
) {
existingLinks = [selected_link];
if (selectedLinks.length() > 0) {
selectedLinks.forEach(function(link) {
if (((mousedown_port_type === PORT_TYPE_OUTPUT &&
link.source === mousedown_node &&
link.sourcePort === mousedown_port_index
) ||
(mousedown_port_type === PORT_TYPE_INPUT &&
link.target === mousedown_node
))) {
existingLinks.push(link);
}
})
} else {
var filter;
if (mousedown_port_type === PORT_TYPE_OUTPUT) {
@@ -1419,7 +1518,7 @@ RED.view = (function() {
} else if (mousedown_node && !quickAddLink) {
showDragLines([{node:mousedown_node,port:mousedown_port_index,portType:mousedown_port_type}]);
}
selected_link = null;
selectedLinks.clear();
}
mousePos = mouse_position;
for (i=0;i<drag_lines.length;i++) {
@@ -1451,7 +1550,7 @@ RED.view = (function() {
RED.nodes.filterLinks({ target: node.n }).length === 0;
}
}
} else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) {
} else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) {
mousePos = mouse_position;
var minX = 0;
var minY = 0;
@@ -1515,8 +1614,16 @@ RED.view = (function() {
gridOffset[0] = node.n.x-(gridSize*Math.floor(node.n.x/gridSize))-gridSize/2;
gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize))-gridSize/2;
} else {
gridOffset[0] = node.n.x-(gridSize*Math.floor((node.n.x-node.n.w/2)/gridSize)+node.n.w/2);
gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize));
var offsetLeft = node.n.x-(gridSize*Math.round((node.n.x-node.n.w/2)/gridSize)+node.n.w/2);
var offsetRight = node.n.x-(gridSize*Math.round((node.n.x+node.n.w/2)/gridSize)-node.n.w/2);
// gridOffset[0] = node.n.x-(gridSize*Math.floor((node.n.x-node.n.w/2)/gridSize)+node.n.w/2);
if (Math.abs(offsetLeft) < Math.abs(offsetRight)) {
gridOffset[0] = offsetLeft
} else {
gridOffset[0] = offsetRight
}
gridOffset[1] = node.n.y-(gridSize*Math.round(node.n.y/gridSize));
// console.log(offsetLeft, offsetRight);
}
if (gridOffset[0] !== 0 || gridOffset[1] !== 0) {
for (i = 0; i<movingSet.length(); i++) {
@@ -1736,6 +1843,11 @@ RED.view = (function() {
} else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey && !d3.event.metaKey ) {
clearSelection();
updateSelection();
} else if (slicePath) {
deleteSelection();
slicePath.remove();
slicePath = null;
RED.view.redraw(true);
}
if (mouse_mode == RED.state.MOVING_ACTIVE) {
if (movingSet.length() > 0) {
@@ -1807,10 +1919,30 @@ RED.view = (function() {
// movingSet.add(mousedown_node);
// }
// }
if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) {
if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.DETACHED_DRAGGING) {
// if (mousedown_node) {
// delete mousedown_node.gSelected;
// }
if (mouse_mode === RED.state.DETACHED_DRAGGING) {
var ns = [];
for (var j=0;j<movingSet.length();j++) {
var n = movingSet.get(j);
if (n.ox !== n.n.x || n.oy !== n.n.y) {
ns.push({n:n.n,ox:n.ox,oy:n.oy,moved:n.n.moved});
n.n.dirty = true;
n.n.moved = true;
}
}
var detachEvent = RED.history.peek();
// The last event in the stack *should* be a multi-event from
// where the links were added/removed
var historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()}
if (detachEvent.t === "multi") {
detachEvent.events.push(historyEvent)
} else {
RED.history.push(historyEvent)
}
}
for (i=0;i<movingSet.length();i++) {
var node = movingSet.get(i);
delete node.ox;
@@ -1855,10 +1987,29 @@ RED.view = (function() {
if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
return;
}
if (mouse_mode === RED.state.IMPORT_DRAGGING) {
if (mouse_mode === RED.state.DETACHED_DRAGGING) {
for (var j=0;j<movingSet.length();j++) {
var n = movingSet.get(j);
n.n.x = n.ox;
n.n.y = n.oy;
}
clearSelection();
RED.history.pop();
mouse_mode = 0;
} else if (mouse_mode === RED.state.IMPORT_DRAGGING) {
clearSelection();
RED.history.pop();
mouse_mode = 0;
} else if (mouse_mode === RED.state.SLICING) {
if (slicePath) {
slicePath.remove();
slicePath = null;
resetMouseVars()
}
clearSelection();
} else if (lasso) {
lasso.remove();
lasso = null;
} else if (activeGroup) {
exitActiveGroup()
} else {
@@ -1870,6 +2021,7 @@ RED.view = (function() {
if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions.single) {
return;
}
selectedLinks.clear();
if (activeGroup) {
var ag = activeGroup;
@@ -1941,7 +2093,6 @@ RED.view = (function() {
}
}
}
selected_link = null;
if (mouse_mode !== RED.state.SELECTING_NODE) {
updateSelection();
}
@@ -1956,7 +2107,7 @@ RED.view = (function() {
n.n.selected = false;
}
movingSet.clear();
selected_link = null;
selectedLinks.clear();
if (activeGroup) {
activeGroup.active = false
activeGroup.dirty = true;
@@ -2050,12 +2201,16 @@ RED.view = (function() {
}
}
}
if (activeFlowLinks.length === 0 && selected_link !== null && selected_link.link) {
activeLinks.push(selected_link);
activeLinkNodes[selected_link.source.id] = selected_link.source;
selected_link.source.dirty = true;
activeLinkNodes[selected_link.target.id] = selected_link.target;
selected_link.target.dirty = true;
if (activeFlowLinks.length === 0 && selectedLinks.length() > 0) {
selectedLinks.forEach(function(link) {
if (link.link) {
activeLinks.push(link);
activeLinkNodes[link.source.id] = link.source;
link.source.dirty = true;
activeLinkNodes[link.target.id] = link.target;
link.target.dirty = true;
}
})
}
} else {
selection.flows = workspaceSelection;
@@ -2066,6 +2221,10 @@ RED.view = (function() {
return value.map(function(n) { return n.id })
} else if (key === 'link') {
return value.source.id+":"+value.sourcePort+":"+value.target.id;
} else if (key === 'links') {
return value.map(function(link) {
return link.source.id+":"+link.sourcePort+":"+link.target.id;
});
}
return value;
});
@@ -2087,7 +2246,7 @@ RED.view = (function() {
}
}
}
function deleteSelection() {
function deleteSelection(reconnectWires) {
if (mouse_mode === RED.state.SELECTING_NODE) {
return;
}
@@ -2135,7 +2294,7 @@ RED.view = (function() {
updateActiveNodes();
updateSelection();
redraw();
} else if (movingSet.length() > 0 || selected_link != null) {
} else if (movingSet.length() > 0 || selectedLinks.length() > 0) {
var result;
var node;
var removedNodes = [];
@@ -2145,6 +2304,16 @@ RED.view = (function() {
var removedSubflowInputs = [];
var removedSubflowStatus;
var subflowInstances = [];
var historyEvents = [];
if (reconnectWires) {
var reconnectResult = RED.nodes.detachNodes(movingSet.nodes())
var addedLinks = reconnectResult.newLinks;
if (addedLinks.length > 0) {
historyEvents.push({ t:'add', links: addedLinks })
}
removedLinks = removedLinks.concat(reconnectResult.removedLinks)
}
var startDirty = RED.nodes.dirty();
var startChanged = false;
@@ -2234,71 +2403,71 @@ RED.view = (function() {
RED.nodes.dirty(true);
}
}
var historyEvent;
if (selected_link && selected_link.link) {
var sourceId = selected_link.source.id;
var targetId = selected_link.target.id;
var sourceIdIndex = selected_link.target.links.indexOf(sourceId);
var targetIdIndex = selected_link.source.links.indexOf(targetId);
historyEvent = {
t:"multi",
events: [
{
if (selectedLinks.length() > 0) {
selectedLinks.forEach(function(link) {
if (link.link) {
var sourceId = link.source.id;
var targetId = link.target.id;
var sourceIdIndex = link.target.links.indexOf(sourceId);
var targetIdIndex = link.source.links.indexOf(targetId);
historyEvents.push({
t: "edit",
node: selected_link.source,
changed: selected_link.source.changed,
node: link.source,
changed: link.source.changed,
changes: {
links: $.extend(true,{},{v:selected_link.source.links}).v
links: $.extend(true,{},{v:link.source.links}).v
}
},
{
})
historyEvents.push({
t: "edit",
node: selected_link.target,
changed: selected_link.target.changed,
node: link.target,
changed: link.target.changed,
changes: {
links: $.extend(true,{},{v:selected_link.target.links}).v
links: $.extend(true,{},{v:link.target.links}).v
}
}
})
link.source.changed = true;
link.target.changed = true;
link.target.links.splice(sourceIdIndex,1);
link.source.links.splice(targetIdIndex,1);
link.source.dirty = true;
link.target.dirty = true;
],
dirty:RED.nodes.dirty()
}
RED.nodes.dirty(true);
selected_link.source.changed = true;
selected_link.target.changed = true;
selected_link.target.links.splice(sourceIdIndex,1);
selected_link.source.links.splice(targetIdIndex,1);
selected_link.source.dirty = true;
selected_link.target.dirty = true;
} else {
if (selected_link) {
RED.nodes.removeLink(selected_link);
removedLinks.push(selected_link);
}
RED.nodes.dirty(true);
historyEvent = {
t:"delete",
nodes:removedNodes,
links:removedLinks,
groups: removedGroups,
subflowOutputs:removedSubflowOutputs,
subflowInputs:removedSubflowInputs,
subflow: {
id: activeSubflow?activeSubflow.id:undefined,
instances: subflowInstances
},
dirty:startDirty
};
if (removedSubflowStatus) {
historyEvent.subflow.status = removedSubflowStatus;
}
} else {
RED.nodes.removeLink(link);
removedLinks.push(link);
}
})
}
RED.nodes.dirty(true);
var historyEvent = {
t:"delete",
nodes:removedNodes,
links:removedLinks,
groups: removedGroups,
subflowOutputs:removedSubflowOutputs,
subflowInputs:removedSubflowInputs,
subflow: {
id: activeSubflow?activeSubflow.id:undefined,
instances: subflowInstances
},
dirty:startDirty
};
if (removedSubflowStatus) {
historyEvent.subflow.status = removedSubflowStatus;
}
if (historyEvents.length > 0) {
historyEvents.unshift(historyEvent);
RED.history.push({
t:"multi",
events: historyEvents
})
} else {
RED.history.push(historyEvent);
}
RED.history.push(historyEvent);
selected_link = null;
selectedLinks.clear();
updateActiveNodes();
updateSelection();
redraw();
@@ -2375,6 +2544,28 @@ RED.view = (function() {
}
}
function detachSelectedNodes() {
var selection = RED.view.selection();
if (selection.nodes) {
const {newLinks, removedLinks} = RED.nodes.detachNodes(selection.nodes);
if (removedLinks.length || newLinks.length) {
RED.history.push({
t: "multi",
events: [
{ t:'delete', links: removedLinks },
{ t:'add', links: newLinks }
],
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true)
}
prepareDrag([selection.nodes[0].x,selection.nodes[0].y]);
mouse_mode = RED.state.DETACHED_DRAGGING;
RED.view.redraw(true);
}
}
function calculateTextWidth(str, className) {
var result = convertLineBreakCharacter(str);
var width = 0;
@@ -2461,7 +2652,7 @@ RED.view = (function() {
activeHoverGroup.hovered = false;
activeHoverGroup = null;
}
d3.select(".red-ui-flow-link-splice").classed("red-ui-flow-link-splice",false);
d3.selectAll(".red-ui-flow-link-splice").classed("red-ui-flow-link-splice",false);
if (spliceTimer) {
clearTimeout(spliceTimer);
spliceTimer = null;
@@ -2709,10 +2900,13 @@ RED.view = (function() {
} else {
resetMouseVars();
}
selected_link = select_link;
mousedown_link = select_link;
if (select_link) {
selectedLinks.clear();
selectedLinks.add(select_link);
updateSelection();
} else {
selectedLinks.clear();
}
}
redraw();
@@ -2721,7 +2915,10 @@ RED.view = (function() {
resetMouseVars();
hideDragLines();
selected_link = select_link;
if (select_link) {
selectedLinks.clear();
selectedLinks.add(select_link);
}
mousedown_link = select_link;
if (select_link) {
updateSelection();
@@ -2894,10 +3091,13 @@ RED.view = (function() {
msn.dx = msn.n.x-mouse[0];
msn.dy = msn.n.y-mouse[1];
}
mouse_offset = d3.mouse(document.body);
if (isNaN(mouse_offset[0])) {
mouse_offset = d3.touches(document.body)[0];
try {
mouse_offset = d3.mouse(document.body);
if (isNaN(mouse_offset[0])) {
mouse_offset = d3.touches(document.body)[0];
}
} catch(err) {
mouse_offset = [0,0]
}
}
@@ -2978,7 +3178,7 @@ RED.view = (function() {
//var touch0 = d3.event;
//var pos = [touch0.pageX,touch0.pageY];
//RED.touch.radialMenu.show(d3.select(this),pos);
if (mouse_mode == RED.state.IMPORT_DRAGGING) {
if (mouse_mode == RED.state.IMPORT_DRAGGING || mouse_mode == RED.state.DETACHED_DRAGGING) {
var historyEvent = RED.history.peek();
if (activeSpliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
@@ -3016,6 +3216,18 @@ RED.view = (function() {
activeHoverGroup = null;
}
if (mouse_mode == RED.state.DETACHED_DRAGGING) {
var ns = [];
for (var j=0;j<movingSet.length();j++) {
var n = movingSet.get(j);
if (n.ox !== n.n.x || n.oy !== n.n.y) {
ns.push({n:n.n,ox:n.ox,oy:n.oy,moved:n.n.moved});
n.n.dirty = true;
n.n.moved = true;
}
}
RED.history.replace({t:"multi",events:[historyEvent,{t:"move",nodes:ns}],dirty: historyEvent.dirty})
}
updateSelection();
RED.nodes.dirty(true);
@@ -3184,7 +3396,9 @@ RED.view = (function() {
// }
// } else
if (d3.event.shiftKey) {
clearSelection();
if (!(d3.event.ctrlKey||d3.event.metaKey)) {
clearSelection();
}
var clickPosition = (d3.event.offsetX/scaleFactor - mousedown_node.x)
var edgeDelta = (mousedown_node.w/2) - Math.abs(clickPosition);
var cnodes;
@@ -3212,7 +3426,7 @@ RED.view = (function() {
mousedown_node.selected = true;
movingSet.add(mousedown_node);
}
selected_link = null;
// selectedLinks.clear();
if (d3.event.button != 2) {
mouse_mode = RED.state.MOVING;
var mouse = d3.touches(this)[0]||d3.mouse(this);
@@ -3338,19 +3552,35 @@ RED.view = (function() {
d3.event.stopPropagation();
return;
}
mousedown_link = d;
clearSelection();
selected_link = mousedown_link;
updateSelection();
redraw();
focusView();
d3.event.stopPropagation();
if (d3.event.metaKey || d3.event.ctrlKey) {
d3.select(this).classed("red-ui-flow-link-splice",true);
var point = d3.mouse(this);
var clickedGroup = getGroupAt(point[0],point[1]);
showQuickAddDialog({position:point, splice:selected_link, group:clickedGroup});
}
if (d3.event.button === 2) {
return
}
mousedown_link = d;
if (!(d3.event.metaKey || d3.event.ctrlKey)) {
clearSelection();
}
if (d3.event.metaKey || d3.event.ctrlKey) {
if (!selectedLinks.has(mousedown_link)) {
selectedLinks.add(mousedown_link);
} else {
if (selectedLinks.length() !== 1) {
selectedLinks.remove(mousedown_link);
}
}
} else {
selectedLinks.add(mousedown_link);
}
updateSelection();
redraw();
focusView();
d3.event.stopPropagation();
if (!mousedown_link.link && movingSet.length() === 0 && (d3.event.touches || d3.event.button === 0) && selectedLinks.length() === 1 && selectedLinks.has(mousedown_link) && (d3.event.metaKey || d3.event.ctrlKey)) {
d3.select(this).classed("red-ui-flow-link-splice",true);
var point = d3.mouse(this);
var clickedGroup = getGroupAt(point[0],point[1]);
showQuickAddDialog({position:point, splice:mousedown_link, group:clickedGroup});
}
}
function linkTouchStart(d) {
if (mouse_mode === RED.state.SELECTING_NODE) {
@@ -3359,7 +3589,8 @@ RED.view = (function() {
}
mousedown_link = d;
clearSelection();
selected_link = mousedown_link;
selectedLinks.clear();
selectedLinks.add(mousedown_link);
updateSelection();
redraw();
focusView();
@@ -3595,7 +3826,7 @@ RED.view = (function() {
function showTouchMenu(obj,pos) {
var mdn = mousedown_node;
var options = [];
options.push({name:"delete",disabled:(movingSet.length()===0 && selected_link === null),onselect:function() {deleteSelection();}});
options.push({name:"delete",disabled:(movingSet.length()===0 && selectedLinks.length() === 0),onselect:function() {deleteSelection();}});
options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection();deleteSelection();}});
options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}});
options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}});
@@ -3904,6 +4135,11 @@ RED.view = (function() {
}
node[0][0].__portGroup__ = document.createElementNS("http://www.w3.org/2000/svg","g");
node[0][0].__portGroup__.__data__ = d;
nodeContents.appendChild(node[0][0].__portGroup__);
var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect");
mainRect.__data__ = d;
mainRect.setAttribute("class", "red-ui-flow-node "+(d.type == "unknown"?"red-ui-flow-node-unknown":""));
@@ -4107,7 +4343,7 @@ RED.view = (function() {
this.__textGroup__.setAttribute("transform", "translate(38,"+yp+")");
}
var inputPorts = thisNode.selectAll(".red-ui-flow-port-input");
var inputPorts = d3.select(this.__portGroup__).selectAll(".red-ui-flow-port-input");
if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) {
inputPorts.each(function(d,i) {
RED.hooks.trigger("viewRemovePort",{
@@ -4118,8 +4354,9 @@ RED.view = (function() {
portIndex: 0
})
}).remove();
this.__inputs__ = [];
} else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) {
var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input");
var inputGroup = d3.select(this.__portGroup__).append("g").attr("class","red-ui-flow-port-input");
var inputGroupPorts;
if (d.type === "link in") {
@@ -4128,7 +4365,8 @@ RED.view = (function() {
.attr("r",5)
.attr("class","red-ui-flow-port red-ui-flow-link-port")
} else {
inputGroupPorts = inputGroup.append("rect").attr("class","red-ui-flow-port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
inputGroupPorts = inputGroup.append("path").attr("class","red-ui-flow-port-background").attr("d","M5 -1.5 h -16 v 13 h 16 z")
inputGroup.append("path").attr("class","red-ui-flow-port red-ui-flow-port-shape").attr("d","M5 0 h -3 c -4 0 -4 0 -4 4 v 2 c 0 4 0 4 4 4 h 3 z ")
}
inputGroup[0][0].__port__ = inputGroupPorts[0][0];
inputGroupPorts[0][0].__data__ = this.__data__;
@@ -4140,6 +4378,8 @@ RED.view = (function() {
.on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
.on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
this.__inputs__.push(inputGroup[0][0]);
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
}
var numOutputs = d.outputs;
@@ -4176,15 +4416,19 @@ RED.view = (function() {
portPort.setAttribute("cy",5);
portPort.setAttribute("r",5);
portPort.setAttribute("class","red-ui-flow-port red-ui-flow-link-port");
portGroup.appendChild(portPort);
} else {
portPort = document.createElementNS("http://www.w3.org/2000/svg","rect");
portPort.setAttribute("rx",3);
portPort.setAttribute("ry",3);
portPort.setAttribute("width",10);
portPort.setAttribute("height",10);
portPort.setAttribute("class","red-ui-flow-port");
portPort = document.createElementNS("http://www.w3.org/2000/svg","path");
portPort.setAttribute("d","M5 -1.5 h 16 v 13 h -16 z ");
portPort.setAttribute("class","red-ui-flow-port-background");
portGroup.appendChild(portPort);
var visiblePort = document.createElementNS("http://www.w3.org/2000/svg","path");
visiblePort.setAttribute("d","M5 0 h 4 c 4 0 4 0 4 4 v 2 c 0 4 0 4 -4 4 h -4 z ");
visiblePort.setAttribute("class","red-ui-flow-port red-ui-flow-port-shape");
portGroup.appendChild(visiblePort);
}
portGroup.appendChild(portPort);
portGroup.__port__ = portPort;
portPort.__data__ = this.__data__;
portPort.__portType__ = PORT_TYPE_OUTPUT;
@@ -4196,7 +4440,7 @@ RED.view = (function() {
portPort.addEventListener("mouseover", portMouseOverProxy);
portPort.addEventListener("mouseout", portMouseOutProxy);
this.appendChild(portGroup);
this.__portGroup__.appendChild(portGroup);
this.__outputs__.push(portGroup);
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
} else {
@@ -4287,6 +4531,14 @@ RED.view = (function() {
// });
}
for (var i=0,l=this.__outputs__.length;i<l;i++) {
this.__outputs__[i].classList.toggle("red-ui-flow-port-connected",RED.nodes.getNodeLinkCount(d.id,PORT_TYPE_OUTPUT,i) > 0 )
}
for (var i=0,l=this.__inputs__.length;i<l;i++) {
this.__inputs__[i].classList.toggle("red-ui-flow-port-connected",RED.nodes.getNodeLinkCount(d.id,PORT_TYPE_INPUT,i) > 0 )
}
if (d.dirtyStatus) {
redrawStatus(d,this);
}
@@ -4332,6 +4584,13 @@ RED.view = (function() {
d3.select(pathBack)
.on("mousedown",linkMouseDown)
.on("touchstart",linkTouchStart)
.on("mousemove", function(d) {
if (mouse_mode === RED.state.SLICING) {
selectedLinks.add(d)
l.classed("red-ui-flow-link-splice",true)
redraw()
}
})
var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path");
pathOutline.__data__ = d;
@@ -4352,7 +4611,7 @@ RED.view = (function() {
link.exit().remove();
link.each(function(d) {
var link = d3.select(this);
if (d.added || d===selected_link || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) {
if (d.added || d.selected || dirtyNodes[d.source.id] || dirtyNodes[d.target.id]) {
var numOutputs = d.source.outputs || 1;
var sourcePort = d.sourcePort || 0;
var y = -((numOutputs-1)/2)*13 +13*sourcePort;
@@ -4360,22 +4619,19 @@ RED.view = (function() {
d.y1 = d.source.y+y;
d.x2 = d.target.x-d.target.w/2;
d.y2 = d.target.y;
// return "M "+d.x1+" "+d.y1+
// " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+
// (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+
// d.x2+" "+d.y2;
var path = generateLinkPath(d.x1,d.y1,d.x2,d.y2,1);
var targetOffset = ARROW_HEADS?(d.link?16:13):0
var path = generateLinkPath(d.x1+5,d.y1,d.x2-targetOffset,d.y2,1);
if (/NaN/.test(path)) {
path = ""
}
this.__pathBack__.setAttribute("d",path);
this.__pathOutline__.setAttribute("d",path);
this.__pathLine__.setAttribute("d",path);
this.__pathLine__.setAttribute("d",path + (ARROW_HEADS?"m0 0, l 0 -2 l 3 2 l -3 2 z":""));
this.__pathLine__.classList.toggle("red-ui-flow-node-disabled",!!(d.source.d || d.target.d));
this.__pathLine__.classList.toggle("red-ui-flow-subflow-link", !d.link && activeSubflow);
}
this.classList.toggle("red-ui-flow-link-selected", !!(d===selected_link||d.selected));
this.classList.toggle("red-ui-flow-link-selected", !!d.selected);
var connectedToUnknown = !!(d.target.type == "unknown" || d.source.type == "unknown");
this.classList.toggle("red-ui-flow-link-unknown",!!(d.target.type == "unknown" || d.source.type == "unknown"))
@@ -5083,8 +5339,9 @@ RED.view = (function() {
if (allNodes.size > 0) {
selection.nodes = Array.from(allNodes);
}
if (selected_link != null) {
selection.link = selected_link;
if (selectedLinks.length() > 0) {
selection.links = selectedLinks.toArray();
selection.link = selection.links[0];
}
return selection;
}

View File

@@ -196,6 +196,12 @@ $view-background: $secondary-background;
$view-select-mode-background: $secondary-background-selected;
$view-grid-color: #eee;
$link-color: #999;
$link-link-color: #aaa;
$link-disabled-color: #ccc;
$link-link-active-color: #ff7f0e;
$link-unknown-color: #f00;
$node-label-color: #333;
$node-port-label-color: #888;
$node-border: #999;
@@ -203,8 +209,13 @@ $node-border-unknown: #f33;
$node-border-placeholder: #aaa;
$node-background-placeholder: #eee;
$node-port-border: $node-border;
$node-port-border-connected: $link-color;
$node-port-background: #d9d9d9;
$node-port-background-hover: #eee;
$node-port-background-connected: $link-color;
$node-icon-color: #fff;
$node-icon-background-color: rgba(0,0,0,0.05);
$node-icon-background-color-fill: #000;
@@ -232,12 +243,6 @@ $node-status-colors: (
$node-selected-color: #ff7f0e;
$port-selected-color: #ff7f0e;
$link-color: #999;
$link-link-color: #aaa;
$link-disabled-color: #ccc;
$link-link-active-color: #ff7f0e;
$link-unknown-color: #f00;
$clipboard-textarea-background: #F3E7E7;

View File

@@ -21,6 +21,13 @@
stroke-dasharray: 10 5;
}
.nr-ui-view-slice {
stroke-width: 1px;
stroke: $view-lasso-stroke;
fill: none;
stroke-dasharray: 10 5;
}
.node_label_italic, // deprecated: use red-ui-flow-node-label-italic
.red-ui-flow-node-label-italic {
font-style: italic;
@@ -178,12 +185,29 @@
}
.red-ui-flow-port {
stroke: $node-border;
stroke: $node-port-border;
stroke-width: 1;
fill: $node-port-background;
cursor: crosshair;
}
.red-ui-flow-port-background {
opacity: 0;
stroke: none;
fill: #f00;
cursor: crosshair;
}
.red-ui-flow-port-shape {
pointer-events: none;
cursor: crosshair;
}
.red-ui-flow-port-connected .red-ui-flow-port {
fill: $node-port-background-connected;
stroke: $node-port-border-connected;
}
.red-ui-flow-node-error {
fill: $node-status-error-background;
stroke: $node-status-error-border;
@@ -273,9 +297,10 @@ g.red-ui-flow-node-selected {
text-anchor:start;
}
.red-ui-flow-port-hovered {
stroke: $port-selected-color;
fill: $port-selected-color;
.red-ui-flow-port-hovered:not(.red-ui-flow-port-background),
.red-ui-flow-port-background.red-ui-flow-port-hovered + .red-ui-flow-port-shape {
stroke: $port-selected-color !important;
fill: $port-selected-color !important;
}
.red-ui-flow-subflow-port {

View File

@@ -204,6 +204,28 @@
font-style: italic;
color: $form-placeholder-color;
}
.red-ui-search-history {
button {
display: none;
position: absolute;
top: 8px;
right: 7px;
}
&:hover button {
display: inline;
}
}
.red-ui-search-historyHeader {
button {
position: absolute;
top: 10px;
right: 7px;
}
}
.red-ui-search-history-result {
}
.red-ui-search-result-action {
color: $primary-text-color;

View File

@@ -75,16 +75,12 @@ module.exports = function(RED) {
node.repeaterSetup = function () {
if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) {
this.repeat = this.repeat * 1000;
if (RED.settings.verbose) {
this.log(RED._("inject.repeat", this));
}
this.debug(RED._("inject.repeat", this));
this.interval_id = setInterval(function() {
node.emit("input", {});
}, this.repeat);
} else if (this.crontab) {
if (RED.settings.verbose) {
this.log(RED._("inject.crontab", this));
}
this.debug(RED._("inject.crontab", this));
this.cronjob = scheduleTask(this.crontab,() => { node.emit("input", {})});
}
};
@@ -148,10 +144,8 @@ module.exports = function(RED) {
}
if (this.interval_id != null) {
clearInterval(this.interval_id);
if (RED.settings.verbose) { this.log(RED._("inject.stopped")); }
} else if (this.cronjob != null) {
this.cronjob.stop();
if (RED.settings.verbose) { this.log(RED._("inject.stopped")); }
delete this.cronjob;
}
};

View File

@@ -86,7 +86,7 @@ module.exports = function(RED) {
});
var cmd = arg.shift();
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(cmd+" ["+arg+"]"); }
node.debug(cmd+" ["+arg+"]");
child = spawn(cmd,arg,node.spawnOpt);
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
var unknownCommand = (child.pid === undefined);
@@ -136,7 +136,7 @@ module.exports = function(RED) {
}
else {
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(arg); }
node.debug(arg);
child = exec(arg, node.execOpt, function (error, stdout, stderr) {
var msg2, msg3;
delete msg.payload;
@@ -155,7 +155,7 @@ module.exports = function(RED) {
if (error.signal) { msg3.payload.signal = error.signal; }
if (error.code === null) { node.status({fill:"red",shape:"dot",text:"killed"}); }
else { node.status({fill:"red",shape:"dot",text:"error:"+error.code}); }
if (RED.settings.verbose) { node.log('error:' + error); }
node.debug('error:' + error);
}
else if (node.oldrc === "false") {
msg3 = RED.util.cloneMessage(msg);

View File

@@ -177,7 +177,8 @@
path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
tls: {type:"tls-config",required: false},
wholemsg: {value:"false"},
hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) }
hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) },
subprotocol: {value:"",required: false}
},
inputs:0,
outputs:0,
@@ -265,7 +266,10 @@
<label for="node-config-input-tls" data-i18n="httpin.tls-config"></label>
<input type="text" id="node-config-input-tls">
</div>
<div class="form-row">
<label for="node-config-input-subprotocol"><i class="fa fa-tag"></i> <span data-i18n="websocket.label.subprotocol"></span></label>
<input type="text" id="node-config-input-subprotocol">
</div>
<div class="form-row">
<label for="node-config-input-wholemsg" data-i18n="websocket.sendrec"></label>
<select type="text" id="node-config-input-wholemsg" style="width: 70%;">

View File

@@ -46,6 +46,12 @@ module.exports = function(RED) {
// Store local copies of the node configuration (as defined in the .html)
node.path = n.path;
if (typeof n.subprotocol === "string") {
// Split the string on comma and trim each result
node.subprotocol = n.subprotocol.split(",").map(v => v.trim())
} else {
node.subprotocol = [];
}
node.wholemsg = (n.wholemsg === "true");
node._inputNodes = []; // collection of nodes that want to receive events
@@ -92,7 +98,7 @@ module.exports = function(RED) {
tlsNode.addTLSOptions(options);
}
}
var socket = new ws(node.path,options);
var socket = new ws(node.path,node.subprotocol,options);
socket.setMaxListeners(0);
node.server = socket; // keep for closing
handleConnection(socket);

View File

@@ -23,9 +23,17 @@
</select>
<span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width:65px">
</div>
<div class="form-row hidden" id="node-input-host-row" style="padding-left: 110px;">
<div class="form-row hidden" id="node-input-host-row" style="padding-left:110px;">
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" placeholder="localhost" style="width: 60%;">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width:auto; vertical-align:top;">
<label for="node-input-usetls" style="width:auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width:auto; margin-left:20px; margin-right:10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row">
<label><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.output"></span></label>
@@ -42,7 +50,7 @@
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:110px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
</div>
<div class="form-row">
@@ -58,17 +66,18 @@
<script type="text/javascript">
RED.nodes.registerType('tcp in',{
category: 'network',
color:"Silver",
color: "Silver",
defaults: {
name: {value:""},
server: {value:"server",required:true},
host: {value:"",validate:function(v) { return (this.server == "server")||v.length > 0;} },
port: {value:"",required:true,validate:RED.validators.number()},
server: {value:"server", required:true},
host: {value:"", validate:function(v) { return (this.server == "server")||v.length > 0;} },
port: {value:"", required:true, validate:RED.validators.number()},
datamode:{value:"stream"},
datatype:{value:"buffer"},
newline:{value:""},
topic: {value:""},
base64: {/*deprecated*/ value:false,required:true}
base64: {/*deprecated*/ value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
},
inputs:0,
outputs:1,
@@ -77,7 +86,7 @@
return this.name || "tcp:"+(this.host?this.host+":":"")+this.port;
},
labelStyle: function() {
return this.name?"node_label_italic":"";
return this.name ? "node_label_italic" : "";
},
oneditprepare: function() {
var updateOptions = function() {
@@ -103,6 +112,27 @@
$("#node-input-server").change(updateOptions);
$("#node-input-datatype").change(updateOptions);
$("#node-input-datamode").change(updateOptions);
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>
@@ -123,6 +153,15 @@
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" style="width: 60%;">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-usetls" style="width: auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row hidden" id="node-input-end-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-end" style="display: inline-block; width: auto; vertical-align: top;">
@@ -144,14 +183,15 @@
<script type="text/javascript">
RED.nodes.registerType('tcp out',{
category: 'network',
color:"Silver",
color: "Silver",
defaults: {
name: {value:""},
host: {value:"",validate:function(v) { return (this.beserver != "client")||v.length > 0;} },
port: {value:"",validate:function(v) { return (this.beserver == "reply")||RED.validators.number()(v); } },
beserver: {value:"client",required:true},
base64: {value:false,required:true},
end: {value:false,required:true},
name: {value:""}
beserver: {value:"client", required:true},
base64: {value:false, required:true},
end: {value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
},
inputs:1,
outputs:0,
@@ -170,18 +210,42 @@
$("#node-input-port-row").hide();
$("#node-input-host-row").hide();
$("#node-input-end-row").hide();
$("#node-input-tls-enable").hide();
} else if (sockettype == "client"){
$("#node-input-port-row").show();
$("#node-input-host-row").show();
$("#node-input-end-row").show();
$("#node-input-tls-enable").show();
} else {
$("#node-input-port-row").show();
$("#node-input-host-row").hide();
$("#node-input-end-row").show();
$("#node-input-tls-enable").show();
}
};
updateOptions();
$("#node-input-beserver").change(updateOptions);
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>
@@ -194,15 +258,23 @@
<span data-i18n="tcpin.label.port"></span>
<input type="text" id="node-input-port" style="width:60px">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-usetls" style="width: auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row">
<label for="node-input-out"><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.return"></span></label>
<label for="node-input-ret"><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.return"></span></label>
<select type="text" id="node-input-ret" style="width:54%;">
<option value="buffer" data-i18n="tcpin.output.buffer"></option>
<option value="string" data-i18n="tcpin.output.string"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-out"> </label>
<label for="node-input-out"><i class="fa fa-sign-out fa-rotate-90"></i> <span data-i18n="tcpin.label.close"></span></label>
<select type="text" id="node-input-out" style="width:54%;">
<option value="time" data-i18n="tcpin.return.timeout"></option>
<option value="char" data-i18n="tcpin.return.character"></option>
@@ -213,6 +285,9 @@
<input type="text" id="node-input-splitc" style="width:50px;">
<span id="node-units"></span>
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:162px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
</div>
<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">
@@ -222,14 +297,16 @@
<script type="text/javascript">
RED.nodes.registerType('tcp request',{
category: 'network',
color:"Silver",
color: "Silver",
defaults: {
name: {value:""},
server: {value:""},
port: {value:"",validate:RED.validators.regex(/^(\d*|)$/)},
out: {value:"time",required:true},
port: {value:"", validate:RED.validators.regex(/^(\d*|)$/)},
out: {value:"time", required:true},
ret: {value:"buffer"},
splitc: {value:"0",required:true},
name: {value:""}
splitc: {value:"0", required:true},
newline: {value:""},
tls: {type:"tls-config", value:'', required:false}
},
inputs:1,
outputs:1,
@@ -246,6 +323,14 @@
$("#node-input-ret").val("buffer");
this.ret = "buffer";
}
$("#node-input-ret").on("change", function() {
if ($("#node-input-ret").val() === "string" && $("#node-input-out").val() === "sit") { $("#node-row-newline").show(); }
else { $("#node-row-newline").hide(); }
});
$("#node-input-out").on("change", function() {
if ($("#node-input-ret").val() === "string" && $("#node-input-out").val() === "sit") { $("#node-row-newline").show(); }
else { $("#node-row-newline").hide(); }
});
$("#node-input-out").on('focus', function () { previous = this.value; }).on("change", function() {
$("#node-input-splitc").show();
if (previous === null) { previous = $("#node-input-out").val(); }
@@ -272,6 +357,27 @@
$("#node-input-splitc").hide();
}
});
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>

View File

@@ -16,13 +16,46 @@
module.exports = function(RED) {
"use strict";
var reconnectTime = RED.settings.socketReconnectTime||10000;
var socketTimeout = RED.settings.socketTimeout||null;
let reconnectTime = RED.settings.socketReconnectTime || 10000;
let socketTimeout = RED.settings.socketTimeout || null;
const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000;
const Denque = require('denque');
var net = require('net');
const net = require('net');
const tls = require('tls');
var connectionPool = {};
let connectionPool = {};
function normalizeConnectArgs(listArgs) {
const args = net._normalizeArgs(listArgs);
const options = args[0];
const cb = args[1];
// If args[0] was options, then normalize dealt with it.
// If args[0] is port, or args[0], args[1] is host, port, we need to
// find the options and merge them in, normalize's options has only
// the host/port/path args that it knows about, not the tls options.
// This means that options.host overrides a host arg.
if (listArgs[1] !== null && typeof listArgs[1] === 'object') {
ObjectAssign(options, listArgs[1]);
} else if (listArgs[2] !== null && typeof listArgs[2] === 'object') {
ObjectAssign(options, listArgs[2]);
}
return cb ? [options, cb] : [options];
}
function getAllowUnauthorized() {
const allowUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
if (allowUnauthorized) {
process.emitWarning(
'Setting the NODE_TLS_REJECT_UNAUTHORIZED ' +
'environment variable to \'0\' makes TLS connections ' +
'and HTTPS requests insecure by disabling ' +
'certificate verification.');
}
return allowUnauthorized;
}
/**
* Enqueue `item` in `queue`
@@ -53,13 +86,14 @@ module.exports = function(RED) {
this.topic = n.topic;
this.stream = (!n.datamode||n.datamode=='stream'); /* stream,single*/
this.datatype = n.datatype||'buffer'; /* buffer,utf8,base64 */
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r");
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.base64 = n.base64;
this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server");
this.closing = false;
this.connected = false;
var node = this;
var count = 0;
if (n.tls) { var tlsNode = RED.nodes.getNode(n.tls); }
if (!node.server) {
var buffer = null;
@@ -70,13 +104,25 @@ module.exports = function(RED) {
node.log(RED._("tcpin.status.connecting",{host:node.host,port:node.port}));
node.status({fill:"grey",shape:"dot",text:"common.status.connecting"});
var id = RED.util.generateId();
client = net.connect(node.port, node.host, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
client.setKeepAlive(true,120000);
var connOpts = {host: node.host};
if (n.tls) {
var connOpts = tlsNode.addTLSOptions({host: node.host});
client = tls.connect(node.port, connOpts, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("status.connected", {host: node.host, port: node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
}
else {
client = net.connect(node.port, node.host, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
}
client.setKeepAlive(true, 120000);
connectionPool[id] = client;
client.on('data', function (data) {
@@ -89,7 +135,7 @@ module.exports = function(RED) {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i]};
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd()};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@@ -150,7 +196,13 @@ module.exports = function(RED) {
});
}
else {
var server = net.createServer(function (socket) {
let srv = net;
let connOpts;
if (n.tls) {
srv = tls;
connOpts = tlsNode.addTLSOptions({});
}
var server = srv.createServer(connOpts, function (socket) {
socket.setKeepAlive(true,120000);
if (socketTimeout !== null) { socket.setTimeout(socketTimeout); }
var id = RED.util.generateId();
@@ -177,7 +229,7 @@ module.exports = function(RED) {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i], ip:socket.remoteAddress, port:socket.remotePort};
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd(), ip:socket.remoteAddress, port:socket.remotePort};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@@ -269,8 +321,9 @@ module.exports = function(RED) {
this.closing = false;
this.connected = false;
var node = this;
if (n.tls) { var tlsNode = RED.nodes.getNode(n.tls); }
if (!node.beserver||node.beserver=="client") {
if (!node.beserver || node.beserver == "client") {
var reconnectTimeout;
var client = null;
var end = false;
@@ -278,11 +331,24 @@ module.exports = function(RED) {
var setupTcpClient = function() {
node.log(RED._("tcpin.status.connecting",{host:node.host,port:node.port}));
node.status({fill:"grey",shape:"dot",text:"common.status.connecting"});
client = net.connect(node.port, node.host, function() {
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
if (n.tls) {
// connOpts = tlsNode.addTLSOptions(connOpts);
// client = tls.connect(connOpts, function() {
var connOpts = tlsNode.addTLSOptions({host: node.host});
client = tls.connect(node.port, connOpts, function() {
// buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("status.connected", {host: node.host, port: node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
else {
client = net.connect(node.port, node.host, function() {
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
client.setKeepAlive(true,120000);
client.on('error', function (err) {
node.log(RED._("tcpin.errors.error",{error:err.toString()}));
@@ -368,7 +434,13 @@ module.exports = function(RED) {
else {
var connectedSockets = [];
node.status({text:RED._("tcpin.status.connections",{count:0})});
var server = net.createServer(function (socket) {
let srv = net;
let connOpts;
if (n.tls) {
srv = tls;
connOpts = tlsNode.addTLSOptions({});
}
var server = srv.createServer(connOpts, function (socket) {
socket.setKeepAlive(true,120000);
if (socketTimeout !== null) { socket.setTimeout(socketTimeout); }
node.log(RED._("tcpin.status.connection-from",{host:socket.remoteAddress, port:socket.remotePort}));
@@ -445,7 +517,11 @@ module.exports = function(RED) {
this.port = Number(n.port);
this.out = n.out;
this.ret = n.ret || "buffer";
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.splitc = n.splitc;
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
}
if (this.out === "immed") { this.splitc = -1; this.out = "time"; }
if (this.out !== "char") { this.splitc = Number(this.splitc); }
@@ -500,12 +576,48 @@ module.exports = function(RED) {
}
else { buf = Buffer.alloc(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully
clients[connection_id].client = net.Socket();
var connOpts = {host:host, port:port};
if (n.tls) {
connOpts = tlsNode.addTLSOptions(connOpts);
const allowUnauthorized = getAllowUnauthorized();
let options = {
rejectUnauthorized: !allowUnauthorized,
ciphers: tls.DEFAULT_CIPHERS,
checkServerIdentity: tls.checkServerIdentity,
minDHSize: 1024,
...connOpts
};
if (!options.keepAlive) { options.singleUse = true; }
const context = options.secureContext || tls.createSecureContext(options);
clients[connection_id].client = new tls.TLSSocket(options.socket, {
allowHalfOpen: options.allowHalfOpen,
pipe: !!options.path,
secureContext: context,
isServer: false,
requestCert: false, // true,
rejectUnauthorized: false, // options.rejectUnauthorized !== false,
session: options.session,
ALPNProtocols: options.ALPNProtocols,
requestOCSP: options.requestOCSP,
enableTrace: options.enableTrace,
pskCallback: options.pskCallback,
highWaterMark: options.highWaterMark,
onread: options.onread,
signal: options.signal,
});
}
else {
clients[connection_id].client = net.Socket();
}
if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);}
if (host && port) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(port, host, function() {
clients[connection_id].client.connect(connOpts, function() {
//node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
if (clients[connection_id] && clients[connection_id].client) {
@@ -528,17 +640,32 @@ module.exports = function(RED) {
else {
node.warn(RED._("tcpin.errors.no-host"));
}
var chunk = "";
clients[connection_id].client.on('data', function(data) {
if (node.out === "sit") { // if we are staying connected just send the buffer
if (clients[connection_id]) {
const msg = clients[connection_id].lastMsg || {};
msg.payload = RED.util.cloneMessage(data);
if (node.ret === "string") {
try { msg.payload = msg.payload.toString(); }
catch(e) { node.error("Failed to create string", msg); }
try {
if (node.newline && node.newline !== "" ) {
chunk += msg.payload.toString();
let parts = chunk.split(node.newline);
for (var p=0; p<parts.length-1; p+=1) {
let m = RED.util.cloneMessage(msg);
m.payload = parts[p] + node.newline.trimEnd();
nodeSend(m);
}
chunk = parts[parts.length-1];
}
else {
msg.payload = msg.payload.toString();
nodeSend(msg);
}
}
catch(e) { node.error(RED._("tcpin.errors.bad-string"), msg); }
}
nodeSend(msg);
else { nodeSend(msg); }
}
}
// else if (node.splitc === 0) {
@@ -675,7 +802,13 @@ module.exports = function(RED) {
//node.warn(RED._("tcpin.errors.connect-timeout"));
if (clients[connection_id].client) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(port, host, function() {
var connOpts = {host:host, port:port};
if (n.tls) {
connOpts = tlsNode.addTLSOptions(connOpts);
}
clients[connection_id].client.connect(connOpts, function() {
clients[connection_id].connected = true;
clients[connection_id].connecting = false;
node.status({fill:"green",shape:"dot",text:"common.status.connected"});

View File

@@ -71,9 +71,7 @@ module.exports = function(RED) {
node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg);
}
else {
if (RED.settings.verbose) {
node.log(RED._("file.status.deletedfile",{file:filename}));
}
node.debug(RED._("file.status.deletedfile",{file:filename}));
nodeSend(msg);
}
done();

View File

@@ -0,0 +1,156 @@
[
{
"id": "62ea32aa.d73aac",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Example: Link Call Node",
"info": "Link call node can call link in node then get result from link out node.",
"x": 230,
"y": 180,
"wires": []
},
{
"id": "c588bc36.87fec",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓ call link in node",
"info": "",
"x": 440,
"y": 220,
"wires": []
},
{
"id": "cd31efb4d2c6967e",
"type": "link call",
"z": "6312c0588348b2d4",
"name": "",
"links": [
"dbc46892c8d14c37"
],
"timeout": "30",
"x": 420,
"y": 260,
"wires": [
[
"c3db64d1d2260340"
]
]
},
{
"id": "dbc46892c8d14c37",
"type": "link in",
"z": "6312c0588348b2d4",
"name": "",
"links": [],
"x": 315,
"y": 340,
"wires": [
[
"e10575d73f2e5352"
]
]
},
{
"id": "6b61792143b3b0a3",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 240,
"y": 260,
"wires": [
[
"cd31efb4d2c6967e"
]
]
},
{
"id": "e10575d73f2e5352",
"type": "change",
"z": "6312c0588348b2d4",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "Hello, World!",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 450,
"y": 340,
"wires": [
[
"cf8438e7137bc0f0"
]
]
},
{
"id": "cf8438e7137bc0f0",
"type": "link out",
"z": "6312c0588348b2d4",
"name": "",
"mode": "return",
"links": [],
"x": 595,
"y": 340,
"wires": []
},
{
"id": "c3db64d1d2260340",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 600,
"y": 260,
"wires": []
},
{
"id": "6d077dfa0987febb",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑called from link call node",
"info": "",
"x": 410,
"y": 380,
"wires": []
},
{
"id": "53b9a0adfd8c4217",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑return to link call node",
"info": "",
"x": 680,
"y": 380,
"wires": []
}
]

View File

@@ -499,7 +499,8 @@
"label": {
"type": "Typ",
"path": "Pfad",
"url": "URL"
"url": "URL",
"subprotocol": "Subprotokoll"
},
"listenon": "Lauschen (listen on)",
"connectto": "Verbinden mit",

View File

@@ -530,7 +530,8 @@
"label": {
"type": "Type",
"path": "Path",
"url": "URL"
"url": "URL",
"subprotocol": "Subprotocol"
},
"listenon": "Listen on",
"connectto": "Connect to",
@@ -579,7 +580,9 @@
"server": "Server",
"return": "Return",
"ms": "ms",
"chars": "chars"
"chars": "chars",
"close": "Close",
"optional": "(optional)"
},
"type": {
"listen": "Listen on",
@@ -596,7 +599,7 @@
"return": {
"timeout": "after a fixed timeout of",
"character": "when character received is",
"number": "a fixed number of chars",
"number": "after a fixed number of characters",
"never": "never - keep connection open",
"immed": "immediately - don't wait for reply"
},
@@ -616,11 +619,11 @@
"timeout": "timeout closed socket port __port__",
"cannot-listen": "unable to listen on port __port__, error: __error__",
"error": "error: __error__",
"socket-error": "socket error from __host__:__port__",
"no-host": "Host and/or port not set",
"connect-timeout": "connect timeout",
"connect-fail": "connect failed"
"connect-fail": "connect failed",
"bad-string": "failed to convert to string"
}
},
"udp": {

View File

@@ -530,7 +530,8 @@
"label": {
"type": "種類",
"path": "パス",
"url": "URL"
"url": "URL",
"subprotocol": "サブプロトコル"
},
"listenon": "待ち受け",
"connectto": "接続",

View File

@@ -433,7 +433,8 @@
"label": {
"type": "종류",
"path": "패스",
"url": "URL"
"url": "URL",
"subprotocol": "서브 프로토콜"
},
"listenon": "대기",
"connectto": "접속",

View File

@@ -461,7 +461,8 @@
"label": {
"type": "Тип",
"path": "Путь",
"url": "URL"
"url": "URL",
"subprotocol": "Подпротокол"
},
"listenon": "Слушать на ...",
"connectto": "Присоединиться к ...",

View File

@@ -454,7 +454,8 @@
"label": {
"type": "类型",
"path": "路径",
"url": "URL"
"url": "URL",
"subprotocol": "子协议"
},
"listenon": "监听",
"connectto": "连接",

View File

@@ -458,7 +458,8 @@
"label": {
"type": "類型",
"path": "路徑",
"url": "URL"
"url": "URL",
"subprotocol": "子协议"
},
"listenon": "監聽",
"connectto": "連接",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,7 +16,7 @@
}
],
"dependencies": {
"@node-red/util": "2.1.4",
"@node-red/util": "2.2.0-beta.1",
"clone": "2.1.2",
"fs-extra": "10.0.0",
"semver": "7.3.5",

View File

@@ -83,6 +83,7 @@ function createNode(flow,config) {
}
}
try {
Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true })
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
newNode = new nodeTypeConstructor(conf);
} catch (err) {

View File

@@ -59,6 +59,9 @@ function Node(n) {
// which we can tolerate as they are the same object.
Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true })
}
if (n._module) {
Object.defineProperty(this,'_module', {value: n._module, enumerable: false, writable: true })
}
this.updateWires(n.wires);
}
@@ -496,7 +499,12 @@ function log_helper(self, level, msg) {
if (self.name) {
o.name = self.name;
}
self._flow.log(o);
// See https://github.com/node-red/node-red/issues/3327
try {
self._flow.log(o);
} catch(err) {
logUnexpectedError(self, err)
}
}
/**
* Log an INFO level message
@@ -576,4 +584,59 @@ Node.prototype.status = function(status) {
this._flow.handleStatus(this,status);
};
function inspectObject(flow) {
try {
let properties = new Set()
let currentObj = flow
do {
if (!Object.getPrototypeOf(currentObj)) { break }
Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
} while ((currentObj = Object.getPrototypeOf(currentObj)))
let propList = [...properties.keys()].map(item => `${item}[${(typeof flow[item])[0]}]`)
propList.sort();
let result = [];
let line = "";
while (propList.length > 0) {
let prop = propList.shift()
if (line.length+prop.length > 80) {
result.push(line)
line = "";
} else {
line += " "+prop
}
}
if (line.length > 0) {
result.push(line);
}
return result.join("\n ")
} catch(err) {
return "Failed to capture object properties: "+err.toString()
}
}
function logUnexpectedError(node, error) {
let moduleInfo = node._module?`${node._module.module}@${node._module.version}`:"undefined"
Log.error(`
********************************************************************
Unexpected Node Error
${error.stack}
Node:
Type: ${node.type}
Module: ${moduleInfo}
ID: ${node._alias||node.id}
Properties:
${inspectObject(node)}
Flow: ${node._flow?node._flow.path:'undefined'}
Type: ${node._flow?node._flow.TYPE:'undefined'}
Properties:
${node._flow?inspectObject(node._flow):'undefined'}
Please report this issue, including the information logged above:
https://github.com/node-red/node-red/issues/
********************************************************************
`)
}
module.exports = Node;

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,8 +16,8 @@
}
],
"dependencies": {
"@node-red/registry": "2.1.4",
"@node-red/util": "2.1.4",
"@node-red/registry": "2.2.0-beta.1",
"@node-red/util": "2.2.0-beta.1",
"async-mutex": "0.3.2",
"clone": "2.1.2",
"express": "4.17.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/util",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "2.1.4",
"version": "2.2.0-beta.1",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@@ -31,10 +31,10 @@
"flow"
],
"dependencies": {
"@node-red/editor-api": "2.1.4",
"@node-red/runtime": "2.1.4",
"@node-red/util": "2.1.4",
"@node-red/nodes": "2.1.4",
"@node-red/editor-api": "2.2.0-beta.1",
"@node-red/runtime": "2.2.0-beta.1",
"@node-red/util": "2.2.0-beta.1",
"@node-red/nodes": "2.2.0-beta.1",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.17.1",

View File

@@ -366,6 +366,18 @@ describe('websocket Node', function() {
});
});
it('should handle protocol property', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws"), subprotocol: "testprotocol1, testprotocol2" }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property("subprotocol", []);
helper.getNode("n2").should.have.property("subprotocol", ["testprotocol1","testprotocol2"]);
done();
});
});
it('should connect to server', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
@@ -378,6 +390,18 @@ describe('websocket Node', function() {
});
});
it('should initiate with subprotocol', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws"), subprotocol: "testprotocol" }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function (sock) {
sock.should.have.property("protocol", "testprotocol")
done();
});
});
});
it('should close on delete', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },

View File

@@ -84,9 +84,17 @@ describe('TCP Request Node', function() {
n2.on("input", msg => {
try {
if (typeof result === 'object') {
msg.should.have.properties(Object.assign({}, result, {payload: Buffer.from(result.payload)}));
if (flow[0].ret === "string") {
msg.should.have.properties(Object.assign({}, result, {payload: result.payload}));
} else {
msg.should.have.properties(Object.assign({}, result, {payload: Buffer.from(result.payload)}));
}
} else {
msg.should.have.property('payload', Buffer.from(result));
if (flow[0].ret === "string") {
msg.should.have.property('payload', result);
} else {
msg.should.have.property('payload', Buffer.from(result));
}
}
done();
} catch(err) {
@@ -245,10 +253,41 @@ describe('TCP Request Node', function() {
}, done);
});
it('should send & receive, then keep connection, and not split return strings', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", ret:"string", newline:"", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo",
topic: 'boo'
}, {
payload: "bar<A>\nfoo",
topic: 'boo'
}], {
payload: "ACK:foobar<A>\nfoo",
topic: 'boo'
}, done);
});
it('should send & receive, then keep connection, and split return strings', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", ret:"string", newline:"<A>\\n", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo",
topic: 'boo'
}, {
payload: "bar<A>\nfoo",
topic: 'boo'
}], {
payload: "ACK:foobar<A>",
topic: 'boo'
}, done);
});
it('should send & recv data to/from server:port from msg', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
testTCPMany(flow, [
{
payload: "f",
host: "localhost",
port: port