1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge branch 'master' into dev

This commit is contained in:
Nick O'Leary 2021-05-04 11:19:05 +01:00
commit 71ba73b38f
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
53 changed files with 581 additions and 205 deletions

View File

@ -1,3 +1,69 @@
### 1.3.4 Maintenance Release
Editor
- Allow nodes to access resolved theme files Fixes #2968
- Fix importing node to currently flow rather than match its old z value
- Don't let 'escape' whilst moving nodes interrupt things Fixes #2960
- Sort context stores in TypedInput and ensure default first Fixes #2954
- Fix margin between nodes on palette (#2947) @kazuhitoyokoi
- Ensure typedInput option is selected in dropdown menu Part of #2945
- Ensure typedInput without value has focus class removed Closes #2945
- TreeList: Fix remove item when depth=0 and wrong gutter calc (#2967) @hanc2006
Runtime
- Handle subflow modules that contain subflows
- Timeout http upgrade requests that are not otherwise handled Fixes #2956
- Fix error on auto commit for no flow change (#2957) @HiroyasuNishiyama
Nodes
- CSV: Fix CSV handling of special chars as separators
- Delay: Give delay node random mina nd max more space so you can see complete value
- Exec: fix grunt fail on exec node test (#2964) @HiroyasuNishiyama
- Function: Ensure function expand button is above vertical scrollbar Fixes #2955
- Inject: Fix inject node output tooltip extra property count
### 1.3.3: Maintenance Release
Editor
- Fix package semver comparison to allow >1 version increment
- Prevent TypedInput label overflowing element Fixes #2941
- Remove TypedInput from tab focus when only one type available
- Make typedInput.disable more consistent in behaviour Fixes #2942
- Fix project credential secret reset handling Part of #2868
Runtime
- Export package version in Grunt file so docs template can access
Nodes
- CSV: ensure CSV node can send false as string
- HTTPIn: handle application/x-protobuf as Buffer type (#2935 #2938) @hardillb
- MQTT: Ensure mqtt-close message is published when closing mqtt nodes
### 1.3.2: Maintenance Release
Runtime
- Handle package.json without dependencies section
Editor
- Fix variable reference error in editableList Fixes #2933
- Fix handling of user-provided keymap Fixes #2926
- Ensure theme login image is passed through to api response Fixes #2929
- Add Japanese translations for Node-RED v1.3.1 (#2930) @kazuhitoyokoi
Nodes
- CSV: Fix CSV parsing with other than , separator
- File out: Fix timing of msg.send to be after close
- Function: describe `node.outputCount` in help text
- MQTT: Fix MQTT Broker TLS config row layout Fixes #2927
- Split: add comment to info re $N being number of messages arriving
### 1.3.1: Maintenance Release ### 1.3.1: Maintenance Release

View File

@ -471,7 +471,8 @@ module.exports = function(grunt) {
], ],
options: { options: {
destination: 'docs', destination: 'docs',
configure: './jsdoc.json' configure: './jsdoc.json',
fred: "hi there"
} }
}, },
_editor: { _editor: {

View File

@ -90,7 +90,7 @@ function getToken(req,res,next) {
return server.token()(req,res,next); return server.token()(req,res,next);
} }
function login(req,res) { async function login(req,res) {
var response = {}; var response = {};
if (settings.adminAuth) { if (settings.adminAuth) {
var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module); var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module);
@ -116,8 +116,9 @@ function login(req,res) {
response.prompts[0].image = theme.serveFile('/login/',mergedAdminAuth.strategy.image); response.prompts[0].image = theme.serveFile('/login/',mergedAdminAuth.strategy.image);
} }
} }
if (theme.context().login && theme.context().login.image) { let themeContext = await theme.context();
response.image = theme.context().login.image; if (themeContext.login && themeContext.login.image) {
response.image = themeContext.login.image;
} }
} }
res.json(response); res.json(response);

View File

@ -129,6 +129,14 @@ module.exports = {
} }
themeContext.page.title = theme.page.title || themeContext.page.title; themeContext.page.title = theme.page.title || themeContext.page.title;
// Store the resolved urls to these resources so nodes (such as Debug)
// can access them
theme.page._ = {
css: themeContext.page.css,
scripts: themeContext.page.scripts,
favicon: themeContext.page.favicon
}
} }
if (theme.header) { if (theme.header) {
@ -223,6 +231,8 @@ module.exports = {
themePlugin.path themePlugin.path
); );
themeContext.page.css = cssFiles.concat(themeContext.page.css || []) themeContext.page.css = cssFiles.concat(themeContext.page.css || [])
theme.page = theme.page || {_:{}}
theme.page._.css = cssFiles.concat(theme.page._.css || [])
} }
if (themePlugin.scripts) { if (themePlugin.scripts) {
const scriptFiles = serveFilesFromTheme( const scriptFiles = serveFilesFromTheme(
@ -232,6 +242,8 @@ module.exports = {
themePlugin.path themePlugin.path
) )
themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || []) themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || [])
theme.page = theme.page || {_:{}}
theme.page._.scripts = cssFiles.concat(theme.page._.scripts || [])
} }
} }
activeThemeInitialised = true; activeThemeInitialised = true;

View File

