',{class:"red-ui-search-result-description"}).appendTo(div);
- var label = object.label;
- object.index += "|"+label.toLowerCase();
+ var label = nodeItem.label;
+ nodeItem.index += "|"+label.toLowerCase();
$('
',{class:"red-ui-search-result-node-label"}).text(label).appendTo(contentDiv);
+ nodeItem.element = container;
+
div.on("click", function(evt) {
evt.preventDefault();
- confirm(object);
+ confirm(nodeItem);
});
+ div.on('mouseenter', function() {
+ const children = searchResults.children(":visible");
+ if (selected > -1 && selected < children.length) {
+ $(children[selected]).removeClass('selected');
+ }
+ const editableListItem = container.parent()
+ selected = children.index(editableListItem);
+ $(children[selected]).addClass('selected');
+ updateSuggestion(nodeItem);
+ })
},
scrollOnAdd: false
});
}
+
+ function updateSuggestion(nodeItem) {
+ if (suggestCallback) {
+ if (!nodeItem) {
+ suggestCallback(null);
+ } else if (nodeItem.nodes) {
+ // This is a multi-node suggestion
+ suggestCallback({
+ nodes: nodeItem.nodes
+ });
+ } else if (nodeItem.type) {
+ // Single node suggestion
+ suggestCallback({
+ nodes: [{
+ x: 0,
+ y: 0,
+ type: nodeItem.type
+ }]
+ });
+ }
+ }
+ }
function confirm(def) {
hide();
- if (!/^_action_:/.test(def.type)) {
+ if (!def.nodes && !/^_action_:/.test(def.type)) {
typesUsed[def.type] = Date.now();
}
- addCallback(def.type);
+ addCallback(def);
}
function handleMouseActivity(evt) {
@@ -274,6 +338,7 @@ RED.typeSearch = (function() {
addCallback = opts.add;
cancelCallback = opts.cancel;
moveCallback = opts.move;
+ suggestCallback = opts.suggest;
RED.events.emit("type-search:open");
//shade.show();
if ($("#red-ui-main-container").height() - opts.y - 195 < 0) {
@@ -294,6 +359,9 @@ RED.typeSearch = (function() {
},200);
}
function hide(fast) {
+ if (suggestCallback) {
+ suggestCallback(null);
+ }
if (visible) {
visible = false;
if (dialog !== null) {
@@ -356,11 +424,11 @@ RED.typeSearch = (function() {
(!filter.output || def.outputs > 0)
}
function refreshTypeList(opts) {
- var i;
+ let i;
searchResults.editableList('empty');
searchInput.searchBox('value','').focus();
selected = -1;
- var common = [
+ const common = [
'inject','debug','function','change','switch','junction'
].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); });
@@ -371,7 +439,7 @@ RED.typeSearch = (function() {
// common.push('_action_:core:split-wire-with-link-nodes')
// }
- var recentlyUsed = Object.keys(typesUsed);
+ let recentlyUsed = Object.keys(typesUsed);
recentlyUsed.sort(function(a,b) {
return typesUsed[b]-typesUsed[a];
});
@@ -379,9 +447,10 @@ RED.typeSearch = (function() {
return applyFilter(opts.filter,t,RED.nodes.getType(t)) && common.indexOf(t) === -1;
});
- var items = [];
+ const items = [];
+
RED.nodes.registry.getNodeTypes().forEach(function(t) {
- var def = RED.nodes.getType(t);
+ const def = RED.nodes.getType(t);
if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
items.push({type:t,def: def, label:getTypeLabel(t,def)});
}
@@ -389,18 +458,46 @@ RED.typeSearch = (function() {
items.push({ type: 'junction', def: { inputs:1, outputs: 1, label: 'junction', type: 'junction'}, label: 'junction' })
items.sort(sortTypeLabels);
- var commonCount = 0;
- var item;
- var index = 0;
+ let index = 0;
+
+ // const suggestionItem = {
+ // suggestionPlaceholder: true,
+ // label: 'loading suggestions...',
+ // separator: true,
+ // i: index++
+ // }
+ // searchResults.editableList('addItem', suggestionItem);
+ // setTimeout(function() {
+ // searchResults.editableList('removeItem', suggestionItem);
+
+ // const suggestedItem = {
+ // suggestion: true,
+ // label: 'Change/Debug Combo',
+ // separator: true,
+ // i: suggestionItem.i,
+ // nodes: [
+ // { id: 'suggestion-1', type: 'change', x: 0, y: 0, wires:[['suggestion-2']] },
+ // { id: 'suggestion-2', type: 'function', outputs: 3, x: 200, y: 0, wires:[['suggestion-3'],['suggestion-4'],['suggestion-6']] },
+ // { id: 'suggestion-3', _g: 'suggestion-group-1', type: 'debug', x: 375, y: -40 },
+ // { id: 'suggestion-4', _g: 'suggestion-group-1', type: 'debug', x: 375, y: 0 },
+ // { id: 'suggestion-5', _g: 'suggestion-group-1', type: 'debug', x: 410, y: 40 },
+ // { id: 'suggestion-6', type: 'junction', wires: [['suggestion-5']], x:325, y:40 }
+ // ]
+ // }
+ // searchResults.editableList('addItem', suggestedItem);
+ // }, 1000)
+
for(i=0;i
').appendTo(pane);
var input;
if (opt.toggle) {
- input = $('
'+RED._(opt.label)+'').appendTo(row).find("input");
+ let label = RED._(opt.label)
+ if (opt.description) {
+ label = `
${label}
${RED._(opt.description)}`;
+ }
+ input = $('
').appendTo(row)
+ $('
'+label+' ').appendTo(row)
input.prop('checked',initialState);
} else if (opt.options) {
$('
'+RED._(opt.label)+' ').appendTo(row);
@@ -210,6 +229,8 @@ RED.userSettings = (function() {
var opt = allSettings[id];
if (opt.local) {
localStorage.setItem(opt.setting,value);
+ } else if (opt.global) {
+ RED.settings.set(opt.setting, value)
} else {
var currentEditorSettings = RED.settings.get('editor') || {};
currentEditorSettings.view = currentEditorSettings.view || {};
@@ -238,7 +259,7 @@ RED.userSettings = (function() {
addPane({
id:'view',
- title: RED._("menu.label.view.view"),
+ title: RED._("menu.label.settings"),
get: createViewPane,
close: function() {
viewSettings.forEach(function(section) {
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
index f4cda8d2c..c09be65ab 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js
@@ -24,7 +24,7 @@ RED.view.annotations = (function() {
refreshAnnotation = !!evt.node[opts.refresh]
delete evt.node[opts.refresh]
} else if (typeof opts.refresh === "function") {
- refreshAnnotation = opts.refresh(evnt.node)
+ refreshAnnotation = opts.refresh(evt.node)
}
if (refreshAnnotation) {
refreshAnnotationElement(annotation.id, annotation.node, annotation.element)
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js
index eecd309d1..f5e0df05f 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js
@@ -176,8 +176,8 @@ RED.view.tools = (function() {
}
nodes.forEach(function(n) {
var modified = false;
- var oldValue = n.l === undefined?true:n.l;
- var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true;
+ var showLabel = n._def.hasOwnProperty("showLabel") ? n._def.showLabel : true;
+ var oldValue = n.l === undefined ? showLabel : n.l;
if (labelShown) {
if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) {
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
index e7820f83a..08ab0ec0a 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js
@@ -100,6 +100,11 @@ RED.view = (function() {
var clipboard = "";
let clipboardSource
+ let currentSuggestion = null;
+ let suggestedNodes = [];
+ let suggestedLinks = [];
+ let suggestedJunctions = [];
+
// Note: these are the permitted status colour aliases. The actual RGB values
// are set in the CSS - flow.scss/colors.scss
const status_colours = {
@@ -548,6 +553,8 @@ RED.view = (function() {
}
}
+ clearSuggestedFlow();
+
RED.menu.setDisabled("menu-item-workspace-edit", activeFlowLocked || activeSubflow || event.workspace === 0);
RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow);
@@ -653,7 +660,7 @@ RED.view = (function() {
return;
}
var historyEvent = result.historyEvent;
- var nn = RED.nodes.add(result.node);
+ var nn = RED.nodes.add(result.node, { source: 'palette' });
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")) {
@@ -1395,6 +1402,20 @@ RED.view = (function() {
var lastAddedX;
var lastAddedWidth;
+ const context = {}
+
+ if (quickAddLink) {
+ context.source = quickAddLink.node.id;
+ context.sourcePort = quickAddLink.port;
+ context.sourcePortType = quickAddLink.portType;
+ if (quickAddLink?.virtualLink) {
+ context.virtualLink = true;
+ }
+ context.flow = RED.nodes.createExportableNodeSet(RED.nodes.getAllFlowNodes(quickAddLink.node))
+ }
+
+ // console.log(context)
+
RED.typeSearch.show({
x:clientX-mainPos.left-node_width/2 - (ox-point[0]),
y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]),
@@ -1430,7 +1451,63 @@ RED.view = (function() {
keepAdding = false;
resetMouseVars();
}
+ if (typeof type !== 'string') {
+ if (type.nodes) {
+ // Importing a flow definition
+ // console.log('Importing flow definition', type.nodes)
+ const importResult = importNodes(type.nodes, {
+ generateIds: true,
+ touchImport: true,
+ notify: false,
+ // Ensure the node gets all of its defaults applied
+ applyNodeDefaults: true
+ })
+ quickAddActive = false;
+ ghostNode.remove();
+
+ if (quickAddLink) {
+ // Need to attach the link to the suggestion. This is assumed to be the first
+ // node in the array - as that's the one we've focussed on.
+ const targetNode = importResult.nodeMap[type.nodes[0].id]
+
+ const drag_line = quickAddLink;
+ let src = null, dst, src_port;
+ if (drag_line.portType === PORT_TYPE_OUTPUT && (targetNode.inputs > 0 || drag_line.virtualLink) ) {
+ src = drag_line.node;
+ src_port = drag_line.port;
+ dst = targetNode;
+ } else if (drag_line.portType === PORT_TYPE_INPUT && (targetNode.outputs > 0 || drag_line.virtualLink)) {
+ src = targetNode;
+ dst = drag_line.node;
+ src_port = 0;
+ }
+ if (src && dst) {
+ var link = {source: src, sourcePort:src_port, target: dst};
+ RED.nodes.addLink(link);
+ const historyEvent = RED.history.peek()
+ if (historyEvent.t === 'add') {
+ historyEvent.links = historyEvent.links || []
+ historyEvent.links.push(link)
+ } else {
+ // TODO: importNodes *can* generate a multi history event
+ // but we don't currently support that
+ }
+ }
+ if (quickAddLink.el) {
+ quickAddLink.el.remove();
+ }
+ quickAddLink = null;
+ }
+ updateActiveNodes();
+ updateSelection();
+ redraw();
+
+ return
+ } else {
+ type = type.type
+ }
+ }
var nn;
var historyEvent;
if (/^_action_:/.test(type)) {
@@ -1479,7 +1556,7 @@ RED.view = (function() {
if (nn.type === 'junction') {
nn = RED.nodes.addJunction(nn);
} else {
- nn = RED.nodes.add(nn);
+ nn = RED.nodes.add(nn, { source: 'typeSearch' });
}
if (quickAddLink) {
var drag_line = quickAddLink;
@@ -1662,6 +1739,22 @@ RED.view = (function() {
quickAddActive = false;
ghostNode.remove();
}
+ },
+ suggest: function (suggestion) {
+ if (suggestion?.nodes?.length > 0) {
+ // Reposition the suggestion relative to the existing ghost node position
+ const deltaX = suggestion.nodes[0].x - point[0]
+ const deltaY = suggestion.nodes[0].y - point[1]
+ suggestion.nodes.forEach(node => {
+ if (Object.hasOwn(node, 'x')) {
+ node.x = node.x - deltaX
+ }
+ if (Object.hasOwn(node, 'y')) {
+ node.y = node.y - deltaY
+ }
+ })
+ }
+ setSuggestedFlow(suggestion);
}
});
@@ -4576,20 +4669,28 @@ RED.view = (function() {
nodeLayer.selectAll(".red-ui-flow-subflow-port-input").remove();
nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove();
}
-
- var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(activeNodes,function(d){return d.id});
+ let nodesToDraw = activeNodes;
+ if (suggestedNodes.length > 0) {
+ nodesToDraw = [...activeNodes, ...suggestedNodes]
+ }
+ var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(nodesToDraw,function(d){return d.id});
node.exit().each(function(d,i) {
- RED.hooks.trigger("viewRemoveNode",{node:d,el:this})
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewRemoveNode",{node:d,el:this})
+ }
}).remove();
var nodeEnter = node.enter().insert("svg:g")
.attr("class", "red-ui-flow-node red-ui-flow-node-group")
- .classed("red-ui-flow-subflow", activeSubflow != null);
+ .classed("red-ui-flow-subflow", activeSubflow != null)
nodeEnter.each(function(d,i) {
this.__outputs__ = [];
this.__inputs__ = [];
var node = d3.select(this);
+ if (d.__ghost) {
+ node.classed("red-ui-flow-node-ghost",true);
+ }
var nodeContents = document.createDocumentFragment();
var isLink = (d.type === "link in" || d.type === "link out")
var hideLabel = d.hasOwnProperty('l')?!d.l : isLink;
@@ -4624,19 +4725,21 @@ RED.view = (function() {
bgButton.setAttribute("width",16);
bgButton.setAttribute("height",node_height-12);
bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
- d3.select(bgButton)
- .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
- .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
- .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
- .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
- var op = 1;
- if (d._def.button.toggle) {
- op = d[d._def.button.toggle]?1:0.2;
- }
- d3.select(this).attr("fill-opacity",op);
- }})
- .on("click",nodeButtonClicked)
- .on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
+ if (!d.__ghost) {
+ d3.select(bgButton)
+ .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
+ .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
+ .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
+ .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
+ var op = 1;
+ if (d._def.button.toggle) {
+ op = d[d._def.button.toggle]?1:0.2;
+ }
+ d3.select(this).attr("fill-opacity",op);
+ }})
+ .on("click",nodeButtonClicked)
+ .on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
+ }
buttonGroup.appendChild(bgButton);
node[0][0].__buttonGroupButton__ = bgButton;
@@ -4651,13 +4754,15 @@ RED.view = (function() {
mainRect.setAttribute("ry", 5);
mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
node[0][0].__mainRect__ = mainRect;
- d3.select(mainRect)
- .on("mouseup",nodeMouseUp)
- .on("mousedown",nodeMouseDown)
- .on("touchstart",nodeTouchStart)
- .on("touchend",nodeTouchEnd)
- .on("mouseover",nodeMouseOver)
- .on("mouseout",nodeMouseOut);
+ if (!d.__ghost) {
+ d3.select(mainRect)
+ .on("mouseup",nodeMouseUp)
+ .on("mousedown",nodeMouseDown)
+ .on("touchstart",nodeTouchStart)
+ .on("touchend",nodeTouchEnd)
+ .on("mouseover",nodeMouseOver)
+ .on("mouseout",nodeMouseOut);
+ }
nodeContents.appendChild(mainRect);
//node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none");
//node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none");
@@ -4739,7 +4844,10 @@ RED.view = (function() {
node[0][0].appendChild(nodeContents);
- RED.hooks.trigger("viewAddNode",{node:d,el:this})
+ if (!d.__ghost) {
+ // Do not trigger hooks for ghost nodes
+ RED.hooks.trigger("viewAddNode",{node:d,el:this})
+ }
});
var nodesReordered = false;
@@ -4862,13 +4970,15 @@ RED.view = (function() {
var inputPorts = thisNode.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",{
- node:d,
- el:self,
- port:d3.select(this)[0][0],
- portType: "input",
- portIndex: 0
- })
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewRemovePort",{
+ node:d,
+ el:self,
+ port:d3.select(this)[0][0],
+ portType: "input",
+ portIndex: 0
+ })
+ }
}).remove();
} 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");
@@ -4886,13 +4996,15 @@ RED.view = (function() {
inputGroupPorts[0][0].__data__ = this.__data__;
inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT;
inputGroupPorts[0][0].__portIndex__ = 0;
- inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
- .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
- .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
- .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);});
- RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
+ if (!d.__ghost) {
+ inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
+ .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
+ .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
+ .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);});
+ RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
+ }
}
var numOutputs = d.outputs;
if (isLink && d.type === "link out") {
@@ -4907,13 +5019,15 @@ RED.view = (function() {
// Remove extra ports
while (this.__outputs__.length > numOutputs) {
var port = this.__outputs__.pop();
- RED.hooks.trigger("viewRemovePort",{
- node:d,
- el:this,
- port:port,
- portType: "output",
- portIndex: this.__outputs__.length
- })
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewRemovePort",{
+ node:d,
+ el:this,
+ port:port,
+ portType: "output",
+ portIndex: this.__outputs__.length
+ })
+ }
port.remove();
}
for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) {
@@ -4941,16 +5055,20 @@ RED.view = (function() {
portPort.__data__ = this.__data__;
portPort.__portType__ = PORT_TYPE_OUTPUT;
portPort.__portIndex__ = portIndex;
- portPort.addEventListener("mousedown", portMouseDownProxy);
- portPort.addEventListener("touchstart", portTouchStartProxy);
- portPort.addEventListener("mouseup", portMouseUpProxy);
- portPort.addEventListener("touchend", portTouchEndProxy);
- portPort.addEventListener("mouseover", portMouseOverProxy);
- portPort.addEventListener("mouseout", portMouseOutProxy);
+ if (!d.__ghost) {
+ portPort.addEventListener("mousedown", portMouseDownProxy);
+ portPort.addEventListener("touchstart", portTouchStartProxy);
+ portPort.addEventListener("mouseup", portMouseUpProxy);
+ portPort.addEventListener("touchend", portTouchEndProxy);
+ portPort.addEventListener("mouseover", portMouseOverProxy);
+ portPort.addEventListener("mouseout", portMouseOutProxy);
+ }
this.appendChild(portGroup);
this.__outputs__.push(portGroup);
- RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
+ }
} else {
portGroup = this.__outputs__[portIndex];
}
@@ -5067,8 +5185,10 @@ RED.view = (function() {
}
}
}
-
- RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
+ if (!d.__ghost) {
+ // Only trigger redraw hooks for non-ghost nodes
+ RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
+ }
});
if (nodesReordered) {
@@ -5077,13 +5197,20 @@ RED.view = (function() {
})
}
+ let junctionsToDraw = activeJunctions;
+ if (suggestedJunctions.length > 0) {
+ junctionsToDraw = [...activeJunctions, ...suggestedJunctions]
+ }
var junction = junctionLayer.selectAll(".red-ui-flow-junction").data(
- activeJunctions,
+ junctionsToDraw,
d => d.id
)
var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction")
junctionEnter.each(function(d,i) {
var junction = d3.select(this);
+ if (d.__ghost) {
+ junction.classed("red-ui-flow-junction-ghost",true);
+ }
var contents = document.createDocumentFragment();
// d.added = true;
var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect");
@@ -5177,8 +5304,12 @@ RED.view = (function() {
})
+ let linksToDraw = activeLinks
+ if (suggestedLinks.length > 0) {
+ linksToDraw = [...activeLinks, ...suggestedLinks]
+ }
var link = linkLayer.selectAll(".red-ui-flow-link").data(
- activeLinks,
+ linksToDraw,
function(d) {
return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i;
}
@@ -5189,44 +5320,50 @@ RED.view = (function() {
var l = d3.select(this);
var pathContents = document.createDocumentFragment();
+ if (d.__ghost) {
+ l.classed("red-ui-flow-link-ghost",true);
+ }
+
d.added = true;
var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path");
pathBack.__data__ = d;
pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":""));
this.__pathBack__ = pathBack;
pathContents.appendChild(pathBack);
- d3.select(pathBack)
- .on("mousedown",linkMouseDown)
- .on("touchstart",linkTouchStart)
- .on("mousemove", function(d) {
- if (mouse_mode === RED.state.SLICING) {
+ if (!d.__ghost) {
+ 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()
- } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) {
- if (!l.classed("red-ui-flow-link-splice")) {
- // Find intersection point
- var lineLength = pathLine.getTotalLength();
- var pos;
- var delta = Infinity;
- for (var i = 0; i < lineLength; i++) {
- var linePos = pathLine.getPointAtLength(i);
- var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
- var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
- var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
- if (posDelta < delta) {
- pos = linePos
- delta = posDelta
- }
- }
- d._sliceLocation = pos
selectedLinks.add(d)
l.classed("red-ui-flow-link-splice",true)
redraw()
+ } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) {
+ if (!l.classed("red-ui-flow-link-splice")) {
+ // Find intersection point
+ var lineLength = pathLine.getTotalLength();
+ var pos;
+ var delta = Infinity;
+ for (var i = 0; i < lineLength; i++) {
+ var linePos = pathLine.getPointAtLength(i);
+ var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
+ var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
+ var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
+ if (posDelta < delta) {
+ pos = linePos
+ delta = posDelta
+ }
+ }
+ d._sliceLocation = pos
+ 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;
@@ -5688,16 +5825,21 @@ RED.view = (function() {
* - generateIds - whether to automatically generate new ids for all imported nodes
* - generateDefaultNames - whether to automatically update any nodes with clashing
* default names
+ * - notify - whether to show a notification if the import was successful
*/
function importNodes(newNodesObj,options) {
options = options || {
addFlow: false,
touchImport: false,
generateIds: false,
- generateDefaultNames: false
+ generateDefaultNames: false,
+ notify: true,
+ applyNodeDefaults: false
}
- var addNewFlow = options.addFlow
- var touchImport = options.touchImport;
+ const addNewFlow = options.addFlow
+ const touchImport = options.touchImport;
+ const showNotification = options.notify ?? true
+ const applyNodeDefaults = options.applyNodeDefaults ?? false
if (mouse_mode === RED.state.SELECTING_NODE) {
return;
@@ -5781,7 +5923,8 @@ RED.view = (function() {
addFlow: addNewFlow,
importMap: options.importMap,
markChanged: true,
- modules: modules
+ modules: modules,
+ applyNodeDefaults: applyNodeDefaults
});
if (importResult) {
var new_nodes = importResult.nodes;
@@ -5792,6 +5935,7 @@ RED.view = (function() {
var new_subflows = importResult.subflows;
var removedNodes = importResult.removedNodes;
var new_default_workspace = importResult.missingWorkspace;
+ const nodeMap = importResult.nodeMap;
if (addNewFlow && new_default_workspace) {
RED.workspaces.show(new_default_workspace.id);
}
@@ -5813,16 +5957,18 @@ RED.view = (function() {
var dx = mouse_position[0];
var dy = mouse_position[1];
- if (movingSet.length() > 0) {
- var root_node = movingSet.get(0).n;
- dx = root_node.x;
- dy = root_node.y;
+ if (!touchImport) {
+ if (movingSet.length() > 0) {
+ const root_node = movingSet.get(0).n;
+ dx = root_node.x;
+ dy = root_node.y;
+ }
}
var minX = 0;
var minY = 0;
var i;
- var node,group;
+ var node;
var l =movingSet.length();
for (i=0;i
0) {
+ counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
+ }
+ if (newNodeCount > 0) {
+ counts.push(RED._("clipboard.node",{count:newNodeCount}));
+ }
+ if (newGroupCount > 0) {
+ counts.push(RED._("clipboard.group",{count:newGroupCount}));
+ }
+ if (newConfigNodeCount > 0) {
+ counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
+ }
+ if (new_subflows.length > 0) {
+ counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
+ }
+ if (removedNodes && removedNodes.length > 0) {
+ counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
+ }
+ if (counts.length > 0) {
+ var countList = "";
+ RED.notify(""+RED._("clipboard.nodesImported")+"
"+countList,{id:"clipboard"});
}
- })
- var newGroupCount = new_groups.length;
- var newJunctionCount = new_junctions.length;
- if (new_workspaces.length > 0) {
- counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
}
- if (newNodeCount > 0) {
- counts.push(RED._("clipboard.node",{count:newNodeCount}));
+ return {
+ nodeMap
}
- if (newGroupCount > 0) {
- counts.push(RED._("clipboard.group",{count:newGroupCount}));
- }
- if (newConfigNodeCount > 0) {
- counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
- }
- if (new_subflows.length > 0) {
- counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
- }
- if (removedNodes && removedNodes.length > 0) {
- counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
- }
- if (counts.length > 0) {
- var countList = "";
- RED.notify(""+RED._("clipboard.nodesImported")+"
"+countList,{id:"clipboard"});
- }
-
+ }
+ return {
+ nodeMap: {}
}
} catch(error) {
if (error.code === "import_conflict") {
@@ -6307,6 +6459,157 @@ RED.view = (function() {
node.highlighted = true;
RED.view.redraw();
}
+
+ /**
+ * Add a suggested flow to the workspace.
+ *
+ * This appears as a ghost set of nodes.
+ *
+ * {
+ * "nodes": [
+ * {
+ * type: "node-type",
+ * x: 0,
+ * y: 0,
+ * }
+ * ]
+ * }
+ * If `nodes` is a single node without an id property, it will be generated
+ * using its default properties.
+ *
+ * If `nodes` has multiple, they must all have ids and will be assumed to be 'importable'.
+ * In other words, a piece of valid flow json.
+ *
+ * Limitations:
+ * - does not support groups, subflows or whole tabs
+ * - does not support config nodes
+ *
+ * To clear the current suggestion, pass in `null`.
+ *
+ *
+ * @param {Object} suggestion - The suggestion object
+ */
+ function setSuggestedFlow (suggestion) {
+ if (!currentSuggestion && !suggestion) {
+ // Avoid unnecessary redraws
+ return
+ }
+ // Clear up any existing suggestion state
+ clearSuggestedFlow()
+ currentSuggestion = suggestion
+ if (suggestion?.nodes?.length > 0) {
+ const nodeMap = {}
+ const links = []
+ suggestion.nodes.forEach(nodeConfig => {
+ if (!nodeConfig.type || nodeConfig.type === 'group' || nodeConfig.type === 'subflow' || nodeConfig.type === 'tab') {
+ // A node type we don't support previewing
+ return
+ }
+
+ let node
+
+ if (nodeConfig.type === 'junction') {
+ node = {
+ _def: {defaults:{}},
+ type: 'junction',
+ z: RED.workspaces.active(),
+ id: RED.nodes.id(),
+ x: nodeConfig.x,
+ y: nodeConfig.y,
+ w: 0, h: 0,
+ outputs: 1,
+ inputs: 1,
+ dirty: true,
+ moved: true
+ }
+ } else {
+ const def = RED.nodes.getType(nodeConfig.type)
+ if (!def || def.category === 'config') {
+ // Unknown node or config node
+ // TODO: unknown node types could happen...
+ return
+ }
+ const result = createNode(nodeConfig.type, nodeConfig.x, nodeConfig.y)
+ if (!result) {
+ return
+ }
+ node = result.node
+ node["_"] = node._def._;
+
+ for (let d in node._def.defaults) {
+ if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'name') {
+ if (nodeConfig[d] !== undefined) {
+ node[d] = nodeConfig[d]
+ } else if (node._def.defaults[d].value) {
+ node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value))
+ }
+ }
+ }
+ suggestedNodes.push(node)
+ }
+ if (node) {
+ node.id = nodeConfig.id || node.id
+ node.__ghost = true;
+ node.dirty = true;
+ nodeMap[node.id] = node
+
+ if (nodeConfig.wires) {
+ nodeConfig.wires.forEach((wire, i) => {
+ if (wire.length > 0) {
+ wire.forEach(targetId => {
+ links.push({
+ sourceId: nodeConfig.id || node.id,
+ sourcePort: i,
+ targetId: targetId,
+ targetPort: 0,
+ __ghost: true
+ })
+ })
+ }
+ })
+ }
+ }
+ })
+ links.forEach(link => {
+ const sourceNode = nodeMap[link.sourceId]
+ const targetNode = nodeMap[link.targetId]
+ if (sourceNode && targetNode) {
+ link.source = sourceNode
+ link.target = targetNode
+ suggestedLinks.push(link)
+ }
+ })
+ }
+ if (ghostNode) {
+ if (suggestedNodes.length > 0) {
+ ghostNode.style('opacity', 0)
+ } else {
+ ghostNode.style('opacity', 1)
+ }
+ }
+ redraw();
+ }
+
+ function clearSuggestedFlow () {
+ currentSuggestion = null
+ suggestedNodes = []
+ suggestedLinks = []
+ }
+
+ function applySuggestedFlow () {
+ if (currentSuggestion && currentSuggestion.nodes) {
+ const nodesToImport = currentSuggestion.nodes
+ setSuggestedFlow(null)
+ return importNodes(nodesToImport, {
+ generateIds: true,
+ touchImport: true,
+ notify: false,
+ // Ensure the node gets all of its defaults applied
+ applyNodeDefaults: true
+ })
+ }
+ }
+
return {
init: init,
state:function(state) {
@@ -6567,6 +6870,8 @@ RED.view = (function() {
width: space_width,
height: space_height
};
- }
+ },
+ setSuggestedFlow,
+ applySuggestedFlow
};
})();
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/base.scss b/packages/node_modules/@node-red/editor-client/src/sass/base.scss
index 63ab6b77f..afbafe049 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/base.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/base.scss
@@ -208,12 +208,10 @@ body {
}
img {
- width: auto\9;
height: auto;
max-width: 100%;
vertical-align: middle;
border: 0;
- -ms-interpolation-mode: bicubic;
}
blockquote {
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss
index ad055b97c..944536845 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss
@@ -161,7 +161,15 @@ svg:not(.red-ui-workspace-lasso-active) {
fill: var(--red-ui-group-default-label-color);
}
+.red-ui-flow-node-ghost {
+ opacity: 0.6;
+ rect.red-ui-flow-node {
+ stroke: var(--red-ui-node-border-placeholder);
+ stroke-dasharray:10,4;
+ stroke-width: 2;
+ }
+}
.red-ui-flow-node-unknown {
stroke-dasharray:10,4;
@@ -401,6 +409,13 @@ g.red-ui-flow-node-selected {
g.red-ui-flow-link-selected path.red-ui-flow-link-line {
stroke: var(--red-ui-node-selected-color);
}
+
+g.red-ui-flow-link-ghost path.red-ui-flow-link-line {
+ stroke: var(--red-ui-node-border-placeholder);
+ stroke-width: 2;
+ stroke-dasharray: 10, 4;
+}
+
g.red-ui-flow-link-unknown path.red-ui-flow-link-line {
stroke: var(--red-ui-link-unknown-color);
stroke-width: 2;
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/forms.scss b/packages/node_modules/@node-red/editor-client/src/sass/forms.scss
index a281b9265..3fa8bcc65 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/forms.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/forms.scss
@@ -216,14 +216,11 @@
.uneditable-input:focus {
border-color: var(--red-ui-form-input-focus-color);
outline: 0;
- outline: thin dotted \9;
}
input[type="radio"],
input[type="checkbox"] {
margin: 4px 0 0;
- margin-top: 1px \9;
- *margin-top: 0;
line-height: normal;
}
@@ -285,12 +282,6 @@
color: var(--red-ui-form-placeholder-color);
}
- input:-ms-input-placeholder,
- div[contenteditable="true"]:-ms-input-placeholder,
- textarea:-ms-input-placeholder {
- color: var(--red-ui-form-placeholder-color);
- }
-
input::-webkit-input-placeholder,
div[contenteditable="true"]::-webkit-input-placeholder,
textarea::-webkit-input-placeholder {
@@ -568,11 +559,7 @@
input.search-query {
padding-right: 14px;
- padding-right: 4px \9;
padding-left: 14px;
- padding-left: 4px \9;
- /* IE7-8 doesn't have border-radius, so don't indent the padding */
-
margin-bottom: 0;
border-radius: 15px;
}
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss
index 6262597a1..486396c59 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss
@@ -18,7 +18,6 @@
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
- -ms-user-select: none;
user-select: none;
}
@@ -26,7 +25,6 @@
-webkit-user-select: auto;
-khtml-user-select: auto;
-moz-user-select: auto;
- -ms-user-select: auto;
user-select: auto;
}
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss
index cdbfa406b..8955887dd 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss
@@ -126,15 +126,20 @@
margin-left: 5px;
}
+ .red-ui-palette-module-deprecated {
+ cursor: pointer;
+ color: var(--red-ui-text-color-error);
+ font-size: 0.7em;
+ border: 1px solid var(--red-ui-text-color-error);
+ border-radius: 30px;
+ padding: 2px 5px;
+ }
+
.red-ui-palette-module-description {
margin-left: 20px;
font-size: 0.9em;
color: var(--red-ui-secondary-text-color);
}
- .red-ui-palette-module-link {
- }
- .red-ui-palette-module-set-button-group {
- }
.red-ui-palette-module-content {
display: none;
padding: 10px 3px;
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss
index 3df2b495b..027e783a3 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss
@@ -205,3 +205,39 @@
background: var(--red-ui-secondary-background);
z-index: 2000;
}
+
+
+.red-ui-popover.red-ui-dialog {
+ z-index: 2003;
+ --red-ui-popover-background: var(--red-ui-secondary-background);
+ --red-ui-popover-border: var(--red-ui-tourGuide-border);
+ --red-ui-popover-color: var(--red-ui-primary-text-color);
+
+ .red-ui-popover-content {
+ h2 {
+ text-align: center;
+ margin-top: 0px;
+ line-height: 1.2em;
+ color: var(--red-ui-tourGuide-heading-color);
+ i.fa {
+ font-size: 1.5em
+ }
+ }
+ }
+
+}
+
+.red-ui-dialog-toolbar {
+ min-height: 36px;
+ position: relative;
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+.red-ui-dialog-body {
+ padding: 20px 40px 10px;
+ a {
+ color: var(--red-ui-text-color-link) !important;
+ text-decoration: none;
+ }
+}
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss b/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss
index 5e0c7fa47..7abae094c 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss
@@ -70,8 +70,14 @@
overflow-y: auto;
}
.red-ui-settings-row {
+ display: flex;
+ gap: 10px;
+ align-items:flex-start;
padding: 5px 10px 2px;
}
+.red-ui-settings-row input[type="checkbox"] {
+ margin-top: 8px;
+}
.red-ui-settings-section {
position: relative;
&:after {
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-auto-complete.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-auto-complete.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-background-deploy.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-background-deploy.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-background-deploy.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-background-deploy.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-config-select.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-config-select.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-diff-update.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-diff-update.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-diff-update.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-diff-update.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer-location.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer-location.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer-location.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer-location.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-plugins.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-plugins.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-sf-config.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-sf-config.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-timestamp-formatting.png
similarity index 100%
rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png
rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-timestamp-formatting.png
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js
new file mode 100644
index 000000000..a55763189
--- /dev/null
+++ b/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js
@@ -0,0 +1,231 @@
+export default {
+ version: "4.0.0",
+ steps: [
+ {
+ titleIcon: "fa fa-map-o",
+ title: {
+ "en-US": "Welcome to Node-RED 4.0!",
+ "ja": "Node-RED 4.0 へようこそ!",
+ "fr": "Bienvenue dans Node-RED 4.0!"
+ },
+ description: {
+ "en-US": "Let's take a moment to discover the new features in this release.
",
+ "ja": "本リリースの新機能を見つけてみましょう。
",
+ "fr": "Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.
"
+ }
+ },
+ {
+ title: {
+ "en-US": "Multiplayer Mode",
+ "ja": "複数ユーザ同時利用モード",
+ "fr": "Mode Multi-utilisateur"
+ },
+ image: '4.0/images/nr4-multiplayer-location.png',
+ description: {
+ "en-US": `This release includes the first small steps towards making Node-RED easier
+ to work with when you have multiple people editing flows at the same time.
+ When this feature is enabled, you will now see who else has the editor open and some
+ basic information on where they are in the editor.
+ Check the release post for details on how to enable this feature in your settings file.
`,
+ "ja": `本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。
+ 本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。
+ 設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。
`,
+ "fr": `Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser
+ lorsque plusieurs personnes modifient des flux en même temps.
+ Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont
+ ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.
+ Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité
+ dans votre fichier de paramètres.
`
+ }
+ },
+ {
+ title: {
+ "en-US": "Better background deploy handling",
+ "ja": "バックグラウンドのデプロイ処理の改善",
+ "fr": "Meilleure gestion du déploiement en arrière-plan"
+ },
+ image: '4.0/images/nr4-background-deploy.png',
+ description: {
+ "en-US": `If another user deploys changes whilst you are editing, we now use a more discrete notification
+ that doesn't stop you continuing your work - especially if they are being very productive and deploying lots
+ of changes.
`,
+ "ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`,
+ "fr": `Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez
+ une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.
`
+ }
+ },
+ {
+ title: {
+ "en-US": "Improved flow diffs",
+ "ja": "フローの差分表示の改善",
+ "fr": "Amélioration des différences de flux"
+ },
+ image: '4.0/images/nr4-diff-update.png',
+ description: {
+ "en-US": `When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration
+ changes and those that have only been moved.
+
When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.
`,
+ "ja": `フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。
+
これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。
`,
+ "fr": `Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les
+ noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.
+
Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les
+ plus importants.
`
+ }
+ },
+ {
+ title: {
+ "en-US": "Better Configuration Node UX",
+ "ja": "設定ノードのUXが向上",
+ "fr": "Meilleure expérience utilisateur du noeud de configuration"
+ },
+ image: '4.0/images/nr4-config-select.png',
+ description: {
+ "en-US": `The Configuration node selection UI has had a small update to have a dedicated 'add' button
+ next to the select box.
+ It's a small change, but should make it easier to work with your config nodes.
`,
+ "ja": `設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。
+ 微修正ですが設定ノードの操作が容易になります。
`,
+ "fr": `L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite
+ mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.
+ C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.
`
+ }
+ },
+ {
+ title: {
+ "en-US": "Timestamp formatting options",
+ "ja": "タイムスタンプの形式の項目",
+ "fr": "Options de formatage de l'horodatage"
+ },
+ image: '4.0/images/nr4-timestamp-formatting.png',
+ description: {
+ "en-US": `Nodes that let you set a timestamp now have options on what format that timestamp should be in.
+ We're keeping it simple to begin with by providing three options:
+
+ Milliseconds since epoch - this is existing behaviour of the timestamp option
+ ISO 8601 - a common format used by many systems
+ JavaScript Date Object
+ `,
+ "ja": `タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。
+ 次の3つの項目を追加したことで、簡単に選択できるようになりました:
+
+ エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目
+ ISO 8601 - 多くのシステムで使用されている共通の形式
+ JavaScript日付オブジェクト
+ `,
+ "fr": `Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.
+ Nous gardons les choses simples en proposant trois options :
+
+ Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage
+ ISO 8601 : un format commun utilisé par de nombreux systèmes
+ Objet Date JavaScript
+ `
+ }
+ },
+ {
+ title: {
+ "en-US": "Auto-complete of flow/global and env types",
+ "ja": "フロー/グローバル、環境変数の型の自動補完",
+ "fr": "Saisie automatique des types de flux/global et env"
+ },
+ image: '4.0/images/nr4-auto-complete.png',
+ description: {
+ "en-US": `The flow/global context inputs and the env input
+ now all include auto-complete suggestions based on the live state of your flows.
+ `,
+ "ja": `flow/globalコンテキストやenvの入力を、現在のフローの状態をもとに自動補完で提案するようになりました。
+ `,
+ "fr": `Les entrées contextuelles flow/global et l'entrée env
+ incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.
+ `,
+ }
+ },
+ {
+ title: {
+ "en-US": "Config node customisation in Subflows",
+ "ja": "サブフローでの設定ノードのカスタマイズ",
+ "fr": "Personnalisation du noeud de configuration dans les sous-flux"
+ },
+ image: '4.0/images/nr4-sf-config.png',
+ description: {
+ "en-US": `Subflows can now be customised to allow each instance to use a different
+ config node of a selected type.
+ For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing
+ of the messages received can be pointed at a different broker.
+ `,
+ "ja": `サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。
+ 例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。
+ `,
+ "fr": `Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un
+ noeud de configuration d'un type sélectionné.
+ Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement
+ des messages reçus peut être pointée vers un autre courtier.
+ `
+ }
+ },
+ {
+ title: {
+ "en-US": "Remembering palette state",
+ "ja": "パレットの状態を維持",
+ "fr": "Mémorisation de l'état de la palette"
+ },
+ description: {
+ "en-US": `The palette now remembers what categories you have hidden between reloads - as well as any
+ filter you have applied.
`,
+ "ja": `パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。
`,
+ "fr": `La palette se souvient désormais des catégories que vous avez masquées entre les rechargements,
+ ainsi que le filtre que vous avez appliqué.
`
+ }
+ },
+ {
+ title: {
+ "en-US": "Plugins shown in the Palette Manager",
+ "ja": "パレット管理にプラグインを表示",
+ "fr": "Affichage des Plugins dans le gestionnaire de palettes"
+ },
+ image: '4.0/images/nr4-plugins.png',
+ description: {
+ "en-US": `The palette manager now shows any plugin modules you have installed, such as
+ node-red-debugger. Previously they would only be shown if the plugins include
+ nodes for the palette.
`,
+ "ja": `パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。
`,
+ "fr": `Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
+ tels que node-red-debugger. Auparavant, ils n'étaient affichés que s'ils contenaient
+ des noeuds pour la palette.
`
+ }
+ },
+ {
+ title: {
+ "en-US": "Node Updates",
+ "ja": "ノードの更新",
+ "fr": "Mises à jour des noeuds"
+ },
+ // image: "images/",
+ description: {
+ "en-US": `The core nodes have received lots of minor fixes, documentation updates and
+ small enhancements. Check the full changelog in the Help sidebar for a full list.
+
+ A fully RFC4180 compliant CSV mode
+ Customisable headers on the WebSocket node
+ Split node now can operate on any message property
+ and lots more...
+ `,
+ "ja": `コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。
+
+ RFC4180に完全に準拠したCSVモード
+ WebSocketノードのカスタマイズ可能なヘッダ
+ Splitノードは、メッセージプロパティで操作できるようになりました
+ 他にも沢山あります...
+ `,
+ "fr": `Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour.
+ Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :
+
+ Un mode CSV entièrement conforme à la norme RFC4180
+ En-têtes personnalisables pour le noeud WebSocket
+ Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message
+ Et bien plus encore...
+ `
+ }
+ }
+ ]
+}
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/missing-modules.png b/packages/node_modules/@node-red/editor-client/src/tours/images/missing-modules.png
new file mode 100644
index 000000000..f96144395
Binary files /dev/null and b/packages/node_modules/@node-red/editor-client/src/tours/images/missing-modules.png differ
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/node-docs.png b/packages/node_modules/@node-red/editor-client/src/tours/images/node-docs.png
new file mode 100644
index 000000000..e0f285a1f
Binary files /dev/null and b/packages/node_modules/@node-red/editor-client/src/tours/images/node-docs.png differ
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/update-notification.png b/packages/node_modules/@node-red/editor-client/src/tours/images/update-notification.png
new file mode 100644
index 000000000..4e4b610e7
Binary files /dev/null and b/packages/node_modules/@node-red/editor-client/src/tours/images/update-notification.png differ
diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js
index 02a559136..8041db469 100644
--- a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js
+++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js
@@ -1,12 +1,12 @@
export default {
- version: "4.0.0",
+ version: "4.1.0",
steps: [
{
titleIcon: "fa fa-map-o",
title: {
- "en-US": "Welcome to Node-RED 4.0!",
- "ja": "Node-RED 4.0 へようこそ!",
- "fr": "Bienvenue dans Node-RED 4.0!"
+ "en-US": "Welcome to Node-RED 4.1!",
+ "ja": "Node-RED 4.1 へようこそ!",
+ "fr": "Bienvenue dans Node-RED 4.1!"
},
description: {
"en-US": "Let's take a moment to discover the new features in this release.
",
@@ -16,184 +16,79 @@ export default {
},
{
title: {
- "en-US": "Multiplayer Mode",
- "ja": "複数ユーザ同時利用モード",
- "fr": "Mode Multi-utilisateur"
+ "en-US": "Update notifications",
+ "ja": "更新の通知",
+ "fr": "Notifications de mise à jour"
},
- image: 'images/nr4-multiplayer-location.png',
+ image: 'images/update-notification.png',
description: {
- "en-US": `This release includes the first small steps towards making Node-RED easier
- to work with when you have multiple people editing flows at the same time.
- When this feature is enabled, you will now see who else has the editor open and some
- basic information on where they are in the editor.
- Check the release post for details on how to enable this feature in your settings file.
`,
- "ja": `本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。
- 本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。
- 設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。
`,
- "fr": `Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser
- lorsque plusieurs personnes modifient des flux en même temps.
- Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont
- ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.
- Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité
- dans votre fichier de paramètres.
`
+ "en-US": `Stay up to date with notifications when there is a new Node-RED version available, or updates to the nodes you have installed
`,
+ "ja": `新バージョンのNode-REDの提供や、インストールしたノードの更新があった時に、通知を受け取ることができます。
`,
+ "fr": `Désormais vous recevrez une notification lorsqu'une nouvelle version de Node-RED ou une nouvelle version relative à un des noeuds que vous avez installés est disponible
`
}
},
{
title: {
- "en-US": "Better background deploy handling",
- "ja": "バックグラウンドのデプロイ処理の改善",
- "fr": "Meilleure gestion du déploiement en arrière-plan"
+ "en-US": "Flow documentation",
+ "ja": "フローのドキュメント",
+ "fr": "Documentation des flux"
},
- image: 'images/nr4-background-deploy.png',
+ image: 'images/node-docs.png',
description: {
- "en-US": `If another user deploys changes whilst you are editing, we now use a more discrete notification
- that doesn't stop you continuing your work - especially if they are being very productive and deploying lots
- of changes.
`,
- "ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`,
- "fr": `Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez
- une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.
`
+ "en-US": `Quickly see which nodes have additional documentation with the new documentation icon.
+ Clicking on the icon opens up the Description tab of the node edit dialog.
`,
+ "ja": `ドキュメントアイコンによって、どのノードにドキュメントが追加されているかをすぐに確認できます。
+ アイコンをクリックすると、ノード編集ダイアログの説明タブが開きます。
`,
+ "fr": `Voyez rapidement quels noeuds ont une documentation supplémentaire avec la nouvelle icône de documentation.
+ Cliquer sur l'icône ouvre l'onglet Description de la boîte de dialogue d'édition du noeud.
`
}
},
{
title: {
- "en-US": "Improved flow diffs",
- "ja": "フローの差分表示の改善",
- "fr": "Amélioration des différences de flux"
+ "en-US": "Palette Manager Improvements",
+ "ja": "パレットの管理の改善",
+ "fr": "Améliorations du Gestionnaire de Palettes"
},
- image: 'images/nr4-diff-update.png',
description: {
- "en-US": `When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration
- changes and those that have only been moved.
-
When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.
`,
- "ja": `フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。
-
これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。
`,
- "fr": `Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les
- noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.
-
Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les
- plus importants.
`
- }
- },
- {
- title: {
- "en-US": "Better Configuration Node UX",
- "ja": "設定ノードのUXが向上",
- "fr": "Meilleure expérience utilisateur du noeud de configuration"
- },
- image: 'images/nr4-config-select.png',
- description: {
- "en-US": `The Configuration node selection UI has had a small update to have a dedicated 'add' button
- next to the select box.
- It's a small change, but should make it easier to work with your config nodes.
`,
- "ja": `設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。
- 微修正ですが設定ノードの操作が容易になります。
`,
- "fr": `L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite
- mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.
- C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.
`
- }
- },
- {
- title: {
- "en-US": "Timestamp formatting options",
- "ja": "タイムスタンプの形式の項目",
- "fr": "Options de formatage de l'horodatage"
- },
- image: 'images/nr4-timestamp-formatting.png',
- description: {
- "en-US": `Nodes that let you set a timestamp now have options on what format that timestamp should be in.
- We're keeping it simple to begin with by providing three options:
+ "en-US": `
There are lots of improvements to the palette manager:
- Milliseconds since epoch - this is existing behaviour of the timestamp option
- ISO 8601 - a common format used by many systems
- JavaScript Date Object
+ Search results are sorted by downloads to help you find the most popular nodes
+ See which nodes have been deprecated by their author and are no longer recommended for use
+ Links to node documentation for the nodes you already have installed
`,
- "ja": `タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。
- 次の3つの項目を追加したことで、簡単に選択できるようになりました:
+ "ja": `
パレットの管理に多くの改善が加えられました:
- エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目
- ISO 8601 - 多くのシステムで使用されている共通の形式
- JavaScript日付オブジェクト
+ 検索結果はダウンロード数順で並べられ、最も人気のあるノードを見つけやすくなりました。
+ 作者によって非推奨とされ、利用が推奨されなくなったノードかを確認できるようになりました。
+ 既にインストールされているノードに、ノードのドキュメントへのリンクが追加されました。
`,
- "fr": `Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.
- Nous gardons les choses simples en proposant trois options :
+ "fr": `
Le Gestionnaire de Palettes a bénéficié de nombreuses améliorations :
- Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage
- ISO 8601 : un format commun utilisé par de nombreux systèmes
- Objet Date JavaScript
+ Les résultats de recherche sont triés par téléchargement pour vous aider à trouver les noeuds les plus populaires.
+ Indique les noeuds obsolètes par leur auteur et dont l'utilisation n'est plus recommandée.
+ Liens vers la documentation des noeuds déjà installés.
`
}
},
{
title: {
- "en-US": "Auto-complete of flow/global and env types",
- "ja": "フロー/グローバル、環境変数の型の自動補完",
- "fr": "Saisie automatique des types de flux/global et env"
+ "en-US": "Installing missing modules",
+ "ja": "不足モジュールのインストール",
+ "fr": "Installation des modules manquants"
},
- image: 'images/nr4-auto-complete.png',
+ image: 'images/missing-modules.png',
description: {
- "en-US": `The flow/global context inputs and the env input
- now all include auto-complete suggestions based on the live state of your flows.
+ "en-US": `Flows exported from Node-RED 4.1 now include information on what additional modules need to be installed.
+ When importing a flow with this information, the editor will let you know what is missing and help to get them installed.
`,
- "ja": `flow/globalコンテキストやenvの入力を、現在のフローの状態をもとに自動補完で提案するようになりました。
+ "ja": `Node-RED 4.1から書き出したフローには、インストールが必要な追加モジュールの情報が含まれる様になりました。
+ この情報を含むフローを読み込むと、エディタは不足しているモジュールを通知し、インストールを支援します。
`,
- "fr": `Les entrées contextuelles flow/global et l'entrée env
- incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.
- `,
- }
- },
- {
- title: {
- "en-US": "Config node customisation in Subflows",
- "ja": "サブフローでの設定ノードのカスタマイズ",
- "fr": "Personnalisation du noeud de configuration dans les sous-flux"
- },
- image: 'images/nr4-sf-config.png',
- description: {
- "en-US": `Subflows can now be customised to allow each instance to use a different
- config node of a selected type.
- For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing
- of the messages received can be pointed at a different broker.
- `,
- "ja": `サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。
- 例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。
- `,
- "fr": `Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un
- noeud de configuration d'un type sélectionné.
- Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement
- des messages reçus peut être pointée vers un autre courtier.
+ "fr": `Les flux exportés depuis Node-RED 4.1 incluent désormais des informations sur les modules supplémentaires à installer.
+ Lors de l'importation d'un flux contenant ces informations, l'éditeur vous indiquera les modules manquants et vous aidera à les installer.
`
}
},
- {
- title: {
- "en-US": "Remembering palette state",
- "ja": "パレットの状態を維持",
- "fr": "Mémorisation de l'état de la palette"
- },
- description: {
- "en-US": `The palette now remembers what categories you have hidden between reloads - as well as any
- filter you have applied.
`,
- "ja": `パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。
`,
- "fr": `La palette se souvient désormais des catégories que vous avez masquées entre les rechargements,
- ainsi que le filtre que vous avez appliqué.
`
- }
- },
- {
- title: {
- "en-US": "Plugins shown in the Palette Manager",
- "ja": "パレット管理にプラグインを表示",
- "fr": "Affichage des Plugins dans le gestionnaire de palettes"
- },
- image: 'images/nr4-plugins.png',
- description: {
- "en-US": `The palette manager now shows any plugin modules you have installed, such as
- node-red-debugger. Previously they would only be shown if the plugins include
- nodes for the palette.
`,
- "ja": `パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。
`,
- "fr": `Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
- tels que node-red-debugger. Auparavant, ils n'étaient affichés que s'ils contenaient
- des noeuds pour la palette.
`
- }
- },
{
title: {
"en-US": "Node Updates",
@@ -205,26 +100,26 @@ export default {
"en-US": `The core nodes have received lots of minor fixes, documentation updates and
small enhancements. Check the full changelog in the Help sidebar for a full list.
- A fully RFC4180 compliant CSV mode
- Customisable headers on the WebSocket node
- Split node now can operate on any message property
+ Support for node: prefixed modules in the Function node
+ The ability to set a global timeout for Function nodes via the runtime settings
+ Better display of error objects in the Debug sidebar
and lots more...
`,
"ja": `コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。
- RFC4180に完全に準拠したCSVモード
- WebSocketノードのカスタマイズ可能なヘッダ
- Splitノードは、メッセージプロパティで操作できるようになりました
- 他にも沢山あります...
+ Functionノードでnode:のプレフィックスモジュールをサポート
+ ランタイム設定からFunctionノードのグローバルタイムアウトを設定可能
+ デバッグサイドバーでのエラーオブジェクトの表示を改善
+ その他、多数...
`,
- "fr": `Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour.
- Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :
-
- Un mode CSV entièrement conforme à la norme RFC4180
- En-têtes personnalisables pour le noeud WebSocket
- Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message
- Et bien plus encore...
- `
+ "fr": `Les noeuds principaux ont bénéficié de nombreux correctifs mineurs, de mises à jour de documentation et d'améliorations mineures.
+ Consultez le journal complet des modifications dans la barre latérale d'aide pour une liste complète.
+
+ Prise en charge des modules préfixés node: dans le noeud Fonction.
+ Possibilité de définir un délai d'expiration global pour les noeuds Fonction via les paramètres d'exécution.
+ Meilleur affichage des objets d'erreur dans la barre latérale de débogage.
+ Et bien plus encore...
+ `
}
}
]
diff --git a/packages/node_modules/@node-red/nodes/core/common/24-complete.js b/packages/node_modules/@node-red/nodes/core/common/24-complete.js
index ea665a265..1ba43a423 100644
--- a/packages/node_modules/@node-red/nodes/core/common/24-complete.js
+++ b/packages/node_modules/@node-red/nodes/core/common/24-complete.js
@@ -20,7 +20,16 @@ module.exports = function(RED) {
function CompleteNode(n) {
RED.nodes.createNode(this,n);
var node = this;
- this.scope = n.scope;
+ this.scope = n.scope || [];
+
+ // auto-filter out any directly connected nodes to avoid simple loopback
+ const w = this.wires.flat();
+ for (let i=0; i < this.scope.length; i++) {
+ if (w.includes(this.scope[i])) {
+ this.scope.splice(i, 1);
+ }
+ }
+
this.on("input",function(msg, send, done) {
send(msg);
done();
diff --git a/packages/node_modules/@node-red/nodes/core/common/25-status.js b/packages/node_modules/@node-red/nodes/core/common/25-status.js
index fc6ccbe29..8c56e2030 100644
--- a/packages/node_modules/@node-red/nodes/core/common/25-status.js
+++ b/packages/node_modules/@node-red/nodes/core/common/25-status.js
@@ -20,7 +20,16 @@ module.exports = function(RED) {
function StatusNode(n) {
RED.nodes.createNode(this,n);
var node = this;
- this.scope = n.scope;
+ this.scope = n.scope || [];
+
+ // auto-filter out any directly connected nodes to avoid simple loopback
+ const w = this.wires.flat();
+ for (let i=0; i < this.scope.length; i++) {
+ if (w.includes(this.scope[i])) {
+ this.scope.splice(i, 1);
+ }
+ }
+
this.on("input", function(msg, send, done) {
send(msg);
done();
diff --git a/packages/node_modules/@node-red/nodes/core/common/98-unknown.html b/packages/node_modules/@node-red/nodes/core/common/98-unknown.html
index 282ad3415..64c9e8e4e 100644
--- a/packages/node_modules/@node-red/nodes/core/common/98-unknown.html
+++ b/packages/node_modules/@node-red/nodes/core/common/98-unknown.html
@@ -3,7 +3,7 @@
diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js
index 71f6e15d6..c0def92b3 100644
--- a/packages/node_modules/@node-red/nodes/core/function/10-function.js
+++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js
@@ -162,6 +162,8 @@ module.exports = function(RED) {
console:console,
util:util,
Buffer:Buffer,
+ URL: URL,
+ URLSearchParams: URLSearchParams,
Date: Date,
RED: {
util: {
@@ -403,6 +405,8 @@ module.exports = function(RED) {
if(node.timeout>0){
finOpt.timeout = node.timeout;
finOpt.breakOnSigint = true;
+ } else if (RED.settings.globalFunctionTimeout > 0){
+ finOpt.timeout = RED.settings.globalFunctionTimeout * 1000
}
}
var promise = Promise.resolve();
@@ -419,8 +423,14 @@ module.exports = function(RED) {
var opts = {};
if (node.timeout>0){
opts = node.timeoutOptions;
+ } else if (RED.settings. globalFunctionTimeout > 0){
+ opts.timeout = RED.settings. globalFunctionTimeout * 1000
+ }
+ try {
+ node.script.runInContext(context,opts);
+ } catch (err) {
+ return done(err);
}
- node.script.runInContext(context,opts);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {
diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.js b/packages/node_modules/@node-red/nodes/core/function/90-exec.js
index 70aec8d2b..23d94059e 100644
--- a/packages/node_modules/@node-red/nodes/core/function/90-exec.js
+++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.js
@@ -109,7 +109,7 @@ module.exports = function(RED) {
child.stderr.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
if (isUtf8(data)) { msg.payload = data.toString(); }
- else { msg.payload = Buffer.from(data); }
+ else { msg.payload = data; }
nodeSend([null,RED.util.cloneMessage(msg),null]);
}
});
@@ -146,7 +146,8 @@ module.exports = function(RED) {
delete msg.payload;
if (stderr) {
msg2 = RED.util.cloneMessage(msg);
- msg2.payload = stderr;
+ msg2.payload = Buffer.from(stderr,"binary");
+ if (isUtf8(msg2.payload)) { msg2.payload = msg2.payload.toString(); }
}
msg.payload = Buffer.from(stdout,"binary");
if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); }
diff --git a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js
index afa0066f4..451035a74 100644
--- a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js
+++ b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js
@@ -675,7 +675,7 @@ module.exports = function(RED) {
node.options.password = node.password;
node.options.keepalive = node.keepalive;
node.options.clean = node.cleansession;
- node.options.clientId = node.clientid || 'nodered_' + RED.util.generateId();
+ node.options.clientId = node.clientid || 'nodered' + RED.util.generateId();
node.options.reconnectPeriod = RED.settings.mqttReconnectTime||5000;
delete node.options.protocolId; //V4+ default
delete node.options.protocolVersion; //V4 default
diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html
index 7cce956bb..9233975a7 100644
--- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html
+++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html
@@ -29,7 +29,7 @@
diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js
index 90c4134a4..0a3ac2560 100644
--- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js
+++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js
@@ -431,7 +431,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
normalisedHeaders[k.toLowerCase()] = response.headers[k]
})
if (normalisedHeaders['www-authenticate']) {
- let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname, normalisedHeaders['www-authenticate'])
+ let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname + requestUrl.search, normalisedHeaders['www-authenticate'])
options.headers.Authorization = authHeader;
}
// response.request.options.merge(options)
@@ -586,9 +586,31 @@ in your Node-RED user directory (${RED.settings.userDir}).
opts.https.certificate = opts.https.cert;
delete opts.https.cert;
}
+ // The got library uses a different case for some https properties compared to the
+ // standard node tls options object.
+ if (opts.https.ALPNProtocols) {
+ opts.https.alpnProtocols = opts.https.ALPNProtocols
+ delete opts.https.ALPNProtocols
+ }
+ // The got library doesn't support servername at this time
+ // https://github.com/sindresorhus/got/issues/2320
+ if (opts.https.servername) {
+ delete opts.https.servername
+ }
} else {
if (msg.hasOwnProperty('rejectUnauthorized')) {
- opts.https = { rejectUnauthorized: msg.rejectUnauthorized };
+ if (typeof msg.rejectUnauthorized === 'boolean') {
+ opts.https = { rejectUnauthorized: msg.rejectUnauthorized }
+ } else if (typeof msg.rejectUnauthorized === 'string') {
+ if (msg.rejectUnauthorized.toLowerCase() === 'true' || msg.rejectUnauthorized.toLowerCase() === 'false') {
+ opts.https = { rejectUnauthorized: (msg.rejectUnauthorized.toLowerCase() === 'true') }
+ } else {
+ node.warn(RED._("httpin.errors.rejectunauthorized-invalid"))
+ }
+ } else {
+ node.warn(RED._("httpin.errors.rejectunauthorized-invalid"))
+ }
+
}
}
diff --git a/packages/node_modules/@node-red/nodes/core/sequence/17-split.html b/packages/node_modules/@node-red/nodes/core/sequence/17-split.html
index b754700cd..975dd593e 100644
--- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.html
+++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.html
@@ -21,8 +21,8 @@
diff --git a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js
index 46ecb2636..5fe6b3c4e 100644
--- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js
+++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js
@@ -151,10 +151,11 @@ module.exports = function(RED) {
if (node.arraySplt === 1) {
m = m[0];
}
- RED.util.setMessageProperty(msg,node.property,m);
- msg.parts.index = i;
+ const newmsg = RED.util.cloneMessage(msg)
+ RED.util.setMessageProperty(newmsg,node.property,m);
+ newmsg.parts.index = i;
pos += node.arraySplt;
- send(RED.util.cloneMessage(msg));
+ send(newmsg);
}
done();
}
diff --git a/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html
index bb02eede0..72718d33f 100644
--- a/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html
+++ b/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html
@@ -81,7 +81,7 @@
Wenn msg.payload ein Objekt ist, setzt der Node automatisch den Inhaltstyp der Anforderung
auf application/json und kodiert den Hauptteil als solchen.
Um die Anforderung als Formulardaten zu kodieren, sollte msg.headers["content-type"] auf
- application/x-wwww-form-urlencoded gesetzt werden.
+
application/x-www-form-urlencoded gesetzt werden.
Datei-Upload
Um einen Datei-Upload umzusetzen, sollte msg.headers["content-type"] auf multipart/form-data
gesetzt werden und das an den Node zu sendende msg.payload muss ein Objekt mit folgender Struktur sein:
diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
index d26f0f56b..6d33e78aa 100644
--- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
+++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json
@@ -406,6 +406,7 @@
"label": {
"unknown": "unknown"
},
+ "manageModules": "Manage modules",
"tip": "
This node is a type unknown to your installation of Node-RED.
If you deploy with the node in this state, it's configuration will be preserved, but the flow will not start until the missing type is installed.
See the Info side bar for more help
"
},
"mqtt": {
@@ -562,7 +563,8 @@
"timeout-isnan": "Timeout value is not a valid number, ignoring",
"timeout-isnegative": "Timeout value is negative, ignoring",
"invalid-payload": "Invalid payload",
- "invalid-url": "Invalid url"
+ "invalid-url": "Invalid url",
+ "rejectunauthorized-invalid": "msg.rejectUnauthorized should be a boolean"
},
"status": {
"requesting": "requesting"
@@ -1017,7 +1019,7 @@
"objectSend": "Send a message for each key/value pair",
"strBuff": "
String /
Buffer ",
"array": "
Array ",
- "splitThe": "Split the",
+ "splitThe": "Split property",
"splitUsing": "Split using",
"splitLength": "Fixed length of",
"stream": "Handle as a stream of messages",
diff --git a/packages/node_modules/@node-red/nodes/locales/fr/messages.json b/packages/node_modules/@node-red/nodes/locales/fr/messages.json
index 238763e3b..e4c2d49ab 100644
--- a/packages/node_modules/@node-red/nodes/locales/fr/messages.json
+++ b/packages/node_modules/@node-red/nodes/locales/fr/messages.json
@@ -406,6 +406,7 @@
"label": {
"unknown": "inconnu"
},
+ "manageModules": "Gérer les modules",
"tip": "
Ce noeud est un type inconnu de votre installation Node-RED.
Si vous déployez avec le noeud dans cet état, sa configuration sera préservée, mais le flux ne démarrera pas avant que le type manquant soit installé.
Consulter la barre latérale d'informations pour plus d'aide
"
},
"mqtt": {
@@ -1017,7 +1018,7 @@
"objectSend": "Envoie un message pour chaque paire clé/valeur",
"strBuff": "
Chaîne /
Tampon ",
"array": "
Tableau ",
- "splitThe": "Diviser le",
+ "splitThe": "Diviser la propriété",
"splitUsing": "Diviser en utilisant",
"splitLength": "Longueur fixe de",
"stream": "Gérer comme un flux de messages",
diff --git a/packages/node_modules/@node-red/nodes/locales/ja/messages.json b/packages/node_modules/@node-red/nodes/locales/ja/messages.json
index 1693f879e..118d6af2c 100644
--- a/packages/node_modules/@node-red/nodes/locales/ja/messages.json
+++ b/packages/node_modules/@node-red/nodes/locales/ja/messages.json
@@ -406,6 +406,7 @@
"label": {
"unknown": "unknown"
},
+ "manageModules": "モジュールを管理",
"tip": "
現在のNode-RED環境では、本ノードの型が不明です。
現在の状態で本ノードをデプロイすると設定は保存されますが、不明なノードがインストールされるまでフローは実行されません。
詳細はノードの「情報」を参照してください。
"
},
"mqtt": {
diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json
index a513e14da..c6b385979 100644
--- a/packages/node_modules/@node-red/nodes/package.json
+++ b/packages/node_modules/@node-red/nodes/package.json
@@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
- "version": "4.1.0-beta.0",
+ "version": "4.1.0-beta.1",
"license": "Apache-2.0",
"repository": {
"type": "git",
@@ -15,7 +15,7 @@
}
],
"dependencies": {
- "acorn": "8.14.1",
+ "acorn": "8.15.0",
"acorn-walk": "8.3.4",
"ajv": "8.17.1",
"body-parser": "1.20.3",
@@ -36,7 +36,7 @@
"js-yaml": "4.1.0",
"media-typer": "1.1.0",
"mqtt": "5.11.0",
- "multer": "1.4.5-lts.2",
+ "multer": "2.0.1",
"mustache": "4.2.0",
"node-watch": "0.7.4",
"on-headers": "1.0.2",
diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js
index 27783be7f..eb27d9411 100644
--- a/packages/node_modules/@node-red/registry/lib/loader.js
+++ b/packages/node_modules/@node-red/registry/lib/loader.js
@@ -406,6 +406,7 @@ async function loadPlugin(plugin) {
}
try {
var r = require(plugin.file);
+ r = r.__esModule ? r.default : r
if (typeof r === "function") {
var red = registryUtil.createNodeApi(plugin);
diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json
index 1e886a159..0014d853a 100644
--- a/packages/node_modules/@node-red/registry/package.json
+++ b/packages/node_modules/@node-red/registry/package.json
@@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
- "version": "4.1.0-beta.0",
+ "version": "4.1.0-beta.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,7 +16,7 @@
}
],
"dependencies": {
- "@node-red/util": "4.1.0-beta.0",
+ "@node-red/util": "4.1.0-beta.1",
"clone": "2.1.2",
"fs-extra": "11.3.0",
"semver": "7.7.1",
diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js
index 1aa335f1a..634f5dbf3 100644
--- a/packages/node_modules/@node-red/runtime/lib/api/settings.js
+++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js
@@ -161,6 +161,8 @@ var api = module.exports = {
safeSettings.diagnostics.ui = false; // cannot have UI without endpoint
}
+ safeSettings.telemetryEnabled = runtime.telemetry.isEnabled()
+
safeSettings.runtimeState = {
//unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false.
enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true,
@@ -213,7 +215,19 @@ var api = module.exports = {
}
var currentSettings = runtime.settings.getUserSettings(username)||{};
currentSettings = extend(currentSettings, opts.settings);
+
try {
+ if (currentSettings.hasOwnProperty("telemetryEnabled")) {
+ // This is a global setting that is being set by the user. It should
+ // not be stored per-user as it applies to the whole runtime.
+ const telemetryEnabled = currentSettings.telemetryEnabled;
+ delete currentSettings.telemetryEnabled;
+ if (telemetryEnabled) {
+ runtime.telemetry.enable()
+ } else {
+ runtime.telemetry.disable()
+ }
+ }
return runtime.settings.setUserSettings(username, currentSettings).then(function() {
runtime.log.audit({event: "settings.update",username:username}, opts.req);
return;
diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js
index 4ac7cfb5b..1ac43032b 100644
--- a/packages/node_modules/@node-red/runtime/lib/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/index.js
@@ -23,6 +23,7 @@ var library = require("./library");
var plugins = require("./plugins");
var settings = require("./settings");
const multiplayer = require("./multiplayer");
+const telemetry = require("./telemetry");
var express = require("express");
var path = require('path');
@@ -135,6 +136,7 @@ function start() {
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json")
.then(function() { return storage.init(runtime)})
.then(function() { return settings.load(storage)})
+ .then(function() { return telemetry.init(runtime)})
.then(function() { return library.init(runtime)})
.then(function() { return multiplayer.init(runtime)})
.then(function() {
@@ -235,8 +237,12 @@ function start() {
}
}
return redNodes.loadContextsPlugin().then(function () {
- redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {});
started = true;
+ redNodes.loadFlows().then(() => {
+ if (started) {
+ redNodes.startFlows()
+ }
+ }).catch(function(err) {});
});
});
});
@@ -337,6 +343,7 @@ var runtime = {
library: library,
exec: exec,
util: util,
+ telemetry: telemetry,
get adminApi() { return adminApi },
get adminApp() { return adminApp },
get nodeApp() { return nodeApp },
diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js
index fbcb44e2f..d8d770677 100644
--- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js
+++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js
@@ -135,7 +135,7 @@ function getLibraryEntry(type,path) {
throw err;
});
} else {
- throw err;
+ throw new Error(`Library Entry not found ${path}`, { cause: err});
}
});
}
diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js
index 96dab3417..983b4ca52 100644
--- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js
@@ -51,6 +51,8 @@ function runGitCommand(args,cwd,env,emit) {
err.code = "git_auth_failed";
} else if(/Authentication failed/i.test(stderr)) {
err.code = "git_auth_failed";
+ } else if (/The requested URL returned error: 403/i.test(stderr)) {
+ err.code = "git_auth_failed";
} else if (/commit your changes or stash/i.test(stderr)) {
err.code = "git_local_overwrite";
} else if (/CONFLICT/.test(err.stdout)) {
diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/index.js b/packages/node_modules/@node-red/runtime/lib/telemetry/index.js
new file mode 100644
index 000000000..8ff3d845a
--- /dev/null
+++ b/packages/node_modules/@node-red/runtime/lib/telemetry/index.js
@@ -0,0 +1,213 @@
+const path = require('path')
+const fs = require('fs/promises')
+const semver = require('semver')
+const cronosjs = require('cronosjs')
+
+const METRICS_DIR = path.join(__dirname, 'metrics')
+const INITIAL_PING_DELAY = 1000 * 60 * 30 // 30 minutes from startup
+
+/** @type {import("got").Got | undefined} */
+let got
+
+let runtime
+
+let scheduleTask
+
+async function gather () {
+ let metricFiles = await fs.readdir(METRICS_DIR)
+ metricFiles = metricFiles.filter(name => /^\d+-.*\.js$/.test(name))
+ metricFiles.sort()
+
+ const metrics = {}
+
+ for (let i = 0, l = metricFiles.length; i < l; i++) {
+ const metricModule = require(path.join(METRICS_DIR, metricFiles[i]))
+ let result = metricModule(runtime)
+ if (!!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
+ result = await result
+ }
+ const keys = Object.keys(result)
+ keys.forEach(key => {
+ const keyParts = key.split('.')
+ let p = metrics
+ keyParts.forEach((part, index) => {
+ if (index < keyParts.length - 1) {
+ if (!p[part]) {
+ p[part] = {}
+ }
+ p = p[part]
+ } else {
+ p[part] = result[key]
+ }
+ })
+ })
+ }
+ return metrics
+}
+
+async function report () {
+ if (!isTelemetryEnabled()) {
+ return
+ }
+ // If enabled, gather metrics
+ const metrics = await gather()
+
+ // Post metrics to endpoint - handle any error silently
+
+ if (!got) {
+ got = (await import('got')).got
+ }
+
+ runtime.log.debug('Sending telemetry')
+ const response = await got.post('https://telemetry.nodered.org/ping', {
+ json: metrics,
+ responseType: 'json',
+ headers: {
+ 'User-Agent': `Node-RED/${runtime.settings.version}`
+ }
+ }).json().catch(err => {
+ // swallow errors
+ runtime.log.debug('Failed to send telemetry: ' + err.toString())
+ })
+ // Example response:
+ // { 'node-red': { latest: '4.0.9', next: '4.1.0-beta.1.9' } }
+ runtime.log.debug(`Telemetry response: ${JSON.stringify(response)}`)
+ // Get response from endpoint
+ if (response?.['node-red']) {
+ const currentVersion = metrics.env['node-red']
+ if (semver.valid(currentVersion)) {
+ const latest = response['node-red'].latest
+ const next = response['node-red'].next
+ let updatePayload
+ if (semver.lt(currentVersion, latest)) {
+ // Case one: current < latest
+ runtime.log.info(`A new version of Node-RED is available: ${latest}`)
+ updatePayload = { version: latest }
+ } else if (semver.gt(currentVersion, latest) && semver.lt(currentVersion, next)) {
+ // Case two: current > latest && current < next
+ runtime.log.info(`A new beta version of Node-RED is available: ${next}`)
+ updatePayload = { version: next }
+ }
+
+ if (updatePayload && isUpdateNotificationEnabled()) {
+ runtime.events.emit("runtime-event",{id:"update-available", payload: updatePayload, retain: true});
+ }
+ }
+ }
+}
+
+function isTelemetryEnabled () {
+ // If NODE_RED_DISABLE_TELEMETRY was set, or --no-telemetry was specified,
+ // the settings object will have been updated to disable telemetry explicitly
+
+ // If there are no telemetry settings then the user has not had a chance
+ // to opt out yet - so keep it disabled until they do
+
+ let telemetrySettings
+ try {
+ telemetrySettings = runtime.settings.get('telemetry')
+ } catch (err) {
+ // Settings not available
+ }
+ let runtimeTelemetryEnabled
+ try {
+ runtimeTelemetryEnabled = runtime.settings.get('telemetryEnabled')
+ } catch (err) {
+ // Settings not available
+ }
+
+ if (telemetrySettings === undefined && runtimeTelemetryEnabled === undefined) {
+ // No telemetry settings - so keep it disabled
+ return undefined
+ }
+
+ // User has made a choice; defer to that
+ if (runtimeTelemetryEnabled !== undefined) {
+ return runtimeTelemetryEnabled
+ }
+
+ // If there are telemetry settings, use what it says
+ if (telemetrySettings && telemetrySettings.enabled !== undefined) {
+ return telemetrySettings.enabled
+ }
+
+ // At this point, we have no sign the user has consented to telemetry, so
+ // keep disabled - but return undefined as a false-like value to distinguish
+ // it from the explicit disable above
+ return undefined
+}
+
+function isUpdateNotificationEnabled () {
+ const telemetrySettings = runtime.settings.get('telemetry') || {}
+ return telemetrySettings.updateNotification !== false
+}
+/**
+ * Start the telemetry schedule
+ */
+function startTelemetry () {
+ if (scheduleTask) {
+ // Already scheduled - nothing left to do
+ return
+ }
+
+ const pingTime = new Date(Date.now() + INITIAL_PING_DELAY)
+ const pingMinutes = pingTime.getMinutes()
+ const pingHours = pingTime.getHours()
+ const pingSchedule = `${pingMinutes} ${pingHours} * * *`
+
+ runtime.log.debug(`Telemetry enabled. Schedule: ${pingSchedule}`)
+
+ scheduleTask = cronosjs.scheduleTask(pingSchedule, () => {
+ report()
+ })
+}
+
+function stopTelemetry () {
+ if (scheduleTask) {
+ runtime.log.debug(`Telemetry disabled`)
+ scheduleTask.stop()
+ scheduleTask = null
+ }
+}
+
+module.exports = {
+ init: (_runtime) => {
+ runtime = _runtime
+
+ if (isTelemetryEnabled()) {
+ startTelemetry()
+ }
+ },
+ /**
+ * Enable telemetry via user opt-in in the editor
+ */
+ enable: () => {
+ if (runtime.settings.available()) {
+ runtime.settings.set('telemetryEnabled', true)
+ }
+ startTelemetry()
+ },
+
+ /**
+ * Disable telemetry via user opt-in in the editor
+ */
+ disable: () => {
+ if (runtime.settings.available()) {
+ runtime.settings.set('telemetryEnabled', false)
+ }
+ stopTelemetry()
+ },
+
+ /**
+ * Get telemetry enabled status
+ * @returns {boolean} true if telemetry is enabled, false if disabled, undefined if not set
+ */
+ isEnabled: isTelemetryEnabled,
+
+ stop: () => {
+ if (scheduleTask) {
+ scheduleTask.stop()
+ scheduleTask = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js
new file mode 100644
index 000000000..acac829fb
--- /dev/null
+++ b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js
@@ -0,0 +1,5 @@
+module.exports = (runtime) => {
+ return {
+ instanceId: runtime.settings.get('instanceId')
+ }
+}
diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js
new file mode 100644
index 000000000..ae2a31859
--- /dev/null
+++ b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js
@@ -0,0 +1,9 @@
+const os = require('os')
+
+module.exports = (_) => {
+ return {
+ 'os.type': os.type(),
+ 'os.release': os.release(),
+ 'os.arch': os.arch()
+ }
+}
diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js
new file mode 100644
index 000000000..173adc752
--- /dev/null
+++ b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js
@@ -0,0 +1,8 @@
+const process = require('process')
+
+module.exports = (runtime) => {
+ return {
+ 'env.nodejs': process.version.replace(/^v/, ''),
+ 'env.node-red': runtime.settings.version
+ }
+}
diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json
index e6f1ca0d0..c2a0a91ef 100644
--- a/packages/node_modules/@node-red/runtime/package.json
+++ b/packages/node_modules/@node-red/runtime/package.json
@@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
- "version": "4.1.0-beta.0",
+ "version": "4.1.0-beta.1",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,13 +16,15 @@
}
],
"dependencies": {
- "@node-red/registry": "4.1.0-beta.0",
- "@node-red/util": "4.1.0-beta.0",
+ "@node-red/registry": "4.1.0-beta.1",
+ "@node-red/util": "4.1.0-beta.1",
"async-mutex": "0.5.0",
"clone": "2.1.2",
+ "cronosjs": "1.7.1",
"express": "4.21.2",
"fs-extra": "11.3.0",
"json-stringify-safe": "5.0.1",
- "rfdc": "^1.3.1"
+ "rfdc": "^1.3.1",
+ "semver": "7.7.1"
}
}
diff --git a/packages/node_modules/@node-red/util/lib/exec.js b/packages/node_modules/@node-red/util/lib/exec.js
index c7197ef65..15b81aa89 100644
--- a/packages/node_modules/@node-red/util/lib/exec.js
+++ b/packages/node_modules/@node-red/util/lib/exec.js
@@ -78,7 +78,7 @@ module.exports = {
stdout: stdout,
stderr: stderr
}
- emit && events.emit("event-log", {id:invocationId,payload:{ts: Date.now(),data:"rc="+code}});
+ emit && events.emit("event-log", {id:invocationId,payload:{ts: Date.now(), data:"rc="+code, end: true}});
if (code === 0) {
resolve(result)
diff --git a/packages/node_modules/@node-red/util/lib/log.js b/packages/node_modules/@node-red/util/lib/log.js
index 14b93d5b5..fa8c0416d 100644
--- a/packages/node_modules/@node-red/util/lib/log.js
+++ b/packages/node_modules/@node-red/util/lib/log.js
@@ -52,11 +52,11 @@ var levelColours = {
10: 'red',
20: 'red',
30: 'yellow',
- 40: 'white',
+ 40: '',
50: 'cyan',
60: 'gray',
- 98: 'white',
- 99: 'white'
+ 98: '',
+ 99: ''
};
var logHandlers = [];
@@ -99,7 +99,12 @@ const utilLog = function (msg, level) {
d.getMinutes().toString().padStart(2, '0'),
d.getSeconds().toString().padStart(2, '0')
].join(':');
- console.log(chalk[levelColours[level] || 'white'](`${d.getDate()} ${months[d.getMonth()]} ${time} - ${msg}`))
+ const logLine = `${d.getDate()} ${months[d.getMonth()]} ${time} - ${msg}`
+ if (levelColours[level]) {
+ console.log(chalk[levelColours[level]](logLine))
+ } else {
+ console.log(logLine)
+ }
}
var consoleLogger = function(msg) {
diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json
index dffc817b0..316d0696f 100644
--- a/packages/node_modules/@node-red/util/package.json
+++ b/packages/node_modules/@node-red/util/package.json
@@ -1,6 +1,6 @@
{
"name": "@node-red/util",
- "version": "4.1.0-beta.0",
+ "version": "4.1.0-beta.1",
"license": "Apache-2.0",
"repository": {
"type": "git",
diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json
index f36273986..d189fb5e3 100644
--- a/packages/node_modules/node-red/package.json
+++ b/packages/node_modules/node-red/package.json
@@ -1,6 +1,6 @@
{
"name": "node-red",
- "version": "4.1.0-beta.0",
+ "version": "4.1.0-beta.1",
"description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org",
"license": "Apache-2.0",
@@ -31,16 +31,16 @@
"flow"
],
"dependencies": {
- "@node-red/editor-api": "4.1.0-beta.0",
- "@node-red/runtime": "4.1.0-beta.0",
- "@node-red/util": "4.1.0-beta.0",
- "@node-red/nodes": "4.1.0-beta.0",
+ "@node-red/editor-api": "4.1.0-beta.1",
+ "@node-red/runtime": "4.1.0-beta.1",
+ "@node-red/util": "4.1.0-beta.1",
+ "@node-red/nodes": "4.1.0-beta.1",
"basic-auth": "2.0.1",
"bcryptjs": "3.0.2",
"cors": "2.8.5",
"express": "4.21.2",
"fs-extra": "11.3.0",
- "node-red-admin": "^4.0.2",
+ "node-red-admin": "^4.1.0",
"nopt": "5.0.0",
"semver": "7.7.1"
},
diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js
index 5f3c9da25..d98b69f8f 100755
--- a/packages/node_modules/node-red/red.js
+++ b/packages/node_modules/node-red/red.js
@@ -63,7 +63,8 @@ var knownOpts = {
"verbose": Boolean,
"safe": Boolean,
"version": Boolean,
- "define": [String, Array]
+ "define": [String, Array],
+ "no-telemetry": Boolean
};
var shortHands = {
"?":["--help"],
@@ -97,6 +98,7 @@ if (parsedArgs.help) {
console.log(" --safe enable safe mode");
console.log(" -D, --define X=Y overwrite value in settings file");
console.log(" --version show version information");
+ console.log(" --no-telemetry do not share usage data with the Node-RED project");
console.log(" -?, --help show this help");
console.log(" admin
run an admin command");
console.log("");
@@ -222,6 +224,10 @@ if (process.env.NODE_RED_ENABLE_TOURS) {
settings.editorTheme.tours = !/^false$/i.test(process.env.NODE_RED_ENABLE_TOURS);
}
+if (parsedArgs.telemetry === false || process.env.NODE_RED_DISABLE_TELEMETRY) {
+ settings.telemetry = settings.telemetry || {};
+ settings.telemetry.enabled = false;
+}
var defaultServerSettings = {
"x-powered-by": false
diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js
index e8bb01228..269cac160 100644
--- a/packages/node_modules/node-red/settings.js
+++ b/packages/node_modules/node-red/settings.js
@@ -273,6 +273,7 @@ module.exports = {
* Runtime Settings
* - lang
* - runtimeState
+ * - telemetry
* - diagnostics
* - logging
* - contextStorage
@@ -311,6 +312,22 @@ module.exports = {
/** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */
ui: false,
},
+ telemetry: {
+ /**
+ * By default, telemetry is disabled until the user provides consent the first
+ * time they open the editor.
+ *
+ * The following property can be uncommented and set to true/false to enable/disable
+ * telemetry without seeking further consent in the editor.
+ * The user can override this setting via the user settings dialog within the editor
+ */
+ // enabled: true,
+ /**
+ * If telemetry is enabled, the editor will notify the user if a new version of Node-RED
+ * is available. Set the following property to false to disable this notification.
+ */
+ // updateNotification: true
+ },
/** Configure the logging output */
logging: {
/** Only console logging is currently supported */
@@ -473,6 +490,7 @@ module.exports = {
* - fileWorkingDirectory
* - functionGlobalContext
* - functionExternalModules
+ * - globalFunctionTimeout
* - functionTimeout
* - nodeMessageBufferMaxLength
* - ui (for use with Node-RED Dashboard)
@@ -499,7 +517,19 @@ module.exports = {
/** Allow the Function node to load additional npm modules directly */
functionExternalModules: true,
- /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */
+
+ /**
+ * The default timeout (in seconds) for all Function nodes.
+ * Individual nodes can set their own timeout value within their configuration.
+ */
+ globalFunctionTimeout: 0,
+
+ /**
+ * Default timeout, in seconds, for the Function node. 0 means no timeout is applied
+ * This value is applied when the node is first added to the workspace - any changes
+ * must then be made with the individual node configurations.
+ * To set a global timeout value, use `globalFunctionTimeout`
+ */
functionTimeout: 0,
/** The following property can be used to set predefined values in Global Context.
diff --git a/test/nodes/core/common/24-complete_spec.js b/test/nodes/core/common/24-complete_spec.js
new file mode 100644
index 000000000..15f27b43b
--- /dev/null
+++ b/test/nodes/core/common/24-complete_spec.js
@@ -0,0 +1,39 @@
+
+var should = require("should");
+var catchNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");
+var helper = require("node-red-node-test-helper");
+
+describe('complete Node', function() {
+
+ afterEach(function() {
+ helper.unload();
+ });
+
+ it('should output a message when called', function(done) {
+ var flow = [ { id:"n1", type:"complete", name:"status", wires:[["n2"]], scope:[] },
+ {id:"n2", type:"helper"} ];
+ helper.load(catchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ n1.should.have.property('name', 'status');
+ n2.on("input", function(msg) {
+ msg.text.should.equal("Oh dear");
+ msg.should.have.property('source');
+ msg.source.should.have.property('id',"12345");
+ msg.source.should.have.property('type',"testnode");
+ msg.source.should.have.property('name',"fred");
+ done();
+ });
+ var mst = {
+ text: "Oh dear",
+ source: {
+ id: "12345",
+ type: "testnode",
+ name: "fred"
+ }
+ }
+ n1.emit("input", mst);
+ });
+ });
+
+});
diff --git a/test/nodes/core/common/25-status_spec.js b/test/nodes/core/common/25-status_spec.js
index 41b0a79c8..9457d4372 100644
--- a/test/nodes/core/common/25-status_spec.js
+++ b/test/nodes/core/common/25-status_spec.js
@@ -25,7 +25,7 @@ describe('status Node', function() {
});
it('should output a message when called', function(done) {
- var flow = [ { id:"n1", type:"status", name:"status", wires:[["n2"]] },
+ var flow = [ { id:"n1", type:"status", name:"status", wires:[["n2"]], scope:[] },
{id:"n2", type:"helper"} ];
helper.load(catchNode, flow, function() {
var n1 = helper.getNode("n1");
diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js
index 56c4ec976..6a04547f4 100644
--- a/test/nodes/core/function/10-function_spec.js
+++ b/test/nodes/core/function/10-function_spec.js
@@ -1451,7 +1451,7 @@ describe('function node', function() {
});
});
- it('check if default function timeout settings are recognized', function (done) {
+ it('check if function timeout settings are recognized', function (done) {
RED.settings.functionTimeout = 0.01;
var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
helper.load(functionNode, flow, function () {
@@ -1479,6 +1479,65 @@ describe('function node', function() {
});
});
+ it('check if default function timeout settings are recognized', function (done) {
+ RED.settings.globalFunctionTimeout = 0.01;
+ var flow = [{id: "n1",type: "function",wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
+ helper.load(functionNode, flow, function () {
+ var n1 = helper.getNode("n1");
+ n1.receive({ payload: "foo", topic: "bar" });
+ setTimeout(function () {
+ try {
+ helper.log().called.should.be.true();
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "function";
+ });
+ logEvents.should.have.length(1);
+ var msg = logEvents[0][0];
+ msg.should.have.property('level', helper.log().ERROR);
+ msg.should.have.property('id', 'n1');
+ msg.should.have.property('type', 'function');
+ should.equal(RED.settings.globalFunctionTimeout, 0.01);
+ should.equal(msg.msg.message, 'Script execution timed out after 10ms');
+ delete RED.settings.globalFunctionTimeout;
+ done();
+ } catch (err) {
+ done(err);
+ }
+ }, 500);
+ });
+ });
+
+ it('check if functionTimeout has higher precedence over default function timeout setting', function (done) {
+ RED.settings.globalFunctionTimeout = 0.02;
+ RED.settings.functionTimeout = 0.01;
+ var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
+ helper.load(functionNode, flow, function () {
+ var n1 = helper.getNode("n1");
+ n1.receive({ payload: "foo", topic: "bar" });
+ setTimeout(function () {
+ try {
+ helper.log().called.should.be.true();
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "function";
+ });
+ logEvents.should.have.length(1);
+ var msg = logEvents[0][0];
+ msg.should.have.property('level', helper.log().ERROR);
+ msg.should.have.property('id', 'n1');
+ msg.should.have.property('type', 'function');
+ should.equal(RED.settings.functionTimeout, 0.01);
+ should.equal(RED.settings.globalFunctionTimeout, 0.02);
+ should.equal(msg.msg.message, 'Script execution timed out after 10ms');
+ delete RED.settings.functionTimeout;
+ delete RED.settings.globalFunctionTimeout;
+ done();
+ } catch (err) {
+ done(err);
+ }
+ }, 500);
+ });
+ });
+
describe("finalize function", function() {
it('should execute', function(done) {
diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js
index 16c38d2e5..0075831c9 100644
--- a/test/nodes/core/network/21-mqtt_spec.js
+++ b/test/nodes/core/network/21-mqtt_spec.js
@@ -58,7 +58,7 @@ describe('MQTT Nodes', function () {
mqttBroker.should.have.property('options');
mqttBroker.options.should.have.property('clean', true);
mqttBroker.options.should.have.property('clientId');
- mqttBroker.options.clientId.should.containEql('nodered_');
+ mqttBroker.options.clientId.should.containEql('nodered');
mqttBroker.options.should.have.property('keepalive').type("number");
mqttBroker.options.should.have.property('reconnectPeriod').type("number");
//as this is not a v5 connection, ensure v5 properties are not present
@@ -894,4 +894,4 @@ function nextTopic(topic) {
return (base_topic + topic + String(topicNo));
}
-//#endregion HELPERS
\ No newline at end of file
+//#endregion HELPERS
diff --git a/test/unit/@node-red/runtime/lib/api/settings_spec.js b/test/unit/@node-red/runtime/lib/api/settings_spec.js
index 9b3b94229..0e9e20422 100644
--- a/test/unit/@node-red/runtime/lib/api/settings_spec.js
+++ b/test/unit/@node-red/runtime/lib/api/settings_spec.js
@@ -57,7 +57,8 @@ describe("runtime-api/settings", function() {
getCredentialKeyType: () => "test-key-type"
},
library: {getLibraries: () => ["lib1"] },
- storage: {}
+ storage: {},
+ telemetry: { isEnabled: () => true }
})
return settings.getRuntimeSettings({}).then(result => {
result.should.have.property("httpNodeRoot","testHttpNodeRoot");
@@ -96,7 +97,8 @@ describe("runtime-api/settings", function() {
getCredentialKeyType: () => "test-key-type"
},
library: {getLibraries: () => { ["lib1"]} },
- storage: {}
+ storage: {},
+ telemetry: { isEnabled: () => true }
})
return settings.getRuntimeSettings({
user: {
@@ -145,7 +147,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
- }
+ },
+ telemetry: { isEnabled: () => true }
})
return settings.getRuntimeSettings({
user: {
@@ -202,7 +205,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
- }
+ },
+ telemetry: { isEnabled: () => true }
})
return settings.getRuntimeSettings({
user: {
@@ -250,7 +254,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
- }
+ },
+ telemetry: { isEnabled: () => true }
})
return settings.getRuntimeSettings({
user: {
@@ -301,7 +306,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
}
- }
+ },
+ telemetry: { isEnabled: () => true }
})
return settings.getRuntimeSettings({
user: {
diff --git a/test/unit/@node-red/runtime/lib/telemetry/index_spec.js b/test/unit/@node-red/runtime/lib/telemetry/index_spec.js
new file mode 100644
index 000000000..7ab1b89d1
--- /dev/null
+++ b/test/unit/@node-red/runtime/lib/telemetry/index_spec.js
@@ -0,0 +1,96 @@
+const should = require("should");
+const NR_TEST_UTILS = require("nr-test-utils");
+
+const telemetryApi = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/index");
+
+describe("telemetry", function() {
+
+ afterEach(function () {
+ telemetryApi.stop()
+ messages = []
+ })
+
+ let messages = []
+
+ function getMockRuntime(settings) {
+ return {
+ settings: {
+ get: key => { return settings[key] },
+ set: (key, value) => { settings[key] = value },
+ available: () => true,
+ },
+ log: {
+ debug: (msg) => { messages.push(msg)}
+ }
+ }
+ }
+
+ // Principles to test:
+ // - No settings at all; disable telemetry
+ // - Runtime settings only; do what it says
+ // - User settings take precedence over runtime settings
+
+ it('Disables telemetry with no settings present', function () {
+ telemetryApi.init(getMockRuntime({}))
+ messages.should.have.length(0)
+ // Returns undefined as we don't know either way
+ ;(telemetryApi.isEnabled() === undefined).should.be.true()
+ })
+ it('Runtime settings - enable', function () {
+ // Enabled in runtime settings
+ telemetryApi.init(getMockRuntime({
+ telemetry: { enabled: true }
+ }))
+ telemetryApi.isEnabled().should.be.true()
+ messages.should.have.length(1)
+ ;/Telemetry enabled/.test(messages[0]).should.be.true()
+ })
+ it('Runtime settings - disable', function () {
+ telemetryApi.init(getMockRuntime({
+ telemetry: { enabled: false },
+ }))
+ // Returns false, not undefined
+ telemetryApi.isEnabled().should.be.false()
+ messages.should.have.length(0)
+ })
+
+ it('User settings - enable overrides runtime settings', function () {
+ telemetryApi.init(getMockRuntime({
+ telemetry: { enabled: false },
+ telemetryEnabled: true
+ }))
+ telemetryApi.isEnabled().should.be.true()
+ messages.should.have.length(1)
+ ;/Telemetry enabled/.test(messages[0]).should.be.true()
+ })
+
+ it('User settings - disable overrides runtime settings', function () {
+ telemetryApi.init(getMockRuntime({
+ telemetry: { enabled: true },
+ telemetryEnabled: false
+ }))
+ telemetryApi.isEnabled().should.be.false()
+ messages.should.have.length(0)
+ })
+
+ it('Can enable/disable telemetry', function () {
+ const settings = {}
+ telemetryApi.init(getMockRuntime(settings))
+ ;(telemetryApi.isEnabled() === undefined).should.be.true()
+
+ telemetryApi.enable()
+
+ telemetryApi.isEnabled().should.be.true()
+ messages.should.have.length(1)
+ ;/Telemetry enabled/.test(messages[0]).should.be.true()
+ settings.should.have.property('telemetryEnabled', true)
+
+ telemetryApi.disable()
+
+ telemetryApi.isEnabled().should.be.false()
+ messages.should.have.length(2)
+ ;/Telemetry disabled/.test(messages[1]).should.be.true()
+ settings.should.have.property('telemetryEnabled', false)
+
+ })
+})
\ No newline at end of file
diff --git a/test/unit/@node-red/runtime/lib/telemetry/metrics/01-core_spec.js b/test/unit/@node-red/runtime/lib/telemetry/metrics/01-core_spec.js
new file mode 100644
index 000000000..d1e012e5a
--- /dev/null
+++ b/test/unit/@node-red/runtime/lib/telemetry/metrics/01-core_spec.js
@@ -0,0 +1,16 @@
+const should = require("should");
+const NR_TEST_UTILS = require("nr-test-utils");
+
+const api = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/metrics/01-core");
+
+describe("telemetry metrics/01-core", function() {
+
+ it('reports core metrics', function () {
+ const result = api({
+ settings: {
+ get: key => { return {instanceId: "1234"}[key]}
+ }
+ })
+ result.should.have.property("instanceId", "1234")
+ })
+})
\ No newline at end of file
diff --git a/test/unit/@node-red/runtime/lib/telemetry/metrics/02-os_spec.js b/test/unit/@node-red/runtime/lib/telemetry/metrics/02-os_spec.js
new file mode 100644
index 000000000..77a4b60af
--- /dev/null
+++ b/test/unit/@node-red/runtime/lib/telemetry/metrics/02-os_spec.js
@@ -0,0 +1,14 @@
+const should = require("should");
+const NR_TEST_UTILS = require("nr-test-utils");
+
+const api = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/metrics/02-os");
+
+describe("telemetry metrics/02-os", function() {
+
+ it('reports os metrics', function () {
+ const result = api()
+ result.should.have.property("os.type")
+ result.should.have.property("os.release")
+ result.should.have.property("os.arch")
+ })
+})
\ No newline at end of file
diff --git a/test/unit/@node-red/runtime/lib/telemetry/metrics/03-env_spec.js b/test/unit/@node-red/runtime/lib/telemetry/metrics/03-env_spec.js
new file mode 100644
index 000000000..eff539270
--- /dev/null
+++ b/test/unit/@node-red/runtime/lib/telemetry/metrics/03-env_spec.js
@@ -0,0 +1,17 @@
+const should = require("should");
+const NR_TEST_UTILS = require("nr-test-utils");
+
+const api = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/metrics/03-env");
+
+describe("telemetry metrics/03-env", function() {
+
+ it('reports env metrics', function () {
+ const result = api({
+ settings: {
+ version: '1.2.3'
+ }
+ })
+ result.should.have.property("env.nodejs", process.version.replace(/^v/, ''))
+ result.should.have.property("env.node-red", '1.2.3')
+ })
+})
\ No newline at end of file