@ -524,8 +524,8 @@
"title": "パレットの管理", "title": "パレットの管理",
"palette": "パレット", "palette": "パレット",
"times": { "times": {
"seconds": "秒前", "seconds": "秒前",
"minutes": "分前", "minutes": "分前",
"minutesV": "__count__ 分前", "minutesV": "__count__ 分前",
"hoursV": "__count__ 時間前", "hoursV": "__count__ 時間前",
"hoursV_plural": "__count__ 時間前", "hoursV_plural": "__count__ 時間前",

View File

@ -1424,6 +1424,8 @@ RED.nodes = (function() {
nid = getID(); nid = getID();
workspace_map[n.id] = nid; workspace_map[n.id] = nid;
n.id = nid; n.id = nid;
} else {
workspace_map[n.id] = n.id;
} }
addWorkspace(n); addWorkspace(n);
RED.workspaces.add(n); RED.workspaces.add(n);
@ -1523,7 +1525,7 @@ RED.nodes = (function() {
} }
} }
} else { } else {
if (n.z && !workspaces[n.z] && !subflow_map[n.z]) { if (n.z && !workspace_map[n.z] && !subflow_map[n.z]) {
n.z = activeWorkspace; n.z = activeWorkspace;
} }
} }
@ -1621,7 +1623,7 @@ RED.nodes = (function() {
node.id = getID(); node.id = getID();
} else { } else {
node.id = n.id; node.id = n.id;
if (node.z == null || (!workspaces[node.z] && !subflow_map[node.z])) { if (node.z == null || (!workspace_map[node.z] && !subflow_map[node.z])) {
if (createMissingWorkspace) { if (createMissingWorkspace) {
if (missingWorkspace === null) { if (missingWorkspace === null) {
missingWorkspace = RED.workspaces.add(null,true); missingWorkspace = RED.workspaces.add(null,true);

View File

@ -1186,22 +1186,6 @@ RED.clipboard = (function() {
} }
} }
function getNodeLabelText(n) {
var label = n.name || n.type+": "+n.id;
if (n._def.label) {
try {
label = (typeof n._def.label === "function" ? n._def.label.call(n) : n._def.label)||"";
} catch(err) {
console.log("Definition error: "+n.type+".label",err);
}
}
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
return label;
}
function getFlowLabel(n) { function getFlowLabel(n) {
n = JSON.parse(JSON.stringify(n)); n = JSON.parse(JSON.stringify(n));
n._def = RED.nodes.getType(n.type) || {}; n._def = RED.nodes.getType(n.type) || {};
@ -1227,16 +1211,8 @@ RED.clipboard = (function() {
if (n._def) { if (n._def) {
n._ = n._def._; n._ = n._def._;
} }
var div = $('<div>',{class:"red-ui-info-outline-item"}); var div = $('<div>',{class:"red-ui-node-list-item"});
RED.utils.createNodeIcon(n).appendTo(div); RED.utils.createNodeIcon(n,true).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
var labelText = getNodeLabelText(n);
var label = $('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv);
if (labelText) {
label.text(labelText)
} else {
label.html(n.type)
}
return div; return div;
} }

View File

@ -71,7 +71,7 @@
var buttons = this.options.buttons || []; var buttons = this.options.buttons || [];
if (this.options.addButton !== false) { if (this.options.addButton !== false) {
var addLabel, addTittle; var addLabel, addTitle;
if (typeof this.options.addButton === 'string') { if (typeof this.options.addButton === 'string') {
addLabel = this.options.addButton addLabel = this.options.addButton
} else { } else {
@ -102,7 +102,7 @@
button.click(evt); button.click(evt);
} }
}); });
if (button.title) { if (button.title) {
element.attr("title", button.title); element.attr("title", button.title);
} }
@ -113,7 +113,7 @@
element.append($("<span></span>").text(" " + button.label)); element.append($("<span></span>").text(" " + button.label));
} }
}); });
if (this.element.css("position") === "absolute") { if (this.element.css("position") === "absolute") {
["top","left","bottom","right"].forEach(function(s) { ["top","left","bottom","right"].forEach(function(s) {
var v = that.element.css(s); var v = that.element.css(s);

View File

@ -312,7 +312,7 @@
} }
if (child.depth !== parent.depth+1) { if (child.depth !== parent.depth+1) {
child.depth = parent.depth+1; child.depth = parent.depth+1;
var labelPaddingWidth = ((child.gutter?child.gutter.width()+2:0)+(child.depth*20)); var labelPaddingWidth = ((child.gutter ? child.gutter[0].offsetWidth + 2 : 0) + (child.depth * 20));
child.treeList.labelPadding.width(labelPaddingWidth+'px'); child.treeList.labelPadding.width(labelPaddingWidth+'px');
if (child.element) { if (child.element) {
$(child.element).css({ $(child.element).css({
@ -348,6 +348,18 @@
that._selected.delete(item); that._selected.delete(item);
delete item.treeList; delete item.treeList;
delete that._items[item.id]; delete that._items[item.id];
if(item.depth === 0) {
for(var key in that._items) {
if (that._items.hasOwnProperty(key)) {
var child = that._items[key];
if(child.parent && child.parent.id === item.id) {
delete that._items[key].treeList;
delete that._items[key];
}
}
}
that._data = that._data.filter(function(data) { return data.id !== item.id})
}
} }
item.treeList.insertChildAt = function(newItem,position,select) { item.treeList.insertChildAt = function(newItem,position,select) {
newItem.parent = item; newItem.parent = item;
@ -480,7 +492,10 @@
if (item.treeList.container) { if (item.treeList.container) {
$(item.element).remove(); $(item.element).remove();
$(element).appendTo(item.treeList.label); $(element).appendTo(item.treeList.label);
var labelPaddingWidth = (item.gutter?item.gutter.width()+2:0)+(item.depth*20); // using the JQuery Object, the gutter width will
// be wrong when the element is reattached the second time
var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (item.depth * 20);
$(element).css({ $(element).css({
width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)" width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)"
}) })
@ -516,7 +531,7 @@
}).appendTo(label) }).appendTo(label)
} }
var labelPaddingWidth = (item.gutter?item.gutter.width()+2:0)+(depth*20); var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (depth * 20)
item.treeList.labelPadding = $('<span>').css({ item.treeList.labelPadding = $('<span>').css({
display: "inline-block", display: "inline-block",
width: labelPaddingWidth+'px' width: labelPaddingWidth+'px'

View File

@ -15,7 +15,7 @@
**/ **/
(function($) { (function($) {
var contextParse = function(v,defaultStore) { var contextParse = function(v,defaultStore) {
var parts = RED.utils.parseContextKey(v, defaultStore); var parts = RED.utils.parseContextKey(v, defaultStore&&defaultStore.value);
return { return {
option: parts.store, option: parts.store,
value: parts.key value: parts.key
@ -279,6 +279,14 @@
var contextStores = RED.settings.context.stores; var contextStores = RED.settings.context.stores;
var contextOptions = contextStores.map(function(store) { var contextOptions = contextStores.map(function(store) {
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'} return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
}).sort(function(A,B) {
if (A.value === RED.settings.context.default) {
return -1;
} else if (B.value === RED.settings.context.default) {
return 1;
} else {
return A.value.localeCompare(B.value);
}
}) })
if (contextOptions.length < 2) { if (contextOptions.length < 2) {
allOptions.flow.options = []; allOptions.flow.options = [];
@ -389,6 +397,11 @@
evt.stopPropagation(); evt.stopPropagation();
}).on('focus', function() { }).on('focus', function() {
that.uiSelect.addClass('red-ui-typedInput-focus'); that.uiSelect.addClass('red-ui-typedInput-focus');
}).on('blur', function() {
var opt = that.typeMap[that.propertyType];
if (opt.hasValue === false) {
that.uiSelect.removeClass('red-ui-typedInput-focus');
}
}) })
// explicitly set optionSelectTrigger display to inline-block otherwise jQ sets it to 'inline' // explicitly set optionSelectTrigger display to inline-block otherwise jQ sets it to 'inline'
@ -438,7 +451,11 @@
}); });
this._showMenu(this.optionMenu,this.optionSelectTrigger); this._showMenu(this.optionMenu,this.optionSelectTrigger);
var selectedOption = this.optionMenu.find("[value='"+this.optionValue+"']"); var targetValue = this.optionValue;
if (this.optionValue === null || this.optionValue === undefined) {
targetValue = this.value();
}
var selectedOption = this.optionMenu.find("[value='"+targetValue+"']");
if (selectedOption.length === 0) { if (selectedOption.length === 0) {
selectedOption = this.optionMenu.children(":first"); selectedOption = this.optionMenu.children(":first");
} }
@ -669,6 +686,11 @@
that.typeMap[result.value] = result; that.typeMap[result.value] = result;
return result; return result;
}); });
if (this.typeList.length < 2) {
this.selectTrigger.attr("tabindex", -1)
} else {
this.selectTrigger.attr("tabindex", 0)
}
this.selectTrigger.toggleClass("disabled", this.typeList.length === 1); this.selectTrigger.toggleClass("disabled", this.typeList.length === 1);
this.selectTrigger.find(".fa-caret-down").toggle(this.typeList.length > 1) this.selectTrigger.find(".fa-caret-down").toggle(this.typeList.length > 1)
if (this.menu) { if (this.menu) {
@ -768,6 +790,11 @@
if (opt.hasValue === false || (opt.showLabel !== false && !opt.icon)) { if (opt.hasValue === false || (opt.showLabel !== false && !opt.icon)) {
this.selectLabel.text(opt.label); this.selectLabel.text(opt.label);
} }
if (opt.label) {
this.selectTrigger.attr("title",opt.label);
} else {
this.selectTrigger.attr("title","");
}
if (opt.hasValue === false) { if (opt.hasValue === false) {
this.selectTrigger.addClass("red-ui-typedInput-full-width"); this.selectTrigger.addClass("red-ui-typedInput-full-width");
} else { } else {
@ -1004,16 +1031,17 @@
this.uiSelect.hide(); this.uiSelect.hide();
}, },
disable: function(val) { disable: function(val) {
if(val === true) { if(val === undefined || !!val ) {
this.uiSelect.attr("disabled", "disabled"); this.uiSelect.attr("disabled", "disabled");
} else if (val === false) {
this.uiSelect.attr("disabled", null); //remove attr
} else { } else {
this.uiSelect.attr("disabled", val); //user value this.uiSelect.attr("disabled", null); //remove attr
} }
}, },
enable: function() {
this.uiSelect.attr("disabled", null); //remove attr
},
disabled: function() { disabled: function() {
return this.uiSelect.attr("disabled"); return this.uiSelect.attr("disabled") === "disabled";
} }
}); });
})(jQuery); })(jQuery);

View File

@ -119,7 +119,7 @@ RED.keyboard = (function() {
} else { } else {
mergedKeymap[action] = [{ mergedKeymap[action] = [{
scope: themeKeymap[action].scope || "*", scope: themeKeymap[action].scope || "*",
key: [themeKeymap[action].key], key: themeKeymap[action].key,
user: false user: false
}] }]
if (mergedKeymap[action][0].scope === "workspace") { if (mergedKeymap[action][0].scope === "workspace") {

View File

@ -331,7 +331,7 @@ RED.palette.editor = (function() {
nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow) nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
nodeEntry.updateButton.text(RED._('palette.editor.updated')).addClass('disabled').css('display', 'inline-block'); nodeEntry.updateButton.text(RED._('palette.editor.updated')).addClass('disabled').css('display', 'inline-block');
} else if (loadedIndex.hasOwnProperty(module)) { } else if (loadedIndex.hasOwnProperty(module)) {
if (semVerCompare(loadedIndex[module].version,moduleInfo.version) === 1) { if (semVerCompare(loadedIndex[module].version,moduleInfo.version) > 0) {
nodeEntry.updateButton.show(); nodeEntry.updateButton.show();
nodeEntry.updateButton.text(RED._('palette.editor.update',{version:loadedIndex[module].version})); nodeEntry.updateButton.text(RED._('palette.editor.update',{version:loadedIndex[module].version}));
} else { } else {

View File

@ -928,11 +928,11 @@ RED.projects.settings = (function() {
saveDisabled = isFlowInvalid || credFileLabelText.text()===""; saveDisabled = isFlowInvalid || credFileLabelText.text()==="";
if (credentialSecretExistingInput.is(":visible")) { if (credentialSecretExistingRow.is(":visible")) {
credentialSecretExistingInput.toggleClass("input-error", credentialSecretExistingInput.val() === ""); credentialSecretExistingInput.toggleClass("input-error", credentialSecretExistingInput.val() === "");
saveDisabled = saveDisabled || credentialSecretExistingInput.val() === ""; saveDisabled = saveDisabled || credentialSecretExistingInput.val() === "";
} }
if (credentialSecretNewInput.is(":visible")) { if (credentialSecretNewRow.is(":visible")) {
credentialSecretNewInput.toggleClass("input-error", credentialSecretNewInput.val() === ""); credentialSecretNewInput.toggleClass("input-error", credentialSecretNewInput.val() === "");
saveDisabled = saveDisabled || credentialSecretNewInput.val() === ""; saveDisabled = saveDisabled || credentialSecretNewInput.val() === "";
} }
@ -1130,7 +1130,7 @@ RED.projects.settings = (function() {
} }
if (credentialSecretResetButton.hasClass('selected') || credentialSecretEditButton.hasClass('selected')) { if (credentialSecretResetButton.hasClass('selected') || credentialSecretEditButton.hasClass('selected')) {
payload.credentialSecret = credentialSecretNewInput.val(); payload.credentialSecret = credentialSecretNewInput.val();
if (credentialSecretExistingInput.is(":visible")) { if (credentialSecretExistingRow.is(":visible")) {
payload.currentCredentialSecret = credentialSecretExistingInput.val(); payload.currentCredentialSecret = credentialSecretExistingInput.val();
} }
} }

View File

@ -230,9 +230,8 @@ RED.sidebar.help = (function() {
} }
function getNodeLabel(n) { function getNodeLabel(n) {
var div = $('<div>',{class:"red-ui-info-outline-item"}); var div = $('<div>',{class:"red-ui-node-list-item"});
RED.utils.createNodeIcon(n).appendTo(div); RED.utils.createNodeIcon(n).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
var label = n.name; var label = n.name;
if (!label && n._def.paletteLabel) { if (!label && n._def.paletteLabel) {
try { try {
@ -241,7 +240,7 @@ RED.sidebar.help = (function() {
} }
} }
label = label || n.type; label = label || n.type;
$('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).text(label).appendTo(contentDiv); $('<div>',{class:"red-ui-node-label"}).text(n.name||n.type).appendTo(div);
return div; return div;
} }

View File

@ -73,36 +73,11 @@ RED.sidebar.info.outliner = (function() {
return item; return item;
} }
function getNodeLabelText(n) {
var label = n.name || n.type+": "+n.id;
if (n._def.label) {
try {
label = (typeof n._def.label === "function" ? n._def.label.call(n) : n._def.label)||"";
} catch(err) {
console.log("Definition error: "+n.type+".label",err);
}
}
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
return label;
}
function getNodeLabel(n) { function getNodeLabel(n) {
var div = $('<div>',{class:"red-ui-info-outline-item"}); var div = $('<div>',{class:"red-ui-node-list-item red-ui-info-outline-item"});
RED.utils.createNodeIcon(n).appendTo(div); RED.utils.createNodeIcon(n, true).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div); div.find(".red-ui-node-label").addClass("red-ui-info-outline-item-label")
var labelText = getNodeLabelText(n);
var label = $('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv);
if (labelText) {
label.text(labelText)
} else {
label.html("&nbsp;")
}
addControls(n, div); addControls(n, div);
return div; return div;
} }
@ -430,7 +405,7 @@ RED.sidebar.info.outliner = (function() {
var existingObject = objects[n.id]; var existingObject = objects[n.id];
var parent = n.g||n.z||"__global__"; var parent = n.g||n.z||"__global__";
var nodeLabelText = getNodeLabelText(n); var nodeLabelText = RED.utils.getNodeLabel(n,n.name || (n.type+": "+n.id));
if (nodeLabelText) { if (nodeLabelText) {
existingObject.element.find(".red-ui-info-outline-item-label").text(nodeLabelText); existingObject.element.find(".red-ui-info-outline-item-label").text(nodeLabelText);
} else { } else {

View File

@ -1125,9 +1125,9 @@ RED.utils = (function() {
imageIconElement.css("backgroundImage", "url("+iconUrl+")"); imageIconElement.css("backgroundImage", "url("+iconUrl+")");
} }
function createNodeIcon(node) { function createNodeIcon(node, includeLabel) {
var def = node._def; var def = node._def;
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}) var nodeDiv = $('<div>',{class:"red-ui-node-icon"})
if (node.type === "_selection_") { if (node.type === "_selection_") {
nodeDiv.addClass("red-ui-palette-icon-selection"); nodeDiv.addClass("red-ui-palette-icon-selection");
} else if (node.type === "group") { } else if (node.type === "group") {
@ -1147,8 +1147,20 @@ RED.utils = (function() {
} }
var icon_url = RED.utils.getNodeIcon(def,node); var icon_url = RED.utils.getNodeIcon(def,node);
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); RED.utils.createIconElement(icon_url, nodeDiv, true);
RED.utils.createIconElement(icon_url, iconContainer, true);
if (includeLabel) {
var container = $('<span>');
nodeDiv.appendTo(container);
var labelText = RED.utils.getNodeLabel(node,node.name || (node.type+": "+node.id));
var label = $('<div>',{class:"red-ui-node-label"}).appendTo(container);
if (labelText) {
label.text(labelText)
} else {
label.html("&nbsp;")
}
return container;
}
return nodeDiv; return nodeDiv;
} }

View File

@ -1751,7 +1751,6 @@ RED.view = (function() {
} }
} }
if (mouse_mode == RED.state.IMPORT_DRAGGING) { if (mouse_mode == RED.state.IMPORT_DRAGGING) {
RED.keyboard.remove("escape");
updateActiveNodes(); updateActiveNodes();
RED.nodes.dirty(true); RED.nodes.dirty(true);
} }
@ -1786,6 +1785,9 @@ RED.view = (function() {
} }
function selectNone() { function selectNone() {
if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
return;
}
if (mouse_mode === RED.state.IMPORT_DRAGGING) { if (mouse_mode === RED.state.IMPORT_DRAGGING) {
clearSelection(); clearSelection();
RED.history.pop(); RED.history.pop();

View File

@ -134,7 +134,7 @@
&:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):first-child { &:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):first-child {
margin-top: 15px; margin-top: 15px;
} }
&:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):first-child { &:not(.red-ui-palette-node-config):not(.red-ui-palette-node-small):last-child {
margin-bottom: 15px; margin-bottom: 15px;
} }
} }
@ -229,3 +229,47 @@
left: 1px; left: 1px;
} }
} }
////////////////
.red-ui-node-list-item {
display: inline-block;
padding: 0;
font-size: 13px;
border: none;
}
.red-ui-node-icon {
display: inline-block;
float:left;
width: 24px;
height: 20px;
margin-top: 1px;
// width: 30px;
// height: 25px;
border-radius: 3px;
border: 1px solid $node-border;
background-position: 5% 50%;
background-repeat: no-repeat;
background-size: contain;
position: relative;
background-color: $node-icon-background-color;
text-align: center;
.red-ui-palette-icon {
width: 20px;
}
.red-ui-palette-icon-fa {
font-size: 14px;
position: relative;
top: -1px;
left: 0px;
}
}
.red-ui-node-label {
margin-left: 32px;
line-height: 23px;
white-space: nowrap;
color: $secondary-text-color;
}

View File

@ -326,13 +326,17 @@ div.red-ui-info-table {
border-bottom: 1px solid $secondary-border-color; border-bottom: 1px solid $secondary-border-color;
} }
} }
.red-ui-info-outline,.red-ui-sidebar-help-toc, #red-ui-clipboard-dialog-import-conflicts-list, #red-ui-clipboard-dialog-export-tab-clipboard-preview { .red-ui-info-outline,
// TODO: remove these classes for 2.0. Keeping in 1.x for backwards compatibility
// of theme generators.
.red-ui-sidebar-help-toc, #red-ui-clipboard-dialog-import-conflicts-list, #red-ui-clipboard-dialog-export-tab-clipboard-preview
{
.red-ui-info-outline-item { .red-ui-info-outline-item {
display: inline-block; display: inline-block;
padding: 0; padding: 0;
font-size: 13px; font-size: 13px;
border: none; border: none;
.red-ui-palette-icon-fa { &:not(.red-ui-node-list-item) .red-ui-palette-icon-fa {
position: relative; position: relative;
top: 1px; top: 1px;
left: 0px; left: 0px;

View File

@ -24,7 +24,7 @@
margin: 0; margin: 0;
vertical-align: middle; vertical-align: middle;
box-sizing: border-box; box-sizing: border-box;
overflow:visible; overflow: hidden;
position: relative; position: relative;
&[disabled] { &[disabled] {
input, button { input, button {
@ -33,7 +33,7 @@
cursor: not-allowed; cursor: not-allowed;
} }
} }
.red-ui-typedInput-input-wrap { .red-ui-typedInput-input-wrap {
flex-grow: 1; flex-grow: 1;
} }

View File

@ -214,7 +214,7 @@
for (var i=0,l=props.length; i<l; i++) { for (var i=0,l=props.length; i<l; i++) {
if (i > 0) lab += "\n"; if (i > 0) lab += "\n";
if (i === 5) { if (i === 5) {
lab += " + "+(props.length-4); lab += "... +"+(props.length-5);
break; break;
} }
lab += props[i].p+": "; lab += props[i].p+": ";

View File

@ -2,7 +2,8 @@ module.exports = function(RED) {
"use strict"; "use strict";
var util = require("util"); var util = require("util");
var events = require("events"); var events = require("events");
//var path = require("path"); const fs = require("fs-extra");
const path = require("path");
var debuglength = RED.settings.debugMaxLength || 1000; var debuglength = RED.settings.debugMaxLength || 1000;
var useColors = RED.settings.debugUseColors || false; var useColors = RED.settings.debugUseColors || false;
util.inspect.styles.boolean = "red"; util.inspect.styles.boolean = "red";
@ -249,11 +250,34 @@ module.exports = function(RED) {
} }
}); });
let cachedDebugView;
RED.httpAdmin.get("/debug/view/view.html", function(req,res) {
if (!cachedDebugView) {
fs.readFile(path.join(__dirname,"lib","debug","view.html")).then(data => {
let customStyles = "";
try {
let customStyleList = RED.settings.editorTheme.page._.css || [];
customStyleList.forEach(style => {
customStyles += `<link rel="stylesheet" href="../../${style}">\n`
})
} catch(err) {}
cachedDebugView = data.toString().replace("<!-- INSERT-THEME-CSS -->",customStyles)
res.set('Content-Type', 'text/html');
res.send(cachedDebugView).end();
}).catch(err => {
res.sendStatus(404);
})
} else {
res.send(cachedDebugView).end();
}
});
// As debug/view/debug-utils.js is loaded via <script> tag, it won't get // As debug/view/debug-utils.js is loaded via <script> tag, it won't get
// the auth header attached. So do not use RED.auth.needsPermission here. // the auth header attached. So do not use RED.auth.needsPermission here.
RED.httpAdmin.get("/debug/view/*",function(req,res) { RED.httpAdmin.get("/debug/view/*",function(req,res) {
var options = { var options = {
root: __dirname + '/lib/debug/', root: path.join(__dirname,"lib","debug"),
dotfiles: 'deny' dotfiles: 'deny'
}; };
res.sendFile(req.params[0], options); res.sendFile(req.params[0], options);

View File

@ -2,6 +2,7 @@
<head> <head>
<link rel="stylesheet" href="../../red/style.min.css"> <link rel="stylesheet" href="../../red/style.min.css">
<link rel="stylesheet" href="../../vendor/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="../../vendor/font-awesome/css/font-awesome.min.css">
<!-- INSERT-THEME-CSS -->
<title>Node-RED Debug Tools</title> <title>Node-RED Debug Tools</title>
</head> </head>
<body class="red-ui-editor red-ui-debug-window"> <body class="red-ui-editor red-ui-debug-window">

View File

@ -74,21 +74,21 @@
<div id="func-tab-init" style="display:none"> <div id="func-tab-init" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative"> <div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div> </div>
</div> </div>
<div id="func-tab-body" style="display:none"> <div id="func-tab-body" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative"> <div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div> <div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div> </div>
</div> </div>
<div id="func-tab-finalize" style="display:none"> <div id="func-tab-finalize" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative"> <div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div> <div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div> </div>
</div> </div>

View File

@ -302,7 +302,7 @@ module.exports = function(RED) {
} }
}); });
if (moduleErrors) { if (moduleErrors) {
throw new Error("Function node failed to load external modules"); throw new Error(RED._("function.error.externalModuleLoadError"));
} }
} }

View File

@ -45,9 +45,9 @@
</div> </div>
<div id="random-details" class="form-row"> <div id="random-details" class="form-row">
<label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> <span data-i18n="delay.between"></span></label> <label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> <span data-i18n="delay.between"></span></label>
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:30px !important"> <input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:50px !important">
<span data-i18n="delay.and"></span> &nbsp;<span data-i18n="delay.and"></span>&nbsp;
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:30px !important"> <input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:50px !important">
<select id="node-input-randomUnits" style="width:140px !important"> <select id="node-input-randomUnits" style="width:140px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option> <option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option> <option value="seconds" data-i18n="delay.secs"></option>

View File

@ -186,8 +186,8 @@
<input type="text" id="node-config-input-port" data-i18n="[placeholder]mqtt.label.port" style="width:55px"> <input type="text" id="node-config-input-port" data-i18n="[placeholder]mqtt.label.port" style="width:55px">
</div> </div>
<div class="form-row" style="height: 34px;"> <div class="form-row" style="height: 34px;">
<input type="checkbox" id="node-config-input-usetls" style="height: 34px; margin: 0 0 0 104px; display: inline-block; width: auto; vertical-align: top;"> <input type="checkbox" id="node-config-input-usetls" style="height: 34px; margin: 0 5px 0 104px; display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-usetls" style="width: 80px; line-height: 34px;"><span data-i18n="mqtt.label.use-tls"></span></label> <label for="node-config-input-usetls" style="width: 100px; line-height: 34px;"><span data-i18n="mqtt.label.use-tls"></span></label>
<span id="node-config-row-tls" class="hide"><input style="width: 320px;" type="text" id="node-config-input-tls"></span> <span id="node-config-row-tls" class="hide"><input style="width: 320px;" type="text" id="node-config-input-tls"></span>
</div> </div>
<div class="form-row"> <div class="form-row">

View File

@ -400,7 +400,15 @@ module.exports = function(RED) {
} }
if (Object.keys(node.users).length === 0) { if (Object.keys(node.users).length === 0) {
if (node.client && node.client.connected) { if (node.client && node.client.connected) {
return node.client.end(done); // Send close message
if (node.closeMessage) {
node.publish(node.closeMessage,function(err) {
node.client.end(done);
});
} else {
node.client.end(done);
}
return;
} else { } else {
node.client.end(); node.client.end();
return done(); return done();
@ -639,10 +647,6 @@ module.exports = function(RED) {
this.on('close', function(done) { this.on('close', function(done) {
this.closing = true; this.closing = true;
if (this.connected) { if (this.connected) {
// Send close message
if (node.closeMessage) {
node.publish(node.closeMessage);
}
this.client.once('close', function() { this.client.once('close', function() {
done(); done();
}); });
@ -873,4 +877,4 @@ module.exports = function(RED) {
} }
} }
RED.nodes.registerType("mqtt out",MQTTOutNode); RED.nodes.registerType("mqtt out",MQTTOutNode);
}; };

View File

@ -46,7 +46,9 @@ module.exports = function(RED) {
isText = true; isText = true;
} else if (parsedType.type !== "application") { } else if (parsedType.type !== "application") {
isText = false; isText = false;
} else if ((parsedType.subtype !== "octet-stream") && (parsedType.subtype !== "cbor")) { } else if ((parsedType.subtype !== "octet-stream")
&& (parsedType.subtype !== "cbor")
&& (parsedType.subtype !== "x-protobuf")) {
checkUTF = true; checkUTF = true;
} else { } else {
// application/octet-stream or application/cbor // application/octet-stream or application/cbor

View File

@ -38,17 +38,18 @@ module.exports = function(RED) {
if (this.hdrout === true) { this.hdrout = "all"; } if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true; var tmpwarn = true;
var node = this; var node = this;
var re = new RegExp(',(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// pass in an array of column names to be trimed, de-quoted and retrimed // pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col) { var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""]; col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim()); col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; } if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; } else { node.goodtmpl = true; }
return col; return col;
} }
var template = clean(node.template); var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === ''; var notemplate = template.length === 1 && template[0] === '';
node.hdrSent = false; node.hdrSent = false;
@ -67,7 +68,7 @@ module.exports = function(RED) {
if (node.hdrout !== "none" && node.hdrSent === false) { if (node.hdrout !== "none" && node.hdrSent === false) {
if ((template.length === 1) && (template[0] === '')) { if ((template.length === 1) && (template[0] === '')) {
if (msg.hasOwnProperty("columns")) { if (msg.hasOwnProperty("columns")) {
template = clean(msg.columns || ""); template = clean(msg.columns || "",",");
} }
else { else {
template = Object.keys(msg.payload[0]); template = Object.keys(msg.payload[0]);
@ -80,7 +81,7 @@ module.exports = function(RED) {
if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) { if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) {
if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; } if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; }
for (var t = 0; t < msg.payload[s].length; t++) { for (var t = 0; t < msg.payload[s].length; t++) {
if (!msg.payload[s][t] && (msg.payload[s][t] !== 0)) { msg.payload[s][t] = ""; } if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; }
if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes
msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""'); msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""');
msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo; msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo;
@ -93,7 +94,7 @@ module.exports = function(RED) {
} }
else { else {
if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) { if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) {
template = clean(msg.columns || ""); template = clean(msg.columns || "",",");
} }
if ((template.length === 1) && (template[0] === '')) { if ((template.length === 1) && (template[0] === '')) {
/* istanbul ignore else */ /* istanbul ignore else */
@ -184,7 +185,7 @@ module.exports = function(RED) {
if ((node.hdrin === true) && first) { // if the template is in the first line if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; } if (line.length - i === 1) { tmp += line[i]; }
template = clean(tmp); template = clean(tmp,node.sep);
first = false; first = false;
} }
else { tmp += line[i]; } else { tmp += line[i]; }

View File

@ -219,6 +219,10 @@
value: "none", value: "none",
label: label label: label
}).text(label).appendTo(encSel); }).text(label).appendTo(encSel);
$("<option/>", {
value: "setbymsg",
label: node._("file.encoding.setbymsg")
}).text(label).appendTo(encSel);
encodings.forEach(function(item) { encodings.forEach(function(item) {
if(Array.isArray(item)) { if(Array.isArray(item)) {
var group = $("<optgroup/>", { var group = $("<optgroup/>", {

View File

@ -69,7 +69,8 @@ module.exports = function(RED) {
fs.unlink(fullFilename, function (err) { fs.unlink(fullFilename, function (err) {
if (err) { if (err) {
node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg); node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg);
} else { }
else {
if (RED.settings.verbose) { if (RED.settings.verbose) {
node.log(RED._("file.status.deletedfile",{file:filename})); node.log(RED._("file.status.deletedfile",{file:filename}));
} }
@ -82,7 +83,8 @@ module.exports = function(RED) {
if (node.createDir) { if (node.createDir) {
try { try {
fs.ensureDirSync(dir); fs.ensureDirSync(dir);
} catch(err) { }
catch(err) {
node.error(RED._("file.errors.createfail",{error:err.toString()}),msg); node.error(RED._("file.errors.createfail",{error:err.toString()}),msg);
done(); done();
return; return;
@ -96,7 +98,11 @@ module.exports = function(RED) {
if (typeof data === "boolean") { data = data.toString(); } if (typeof data === "boolean") { data = data.toString(); }
if (typeof data === "number") { data = data.toString(); } if (typeof data === "number") { data = data.toString(); }
if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; } if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; }
var buf = encode(data, node.encoding); var buf;
if (node.encoding === "setbymsg") {
buf = encode(data, msg.encoding || "none");
}
else { buf = encode(data, node.encoding); }
if (node.overwriteFile === "true") { if (node.overwriteFile === "true") {
var wstream = fs.createWriteStream(fullFilename, { encoding:'binary', flags:'w', autoClose:true }); var wstream = fs.createWriteStream(fullFilename, { encoding:'binary', flags:'w', autoClose:true });
node.wstream = wstream; node.wstream = wstream;
@ -105,10 +111,11 @@ module.exports = function(RED) {
done(); done();
}); });
wstream.on("open", function() { wstream.on("open", function() {
wstream.end(buf, function() { wstream.once("close", function() {
nodeSend(msg); nodeSend(msg);
done(); done();
}); });
wstream.end(buf);
}) })
return; return;
} }
@ -130,7 +137,8 @@ module.exports = function(RED) {
delete node.wstream; delete node.wstream;
delete node.wstreamIno; delete node.wstreamIno;
} }
} catch(err) { }
catch(err) {
// File does not exist // File does not exist
recreateStream = true; recreateStream = true;
node.wstream.end(); node.wstream.end();
@ -158,14 +166,16 @@ module.exports = function(RED) {
nodeSend(msg); nodeSend(msg);
done(); done();
}); });
} else { }
else {
// Dynamic filename - write and close the stream // Dynamic filename - write and close the stream
node.wstream.end(buf, function() { node.wstream.once("close", function() {
nodeSend(msg); nodeSend(msg);
delete node.wstream; delete node.wstream;
delete node.wstreamIno; delete node.wstreamIno;
done(); done();
}); });
node.wstream.end(buf);
} }
} }
} }
@ -284,7 +294,6 @@ module.exports = function(RED) {
ch = "\n"; ch = "\n";
type = "string"; type = "string";
} }
var hwm;
var getout = false; var getout = false;
var rs = fs.createReadStream(fullFilename) var rs = fs.createReadStream(fullFilename)
@ -348,16 +357,17 @@ module.exports = function(RED) {
nodeSend(msg); nodeSend(msg);
} }
else if (node.format === "lines") { else if (node.format === "lines") {
var m = { payload: spare, var m = {
topic:msg.topic, payload: spare,
parts: { topic:msg.topic,
index: count, parts: {
count: count+1, index: count,
ch: ch, count: count+1,
type: type, ch: ch,
id: msg._msgid type: type,
} id: msg._msgid
}; }
};
nodeSend(m); nodeSend(m);
} }
else if (getout) { // last chunk same size as high water mark - have to send empty extra packet. else if (getout) { // last chunk same size as high water mark - have to send empty extra packet.

View File

@ -363,7 +363,7 @@
"keepalive": "Keep-Alive", "keepalive": "Keep-Alive",
"cleansession": "Bereinigte Sitzung (clean session) verwenden", "cleansession": "Bereinigte Sitzung (clean session) verwenden",
"cleanstart": "Verwende bereinigten Start", "cleanstart": "Verwende bereinigten Start",
"use-tls": "Sichere Verbindung (SSL/TLS) verwenden", "use-tls": "TLS",
"tls-config": "TLS-Konfiguration", "tls-config": "TLS-Konfiguration",
"verify-server-cert": "Server-Zertifikat überprüfen", "verify-server-cert": "Server-Zertifikat überprüfen",
"compatmode": "MQTT 3.1 unterstützen", "compatmode": "MQTT 3.1 unterstützen",

View File

@ -52,10 +52,11 @@
pass <code>msg</code> as a second argument to <code>node.error</code>:</p> pass <code>msg</code> as a second argument to <code>node.error</code>:</p>
<pre>node.error("Error",msg);</pre> <pre>node.error("Error",msg);</pre>
<h4>Accessing Node Information</h4> <h4>Accessing Node Information</h4>
<p>In the function block, id and name of the node can be referenced using the following properties:</p> <p>The following properties are available to access information about the node:</p>
<ul> <ul>
<li><code>node.id</code> - id of the node</li> <li><code>node.id</code> - id of the node</li>
<li><code>node.name</code> - name of the node</li> <li><code>node.name</code> - name of the node</li>
<li><code>node.outputCount</code> - number of node outputs</li>
</ul> </ul>
<h4>Using environment variables</h4> <h4>Using environment variables</h4>
<p>Environment variables can be accessed using <code>env.get("MY_ENV_VAR")</code>.</p> <p>Environment variables can be accessed using <code>env.get("MY_ENV_VAR")</code>.</p>

View File

@ -227,6 +227,7 @@
"error": { "error": {
"externalModuleNotAllowed": "Function node not allowed to load external modules", "externalModuleNotAllowed": "Function node not allowed to load external modules",
"moduleNotAllowed": "Module __module__ not allowed", "moduleNotAllowed": "Module __module__ not allowed",
"externalModuleLoadError": "Function node failed to load external modules",
"moduleLoadError": "Failed to load module __module__: __error__", "moduleLoadError": "Failed to load module __module__: __error__",
"moduleNameError": "Invalid module variable name: __name__", "moduleNameError": "Invalid module variable name: __name__",
"moduleNameReserved": "Reserved variable name: __name__", "moduleNameReserved": "Reserved variable name: __name__",
@ -877,6 +878,7 @@
}, },
"encoding": { "encoding": {
"none": "default", "none": "default",
"setbymsg": "set by msg.encoding",
"native": "Native", "native": "Native",
"unicode": "Unicode", "unicode": "Unicode",
"japanese": "Japanese", "japanese": "Japanese",

View File

@ -91,7 +91,7 @@
</ul> </ul>
</dd> </dd>
<dt class="optional">complete</dt> <dt class="optional">complete</dt>
<dd>If set, the node will append the payload, and then send the output message in its current state. <dd>If set, the node will append the payload, and then send the output message in its current state.
If you don't wish to append the payload, delete it from the msg.</dd> If you don't wish to append the payload, delete it from the msg.</dd>
</dl> </dl>
<h3>Details</h3> <h3>Details</h3>
@ -150,6 +150,7 @@
<p>By default, the reduce expression is applied in order, from the first <p>By default, the reduce expression is applied in order, from the first
to the last message of the sequence. It can optionally be applied in to the last message of the sequence. It can optionally be applied in
reverse order.</p> reverse order.</p>
<p>$N is the number of messages that arrive - even if they are identical.</p>
</dl> </dl>
<p><b>Example:</b> the following settings, given a sequence of numeric values, <p><b>Example:</b> the following settings, given a sequence of numeric values,
calculates the average value: calculates the average value:

View File

@ -21,6 +21,8 @@
<dl class="message-properties"> <dl class="message-properties">
<dt class="optional">filename <span class="property-type">string</span></dt> <dt class="optional">filename <span class="property-type">string</span></dt>
<dd>If not configured in the node, this optional property sets the name of the file to be updated.</dd> <dd>If not configured in the node, this optional property sets the name of the file to be updated.</dd>
<dt class="optional">encoding <span class="property-type">string</span></dt>
<dd>If encoding is configured to be set by msg, then this optional property can set the encoding.</dt>
</dl> </dl>
<h3>Output</h3> <h3>Output</h3>
<p>On completion of write, input message is sent to output port.</p> <p>On completion of write, input message is sent to output port.</p>

View File

@ -44,10 +44,11 @@
<p>catchードを用いてエラー処理が可能です。catchードで処理させるためには、<code>msg</code><code>node.error</code>の第二引数として渡します:</p> <p>catchードを用いてエラー処理が可能です。catchードで処理させるためには、<code>msg</code><code>node.error</code>の第二引数として渡します:</p>
<pre>node.error("エラー",msg);</pre> <pre>node.error("エラー",msg);</pre>
<h4>ノード情報の参照</h4> <h4>ノード情報の参照</h4>
<p>コード中ではードのIDおよび名前を以下のプロパティで参照できます:</p> <p>ノードに関する情報を参照するための以下のプロパティを利用できます:</p>
<ul> <ul>
<li><code>node.id</code> - ードのID</li> <li><code>node.id</code> - ードのID</li>
<li><code>node.name</code> - ノードの名称</li> <li><code>node.name</code> - ノードの名称</li>
<li><code>node.outputCount</code> - ノードの出力数</li>
</ul> </ul>
<h4>環境変数の利用</h4> <h4>環境変数の利用</h4>
<p>環境変数は<code>env.get("MY_ENV_VAR")</code>により参照できます。</p> <p>環境変数は<code>env.get("MY_ENV_VAR")</code>により参照できます。</p>

View File

@ -227,6 +227,7 @@
"error": { "error": {
"externalModuleNotAllowed": "Functionードは、外部モジュールを読み込みできません", "externalModuleNotAllowed": "Functionードは、外部モジュールを読み込みできません",
"moduleNotAllowed": "モジュール __module__ は利用できません", "moduleNotAllowed": "モジュール __module__ は利用できません",
"externalModuleLoadError": "Functionードは、外部モジュールの読み込みに失敗しました",
"moduleLoadError": "モジュール __module__ の読み込みに失敗しました: __error__", "moduleLoadError": "モジュール __module__ の読み込みに失敗しました: __error__",
"moduleNameError": "モジュール変数名が不正です: __name__", "moduleNameError": "モジュール変数名が不正です: __name__",
"moduleNameReserved": "予約された変数名です: __name__", "moduleNameReserved": "予約された変数名です: __name__",
@ -875,6 +876,7 @@
}, },
"encoding": { "encoding": {
"none": "デフォルト", "none": "デフォルト",
"setbymsg": "msg.encodingで設定",
"native": "ネイティブ", "native": "ネイティブ",
"unicode": "UNICODE", "unicode": "UNICODE",
"japanese": "日本", "japanese": "日本",

View File

@ -20,6 +20,8 @@
<dl class="message-properties"> <dl class="message-properties">
<dt class="optional">filename <span class="property-type">文字列</span></dt> <dt class="optional">filename <span class="property-type">文字列</span></dt>
<dd>対象ファイル名をノードに設定していない場合、このプロパティでファイルを指定できます</dd> <dd>対象ファイル名をノードに設定していない場合、このプロパティでファイルを指定できます</dd>
<dt class="optional">encoding <span class="property-type">文字列</span></dt>
<dd>エンコーディングをmsgで設定する構成にした際は、この任意のプロパティでエンコーディングを設定できます。</dt>
</dl> </dl>
<h3>出力</h3> <h3>出力</h3>
<p>書き込みの完了時、入力メッセージを出力端子に送出します。</p> <p>書き込みの完了時、入力メッセージを出力端子に送出します。</p>

View File

@ -329,7 +329,7 @@
"port": "포트", "port": "포트",
"keepalive": "킵 얼라이브 시간", "keepalive": "킵 얼라이브 시간",
"cleansession": "세션 초기화", "cleansession": "세션 초기화",
"use-tls": "SSL/TLS접속을 사용", "use-tls": "사용TLS",
"tls-config": "TLS설정", "tls-config": "TLS설정",
"verify-server-cert": "서버인증서를 확인", "verify-server-cert": "서버인증서를 확인",
"compatmode": "구 MQTT 3.1서포트" "compatmode": "구 MQTT 3.1서포트"

View File

@ -352,7 +352,7 @@
"port": "Порт", "port": "Порт",
"keepalive": "Keep-alive время (сек)", "keepalive": "Keep-alive время (сек)",
"cleansession": "Использовать чистую сессию", "cleansession": "Использовать чистую сессию",
"use-tls": "Включить безопасное (SSL/TLS) соединение", "use-tls": "TLS",
"tls-config":"Конфигурация TLS", "tls-config":"Конфигурация TLS",
"verify-server-cert":"Проверить сертификат сервера", "verify-server-cert":"Проверить сертификат сервера",
"compatmode": "Использовать устаревшую поддержку MQTT 3.1" "compatmode": "Использовать устаревшую поддержку MQTT 3.1"

View File

@ -349,7 +349,7 @@
"port": "端口", "port": "端口",
"keepalive": "Keepalive计时(秒)", "keepalive": "Keepalive计时(秒)",
"cleansession": "使用新的会话", "cleansession": "使用新的会话",
"use-tls": "使用安全连接 (SSL/TLS)", "use-tls": "使用 TLS",
"tls-config": "TLS 设置", "tls-config": "TLS 设置",
"verify-server-cert": "验证服务器证书", "verify-server-cert": "验证服务器证书",
"compatmode": "使用旧式MQTT 3.1支持" "compatmode": "使用旧式MQTT 3.1支持"

View File

@ -353,7 +353,7 @@
"port": "埠", "port": "埠",
"keepalive": "Keepalive計時(秒)", "keepalive": "Keepalive計時(秒)",
"cleansession": "使用新的會話", "cleansession": "使用新的會話",
"use-tls": "使用安全連接 (SSL/TLS)", "use-tls": "使用 TLS",
"tls-config": "TLS 設置", "tls-config": "TLS 設置",
"verify-server-cert": "驗證伺服器憑證", "verify-server-cert": "驗證伺服器憑證",
"compatmode": "使用舊式MQTT 3.1支援" "compatmode": "使用舊式MQTT 3.1支援"

View File

@ -472,7 +472,7 @@ function getPackageList() {
try { try {
var userPackage = path.join(settings.userDir,"package.json"); var userPackage = path.join(settings.userDir,"package.json");
var pkg = JSON.parse(fs.readFileSync(userPackage,"utf-8")); var pkg = JSON.parse(fs.readFileSync(userPackage,"utf-8"));
return pkg.dependencies; return pkg.dependencies || {};
} catch(err) { } catch(err) {
log.error(err); log.error(err);
} }

View File

@ -95,6 +95,9 @@ function createNode(flow,config) {
} else if (nodeTypeConstructor) { } else if (nodeTypeConstructor) {
// console.log(nodeTypeConstructor) // console.log(nodeTypeConstructor)
var subflowConfig = parseConfig([nodeTypeConstructor.subflow].concat(nodeTypeConstructor.subflow.flow)); var subflowConfig = parseConfig([nodeTypeConstructor.subflow].concat(nodeTypeConstructor.subflow.flow));
var subflowInstanceConfig = subflowConfig.subflows[nodeTypeConstructor.subflow.id];
delete subflowConfig.subflows[nodeTypeConstructor.subflow.id];
subflowInstanceConfig.subflows = subflowConfig.subflows;
var instanceConfig = clone(config); var instanceConfig = clone(config);
instanceConfig.env = clone(nodeTypeConstructor.subflow.env); instanceConfig.env = clone(nodeTypeConstructor.subflow.env);
@ -124,7 +127,7 @@ function createNode(flow,config) {
nodeTypeConstructor.type, nodeTypeConstructor.type,
flow, flow,
flow.global, flow.global,
subflowConfig.subflows[nodeTypeConstructor.subflow.id], subflowInstanceConfig,
instanceConfig instanceConfig
); );
subflow.start(); subflow.start();

View File

@ -63,6 +63,27 @@ var server;
*/ */
function init(userSettings,httpServer,_adminApi) { function init(userSettings,httpServer,_adminApi) {
server = httpServer; server = httpServer;
if (server && server.on) {
// Add a listener to the upgrade event so that we can properly timeout connection
// attempts that do not get handled by any nodes in the user's flow.
// See #2956
server.on('upgrade',(request, socket, head) => {
// Add a no-op handler to the error event in case nothing upgrades this socket
// before the remote end closes it. This ensures we don't get as uncaughtException
socket.on("error", err => {})
setTimeout(function() {
// If this request has been handled elsewhere, the upgrade will have
// been completed and bytes written back to the client.
// If nothing has been written on the socket, nothing has handled the
// upgrade, so we can consider this an unhandled upgrade.
if (socket.bytesWritten === 0) {
socket.destroy();
}
},userSettings.inboundWebSocketTimeout || 5000)
});
}
userSettings.version = getVersion(); userSettings.version = getVersion();
settings.init(userSettings); settings.init(userSettings);

View File

@ -343,7 +343,11 @@ var api = module.exports = {
if (newCreds) { if (newCreds) {
delete node.credentials; delete node.credentials;
var savedCredentials = credentialCache[nodeID] || {}; var savedCredentials = credentialCache[nodeID] || {};
if (/^subflow(:|$)/.test(nodeType)) { // Need to check the type of constructor for this node.
// - Function : regular node
// - !Function: subflow module
if (/^subflow(:|$)/.test(nodeType) || typeof runtime.nodes.getType(nodeType) !== 'function') {
for (cred in newCreds) { for (cred in newCreds) {
if (newCreds.hasOwnProperty(cred)) { if (newCreds.hasOwnProperty(cred)) {
if (newCreds[cred] === "__PWRD__") { if (newCreds[cred] === "__PWRD__") {

View File

@ -612,8 +612,15 @@ async function saveFlows(flows, user) {
var workflowMode = (gitSettings.workflow||{}).mode || settings.editorTheme.projects.workflow.mode; var workflowMode = (gitSettings.workflow||{}).mode || settings.editorTheme.projects.workflow.mode;
if (workflowMode === 'auto') { if (workflowMode === 'auto') {
return activeProject.stageFile([flowsFullPath, credentialsFile]).then(() => { return activeProject.stageFile([flowsFullPath, credentialsFile]).then(() => {
return activeProject.commit(user,{message:"Update flow files"}) return activeProject.status(user, false).then((result) => {
}) const items = Object.values(result.files || {});
// check if saved flow make modification to repository
if (items.findIndex((item) => (item.status === "M ")) < 0) {
return Promise.resolve();
}
return activeProject.commit(user,{message:"Update flow files"})
});
});
} }
} }
}); });

View File

@ -57,6 +57,11 @@ module.exports = {
// defaults to the working directory of the Node-RED process. // defaults to the working directory of the Node-RED process.
//fileWorkingDirectory: "", //fileWorkingDirectory: "",
// Timeout in milliseconds for inbound WebSocket connections that do not
// match any configured node.
// defaults to 5000
//inboundWebSocketTimeout: 5000,
// The maximum length, in characters, of any message sent to the debug sidebar tab // The maximum length, in characters, of any message sent to the debug sidebar tab
debugMaxLength: 1000, debugMaxLength: 1000,

View File

@ -541,13 +541,17 @@ describe('exec node', function() {
var n2 = helper.getNode("n2"); var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3"); var n3 = helper.getNode("n3");
var n4 = helper.getNode("n4"); var n4 = helper.getNode("n4");
var payload = "";
n2.on("input", function(msg) { n2.on("input", function(msg) {
//console.log(msg); //console.log(msg);
try { try {
msg.should.have.property("payload"); msg.should.have.property("payload");
msg.payload.should.be.a.String(); msg.payload.should.be.a.String();
msg.payload.should.equal(expected); payload += msg.payload;
done(); if (payload.endsWith("\n")) {
payload.should.equal(expected);
done();
}
} }
catch(err) { done(err); } catch(err) { done(err); }
}); });
@ -567,6 +571,7 @@ describe('exec node', function() {
{id:"n2", type:"helper"},{id:"n3", type:"helper"},{id:"n4", type:"helper"}]; {id:"n2", type:"helper"},{id:"n3", type:"helper"},{id:"n4", type:"helper"}];
expected = "12345 deg C\n"; expected = "12345 deg C\n";
} }
var payload = "";
helper.load(execNode, flow, function() { helper.load(execNode, flow, function() {
var n1 = helper.getNode("n1"); var n1 = helper.getNode("n1");
@ -578,8 +583,11 @@ describe('exec node', function() {
try { try {
msg.should.have.property("payload"); msg.should.have.property("payload");
msg.payload.should.be.a.String(); msg.payload.should.be.a.String();
msg.payload.should.equal(expected); payload += msg.payload;
done(); if (payload.endsWith("\n")) {
payload.should.equal(expected);
done();
}
} }
catch(err) { done(err); } catch(err) { done(err); }
}); });
@ -661,8 +669,16 @@ describe('exec node', function() {
}; };
n2.on("input", function(msg) { n2.on("input", function(msg) {
messages[0] = msg; var payload = msg.payload;
completeTest(); if (messages[0]) {
messages[0].payload += payload;
}
else {
messages[0] = msg;
}
if (payload.endsWith("\n")) {
completeTest();
}
}); });
n4.on("input", function(msg) { n4.on("input", function(msg) {
messages[1] = msg; messages[1] = msg;
@ -869,8 +885,16 @@ describe('exec node', function() {
}; };
n2.on("input", function(msg) { n2.on("input", function(msg) {
messages[0] = msg; var payload = msg.payload;
completeTest(); if (messages[0]) {
messages[0].payload += payload;
}
else {
messages[0] = msg;
}
if (payload.endsWith("\n")) {
completeTest();
}
}); });
n4.on("input", function(msg) { n4.on("input", function(msg) {
messages[1] = msg; messages[1] = msg;

View File

@ -87,6 +87,57 @@ describe('CSV node', function() {
}); });
}); });
it('should convert a simple string to a javascript object with | separator (no template)', function(done) {
var flow = [ { id:"n1", type:"csv", sep:"|", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 });
msg.should.have.property('columns', "col1,col2,col3,col4");
check_parts(msg, 0, 1);
done();
});
var testString = "1|2|3|4"+String.fromCharCode(10);
n1.emit("input", {payload:testString});
});
});
it('should convert a simple string to a javascript object with tab separator (with template)', function(done) {
var flow = [ { id:"n1", type:"csv", sep:"\t", temp:"A,B,,D", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { A: 1, B: 2, D: 4 });
msg.should.have.property('columns', "A,B,D");
check_parts(msg, 0, 1);
done();
});
var testString = "1\t2\t3\t4"+String.fromCharCode(10);
n1.emit("input", {payload:testString});
});
});
it('should convert a simple string to a javascript object with space separator (with spaced template)', function(done) {
var flow = [ { id:"n1", type:"csv", sep:" ", temp:"A, B, , D", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { A: 1, B: 2, D: 4 });
msg.should.have.property('columns', "A,B,D");
check_parts(msg, 0, 1);
done();
});
var testString = "1 2 3 4"+String.fromCharCode(10);
n1.emit("input", {payload:testString});
});
});
it('should remove quotes and whitespace from template', function(done) { it('should remove quotes and whitespace from template', function(done) {
var flow = [ { id:"n1", type:"csv", temp:'"a", "b" , " c "," d " ', wires:[["n2"]] }, var flow = [ { id:"n1", type:"csv", temp:'"a", "b" , " c "," d " ', wires:[["n2"]] },
{id:"n2", type:"helper"} ]; {id:"n2", type:"helper"} ];
@ -170,6 +221,58 @@ describe('CSV node', function() {
n1.emit("input", {payload:testString}); n1.emit("input", {payload:testString});
}); });
}); });
it('should allow passing in a template as first line of CSV (not comma)', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"", hdrin:true, sep:";", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c;c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,c;c,"d, d"');
check_parts(msg, 0, 1);
done();
});
var testString = 'a;b b;"c;c";" d, d "'+"\n"+"1;2;3;4"+String.fromCharCode(10);
n1.emit("input", {payload:testString});
});
});
it('should allow passing in a template as first line of CSV (special char /)', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"", hdrin:true, sep:"/", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c/c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,c/c,"d, d"');
check_parts(msg, 0, 1);
done();
});
var testString = 'a/b b/"c/c"/" d, d "'+"\n"+"1/2/3/4"+String.fromCharCode(10);
n1.emit("input", {payload:testString});
});
});
it('should allow passing in a template as first line of CSV (special char \\)', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"", hdrin:true, sep:"\\", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c\\c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,c\\c,"d, d"');
check_parts(msg, 0, 1);
done();
});
var testString = 'a\\b b\\"c\\c"\\" d, d "'+"\n"+"1\\2\\3\\4"+String.fromCharCode(10);
n1.emit("input", {payload:testString});
});
});
it('should leave numbers starting with 0, e and + as strings (except 0.)', function(done) { it('should leave numbers starting with 0, e and + as strings (except 0.)', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] }, var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", wires:[["n2"]] },
{id:"n2", type:"helper"} ]; {id:"n2", type:"helper"} ];
@ -609,6 +712,24 @@ describe('CSV node', function() {
}); });
}); });
it('should convert a simple object back to a tsv using a tab as a separator', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"", sep:"\t", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n');
done();
}
catch(e) { done(e); }
});
var testJson = { d:1, b:"foo", c:"ba\"r", a:"di,ng" };
n1.emit("input", {payload:testJson});
});
});
it('should handle a template with spaces in the property names', function(done) { it('should handle a template with spaces in the property names', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"a,b o,c p,,e", wires:[["n2"]] }, var flow = [ { id:"n1", type:"csv", temp:"a,b o,c p,,e", wires:[["n2"]] },
{id:"n2", type:"helper"} ]; {id:"n2", type:"helper"} ];

View File

@ -36,102 +36,96 @@ describe('red/runtime/nodes/credentials', function() {
index.clearRegistry(); index.clearRegistry();
}); });
it('loads provided credentials',function(done) { it('loads provided credentials',function() {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings
}); });
credentials.load({"a":{"b":1,"c":2}}).then(function() { return credentials.load({"a":{"b":1,"c":2}}).then(function() {
credentials.get("a").should.have.property('b',1); credentials.get("a").should.have.property('b',1);
credentials.get("a").should.have.property('c',2); credentials.get("a").should.have.property('c',2);
done();
}); });
}); });
it('adds a new credential',function(done) { it('adds a new credential',function() {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings
}); });
credentials.load({"a":{"b":1,"c":2}}).then(function() { return credentials.load({"a":{"b":1,"c":2}}).then(function() {
credentials.dirty().should.be.false(); credentials.dirty().should.be.false();
should.not.exist(credentials.get("b")); should.not.exist(credentials.get("b"));
credentials.add("b",{"foo":"bar"}).then(function() { return credentials.add("b",{"foo":"bar"}).then(function() {
credentials.get("b").should.have.property("foo","bar"); credentials.get("b").should.have.property("foo","bar");
credentials.dirty().should.be.true(); credentials.dirty().should.be.true();
done();
}); });
}); });
}); });
it('deletes an existing credential',function(done) { it('deletes an existing credential',function() {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings
}); });
credentials.load({"a":{"b":1,"c":2}}).then(function() { return credentials.load({"a":{"b":1,"c":2}}).then(function() {
credentials.dirty().should.be.false(); credentials.dirty().should.be.false();
credentials.delete("a"); credentials.delete("a");
should.not.exist(credentials.get("a")); should.not.exist(credentials.get("a"));
credentials.dirty().should.be.true(); credentials.dirty().should.be.true();
done();
}); });
}); });
it('exports the credentials, clearing dirty flag', function(done) { it('exports the credentials, clearing dirty flag', function() {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings
}); });
var creds = {"a":{"b":1,"c":2}}; var creds = {"a":{"b":1,"c":2}};
credentials.load(creds).then(function() { return credentials.load(creds).then(function() {
credentials.add("b",{"foo":"bar"}).then(function() { return credentials.add("b",{"foo":"bar"})
credentials.dirty().should.be.true(); }).then(function() {
credentials.export().then(function(exported) { credentials.dirty().should.be.true();
exported.should.eql(creds); return credentials.export().then(function(exported) {
credentials.dirty().should.be.false(); exported.should.eql(creds);
done(); credentials.dirty().should.be.false();
}) })
});
}); });
}) })
describe("#clean",function() { describe("#clean",function() {
it("removes credentials of unknown nodes",function(done) { it("removes credentials of unknown nodes",function() {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings,
nodes: { getType: () => function(){} }
}); });
var creds = {"a":{"b":1,"c":2},"b":{"d":3}}; var creds = {"a":{"b":1,"c":2},"b":{"d":3}};
credentials.load(creds).then(function() { return credentials.load(creds).then(function() {
credentials.dirty().should.be.false(); credentials.dirty().should.be.false();
should.exist(credentials.get("a")); should.exist(credentials.get("a"));
should.exist(credentials.get("b")); should.exist(credentials.get("b"));
credentials.clean([{id:"b"}]).then(function() { return credentials.clean([{id:"b"}]).then(function() {
credentials.dirty().should.be.true(); credentials.dirty().should.be.true();
should.not.exist(credentials.get("a")); should.not.exist(credentials.get("a"));
should.exist(credentials.get("b")); should.exist(credentials.get("b"));
done();
}); });
}); });
}); });
it("extracts credentials of known nodes",function(done) { it("extracts credentials of known nodes",function() {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings,
nodes: { getType: () => function(){} }
}); });
credentials.register("testNode",{"b":"text","c":"password"}) credentials.register("testNode",{"b":"text","c":"password"})
var creds = {"a":{"b":1,"c":2}}; var creds = {"a":{"b":1,"c":2}};
var newConfig = [{id:"a",type:"testNode",credentials:{"b":"newBValue","c":"newCValue"}}]; var newConfig = [{id:"a",type:"testNode",credentials:{"b":"newBValue","c":"newCValue"}}];
credentials.load(creds).then(function() { return credentials.load(creds).then(function() {
credentials.dirty().should.be.false(); credentials.dirty().should.be.false();
credentials.clean(newConfig).then(function() { return credentials.clean(newConfig).then(function() {
credentials.dirty().should.be.true(); credentials.dirty().should.be.true();
credentials.get("a").should.have.property('b',"newBValue"); credentials.get("a").should.have.property('b',"newBValue");
credentials.get("a").should.have.property('c',"newCValue"); credentials.get("a").should.have.property('c',"newCValue");
should.not.exist(newConfig[0].credentials); should.not.exist(newConfig[0].credentials);
done();
}); });
}); });
}); });
@ -139,12 +133,13 @@ describe('red/runtime/nodes/credentials', function() {
}); });
it('warns if a node has no credential definition', function(done) { it('warns if a node has no credential definition', function() {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings,
nodes: { getType: () => function(){} }
}); });
credentials.load({}).then(function() { return credentials.load({}).then(function() {
var node = {id:"node",type:"test",credentials:{ var node = {id:"node",type:"test",credentials:{
user1:"newUser", user1:"newUser",
password1:"newPassword" password1:"newPassword"
@ -154,14 +149,14 @@ describe('red/runtime/nodes/credentials', function() {
log.warn.called.should.be.true(); log.warn.called.should.be.true();
should.not.exist(node.credentials); should.not.exist(node.credentials);
log.warn.restore(); log.warn.restore();
done();
}); });
}) })
it('extract credential updates in the provided node', function(done) { it('extract credential updates in the provided node', function(done) {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings,
nodes: { getType: () => function(){} }
}); });
var defintion = { var defintion = {
user1:{type:"text"}, user1:{type:"text"},
@ -205,7 +200,8 @@ describe('red/runtime/nodes/credentials', function() {
it('extract ignores node without credentials', function(done) { it('extract ignores node without credentials', function(done) {
credentials.init({ credentials.init({
log: log, log: log,
settings: encryptionDisabledSettings settings: encryptionDisabledSettings,
nodes: { getType: () => function(){} }
}); });
credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() { credentials.load({"node":{user1:"abc",password1:"123"}}).then(function() {
var node = {id:"node",type:"test"}; var node = {id:"node",type:"test"};
@ -233,7 +229,8 @@ describe('red/runtime/nodes/credentials', function() {
delete settings[key]; delete settings[key];
return Promise.resolve(); return Promise.resolve();
} }
} },
nodes: { getType: () => function(){} }
} }
it('migrates to encrypted and generates default key', function(done) { it('migrates to encrypted and generates default key', function(done) {
settings = {}; settings = {};
@ -341,7 +338,7 @@ describe('red/runtime/nodes/credentials', function() {
}); });
}); });
}); });
it('migrates from default key to user key', function(done) { it('migrates from default key to user key', function() {
settings = { settings = {
_credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a", _credentialSecret: "e3a36f47f005bf2aaa51ce3fc6fcaafd79da8d03f2b1a9281f8fb0a285e6255a",
credentialSecret: "aaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbcccccccccccccddddddddddddeeeee" credentialSecret: "aaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbcccccccccccccddddddddddddeeeee"
@ -349,21 +346,20 @@ describe('red/runtime/nodes/credentials', function() {
// {"node":{user1:"abc",password1:"123"}} // {"node":{user1:"abc",password1:"123"}}
var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"}; var cryptedFlows = {"$":"5b89d8209b5158a3c313675561b1a5b5phN1gDBe81Zv98KqS/hVDmc9EKvaKqRIvcyXYvBlFNzzzJtvN7qfw06i"};
credentials.init(runtime); credentials.init(runtime);
credentials.load(cryptedFlows).then(function() { return credentials.load(cryptedFlows).then(function() {
credentials.dirty().should.be.true(); credentials.dirty().should.be.true();
should.exist(credentials.get("node")); should.exist(credentials.get("node"));
credentials.export().then(function(result) { return credentials.export().then(function(result) {
result.should.have.a.property("$"); result.should.have.a.property("$");
settings.should.not.have.a.property("_credentialSecret"); settings.should.not.have.a.property("_credentialSecret");
// reset everything - but with _credentialSecret still set // reset everything - but with _credentialSecret still set
credentials.init(runtime); credentials.init(runtime);
// load the freshly encrypted version // load the freshly encrypted version
credentials.load(result).then(function() { return credentials.load(result).then(function() {
should.exist(credentials.get("node")); should.exist(credentials.get("node"));
credentials.get("node").should.have.a.property("user1","abc"); credentials.get("node").should.have.a.property("user1","abc");
credentials.get("node").should.have.a.property("password1","123"); credentials.get("node").should.have.a.property("password1","123");
done();
}) })
}); });
}); });
@ -459,7 +455,8 @@ describe('red/runtime/nodes/credentials', function() {
set: function(key,value) { set: function(key,value) {
throw new Error(); throw new Error();
} }
} },
nodes: { getType: () => function(){} }
} }
// {"node":{user1:"abc",password1:"123"}} // {"node":{user1:"abc",password1:"123"}}
credentials.init(runtime); credentials.init(runtime);