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 runtime-api

This commit is contained in:
Nick O'Leary 2018-07-29 23:47:19 +01:00
commit 9c4a712dc7
147 changed files with 12297 additions and 2046 deletions

View File

@ -1,3 +1,65 @@
#### 0.19: Milestone Release
Editor
- Add editorTheme.palette.theme to allow overriding colours
- Index all node properties when searching Fixes #1446
- Handle NaN and Infinity properly in debug sidebar Fixes #1778 #1779
- Prevent horizontal scroll when palette name cannot wrap
- Ignore middle-click on node/ports to enable panning
- Better wire layout when looping back
- fix appearence of retry button of remote branch management dialog
- Handle releasing ctrl when using quick-add node dialog
- Add $env function to JSONata expressions
- Widen support for env var to use ${} or $() syntax
- Add env-var support to TypedInput
- Show unknown node properties in info tab
- Add node icon picker widget
- Only edit nodes on dbl click on primary button with no modifiers
- Allow subflows to be put in any palette category
- Add flow navigator widget
- Cache flow library result to improve response time Fixes #1753
- Add middle-button-drag to pan the workspace
- allow multi-line category name in editor
- Redesign sidebar tabs
- Do not disable the export-clipboard menu option with empty selection
Nodes
- Change: Ensure runtime errors in Change node can be caught Fixes #1769
- File: Add output to File Out node
- Function: add expandable JavaScript editor pane
- Function: allow id and name reference in function node code (#1731)
- HTTP Request: Move to request module
- HTTP: Ensure apiMaxLength applies to HTTP Nodes Fixes #1278
- Join: accumulate top level properties
- Join: allow environment variable as reduce init value
- JSON: add JSON schema validation via msg.schema
- Pi: Let nrgpio code work with python 3
- Pi: let Pi nodes be visible/editable on all platforms
- Switch: add isEmpty rule
- TCP: queue messages while connecting; closes #1414
- TLS: Add servername option to TLS config node for SNI Fixes #1805
- UDP: Don't accidentally re-use udp port when set to not do so
Persistent Context
- Add Context data sidebar
- Add persistable context option
- Add default memory store
- Add file-based context store
- Add async mode to evaluateJSONataExpression
- Update RED.util.evaluateNodeProperty to support context stores
Runtime
- Support flow.disabled and .info in /flow API
- Node errors should be Strings not Errors Fixes #1781
- Add detection of connection timeout in git communication Fixes #1770
- Handle loading empty nodesDir
- Add 'private' property to userDir generated package.json
- Add RED.require to allow nodes to access other modules
#### 0.18.7: Maintenance Release #### 0.18.7: Maintenance Release
Editor Fixes Editor Fixes

View File

@ -24,6 +24,10 @@ module.exports = function(grunt) {
nodemonArgs.push(flowFile); nodemonArgs.push(flowFile);
} }
var nonHeadless = grunt.option('non-headless');
if (nonHeadless) {
process.env.NODE_RED_NON_HEADLESS = 'true';
}
grunt.initConfig({ grunt.initConfig({
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
paths: { paths: {
@ -144,10 +148,12 @@ module.exports = function(grunt) {
"editor/js/ui/keyboard.js", "editor/js/ui/keyboard.js",
"editor/js/ui/workspaces.js", "editor/js/ui/workspaces.js",
"editor/js/ui/view.js", "editor/js/ui/view.js",
"editor/js/ui/view-navigator.js",
"editor/js/ui/sidebar.js", "editor/js/ui/sidebar.js",
"editor/js/ui/palette.js", "editor/js/ui/palette.js",
"editor/js/ui/tab-info.js", "editor/js/ui/tab-info.js",
"editor/js/ui/tab-config.js", "editor/js/ui/tab-config.js",
"editor/js/ui/tab-context.js",
"editor/js/ui/palette-editor.js", "editor/js/ui/palette-editor.js",
"editor/js/ui/editor.js", "editor/js/ui/editor.js",
"editor/js/ui/editors/*.js", "editor/js/ui/editors/*.js",

BIN
editor/icons/file-in.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

BIN
editor/icons/file-out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

View File

@ -10,6 +10,7 @@
"ctrl-g i": "core:show-info-tab", "ctrl-g i": "core:show-info-tab",
"ctrl-g d": "core:show-debug-tab", "ctrl-g d": "core:show-debug-tab",
"ctrl-g c": "core:show-config-tab", "ctrl-g c": "core:show-config-tab",
"ctrl-g x": "core:show-context-tab",
"ctrl-e": "core:show-export-dialog", "ctrl-e": "core:show-export-dialog",
"ctrl-i": "core:show-import-dialog", "ctrl-i": "core:show-import-dialog",
"ctrl-space": "core:toggle-sidebar", "ctrl-space": "core:toggle-sidebar",

View File

@ -133,7 +133,7 @@ RED.nodes = (function() {
registerNodeType: function(nt,def) { registerNodeType: function(nt,def) {
nodeDefinitions[nt] = def; nodeDefinitions[nt] = def;
def.type = nt; def.type = nt;
if (def.category != "subflows") { if (nt.substring(0,8) != "subflow:") {
def.set = nodeSets[typeToId[nt]]; def.set = nodeSets[typeToId[nt]];
nodeSets[typeToId[nt]].added = true; nodeSets[typeToId[nt]].added = true;
nodeSets[typeToId[nt]].enabled = true; nodeSets[typeToId[nt]].enabled = true;
@ -356,7 +356,7 @@ RED.nodes = (function() {
defaults:{name:{value:""}}, defaults:{name:{value:""}},
info: sf.info, info: sf.info,
icon: function() { return sf.icon||"subflow.png" }, icon: function() { return sf.icon||"subflow.png" },
category: "subflows", category: sf.category || "subflows",
inputs: sf.in.length, inputs: sf.in.length,
outputs: sf.out.length, outputs: sf.out.length,
color: "#da9", color: "#da9",
@ -519,6 +519,7 @@ RED.nodes = (function() {
node.type = n.type; node.type = n.type;
node.name = n.name; node.name = n.name;
node.info = n.info; node.info = n.info;
node.category = n.category;
node.in = []; node.in = [];
node.out = []; node.out = [];

View File

@ -276,10 +276,21 @@ RED.clipboard = (function() {
if (typeof value !== "string" ) { if (typeof value !== "string" ) {
value = JSON.stringify(value, function(key,value) { value = JSON.stringify(value, function(key,value) {
if (value !== null && typeof value === 'object') { if (value !== null && typeof value === 'object') {
if (value.__encoded__ && value.hasOwnProperty('data') && value.hasOwnProperty('length')) { if (value.__enc__) {
if (value.hasOwnProperty('data') && value.hasOwnProperty('length')) {
truncated = value.data.length !== value.length; truncated = value.data.length !== value.length;
return value.data; return value.data;
} }
if (value.type === 'function' || value.type === 'internal') {
return undefined
}
if (value.type === 'number') {
// Handle NaN and Infinity - they are not permitted
// in JSON. We can either substitute with a String
// representation or null
return null;
}
}
} }
return value; return value;
}); });

View File

@ -48,23 +48,31 @@ RED.popover = (function() {
var openPopup = function(instant) { var openPopup = function(instant) {
if (active) { if (active) {
div = $('<div class="red-ui-popover red-ui-popover-'+direction+'"></div>').appendTo("body"); div = $('<div class="red-ui-popover red-ui-popover-'+direction+'"></div>');
if (size !== "default") { if (size !== "default") {
div.addClass("red-ui-popover-size-"+size); div.addClass("red-ui-popover-size-"+size);
} }
if (typeof content === 'function') { if (typeof content === 'function') {
content.call(res).appendTo(div); var result = content.call(res);
if (result === null) {
return;
}
if (typeof result === 'string') {
div.text(result);
} else {
div.append(result);
}
} else { } else {
div.html(content); div.html(content);
} }
if (width !== "auto") { if (width !== "auto") {
div.width(width); div.width(width);
} }
div.appendTo("body");
var targetPos = target.offset(); var targetPos = target.offset();
var targetWidth = target.width(); var targetWidth = target.outerWidth();
var targetHeight = target.height(); var targetHeight = target.outerHeight();
var divHeight = div.height(); var divHeight = div.height();
var divWidth = div.width(); var divWidth = div.width();
if (direction === 'right') { if (direction === 'right') {
@ -147,7 +155,17 @@ RED.popover = (function() {
} }
return { return {
create: createPopover create: createPopover,
tooltip: function(target,content) {
RED.popover.create({
target:target,
trigger: "hover",
size: "small",
direction: "bottom",
content: content,
delay: { show: 550, hide: 10 }
});
}
} }
})(); })();

View File

@ -17,6 +17,9 @@
RED.tabs = (function() { RED.tabs = (function() {
var defaultTabIcon = "fa fa-lemon-o";
function createTabs(options) { function createTabs(options) {
var tabs = {}; var tabs = {};
var pinnedTabsCount = 0; var pinnedTabsCount = 0;
@ -71,6 +74,7 @@ RED.tabs = (function() {
var id = $(el).data('tabId'); var id = $(el).data('tabId');
var opt = { var opt = {
id:"red-ui-tabs-menu-option-"+id, id:"red-ui-tabs-menu-option-"+id,
icon: tabs[id].iconClass || defaultTabIcon,
label: tabs[id].name, label: tabs[id].name,
onselect: function() { onselect: function() {
activateTab(id); activateTab(id);
@ -90,12 +94,12 @@ RED.tabs = (function() {
collapsibleMenu.on('mouseleave', function(){ $(this).hide() }); collapsibleMenu.on('mouseleave', function(){ $(this).hide() });
collapsibleMenu.on('mouseup', function() { $(this).hide() }); collapsibleMenu.on('mouseup', function() { $(this).hide() });
collapsibleMenu.appendTo("body"); collapsibleMenu.appendTo("body");
}
var elementPos = selectButton.offset(); var elementPos = selectButton.offset();
collapsibleMenu.css({ collapsibleMenu.css({
top: (elementPos.top+selectButton.height()-20)+"px", top: (elementPos.top+selectButton.height()-20)+"px",
left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px" left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px"
}) })
}
collapsibleMenu.toggle(); collapsibleMenu.toggle();
}) })
@ -170,8 +174,8 @@ RED.tabs = (function() {
ul.children().css({"transition": "width 100ms"}); ul.children().css({"transition": "width 100ms"});
link.parent().addClass("active"); link.parent().addClass("active");
var parentId = link.parent().attr('id'); var parentId = link.parent().attr('id');
wrapper.find(".red-ui-tab-link-button").removeClass("active"); wrapper.find(".red-ui-tab-link-button").removeClass("active selected");
$("#"+parentId+"-link-button").addClass("active"); $("#"+parentId+"-link-button").addClass("active selected");
if (options.scrollable) { if (options.scrollable) {
var pos = link.parent().position().left; var pos = link.parent().position().left;
if (pos-21 < 0) { if (pos-21 < 0) {
@ -339,7 +343,7 @@ RED.tabs = (function() {
if (tab.iconClass) { if (tab.iconClass) {
$('<i>',{class:tab.iconClass}).appendTo(pinnedLink); $('<i>',{class:tab.iconClass}).appendTo(pinnedLink);
} else { } else {
$('<i>',{class:"fa fa-lemon-o"}).appendTo(pinnedLink); $('<i>',{class:defaultTabIcon}).appendTo(pinnedLink);
} }
pinnedLink.click(function(evt) { pinnedLink.click(function(evt) {
evt.preventDefault(); evt.preventDefault();
@ -349,14 +353,7 @@ RED.tabs = (function() {
pinnedLink.addClass("red-ui-tab-link-button-pinned"); pinnedLink.addClass("red-ui-tab-link-button-pinned");
pinnedTabsCount++; pinnedTabsCount++;
} }
RED.popover.create({ RED.popover.tooltip($(pinnedLink), tab.name);
target:$(pinnedLink),
trigger: "hover",
size: "small",
direction: "bottom",
content: tab.name,
delay: { show: 550, hide: 10 }
});
} }
link.on("click",onTabClick); link.on("click",onTabClick);
@ -370,7 +367,6 @@ RED.tabs = (function() {
removeTab(tab.id); removeTab(tab.id);
}); });
} }
updateTabWidths();
if (options.onadd) { if (options.onadd) {
options.onadd(tab); options.onadd(tab);
} }
@ -455,6 +451,9 @@ RED.tabs = (function() {
} }
}) })
} }
setTimeout(function() {
updateTabWidths();
},10);
collapsibleMenu = null; collapsibleMenu = null;
}, },
removeTab: removeTab, removeTab: removeTab,

View File

@ -14,10 +14,38 @@
* limitations under the License. * limitations under the License.
**/ **/
(function($) { (function($) {
var contextParse = function(v) {
var parts = RED.utils.parseContextKey(v);
return {
option: parts.store,
value: parts.key
}
}
var contextExport = function(v,opt) {
if (!opt) {
return v;
}
var store = ((typeof opt === "string")?opt:opt.value)
if (store !== RED.settings.context.default) {
return "#:("+store+")::"+v;
} else {
return v;
}
}
var allOptions = { var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression}, msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression},
flow: {value:"flow",label:"flow.",validate:RED.utils.validatePropertyExpression}, flow: {value:"flow",label:"flow.",hasValue:true,
global: {value:"global",label:"global.",validate:RED.utils.validatePropertyExpression}, options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport
},
global: {value:"global",label:"global.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport
},
str: {value:"str",label:"string",icon:"red/images/typedInput/az.png"}, str: {value:"str",label:"string",icon:"red/images/typedInput/az.png"},
num: {value:"num",label:"number",icon:"red/images/typedInput/09.png",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/}, num: {value:"num",label:"number",icon:"red/images/typedInput/09.png",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/},
bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]}, bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]},
@ -87,25 +115,40 @@
$.widget( "nodered.typedInput", { $.widget( "nodered.typedInput", {
_create: function() { _create: function() {
try {
if (!nlsd && RED && RED._) { if (!nlsd && RED && RED._) {
for (var i in allOptions) { for (var i in allOptions) {
if (allOptions.hasOwnProperty(i)) { if (allOptions.hasOwnProperty(i)) {
allOptions[i].label = RED._("typedInput.type."+i,{defaultValue:allOptions[i].label}); allOptions[i].label = RED._("typedInput.type."+i,{defaultValue:allOptions[i].label});
} }
} }
var contextStores = RED.settings.context.stores;
var contextOptions = contextStores.map(function(store) {
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database" style="color: #'+(store==='memory'?'ddd':'777')+'"></i>'}
})
if (contextOptions.length < 2) {
allOptions.flow.options = [];
allOptions.global.options = [];
} else {
allOptions.flow.options = contextOptions;
allOptions.global.options = contextOptions;
}
} }
nlsd = true; nlsd = true;
var that = this; var that = this;
this.disarmClick = false; this.disarmClick = false;
this.input = $('<input type="text"></input>');
this.input.insertAfter(this.element);
this.input.val(this.element.val());
this.element.addClass('red-ui-typedInput'); this.element.addClass('red-ui-typedInput');
this.uiWidth = this.element.outerWidth(); this.uiWidth = this.element.outerWidth();
this.elementDiv = this.element.wrap("<div>").parent().addClass('red-ui-typedInput-input'); this.elementDiv = this.input.wrap("<div>").parent().addClass('red-ui-typedInput-input');
this.uiSelect = this.elementDiv.wrap( "<div>" ).parent(); this.uiSelect = this.elementDiv.wrap( "<div>" ).parent();
var attrStyle = this.element.attr('style'); var attrStyle = this.element.attr('style');
var m; var m;
if ((m = /width\s*:\s*(calc\s*\(.*\)|\d+(%|px))/i.exec(attrStyle)) !== null) { if ((m = /width\s*:\s*(calc\s*\(.*\)|\d+(%|px))/i.exec(attrStyle)) !== null) {
this.element.css('width','100%'); this.input.css('width','100%');
this.uiSelect.width(m[1]); this.uiSelect.width(m[1]);
this.uiWidth = null; this.uiWidth = null;
} else { } else {
@ -114,17 +157,19 @@
["Right","Left"].forEach(function(d) { ["Right","Left"].forEach(function(d) {
var m = that.element.css("margin"+d); var m = that.element.css("margin"+d);
that.uiSelect.css("margin"+d,m); that.uiSelect.css("margin"+d,m);
that.element.css("margin"+d,0); that.input.css("margin"+d,0);
}); });
this.uiSelect.addClass("red-ui-typedInput-container"); this.uiSelect.addClass("red-ui-typedInput-container");
this.element.attr('type','hidden');
this.options.types = this.options.types||Object.keys(allOptions); this.options.types = this.options.types||Object.keys(allOptions);
this.selectTrigger = $('<button tabindex="0"></button>').prependTo(this.uiSelect); this.selectTrigger = $('<button tabindex="0"></button>').prependTo(this.uiSelect);
if (this.options.types.length > 1) { $('<i class="red-ui-typedInput-icon fa fa-sort-desc"></i>').toggle(this.options.types.length > 1).appendTo(this.selectTrigger);
$('<i class="fa fa-sort-desc"></i>').appendTo(this.selectTrigger);
} this.selectLabel = $('<span class="red-ui-typedInput-type-label"></span>').appendTo(this.selectTrigger);
this.selectLabel = $('<span></span>').appendTo(this.selectTrigger);
this.types(this.options.types); this.types(this.options.types);
@ -138,14 +183,16 @@
this.typeField = $("<input>",{type:'hidden'}).appendTo(this.uiSelect); this.typeField = $("<input>",{type:'hidden'}).appendTo(this.uiSelect);
} }
this.element.on('focus', function() { this.input.on('focus', function() {
that.uiSelect.addClass('red-ui-typedInput-focus'); that.uiSelect.addClass('red-ui-typedInput-focus');
}); });
this.element.on('blur', function() { this.input.on('blur', function() {
that.uiSelect.removeClass('red-ui-typedInput-focus'); that.uiSelect.removeClass('red-ui-typedInput-focus');
}); });
this.element.on('change', function() { this.input.on('change', function() {
that.validate(); that.validate();
that.element.val(that.value());
that.element.trigger('change',that.propertyType,that.value());
}) })
this.selectTrigger.click(function(event) { this.selectTrigger.click(function(event) {
event.preventDefault(); event.preventDefault();
@ -161,8 +208,11 @@
}) })
// 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'
this.optionSelectTrigger = $('<button tabindex="0" class="red-ui-typedInput-option-trigger" style="display:inline-block"><span class="red-ui-typedInput-option-caret"><i class="fa fa-sort-desc"></i></span></button>').appendTo(this.uiSelect); this.optionSelectTrigger = $('<button tabindex="0" class="red-ui-typedInput-option-trigger" style="display:inline-block"><span class="red-ui-typedInput-option-caret"><i class="red-ui-typedInput-icon fa fa-sort-desc"></i></span></button>').appendTo(this.uiSelect);
this.optionSelectLabel = $('<span class="red-ui-typedInput-option-label"></span>').prependTo(this.optionSelectTrigger); this.optionSelectLabel = $('<span class="red-ui-typedInput-option-label"></span>').prependTo(this.optionSelectTrigger);
RED.popover.tooltip(this.optionSelectLabel,function() {
return that.optionValue;
});
this.optionSelectTrigger.click(function(event) { this.optionSelectTrigger.click(function(event) {
event.preventDefault(); event.preventDefault();
that._showOptionSelectMenu(); that._showOptionSelectMenu();
@ -177,17 +227,18 @@
that.uiSelect.addClass('red-ui-typedInput-focus'); that.uiSelect.addClass('red-ui-typedInput-focus');
}); });
this.optionExpandButton = $('<button tabindex="0" class="red-ui-typedInput-option-expand" style="display:inline-block"><i class="fa fa-ellipsis-h"></i></button>').appendTo(this.uiSelect); this.optionExpandButton = $('<button tabindex="0" class="red-ui-typedInput-option-expand" style="display:inline-block"><i class="red-ui-typedInput-icon fa fa-ellipsis-h"></i></button>').appendTo(this.uiSelect);
this.type(this.options.default||this.typeList[0].value); this.type(this.options.default||this.typeList[0].value);
}catch(err) {
console.log(err.stack);
}
}, },
_showTypeMenu: function() { _showTypeMenu: function() {
if (this.typeList.length > 1) { if (this.typeList.length > 1) {
this._showMenu(this.menu,this.selectTrigger); this._showMenu(this.menu,this.selectTrigger);
this.menu.find("[value='"+this.propertyType+"']").focus(); this.menu.find("[value='"+this.propertyType+"']").focus();
} else { } else {
this.element.focus(); this.input.focus();
} }
}, },
_showOptionSelectMenu: function() { _showOptionSelectMenu: function() {
@ -196,8 +247,8 @@
minWidth:this.optionSelectLabel.width() minWidth:this.optionSelectLabel.width()
}); });
this._showMenu(this.optionMenu,this.optionSelectLabel); this._showMenu(this.optionMenu,this.optionSelectTrigger);
var selectedOption = this.optionMenu.find("[value='"+this.value()+"']"); var selectedOption = this.optionMenu.find("[value='"+this.optionValue+"']");
if (selectedOption.length === 0) { if (selectedOption.length === 0) {
selectedOption = this.optionMenu.children(":first"); selectedOption = this.optionMenu.children(":first");
} }
@ -209,7 +260,7 @@
$(document).off("mousedown.close-property-select"); $(document).off("mousedown.close-property-select");
menu.hide(); menu.hide();
if (this.elementDiv.is(":visible")) { if (this.elementDiv.is(":visible")) {
this.element.focus(); this.input.focus();
} else if (this.optionSelectTrigger.is(":visible")){ } else if (this.optionSelectTrigger.is(":visible")){
this.optionSelectTrigger.focus(); this.optionSelectTrigger.focus();
} else { } else {
@ -228,10 +279,19 @@
op.text(opt.label); op.text(opt.label);
} }
if (opt.icon) { if (opt.icon) {
if (opt.icon.indexOf("<") === 0) {
$(opt.icon).prependTo(op);
} else if (opt.icon.indexOf("/") !== -1) {
$('<img>',{src:opt.icon,style:"margin-right: 4px; height: 18px;"}).prependTo(op); $('<img>',{src:opt.icon,style:"margin-right: 4px; height: 18px;"}).prependTo(op);
} else {
$('<i>',{class:"red-ui-typedInput-icon "+opt.icon}).prependTo(op);
}
} else { } else {
op.css({paddingLeft: "18px"}); op.css({paddingLeft: "18px"});
} }
if (!opt.icon && !opt.label) {
op.text(opt.value);
}
op.click(function(event) { op.click(function(event) {
event.preventDefault(); event.preventDefault();
@ -310,7 +370,8 @@
if (this.uiWidth !== null) { if (this.uiWidth !== null) {
this.uiSelect.width(this.uiWidth); this.uiSelect.width(this.uiWidth);
} }
if (this.typeMap[this.propertyType] && this.typeMap[this.propertyType].hasValue === false) { var type = this.typeMap[this.propertyType];
if (type && type.hasValue === false) {
this.selectTrigger.addClass("red-ui-typedInput-full-width"); this.selectTrigger.addClass("red-ui-typedInput-full-width");
} else { } else {
this.selectTrigger.removeClass("red-ui-typedInput-full-width"); this.selectTrigger.removeClass("red-ui-typedInput-full-width");
@ -320,13 +381,68 @@
this.elementDiv.css('right',"22px"); this.elementDiv.css('right',"22px");
} else { } else {
this.elementDiv.css('right','0'); this.elementDiv.css('right','0');
this.input.css({
'border-top-right-radius': '4px',
'border-bottom-right-radius': '4px'
});
} }
// if (this.optionSelectTrigger) {
// this.optionSelectTrigger.css({'left':(labelWidth)+"px",'width':'calc( 100% - '+labelWidth+'px )'});
// }
if (this.optionSelectTrigger) { if (this.optionSelectTrigger) {
this.optionSelectTrigger.css({'left':(labelWidth)+"px",'width':'calc( 100% - '+labelWidth+'px )'}); if (type && type.options && type.hasValue === true) {
this.optionSelectLabel.css({'left':'auto'})
var lw = this._getLabelWidth(this.optionSelectLabel);
this.optionSelectTrigger.css({'width':(23+lw)+"px"});
this.elementDiv.css('right',(23+lw)+"px");
this.input.css({
'border-top-right-radius': 0,
'border-bottom-right-radius': 0
});
} else {
this.optionSelectLabel.css({'left':'0'})
this.optionSelectTrigger.css({'width':'calc( 100% - '+labelWidth+'px )'});
if (!this.optionExpandButton.is(":visible")) {
this.elementDiv.css({'right':0});
this.input.css({
'border-top-right-radius': '4px',
'border-bottom-right-radius': '4px'
});
}
}
} }
} }
}, },
_updateOptionSelectLabel: function(o) {
var opt = this.typeMap[this.propertyType];
this.optionSelectLabel.empty();
if (o.icon) {
if (o.icon.indexOf("<") === 0) {
$(o.icon).prependTo(this.optionSelectLabel);
} else if (o.icon.indexOf("/") !== -1) {
// url
$('<img>',{src:o.icon,style:"height: 18px;"}).prependTo(this.optionSelectLabel);
} else {
// icon class
$('<i>',{class:"red-ui-typedInput-icon "+o.icon}).prependTo(this.optionSelectLabel);
}
} else if (o.label) {
this.optionSelectLabel.text(o.label);
} else {
this.optionSelectLabel.text(o.value);
}
if (opt.hasValue) {
this.optionValue = o.value;
this._resize();
this.input.trigger('change',this.propertyType,this.value());
}
},
_destroy: function() { _destroy: function() {
if (this.optionMenu) {
this.optionMenu.remove();
}
this.menu.remove(); this.menu.remove();
}, },
types: function(types) { types: function(types) {
@ -344,13 +460,18 @@
return result; return result;
}); });
this.selectTrigger.toggleClass("disabled", this.typeList.length === 1); this.selectTrigger.toggleClass("disabled", this.typeList.length === 1);
this.selectTrigger.find(".fa-sort-desc").toggle(this.typeList.length > 1)
if (this.menu) { if (this.menu) {
this.menu.remove(); this.menu.remove();
} }
this.menu = this._createMenu(this.typeList, function(v) { that.type(v) }); this.menu = this._createMenu(this.typeList, function(v) { that.type(v) });
if (currentType && !this.typeMap.hasOwnProperty(currentType)) { if (currentType && !this.typeMap.hasOwnProperty(currentType)) {
this.type(this.typeList[0].value); this.type(this.typeList[0].value);
} else {
this.propertyType = null;
this.type(currentType);
} }
setTimeout(function() {that._resize();},0);
}, },
width: function(desiredWidth) { width: function(desiredWidth) {
this.uiWidth = desiredWidth; this.uiWidth = desiredWidth;
@ -358,33 +479,33 @@
}, },
value: function(value) { value: function(value) {
if (!arguments.length) { if (!arguments.length) {
return this.element.val(); var v = this.input.val();
if (this.typeMap[this.propertyType].export) {
v = this.typeMap[this.propertyType].export(v,this.optionValue)
}
return v;
} else { } else {
var selectedOption;
if (this.typeMap[this.propertyType].options) { if (this.typeMap[this.propertyType].options) {
var validValue = false;
var label;
for (var i=0;i<this.typeMap[this.propertyType].options.length;i++) { for (var i=0;i<this.typeMap[this.propertyType].options.length;i++) {
var op = this.typeMap[this.propertyType].options[i]; var op = this.typeMap[this.propertyType].options[i];
if (typeof op === "string") { if (typeof op === "string") {
if (op === value) { if (op === value) {
label = value; selectedOption = this.activeOptions[op];
validValue = true;
break; break;
} }
} else if (op.value === value) { } else if (op.value === value) {
label = op.label||op.value; selectedOption = op;
validValue = true;
break; break;
} }
} }
if (!validValue) { if (!selectedOption) {
value = ""; selectedOption = {value:""}
label = "";
} }
this.optionSelectLabel.text(label); this._updateOptionSelectLabel(selectedOption)
} }
this.element.val(value); this.input.val(value);
this.element.trigger('change',this.type(),value); this.input.trigger('change',this.type(),value);
} }
}, },
type: function(type) { type: function(type) {
@ -395,7 +516,9 @@
var opt = this.typeMap[type]; var opt = this.typeMap[type];
if (opt && this.propertyType !== type) { if (opt && this.propertyType !== type) {
this.propertyType = type; this.propertyType = type;
if (this.typeField) {
this.typeField.val(type); this.typeField.val(type);
}
this.selectLabel.empty(); this.selectLabel.empty();
var image; var image;
if (opt.icon) { if (opt.icon) {
@ -412,24 +535,41 @@
} }
if (this.optionSelectTrigger) { if (this.optionSelectTrigger) {
this.optionSelectTrigger.show(); this.optionSelectTrigger.show();
if (!opt.hasValue) {
this.elementDiv.hide(); this.elementDiv.hide();
this.optionMenu = this._createMenu(opt.options,function(v){ } else {
that.optionSelectLabel.text(v); this.elementDiv.show();
that.value(v); }
this.activeOptions = {};
opt.options.forEach(function(o) {
if (typeof o === 'string') {
that.activeOptions[o] = {label:o,value:o};
} else {
that.activeOptions[o.value] = o;
}
});
if (!that.activeOptions.hasOwnProperty(that.optionValue)) {
that.optionValue = null;
}
this.optionMenu = this._createMenu(opt.options,function(v){
that._updateOptionSelectLabel(that.activeOptions[v]);
if (!opt.hasValue) {
that.value(that.activeOptions[v].value)
}
}); });
var currentVal = this.element.val();
var validValue = false;
var op; var op;
if (!opt.hasValue) {
var currentVal = this.input.val();
var validValue = false;
for (var i=0;i<opt.options.length;i++) { for (var i=0;i<opt.options.length;i++) {
op = opt.options[i]; op = opt.options[i];
if (typeof op === "string") { if (typeof op === "string" && op === currentVal) {
if (op === currentVal) { that._updateOptionSelectLabel({value:currentVal});
this.optionSelectLabel.text(currentVal);
validValue = true; validValue = true;
break; break;
}
} else if (op.value === currentVal) { } else if (op.value === currentVal) {
this.optionSelectLabel.text(op.label||op.value); that._updateOptionSelectLabel(op);
validValue = true; validValue = true;
break; break;
} }
@ -438,11 +578,45 @@
op = opt.options[0]; op = opt.options[0];
if (typeof op === "string") { if (typeof op === "string") {
this.value(op); this.value(op);
that._updateOptionSelectLabel({value:op});
} else { } else {
this.value(op.value); this.value(op.value);
that._updateOptionSelectLabel(op);
}
}
} else {
var selectedOption = this.optionValue||opt.options[0];
if (opt.parse) {
var parts = opt.parse(this.input.val());
if (parts.option) {
selectedOption = parts.option;
if (!this.activeOptions.hasOwnProperty(selectedOption)) {
parts.option = Object.keys(this.activeOptions)[0];
selectedOption = parts.option
}
}
this.input.val(parts.value);
if (opt.export) {
this.element.val(opt.export(parts.value,parts.option||selectedOption));
}
}
if (typeof selectedOption === "string") {
this.optionValue = selectedOption;
if (!this.activeOptions.hasOwnProperty(selectedOption)) {
selectedOption = Object.keys(this.activeOptions)[0];
}
if (!selectedOption) {
this.optionSelectTrigger.hide();
} else {
this._updateOptionSelectLabel(this.activeOptions[selectedOption]);
}
} else if (selectedOption) {
this.optionValue = selectedOption.value;
this._updateOptionSelectLabel(selectedOption);
} else {
this.optionSelectTrigger.hide();
} }
} }
console.log(validValue);
} }
} else { } else {
if (this.optionMenu) { if (this.optionMenu) {
@ -453,16 +627,17 @@
this.optionSelectTrigger.hide(); this.optionSelectTrigger.hide();
} }
if (opt.hasValue === false) { if (opt.hasValue === false) {
this.oldValue = this.element.val(); this.oldValue = this.input.val();
this.element.val(""); this.input.val("");
this.elementDiv.hide(); this.elementDiv.hide();
} else { } else {
if (this.oldValue !== undefined) { if (this.oldValue !== undefined) {
this.element.val(this.oldValue); this.input.val(this.oldValue);
delete this.oldValue; delete this.oldValue;
} }
this.elementDiv.show(); this.elementDiv.show();
} }
if (this.optionExpandButton) {
if (opt.expand && typeof opt.expand === 'function') { if (opt.expand && typeof opt.expand === 'function') {
this.optionExpandButton.show(); this.optionExpandButton.show();
this.optionExpandButton.off('click'); this.optionExpandButton.off('click');
@ -473,7 +648,8 @@
} else { } else {
this.optionExpandButton.hide(); this.optionExpandButton.hide();
} }
this.element.trigger('change',this.propertyType,this.value()); }
this.input.trigger('change',this.propertyType,this.value());
} }
if (image) { if (image) {
image.onload = function() { that._resize(); } image.onload = function() { that._resize(); }

View File

@ -490,7 +490,7 @@ RED.diff = (function() {
} }
function createNodeIcon(node,def) { function createNodeIcon(node,def) {
var nodeDiv = $("<div>",{class:"node-diff-node-entry-node"}); var nodeDiv = $("<div>",{class:"node-diff-node-entry-node"});
var colour = def.color; var colour = RED.utils.getNodeColor(node.type,def);
var icon_url = RED.utils.getNodeIcon(def,node); var icon_url = RED.utils.getNodeIcon(def,node);
if (node.type === 'tab') { if (node.type === 'tab') {
colour = "#C0DEED"; colour = "#C0DEED";

View File

@ -108,17 +108,6 @@ RED.editor = (function() {
} }
} }
} }
if (node.icon) {
var iconPath = RED.utils.separateIconPath(node.icon);
if (!iconPath.module) {
return isValid;
}
var iconSets = RED.nodes.getIconSets();
var iconFileList = iconSets[iconPath.module];
if (!iconFileList || iconFileList.indexOf(iconPath.file) === -1) {
isValid = false;
}
}
return isValid; return isValid;
} }
@ -170,27 +159,6 @@ RED.editor = (function() {
} }
} }
} }
validateIcon(node);
}
function validateIcon(node) {
if (node._def.hasOwnProperty("defaults") && !node._def.defaults.hasOwnProperty("icon") && node.icon) {
var iconPath = RED.utils.separateIconPath(node.icon);
var iconSets = RED.nodes.getIconSets();
var iconFileList = iconSets[iconPath.module];
var iconModule = $("#node-settings-icon-module");
var iconFile = $("#node-settings-icon-file");
if (!iconFileList) {
iconModule.addClass("input-error");
iconFile.removeClass("input-error");
} else if (iconFileList.indexOf(iconPath.file) === -1) {
iconModule.removeClass("input-error");
iconFile.addClass("input-error");
} else {
iconModule.removeClass("input-error");
iconFile.removeClass("input-error");
}
}
} }
function validateNodeEditorProperty(node,defaults,property,prefix) { function validateNodeEditorProperty(node,defaults,property,prefix) {
@ -528,6 +496,8 @@ RED.editor = (function() {
label = node.type; label = node.type;
if (node.type === '_expression') { if (node.type === '_expression') {
label = RED._("expressionEditor.title"); label = RED._("expressionEditor.title");
} else if (node.type === '_js') {
label = RED._("jsEditor.title");
} else if (node.type === '_json') { } else if (node.type === '_json') {
label = RED._("jsonEditor.title"); label = RED._("jsonEditor.title");
} else if (node.type === '_markdown') { } else if (node.type === '_markdown') {
@ -711,6 +681,97 @@ RED.editor = (function() {
} }
return result; return result;
} }
function showIconPicker(container, node, iconPath, done) {
var containerPos = container.offset();
var pickerBackground = $('<div>').css({
position: "absolute",top:0,bottom:0,left:0,right:0,zIndex:20
}).appendTo("body");
var top = containerPos.top - 30;
if (top+280 > $( window ).height()) {
top = $( window ).height() - 280;
}
var picker = $('<div class="red-ui-icon-picker">').css({
top: top+"px",
left: containerPos.left+"px",
}).appendTo("body");
var hide = function() {
pickerBackground.remove();
picker.remove();
RED.keyboard.remove("escape");
}
RED.keyboard.add("*","escape",function(){hide()});
pickerBackground.on("mousedown", hide);
var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(picker);
searchInput = $('<input type="text">').attr("placeholder",RED._("editor.searchIcons")).appendTo(searchDiv).searchBox({
delay: 50,
change: function() {
var searchTerm = $(this).val().trim();
if (searchTerm === "") {
iconList.find(".red-ui-icon-list-module").show();
iconList.find(".red-ui-icon-list-icon").show();
} else {
iconList.find(".red-ui-icon-list-module").hide();
iconList.find(".red-ui-icon-list-icon").each(function(i,n) {
if ($(n).data('icon').indexOf(searchTerm) === -1) {
$(n).hide();
} else {
$(n).show();
}
});
}
}
});
var row = $('<div>').appendTo(picker);
var iconList = $('<div class="red-ui-icon-list">').appendTo(picker);
var metaRow = $('<div class="red-ui-icon-meta"></div>').appendTo(picker);
var summary = $('<span>').appendTo(metaRow);
var resetButton = $('<button class="editor-button editor-button-small">'+RED._("editor.useDefault")+'</button>').appendTo(metaRow).click(function(e) {
e.preventDefault();
hide();
done(null);
});
var iconSets = RED.nodes.getIconSets();
Object.keys(iconSets).forEach(function(moduleName) {
var icons = iconSets[moduleName];
if (icons.length > 0) {
// selectIconModule.append($("<option></option>").val(moduleName).text(moduleName));
var header = $('<div class="red-ui-icon-list-module"></div>').text(moduleName).appendTo(iconList);
$('<i class="fa fa-cube"></i>').prependTo(header);
icons.forEach(function(icon) {
var iconDiv = $('<div>',{class:"red-ui-icon-list-icon"}).appendTo(iconList);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconDiv);
var colour = RED.utils.getNodeColor(node.type, node._def);
var icon_url = "icons/"+moduleName+"/"+icon;
iconDiv.data('icon',icon_url)
nodeDiv.css('backgroundColor',colour);
var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv);
$('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
if (iconPath.module === moduleName && iconPath.file === icon) {
iconDiv.addClass("selected");
}
iconDiv.on("mouseover", function() {
summary.text(icon);
})
iconDiv.on("mouseout", function() {
summary.html("&nbsp;");
})
iconDiv.click(function() {
hide();
done(moduleName+"/"+icon);
})
})
}
});
picker.slideDown(100);
searchInput.focus();
}
function buildLabelForm(container,node) { function buildLabelForm(container,node) {
var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container); var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container);
@ -748,81 +809,36 @@ RED.editor = (function() {
} }
if ((!node._def.defaults || !node._def.defaults.hasOwnProperty("icon"))) { if ((!node._def.defaults || !node._def.defaults.hasOwnProperty("icon"))) {
$('<div class="form-row"><div id="node-settings-icon"></div></div>').appendTo(dialogForm); $('<hr>').appendTo(dialogForm);
var iconDiv = $("#node-settings-icon"); var iconRow = $('<div class="form-row"></div>').appendTo(dialogForm);
$('<label data-i18n="editor.settingIcon">').appendTo(iconDiv); $('<label style="width: 50px" data-i18n="editor.settingIcon">').appendTo(iconRow);
var iconForm = $('<div>',{class:"node-label-form-row"});
iconForm.appendTo(iconDiv);
$('<label>').appendTo(iconForm);
var selectIconModule = $('<select id="node-settings-icon-module"><option value=""></option></select>').appendTo(iconForm); var iconButton = $('<button class="editor-button">').appendTo(iconRow);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconButton);
var colour = RED.utils.getNodeColor(node.type, node._def);
var icon_url = RED.utils.getNodeIcon(node._def,node);
nodeDiv.css('backgroundColor',colour);
var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv);
var iconDiv = $('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
iconButton.click(function(e) {
e.preventDefault();
var iconPath; var iconPath;
if (node.icon) { var icon = $("#node-settings-icon").text()||"";
iconPath = RED.utils.separateIconPath(node.icon); if (icon) {
iconPath = RED.utils.separateIconPath(icon);
} else { } else {
iconPath = RED.utils.getDefaultNodeIcon(node._def, node); iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
} }
var iconSets = RED.nodes.getIconSets(); showIconPicker(iconRow,node,iconPath,function(newIcon) {
Object.keys(iconSets).forEach(function(moduleName) { $("#node-settings-icon").text(newIcon||"");
selectIconModule.append($("<option></option>").val(moduleName).text(moduleName)); var icon_url = RED.utils.getNodeIcon(node._def,{type:node.type,icon:newIcon});
iconDiv.css("backgroundImage","url("+icon_url+")");
}); });
if (iconPath.module && !iconSets[iconPath.module]) { })
selectIconModule.append($("<option disabled></option>").val(iconPath.module).text(iconPath.module)); $('<div class="uneditable-input" id="node-settings-icon">').text(node.icon).appendTo(iconRow);
} }
selectIconModule.val(iconPath.module);
var iconModuleHidden = $('<input type="hidden" id="node-settings-icon-module-hidden"></input>').appendTo(iconForm);
iconModuleHidden.val(iconPath.module);
var selectIconFile = $('<select id="node-settings-icon-file"><option value=""></option></select>').appendTo(iconForm);
selectIconModule.change(function() {
moduleChange(selectIconModule, selectIconFile, iconModuleHidden, iconFileHidden, iconSets, true);
});
var iconFileHidden = $('<input type="hidden" id="node-settings-icon-file-hidden"></input>').appendTo(iconForm);
iconFileHidden.val(iconPath.file);
selectIconFile.change(function() {
selectIconFile.removeClass("input-error");
var fileName = selectIconFile.val();
iconFileHidden.val(fileName);
});
var clear = $('<button class="editor-button editor-button-small"><i class="fa fa-times"></i></button>').appendTo(iconForm);
clear.click(function(evt) {
evt.preventDefault();
var iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
selectIconModule.val(iconPath.module);
moduleChange(selectIconModule, selectIconFile, iconModuleHidden, iconFileHidden, iconSets, true);
selectIconFile.removeClass("input-error");
selectIconFile.val(iconPath.file);
iconFileHidden.val(iconPath.file);
});
moduleChange(selectIconModule, selectIconFile, iconModuleHidden, iconFileHidden, iconSets, false);
var iconFileList = iconSets[selectIconModule.val()];
if (!iconFileList || iconFileList.indexOf(iconPath.file) === -1) {
selectIconFile.append($("<option disabled></option>").val(iconPath.file).text(iconPath.file));
}
selectIconFile.val(iconPath.file);
}
}
function moduleChange(selectIconModule, selectIconFile, iconModuleHidden, iconFileHidden, iconSets, updateIconFile) {
selectIconFile.children().remove();
var moduleName = selectIconModule.val();
if (moduleName !== null) {
iconModuleHidden.val(moduleName);
}
var iconFileList = iconSets[moduleName];
if (iconFileList) {
iconFileList.forEach(function(fileName) {
if (updateIconFile) {
updateIconFile = false;
iconFileHidden.val(fileName);
}
selectIconFile.append($("<option></option>").val(fileName).text(fileName));
});
}
selectIconFile.prop("disabled", !iconFileList);
selectIconFile.removeClass("input-error");
selectIconModule.removeClass("input-error");
} }
function updateLabels(editing_node, changes, outputMap) { function updateLabels(editing_node, changes, outputMap) {
@ -1086,9 +1102,7 @@ RED.editor = (function() {
} }
if (!editing_node._def.defaults || !editing_node._def.defaults.hasOwnProperty("icon")) { if (!editing_node._def.defaults || !editing_node._def.defaults.hasOwnProperty("icon")) {
var iconModule = $("#node-settings-icon-module-hidden").val(); var icon = $("#node-settings-icon").text()||""
var iconFile = $("#node-settings-icon-file-hidden").val();
var icon = (iconModule && iconFile) ? iconModule+"/"+iconFile : "";
if (!isDefaultIcon) { if (!isDefaultIcon) {
if (icon !== editing_node.icon) { if (icon !== editing_node.icon) {
changes.icon = editing_node.icon; changes.icon = editing_node.icon;
@ -1692,15 +1706,28 @@ RED.editor = (function() {
if (updateLabels(editing_node, changes, null)) { if (updateLabels(editing_node, changes, null)) {
changed = true; changed = true;
} }
var iconModule = $("#node-settings-icon-module-hidden").val(); var icon = $("#node-settings-icon").text()||"";
var iconFile = $("#node-settings-icon-file-hidden").val();
var icon = (iconModule && iconFile) ? iconModule+"/"+iconFile : "";
if ((editing_node.icon === undefined && icon !== "node-red/subflow.png") || if ((editing_node.icon === undefined && icon !== "node-red/subflow.png") ||
(editing_node.icon !== undefined && editing_node.icon !== icon)) { (editing_node.icon !== undefined && editing_node.icon !== icon)) {
changes.icon = editing_node.icon; changes.icon = editing_node.icon;
editing_node.icon = icon; editing_node.icon = icon;
changed = true; changed = true;
} }
var newCategory = $("#subflow-input-category").val().trim();
if (newCategory === "_custom_") {
newCategory = $("#subflow-input-custom-category").val().trim();
if (newCategory === "") {
newCategory = editing_node.category;
}
}
if (newCategory === 'subflows') {
newCategory = '';
}
if (newCategory != editing_node.category) {
changes['category'] = editing_node.category;
editing_node.category = newCategory;
changed = true;
}
RED.palette.refresh(); RED.palette.refresh();
@ -1773,8 +1800,6 @@ RED.editor = (function() {
}); });
portLabels.content.addClass("editor-tray-content"); portLabels.content.addClass("editor-tray-content");
if (editing_node) { if (editing_node) {
RED.sidebar.info.refresh(editing_node); RED.sidebar.info.refresh(editing_node);
} }
@ -1787,6 +1812,33 @@ RED.editor = (function() {
$("#subflow-input-name").val(subflow.name); $("#subflow-input-name").val(subflow.name);
RED.text.bidi.prepareInput($("#subflow-input-name")); RED.text.bidi.prepareInput($("#subflow-input-name"));
$("#subflow-input-category").empty();
var categories = RED.palette.getCategories();
categories.sort(function(A,B) {
return A.label.localeCompare(B.label);
})
categories.forEach(function(cat) {
$("#subflow-input-category").append($("<option></option>").val(cat.id).text(cat.label));
})
$("#subflow-input-category").append($("<option></option>").attr('disabled',true).text("---"));
$("#subflow-input-category").append($("<option></option>").val("_custom_").text(RED._("palette.addCategory")));
$("#subflow-input-category").change(function() {
var val = $(this).val();
if (val === "_custom_") {
$("#subflow-input-category").width(120);
$("#subflow-input-custom-category").show();
} else {
$("#subflow-input-category").width(250);
$("#subflow-input-custom-category").hide();
}
})
$("#subflow-input-category").val(subflow.category||"subflows");
subflowEditor.getSession().setValue(subflow.info||"",-1); subflowEditor.getSession().setValue(subflow.info||"",-1);
var userCount = 0; var userCount = 0;
var subflowType = "subflow:"+editing_node.id; var subflowType = "subflow:"+editing_node.id;
@ -1799,7 +1851,6 @@ RED.editor = (function() {
$("#subflow-dialog-user-count").text(RED._("subflow.subflowInstances", {count:userCount})).show(); $("#subflow-dialog-user-count").text(RED._("subflow.subflowInstances", {count:userCount})).show();
buildLabelForm(portLabels.content,subflow); buildLabelForm(portLabels.content,subflow);
validateIcon(subflow);
trayBody.i18n(); trayBody.i18n();
}, },
close: function() { close: function() {
@ -1857,6 +1908,7 @@ RED.editor = (function() {
edit: showEditDialog, edit: showEditDialog,
editConfig: showEditConfigNodeDialog, editConfig: showEditConfigNodeDialog,
editSubflow: showEditSubflowDialog, editSubflow: showEditSubflowDialog,
editJavaScript: function(options) { showTypeEditor("_js",options) },
editExpression: function(options) { showTypeEditor("_expression", options) }, editExpression: function(options) { showTypeEditor("_expression", options) },
editJSON: function(options) { showTypeEditor("_json", options) }, editJSON: function(options) { showTypeEditor("_json", options) },
editMarkdown: function(options) { showTypeEditor("_markdown", options) }, editMarkdown: function(options) { showTypeEditor("_markdown", options) },

103
editor/js/ui/editors/js.js Normal file
View File

@ -0,0 +1,103 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
RED.editor.types._js = (function() {
var template = '<script type="text/x-red" data-template-name="_js"><div class="form-row node-text-editor-row" style="width: 700px"><div style="height: 200px;min-height: 150px;" class="node-text-editor" id="node-input-js"></div></div></script>';
return {
init: function() {
$(template).appendTo(document.body);
},
show: function(options) {
var value = options.value;
var onComplete = options.complete;
var type = "_js"
RED.view.state(RED.state.EDITING);
var expressionEditor;
var changeTimer;
var trayOptions = {
title: options.title,
width: "inherit",
buttons: [
{
id: "node-dialog-cancel",
text: RED._("common.label.cancel"),
click: function() {
RED.tray.close();
}
},
{
id: "node-dialog-ok",
text: RED._("common.label.done"),
class: "primary",
click: function() {
onComplete(expressionEditor.getValue(),expressionEditor.getCursorPosition());
RED.tray.close();
}
}
],
resize: function(dimensions) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var editorRow = $("#dialog-form>div.node-text-editor-row");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
height -= (parseInt($("#dialog-form").css("marginTop"))+parseInt($("#dialog-form").css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
expressionEditor.resize();
},
open: function(tray) {
var trayBody = tray.find('.editor-tray-body');
var dialogForm = RED.editor.buildEditForm(tray.find('.editor-tray-body'),'dialog-form',type,'editor');
expressionEditor = RED.editor.createEditor({
id: 'node-input-js',
mode: 'ace/mode/javascript',
value: value,
globals: {
msg:true,
context:true,
RED: true,
util: true,
flow: true,
global: true,
console: true,
Buffer: true,
setTimeout: true,
clearTimeout: true,
setInterval: true,
clearInterval: true
}
});
if (options.cursor) {
expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);
}
dialogForm.i18n();
},
close: function() {
expressionEditor.destroy();
if (options.onclose) {
options.onclose();
}
},
show: function() {}
}
RED.tray.show(trayOptions);
}
}
})();

View File

@ -96,7 +96,7 @@ RED.notifications = (function() {
if (options.buttons) { if (options.buttons) {
var buttonSet = $('<div style="margin-top: 20px;" class="ui-dialog-buttonset"></div>').appendTo(n) var buttonSet = $('<div style="margin-top: 20px;" class="ui-dialog-buttonset"></div>').appendTo(n)
options.buttons.forEach(function(buttonDef) { options.buttons.forEach(function(buttonDef) {
var b = $('<button>').text(buttonDef.text).click(buttonDef.click).appendTo(buttonSet); var b = $('<button>').html(buttonDef.text).click(buttonDef.click).appendTo(buttonSet);
if (buttonDef.id) { if (buttonDef.id) {
b.attr('id',buttonDef.id); b.attr('id',buttonDef.id);
} }

View File

@ -233,7 +233,7 @@ RED.palette.editor = (function() {
if (set.enabled) { if (set.enabled) {
var def = RED.nodes.getType(t); var def = RED.nodes.getType(t);
if (def && def.color) { if (def && def.color) {
swatch.css({background:def.color}); swatch.css({background:RED.utils.getNodeColor(t,def)});
swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))}) swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))})
} else { } else {

View File

@ -21,7 +21,18 @@ RED.palette = (function() {
var categoryContainers = {}; var categoryContainers = {};
function createCategoryContainer(category, label) {
function createCategory(originalCategory,rootCategory,category,ns) {
if ($("#palette-base-category-"+rootCategory).length === 0) {
createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
}
$("#palette-container-"+rootCategory).show();
if ($("#palette-"+category).length === 0) {
$("#palette-base-category-"+rootCategory).append('<div id="palette-'+category+'"></div>');
}
}
function createCategoryContainer(originalCategory,category, labelId) {
var label = RED._(labelId, {defaultValue:category});
label = (label || category).replace(/_/g, " "); label = (label || category).replace(/_/g, " ");
var catDiv = $('<div id="palette-container-'+category+'" class="palette-category palette-close hide">'+ var catDiv = $('<div id="palette-container-'+category+'" class="palette-category palette-close hide">'+
'<div id="palette-header-'+category+'" class="palette-header"><i class="expanded fa fa-angle-down"></i><span>'+label+'</span></div>'+ '<div id="palette-header-'+category+'" class="palette-header"><i class="expanded fa fa-angle-down"></i><span>'+label+'</span></div>'+
@ -31,7 +42,8 @@ RED.palette = (function() {
'<div id="palette-'+category+'-function"></div>'+ '<div id="palette-'+category+'-function"></div>'+
'</div>'+ '</div>'+
'</div>').appendTo("#palette-container"); '</div>').appendTo("#palette-container");
catDiv.data('category',originalCategory);
catDiv.data('label',label);
categoryContainers[category] = { categoryContainers[category] = {
container: catDiv, container: catDiv,
close: function() { close: function() {
@ -133,6 +145,7 @@ RED.palette = (function() {
} }
if (exclusion.indexOf(def.category)===-1) { if (exclusion.indexOf(def.category)===-1) {
var originalCategory = def.category;
var category = def.category.replace(/ /g,"_"); var category = def.category.replace(/ /g,"_");
var rootCategory = category.split("-")[0]; var rootCategory = category.split("-")[0];
@ -153,14 +166,13 @@ RED.palette = (function() {
d.className="palette_node"; d.className="palette_node";
if (def.icon) { if (def.icon) {
var icon_url = RED.utils.getNodeIcon(def); var icon_url = RED.utils.getNodeIcon(def);
var iconContainer = $('<div/>',{class:"palette_icon_container"+(def.align=="right"?" palette_icon_container_right":"")}).appendTo(d); var iconContainer = $('<div/>',{class:"palette_icon_container"+(def.align=="right"?" palette_icon_container_right":"")}).appendTo(d);
$('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer); $('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
} }
d.style.backgroundColor = def.color; d.style.backgroundColor = RED.utils.getNodeColor(nt,def);
if (def.outputs > 0) { if (def.outputs > 0) {
var portOut = document.createElement("div"); var portOut = document.createElement("div");
@ -174,21 +186,12 @@ RED.palette = (function() {
d.appendChild(portIn); d.appendChild(portIn);
} }
if ($("#palette-base-category-"+rootCategory).length === 0) { createCategory(def.category,rootCategory,category,(coreCategories.indexOf(rootCategory) !== -1)?"node-red":def.set.id);
if(coreCategories.indexOf(rootCategory) !== -1){
createCategoryContainer(rootCategory, RED._("node-red:palette.label."+rootCategory, {defaultValue:rootCategory}));
} else {
var ns = def.set.id;
createCategoryContainer(rootCategory, RED._(ns+":palette.label."+rootCategory, {defaultValue:rootCategory}));
}
}
$("#palette-container-"+rootCategory).show();
if ($("#palette-"+category).length === 0) {
$("#palette-base-category-"+rootCategory).append('<div id="palette-'+category+'"></div>');
}
$("#palette-"+category).append(d); $("#palette-"+category).append(d);
$(d).data('category',rootCategory);
d.onmousedown = function(e) { e.preventDefault(); }; d.onmousedown = function(e) { e.preventDefault(); };
var popover = RED.popover.create({ var popover = RED.popover.create({
@ -308,7 +311,7 @@ RED.palette = (function() {
}); });
var nodeInfo = null; var nodeInfo = null;
if (def.category == "subflows") { if (nt.indexOf("subflow:") === 0) {
$(d).dblclick(function(e) { $(d).dblclick(function(e) {
RED.workspaces.show(nt.substring(8)); RED.workspaces.show(nt.substring(8));
e.preventDefault(); e.preventDefault();
@ -382,6 +385,31 @@ RED.palette = (function() {
} }
setLabel(sf.type+":"+sf.id,paletteNode,sf.name,marked(sf.info||"")); setLabel(sf.type+":"+sf.id,paletteNode,sf.name,marked(sf.info||""));
setIcon(paletteNode,sf); setIcon(paletteNode,sf);
var currentCategory = paletteNode.data('category');
var newCategory = (sf.category||"subflows");
if (currentCategory !== newCategory) {
var category = newCategory.replace(/ /g,"_");
createCategory(newCategory,category,category,"node-red");
var currentCategoryNode = paletteNode.closest(".palette-category");
var newCategoryNode = $("#palette-"+category);
newCategoryNode.append(paletteNode);
if (newCategoryNode.find(".palette_node").length === 1) {
categoryContainers[category].open();
}
paletteNode.data('category',newCategory);
if (currentCategoryNode.find(".palette_node").length === 0) {
if (currentCategoryNode.find("i").hasClass("expanded")) {
currentCategoryNode.find(".palette-content").slideToggle();
currentCategoryNode.find("i").toggleClass("expanded");
}
}
}
}); });
} }
@ -470,7 +498,7 @@ RED.palette = (function() {
categoryList = coreCategories categoryList = coreCategories
} }
categoryList.forEach(function(category){ categoryList.forEach(function(category){
createCategoryContainer(category, RED._("palette.label."+category,{defaultValue:category})); createCategoryContainer(category, category, "palette.label."+category);
}); });
$("#palette-collapse-all").on("click", function(e) { $("#palette-collapse-all").on("click", function(e) {
@ -490,13 +518,20 @@ RED.palette = (function() {
} }
}); });
} }
function getCategories() {
var categories = [];
$("#palette-container .palette-category").each(function(i,d) {
categories.push({id:$(d).data('category'),label:$(d).data('label')});
})
return categories;
}
return { return {
init: init, init: init,
add:addNodeType, add:addNodeType,
remove:removeNodeType, remove:removeNodeType,
hide:hideNodeType, hide:hideNodeType,
show:showNodeType, show:showNodeType,
refresh:refreshNodeTypes refresh:refreshNodeTypes,
getCategories: getCategories
}; };
})(); })();

View File

@ -622,7 +622,7 @@ RED.projects = (function() {
}, },
400: { 400: {
'project_exists': function(error) { 'project_exists': function(error) {
console.log(RED._("projects.clone-project.already-exists")); console.log(RED._("projects.clone-project.already-exists2"));
}, },
'git_error': function(error) { 'git_error': function(error) {
console.log(RED._("projects.clone-project.git-error"),error); console.log(RED._("projects.clone-project.git-error"),error);
@ -1983,7 +1983,7 @@ RED.projects = (function() {
notification.close(); notification.close();
} }
},{ },{
text: $('<span><i class="fa fa-refresh"></i> Retry</span>'), text: '<span><i class="fa fa-refresh"></i> ' +RED._("projects.send-req.retry") +'</span>',
click: function() { click: function() {
body = body || {}; body = body || {};
var authBody = {}; var authBody = {};

View File

@ -26,6 +26,24 @@ RED.search = (function() {
var keys = []; var keys = [];
var results = []; var results = [];
function indexProperty(node,label,property) {
if (typeof property === 'string' || typeof property === 'number') {
property = (""+property).toLowerCase();
index[property] = index[property] || {};
index[property][node.id] = {node:node,label:label};
} else if (Array.isArray(property)) {
property.forEach(function(prop) {
indexProperty(node,label,prop);
})
} else if (typeof property === 'object') {
for (var prop in property) {
if (property.hasOwnProperty(prop)) {
indexProperty(node,label,property[prop])
}
}
}
}
function indexNode(n) { function indexNode(n) {
var l = RED.utils.getNodeLabel(n); var l = RED.utils.getNodeLabel(n);
if (l) { if (l) {
@ -42,17 +60,11 @@ RED.search = (function() {
} }
for (var i=0;i<properties.length;i++) { for (var i=0;i<properties.length;i++) {
if (n.hasOwnProperty(properties[i])) { if (n.hasOwnProperty(properties[i])) {
var v = n[properties[i]]; indexProperty(n, l, n[properties[i]]);
if (typeof v === 'string' || typeof v === 'number') {
v = (""+v).toLowerCase();
index[v] = index[v] || {};
index[v][n.id] = {node:n,label:l};
} }
} }
} }
}
function indexWorkspace() { function indexWorkspace() {
index = {}; index = {};
RED.nodes.eachWorkspace(indexNode); RED.nodes.eachWorkspace(indexNode);
@ -181,7 +193,7 @@ RED.search = (function() {
var div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container); var div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div); var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
var colour = def.color; var colour = RED.utils.getNodeColor(node.type,def);
var icon_url = RED.utils.getNodeIcon(def,node); var icon_url = RED.utils.getNodeIcon(def,node);
if (node.type === 'tab') { if (node.type === 'tab') {
colour = "#C0DEED"; colour = "#C0DEED";

View File

@ -218,6 +218,7 @@ RED.sidebar = (function() {
showSidebar(); showSidebar();
RED.sidebar.info.init(); RED.sidebar.info.init();
RED.sidebar.config.init(); RED.sidebar.config.init();
RED.sidebar.context.init();
// hide info bar at start if screen rather narrow... // hide info bar at start if screen rather narrow...
if ($(window).width() < 600) { RED.menu.setSelected("menu-item-sidebar",false); } if ($(window).width() < 600) { RED.menu.setSelected("menu-item-sidebar",false); }
} }

292
editor/js/ui/tab-context.js Normal file
View File

@ -0,0 +1,292 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
RED.sidebar.context = (function() {
var content;
var sections;
var localCache = {};
var nodeSection;
// var subflowSection;
var flowSection;
var globalSection;
var currentNode;
var currentFlow;
function init() {
content = $("<div>").css({"position":"relative","height":"100%"});
content.className = "sidebar-context"
// var toolbar = $('<div class="sidebar-header">'+
// '</div>').appendTo(content);
var footerToolbar = $('<div>'+
// '<span class="button-group"><a class="sidebar-footer-button" href="#" data-i18n="[title]node-red:debug.sidebar.openWindow"><i class="fa fa-desktop"></i></a></span> ' +
'</div>');
var stackContainer = $("<div>",{class:"sidebar-context-stack"}).appendTo(content);
sections = RED.stack.create({
container: stackContainer
});
nodeSection = sections.add({
title: RED._("sidebar.context.node"),
collapsible: true,
// onexpand: function() {
// updateNode(currentNode,true);
// }
});
nodeSection.expand();
nodeSection.content.css({height:"100%"});
nodeSection.timestamp = $('<div class="sidebar-context-updated">&nbsp;</div>').appendTo(nodeSection.content);
var table = $('<table class="node-info"></table>').appendTo(nodeSection.content);
nodeSection.table = $('<tbody>').appendTo(table);
var bg = $('<div style="float: right"></div>').appendTo(nodeSection.header);
$('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>')
.appendTo(bg)
.click(function(evt) {
evt.stopPropagation();
evt.preventDefault();
updateNode(currentNode, true);
})
// subflowSection = sections.add({
// title: "Subflow",
// collapsible: true
// });
// subflowSection.expand();
// subflowSection.content.css({height:"100%"});
// bg = $('<div style="float: right"></div>').appendTo(subflowSection.header);
// $('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>')
// .appendTo(bg)
// .click(function(evt) {
// evt.stopPropagation();
// evt.preventDefault();
// })
//
// subflowSection.container.hide();
flowSection = sections.add({
title: RED._("sidebar.context.flow"),
collapsible: true
});
flowSection.expand();
flowSection.content.css({height:"100%"});
flowSection.timestamp = $('<div class="sidebar-context-updated">&nbsp;</div>').appendTo(flowSection.content);
var table = $('<table class="node-info"></table>').appendTo(flowSection.content);
flowSection.table = $('<tbody>').appendTo(table);
bg = $('<div style="float: right"></div>').appendTo(flowSection.header);
$('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>')
.appendTo(bg)
.click(function(evt) {
evt.stopPropagation();
evt.preventDefault();
updateFlow(currentFlow);
})
globalSection = sections.add({
title: RED._("sidebar.context.global"),
collapsible: true
});
globalSection.expand();
globalSection.content.css({height:"100%"});
globalSection.timestamp = $('<div class="sidebar-context-updated">&nbsp;</div>').appendTo(globalSection.content);
var table = $('<table class="node-info"></table>').appendTo(globalSection.content);
globalSection.table = $('<tbody>').appendTo(table);
bg = $('<div style="float: right"></div>').appendTo(globalSection.header);
$('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>')
.appendTo(bg)
.click(function(evt) {
evt.stopPropagation();
evt.preventDefault();
updateEntry(globalSection,"context/global","global");
})
RED.actions.add("core:show-context-tab",show);
RED.sidebar.addTab({
id: "context",
label: RED._("sidebar.context.label"),
name: RED._("sidebar.context.name"),
iconClass: "fa fa-database",
content: content,
toolbar: footerToolbar,
// pinned: true,
enableOnEdit: false
});
// var toggleLiveButton = $("#sidebar-context-toggle-live");
// toggleLiveButton.click(function(evt) {
// evt.preventDefault();
// if ($(this).hasClass("selected")) {
// $(this).removeClass("selected");
// $(this).find("i").removeClass("fa-pause");
// $(this).find("i").addClass("fa-play");
// } else {
// $(this).addClass("selected");
// $(this).find("i").removeClass("fa-play");
// $(this).find("i").addClass("fa-pause");
// }
// });
// RED.popover.tooltip(toggleLiveButton, function() {
// if (toggleLiveButton.hasClass("selected")) {
// return "Pause live updates"
// } else {
// return "Start live updates"
// }
// });
RED.events.on("view:selection-changed", function(event) {
var selectedNode = event.nodes && event.nodes.length === 1 && event.nodes[0];
updateNode(selectedNode);
})
RED.events.on("workspace:change", function(event) {
updateFlow(RED.nodes.workspace(event.workspace));
})
updateEntry(globalSection,"context/global","global");
}
function updateNode(node,force) {
currentNode = node;
if (force) {
if (node) {
updateEntry(nodeSection,"context/node/"+node.id,node.id);
// if (/^subflow:/.test(node.type)) {
// subflowSection.container.show();
// updateEntry(subflowSection,"context/flow/"+node.id,node.id);
// } else {
// subflowSection.container.hide();
// }
} else {
// subflowSection.container.hide();
updateEntry(nodeSection)
}
} else {
$(nodeSection.table).empty();
if (node) {
$('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.refresh"></td></tr>').appendTo(nodeSection.table).i18n();
} else {
$('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.none"></td></tr>').appendTo(nodeSection.table).i18n();
}
nodeSection.timestamp.html("&nbsp;");
}
}
function updateFlow(flow) {
currentFlow = flow;
if (flow) {
updateEntry(flowSection,"context/flow/"+flow.id,flow.id);
} else {
updateEntry(flowSection)
}
}
function refreshEntry(section,baseUrl,id) {
var contextStores = RED.settings.context.stores;
var container = section.table;
$.getJSON(baseUrl, function(data) {
$(container).empty();
var sortedData = {};
for (var store in data) {
if (data.hasOwnProperty(store)) {
for (var key in data[store]) {
if (data[store].hasOwnProperty(key)) {
if (!sortedData.hasOwnProperty(key)) {
sortedData[key] = [];
}
data[store][key].store = store;
sortedData[key].push(data[store][key])
}
}
}
}
var keys = Object.keys(sortedData);
keys.sort();
var l = keys.length;
for (var i = 0; i < l; i++) {
sortedData[keys[i]].forEach(function(v) {
var k = keys[i];
var l2 = sortedData[k].length;
var propRow = $('<tr class="node-info-node-row"><td class="sidebar-context-property"></td><td></td></tr>').appendTo(container);
var obj = $(propRow.children()[0]);
obj.text(k);
var tools = $('<span class="debug-message-tools button-group"></span>').appendTo(obj);
var refreshItem = $('<button class="editor-button editor-button-small"><i class="fa fa-refresh"></i></button>').appendTo(tools).click(function(e) {
e.preventDefault();
e.stopPropagation();
$.getJSON(baseUrl+"/"+k+"?store="+v.store, function(data) {
$(propRow.children()[1]).empty();
var payload = data.msg;
var format = data.format;
payload = RED.utils.decodeObject(payload,format);
RED.utils.createObjectElement(payload, {
typeHint: data.format,
sourceId: id+"."+k
}).appendTo(propRow.children()[1]);
})
});
var payload = v.msg;
var format = v.format;
payload = RED.utils.decodeObject(payload,format);
RED.utils.createObjectElement(payload, {
typeHint: v.format,
sourceId: id+"."+k
}).appendTo(propRow.children()[1]);
if (contextStores.length > 1) {
$("<span>",{class:"sidebar-context-property-storename"}).text(v.store).appendTo($(propRow.children()[0]))
}
});
}
if (l === 0) {
$('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.empty"></td></tr>').appendTo(container).i18n();
}
$(section.timestamp).text(new Date().toLocaleString());
});
}
function updateEntry(section,baseUrl,id) {
var container = section.table;
if (id) {
refreshEntry(section,baseUrl,id);
} else {
$(container).empty();
$('<tr class="node-info-node-row red-ui-search-empty blank" colspan="2"><td data-i18n="sidebar.context.none"></td></tr>').appendTo(container).i18n();
}
}
function show() {
RED.sidebar.show("context");
}
return {
init: init
}
})();

View File

@ -170,25 +170,41 @@ RED.sidebar.info = (function() {
if (node.type === "tab") { if (node.type === "tab") {
propRow = $('<tr class="node-info-node-row"><td>'+RED._("sidebar.info.status")+'</td><td></td></tr>').appendTo(tableBody); propRow = $('<tr class="node-info-node-row"><td>'+RED._("sidebar.info.status")+'</td><td></td></tr>').appendTo(tableBody);
$(propRow.children()[1]).text((!!!node.disabled)?RED._("sidebar.info.enabled"):RED._("sidebar.info.disabled")) $(propRow.children()[1]).text((!!!node.disabled)?RED._("sidebar.info.enabled"):RED._("sidebar.info.disabled"))
} else if (node.type === "subflow") {
propRow = $('<tr class="node-info-node-row"><td>'+RED._("subflow.category")+'</td><td></td></tr>').appendTo(tableBody);
var category = node.category||"subflows";
$(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category}))
} }
} else { } else {
propRow = $('<tr class="node-info-node-row"><td>'+RED._("sidebar.info.node")+"</td><td></td></tr>").appendTo(tableBody); propRow = $('<tr class="node-info-node-row"><td>'+RED._("sidebar.info.node")+"</td><td></td></tr>").appendTo(tableBody);
RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]); RED.utils.createObjectElement(node.id).appendTo(propRow.children()[1]);
if (node.type !== "subflow" && node.name) { if (node.type !== "subflow" && node.type !== "unknown" && node.name) {
propRow = $('<tr class="node-info-node-row"><td>'+RED._("common.label.name")+'</td><td></td></tr>').appendTo(tableBody); propRow = $('<tr class="node-info-node-row"><td>'+RED._("common.label.name")+'</td><td></td></tr>').appendTo(tableBody);
$('<span class="bidiAware" dir="'+RED.text.bidi.resolveBaseTextDir(node.name)+'"></span>').text(node.name).appendTo(propRow.children()[1]); $('<span class="bidiAware" dir="'+RED.text.bidi.resolveBaseTextDir(node.name)+'"></span>').text(node.name).appendTo(propRow.children()[1]);
} }
if (!m) { if (!m) {
propRow = $('<tr class="node-info-node-row"><td>'+RED._("sidebar.info.type")+"</td><td></td></tr>").appendTo(tableBody); propRow = $('<tr class="node-info-node-row"><td>'+RED._("sidebar.info.type")+"</td><td></td></tr>").appendTo(tableBody);
$(propRow.children()[1]).text(node.type); $(propRow.children()[1]).text((node.type === "unknown")?node._orig.type:node.type);
if (node.type === "unknown") {
$('<span style="float: right; font-size: 0.8em"><i class="fa fa-warning"></i></span>').prependTo($(propRow.children()[1]))
}
} }
if (!m && node.type != "subflow" && node.type != "comment") { if (!m && node.type != "subflow" && node.type != "comment") {
if (node._def) { var defaults;
if (node.type === 'unknown') {
defaults = {};
Object.keys(node._orig).forEach(function(k) {
if (k !== 'type') {
defaults[k] = {};
}
})
} else if (node._def) {
defaults = node._def.defaults;
}
if (defaults) {
var count = 0; var count = 0;
var defaults = node._def.defaults;
for (var n in defaults) { for (var n in defaults) {
if (n != "name" && defaults.hasOwnProperty(n)) { if (n != "name" && defaults.hasOwnProperty(n)) {
var val = node[n]; var val = node[n];
@ -205,7 +221,7 @@ RED.sidebar.info = (function() {
var div = $('<span>',{class:""}).appendTo(container); var div = $('<span>',{class:""}).appendTo(container);
var nodeDiv = $('<div>',{class:"palette_node palette_node_small"}).appendTo(div); var nodeDiv = $('<div>',{class:"palette_node palette_node_small"}).appendTo(div);
var colour = configNode._def.color; var colour = RED.utils.getNodeColor(configNode.type,configNode._def);
var icon_url = RED.utils.getNodeIcon(configNode._def); var icon_url = RED.utils.getNodeIcon(configNode._def);
nodeDiv.css({'backgroundColor':colour, "cursor":"pointer"}); nodeDiv.css({'backgroundColor':colour, "cursor":"pointer"});
var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv); var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv);
@ -235,6 +251,9 @@ RED.sidebar.info = (function() {
} }
} }
if (m) { if (m) {
propRow = $('<tr class="node-info-node-row"><td>'+RED._("subflow.category")+'</td><td></td></tr>').appendTo(tableBody);
var category = subflowNode.category||"subflows";
$(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category}))
$('<tr class="node-info-subflow-row"><td>'+RED._("sidebar.info.instances")+"</td><td>"+subflowUserCount+'</td></tr>').appendTo(tableBody); $('<tr class="node-info-subflow-row"><td>'+RED._("sidebar.info.instances")+"</td><td>"+subflowUserCount+'</td></tr>').appendTo(tableBody);
} }

View File

@ -128,7 +128,7 @@ RED.typeSearch = (function() {
var div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container); var div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div); var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
var colour = def.color; var colour = RED.utils.getNodeColor(object.type,def);
var icon_url = RED.utils.getNodeIcon(def); var icon_url = RED.utils.getNodeIcon(def);
nodeDiv.css('backgroundColor',colour); nodeDiv.css('backgroundColor',colour);

View File

@ -34,6 +34,10 @@ RED.utils = (function() {
result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('buffer['+value.length+']'); result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('buffer['+value.length+']');
} else if (value.hasOwnProperty('type') && value.type === 'array' && value.hasOwnProperty('data')) { } else if (value.hasOwnProperty('type') && value.type === 'array' && value.hasOwnProperty('data')) {
result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('array['+value.length+']'); result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('array['+value.length+']');
} else if (value.hasOwnProperty('type') && value.type === 'function') {
result = $('<span class="debug-message-object-value debug-message-type-meta"></span>').text('function');
} else if (value.hasOwnProperty('type') && value.type === 'number') {
result = $('<span class="debug-message-object-value debug-message-type-number"></span>').text(value.data);
} else { } else {
result = $('<span class="debug-message-object-value debug-message-type-meta">object</span>'); result = $('<span class="debug-message-object-value debug-message-type-meta">object</span>');
} }
@ -45,6 +49,8 @@ RED.utils = (function() {
subvalue = sanitize(value); subvalue = sanitize(value);
} }
result = $('<span class="debug-message-object-value debug-message-type-string"></span>').html('"'+formatString(subvalue)+'"'); result = $('<span class="debug-message-object-value debug-message-type-string"></span>').html('"'+formatString(subvalue)+'"');
} else if (typeof value === 'number') {
result = $('<span class="debug-message-object-value debug-message-type-number"></span>').text(""+value);
} else { } else {
result = $('<span class="debug-message-object-value debug-message-type-other"></span>').text(""+value); result = $('<span class="debug-message-object-value debug-message-type-other"></span>').text(""+value);
} }
@ -125,7 +131,7 @@ RED.utils = (function() {
e.stopPropagation(); e.stopPropagation();
RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue"); RED.clipboard.copyText(msg,copyPayload,"clipboard.copyMessageValue");
}) })
if (strippedKey !== '') { if (strippedKey !== undefined && strippedKey !== '') {
var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey); var isPinned = pinnedPaths[sourceId].hasOwnProperty(strippedKey);
var pinPath = $('<button class="editor-button editor-button-small debug-message-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).click(function(e) { var pinPath = $('<button class="editor-button editor-button-small debug-message-tools-pin"><i class="fa fa-map-pin"></i></button>').appendTo(tools).click(function(e) {
@ -292,13 +298,18 @@ RED.utils = (function() {
var isArray = Array.isArray(obj); var isArray = Array.isArray(obj);
var isArrayObject = false; var isArrayObject = false;
if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__encoded__ && obj.type === 'array') || obj.type === 'Buffer')) { if (obj && typeof obj === 'object' && obj.hasOwnProperty('type') && obj.hasOwnProperty('data') && ((obj.__enc__ && obj.type === 'array') || obj.type === 'Buffer')) {
isArray = true; isArray = true;
isArrayObject = true; isArrayObject = true;
} }
if (obj === null || obj === undefined) { if (obj === null || obj === undefined) {
$('<span class="debug-message-type-null">'+obj+'</span>').appendTo(entryObj); $('<span class="debug-message-type-null">'+obj+'</span>').appendTo(entryObj);
} else if (obj.__enc__ && obj.type === 'number') {
e = $('<span class="debug-message-type-number debug-message-object-header"></span>').text(obj.data).appendTo(entryObj);
} else if (typeHint === "function" || (obj.__enc__ && obj.type === 'function')) {
e = $('<span class="debug-message-type-meta debug-message-object-header"></span>').text("function").appendTo(entryObj);
} else if (typeHint === "internal" || (obj.__enc__ && obj.type === 'internal')) {
e = $('<span class="debug-message-type-meta debug-message-object-header"></span>').text("[internal]").appendTo(entryObj);
} else if (typeof obj === 'string') { } else if (typeof obj === 'string') {
if (/[\t\n\r]/.test(obj)) { if (/[\t\n\r]/.test(obj)) {
element.addClass('collapsed'); element.addClass('collapsed');
@ -343,7 +354,7 @@ RED.utils = (function() {
if (originalLength === undefined) { if (originalLength === undefined) {
originalLength = data.length; originalLength = data.length;
} }
if (data.__encoded__) { if (data.__enc__) {
data = data.data; data = data.data;
} }
type = obj.type.toLowerCase(); type = obj.type.toLowerCase();
@ -783,6 +794,40 @@ RED.utils = (function() {
return RED.text.bidi.enforceTextDirectionWithUCC(l); return RED.text.bidi.enforceTextDirectionWithUCC(l);
} }
var nodeColorCache = {};
function getNodeColor(type, def) {
var result = def.color;
var paletteTheme = RED.settings.theme('palette.theme') || [];
if (paletteTheme.length > 0) {
if (!nodeColorCache.hasOwnProperty(type)) {
var l = paletteTheme.length;
for (var i=0;i<l;i++ ){
var themeRule = paletteTheme[i];
if (themeRule.hasOwnProperty('category')) {
if (!themeRule.hasOwnProperty('_category')) {
themeRule._category = new RegExp(themeRule.category);
}
if (!themeRule._category.test(def.category)) {
continue;
}
}
if (themeRule.hasOwnProperty('type')) {
if (!themeRule.hasOwnProperty('_type')) {
themeRule._type = new RegExp(themeRule.type);
}
if (!themeRule._type.test(type)) {
continue;
}
}
nodeColorCache[type] = themeRule.color || def.color;
break;
}
}
result = nodeColorCache[type];
}
return result;
}
function addSpinnerOverlay(container,contain) { function addSpinnerOverlay(container,contain) {
var spinner = $('<div class="projects-dialog-spinner "><img src="red/images/spin.svg"/></div>').appendTo(container); var spinner = $('<div class="projects-dialog-spinner "><img src="red/images/spin.svg"/></div>').appendTo(container);
if (contain) { if (contain) {
@ -791,6 +836,47 @@ RED.utils = (function() {
return spinner; return spinner;
} }
function decodeObject(payload,format) {
if ((format === 'number') && (payload === "NaN")) {
payload = Number.NaN;
} else if ((format === 'number') && (payload === "Infinity")) {
payload = Infinity;
} else if ((format === 'number') && (payload === "-Infinity")) {
payload = -Infinity;
} else if (format === 'Object' || /^array/.test(format) || format === 'boolean' || format === 'number' ) {
payload = JSON.parse(payload);
} else if (/error/i.test(format)) {
payload = JSON.parse(payload);
payload = (payload.name?payload.name+": ":"")+payload.message;
} else if (format === 'null') {
payload = null;
} else if (format === 'undefined') {
payload = undefined;
} else if (/^buffer/.test(format)) {
var buffer = payload;
payload = [];
for (var c = 0; c < buffer.length; c += 2) {
payload.push(parseInt(buffer.substr(c, 2), 16));
}
}
return payload;
}
function parseContextKey(key) {
var parts = {};
var m = /^#:\((\S+?)\)::(.*)$/.exec(key);
if (m) {
parts.store = m[1];
parts.key = m[2];
} else {
parts.key = key;
if (RED.settings.context) {
parts.store = RED.settings.context.default;
}
}
return parts;
}
return { return {
createObjectElement: buildMessageElement, createObjectElement: buildMessageElement,
getMessageProperty: getMessageProperty, getMessageProperty: getMessageProperty,
@ -800,6 +886,9 @@ RED.utils = (function() {
getDefaultNodeIcon: getDefaultNodeIcon, getDefaultNodeIcon: getDefaultNodeIcon,
getNodeIcon: getNodeIcon, getNodeIcon: getNodeIcon,
getNodeLabel: getNodeLabel, getNodeLabel: getNodeLabel,
addSpinnerOverlay: addSpinnerOverlay getNodeColor: getNodeColor,
addSpinnerOverlay: addSpinnerOverlay,
decodeObject: decodeObject,
parseContextKey: parseContextKey
} }
})(); })();

View File

@ -0,0 +1,164 @@
/**
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
RED.view.navigator = (function() {
var nav_scale = 25;
var nav_width = 5000/nav_scale;
var nav_height = 5000/nav_scale;
var navContainer;
var navBox;
var navBorder;
var navVis;
var scrollPos;
var scaleFactor;
var chartSize;
var dimensions;
var isDragging;
var isShowing = false;
function refreshNodes() {
if (!isShowing) {
return;
}
var navNode = navVis.selectAll(".navnode").data(RED.view.getActiveNodes(),function(d){return d.id});
navNode.exit().remove();
navNode.enter().insert("rect")
.attr('class','navnode')
.attr("pointer-events", "none");
navNode.each(function(d) {
d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale })
.attr("y",function(d) { return (d.y-d.h/2)/nav_scale })
.attr("width",function(d) { return Math.max(9,d.w/nav_scale) })
.attr("height",function(d) { return Math.max(3,d.h/nav_scale) })
.attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);})
});
}
function onScroll() {
if (!isDragging) {
resizeNavBorder();
}
}
function resizeNavBorder() {
if (navBorder) {
scaleFactor = RED.view.scale();
chartSize = [ $("#chart").width(), $("#chart").height()];
scrollPos = [$("#chart").scrollLeft(),$("#chart").scrollTop()];
navBorder.attr('x',scrollPos[0]/nav_scale)
.attr('y',scrollPos[1]/nav_scale)
.attr('width',chartSize[0]/nav_scale/scaleFactor)
.attr('height',chartSize[1]/nav_scale/scaleFactor)
}
}
return {
init: function() {
$(window).resize(resizeNavBorder);
RED.events.on("sidebar:resize",resizeNavBorder);
var hideTimeout;
navContainer = $('<div>').css({
"position":"absolute",
"bottom":$("#workspace-footer").height(),
"right":0,
zIndex: 1
}).appendTo("#workspace").hide();
navBox = d3.select(navContainer[0])
.append("svg:svg")
.attr("width", nav_width)
.attr("height", nav_height)
.attr("pointer-events", "all")
.style({
position: "absolute",
bottom: 0,
right:0,
zIndex: 101,
"border-left": "1px solid #ccc",
"border-top": "1px solid #ccc",
background: "rgba(245,245,245,0.5)",
"box-shadow": "-1px 0 3px rgba(0,0,0,0.1)"
});
navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({
fill:"none",
stroke:"none",
pointerEvents:"all"
}).on("mousedown", function() {
// Update these in case they have changed
scaleFactor = RED.view.scale();
chartSize = [ $("#chart").width(), $("#chart").height()];
dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor];
var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
navBorder.attr('x',newX).attr('y',newY);
isDragging = true;
$("#chart").scrollLeft(newX*nav_scale*scaleFactor);
$("#chart").scrollTop(newY*nav_scale*scaleFactor);
}).on("mousemove", function() {
if (!isDragging) { return }
if (d3.event.buttons === 0) {
isDragging = false;
return;
}
var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
navBorder.attr('x',newX).attr('y',newY);
$("#chart").scrollLeft(newX*nav_scale*scaleFactor);
$("#chart").scrollTop(newY*nav_scale*scaleFactor);
}).on("mouseup", function() {
isDragging = false;
})
navBorder = navBox.append("rect")
.attr("stroke-dasharray","5,5")
.attr("pointer-events", "none")
.style({
stroke: "#999",
strokeWidth: 1,
fill: "white",
});
navVis = navBox.append("svg:g")
$("#btn-navigate").click(function(evt) {
evt.preventDefault();
if (!isShowing) {
isShowing = true;
$("#btn-navigate").addClass("selected");
resizeNavBorder();
refreshNodes();
$("#chart").on("scroll",onScroll);
navContainer.fadeIn(200);
} else {
isShowing = false;
navContainer.fadeOut(100);
$("#chart").off("scroll",onScroll);
$("#btn-navigate").removeClass("selected");
}
})
},
refresh: refreshNodes,
resize: resizeNavBorder
}
})();

View File

@ -59,7 +59,9 @@ RED.view = (function() {
dblClickPrimed = null, dblClickPrimed = null,
clickTime = 0, clickTime = 0,
clickElapsed = 0, clickElapsed = 0,
scroll_position; scroll_position = [],
quickAddActive = false,
quickAddLink = null;
var clipboard = ""; var clipboard = "";
@ -332,6 +334,8 @@ RED.view = (function() {
redraw(); redraw();
}); });
RED.view.navigator.init();
$("#btn-zoom-out").click(function() {zoomOut();}); $("#btn-zoom-out").click(function() {zoomOut();});
$("#btn-zoom-zero").click(function() {zoomZero();}); $("#btn-zoom-zero").click(function() {zoomZero();});
$("#btn-zoom-in").click(function() {zoomIn();}); $("#btn-zoom-in").click(function() {zoomIn();});
@ -460,6 +464,82 @@ RED.view = (function() {
RED.actions.add("core:step-selection-left", function() { moveSelection(-20,0);}); RED.actions.add("core:step-selection-left", function() { moveSelection(-20,0);});
} }
function generateLinkPath(origX,origY, destX, destY, sc) {
var dy = destY-origY;
var dx = destX-origX;
var delta = Math.sqrt(dy*dy+dx*dx);
var scale = lineCurveScale;
var scaleY = 0;
if (dx*sc > 0) {
if (delta < node_width) {
scale = 0.75-0.75*((node_width-delta)/node_width);
// scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
// if (Math.abs(dy) < 3*node_height) {
// scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
// }
}
} else {
scale = 0.4-0.2*(Math.max(0,(node_width-Math.min(Math.abs(dx),Math.abs(dy)))/node_width));
}
if (dx*sc > 0) {
return "M "+origX+" "+origY+
" C "+(origX+sc*(node_width*scale))+" "+(origY+scaleY*node_height)+" "+
(destX-sc*(scale)*node_width)+" "+(destY-scaleY*node_height)+" "+
destX+" "+destY
} else {
var midX = Math.floor(destX-dx/2);
var midY = Math.floor(destY-dy/2);
//
if (dy === 0) {
midY = destY + node_height;
}
var cp_height = node_height/2;
var y1 = (destY + midY)/2
var topX =origX + sc*node_width*scale;
var topY = dy>0?Math.min(y1 - dy/2 , origY+cp_height):Math.max(y1 - dy/2 , origY-cp_height);
var bottomX = destX - sc*node_width*scale;
var bottomY = dy>0?Math.max(y1, destY-cp_height):Math.min(y1, destY+cp_height);
var x1 = (origX+topX)/2;
var scy = dy>0?1:-1;
var cp = [
// Orig -> Top
[x1,origY],
[topX,dy>0?Math.max(origY, topY-cp_height):Math.min(origY, topY+cp_height)],
// Top -> Mid
// [Mirror previous cp]
[x1,dy>0?Math.min(midY, topY+cp_height):Math.max(midY, topY-cp_height)],
// Mid -> Bottom
// [Mirror previous cp]
[bottomX,dy>0?Math.max(midY, bottomY-cp_height):Math.min(midY, bottomY+cp_height)],
// Bottom -> Dest
// [Mirror previous cp]
[(destX+bottomX)/2,destY]
];
if (cp[2][1] === topY+scy*cp_height) {
if (Math.abs(dy) < cp_height*10) {
cp[1][1] = topY-scy*cp_height/2;
cp[3][1] = bottomY-scy*cp_height/2;
}
cp[2][0] = topX;
}
return "M "+origX+" "+origY+
" C "+
cp[0][0]+" "+cp[0][1]+" "+
cp[1][0]+" "+cp[1][1]+" "+
topX+" "+topY+
" S "+
cp[2][0]+" "+cp[2][1]+" "+
midX+" "+midY+
" S "+
cp[3][0]+" "+cp[3][1]+" "+
bottomX+" "+bottomY+
" S "+
cp[4][0]+" "+cp[4][1]+" "+
destX+" "+destY
}
}
function addNode(type,x,y) { function addNode(type,x,y) {
var m = /^subflow:(.+)$/.exec(type); var m = /^subflow:(.+)$/.exec(type);
@ -567,14 +647,16 @@ RED.view = (function() {
mouse_mode = RED.state.QUICK_JOINING; mouse_mode = RED.state.QUICK_JOINING;
$(window).on('keyup',disableQuickJoinEventHandler); $(window).on('keyup',disableQuickJoinEventHandler);
} }
quickAddActive = true;
RED.typeSearch.show({ RED.typeSearch.show({
x:d3.event.clientX-mainPos.left-node_width/2, x:d3.event.clientX-mainPos.left-node_width/2,
y:d3.event.clientY-mainPos.top-node_height/2, y:d3.event.clientY-mainPos.top-node_height/2,
cancel: function() { cancel: function() {
quickAddActive = false;
resetMouseVars(); resetMouseVars();
}, },
add: function(type) { add: function(type) {
quickAddActive = false;
var result = addNode(type); var result = addNode(type);
if (!result) { if (!result) {
return; return;
@ -583,11 +665,10 @@ RED.view = (function() {
var historyEvent = result.historyEvent; var historyEvent = result.historyEvent;
nn.x = point[0]; nn.x = point[0];
nn.y = point[1]; nn.y = point[1];
if (mouse_mode === RED.state.QUICK_JOINING) { if (mouse_mode === RED.state.QUICK_JOINING || quickAddLink) {
if (drag_lines.length > 0) { if (quickAddLink || drag_lines.length > 0) {
var drag_line = drag_lines[0]; var drag_line = quickAddLink||drag_lines[0];
var src = null,dst,src_port; var src = null,dst,src_port;
if (drag_line.portType === PORT_TYPE_OUTPUT && nn.inputs > 0) { if (drag_line.portType === PORT_TYPE_OUTPUT && nn.inputs > 0) {
src = drag_line.node; src = drag_line.node;
src_port = drag_line.port; src_port = drag_line.port;
@ -602,9 +683,9 @@ RED.view = (function() {
RED.nodes.addLink(link); RED.nodes.addLink(link);
historyEvent.links = [link]; historyEvent.links = [link];
hideDragLines(); hideDragLines();
if (drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) { if (!quickAddLink && drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) {
showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]); showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]);
} else if (drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) { } else if (!quickAddLink && drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) {
showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]); showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]);
} else { } else {
resetMouseVars(); resetMouseVars();
@ -622,9 +703,9 @@ RED.view = (function() {
resetMouseVars(); resetMouseVars();
} }
} }
quickAddLink = null;
} }
RED.history.push(historyEvent); RED.history.push(historyEvent);
RED.nodes.add(nn); RED.nodes.add(nn);
RED.editor.validateNode(nn); RED.editor.validateNode(nn);
@ -789,28 +870,7 @@ RED.view = (function() {
var sc = (drag_line.portType === PORT_TYPE_OUTPUT)?1:-1; var sc = (drag_line.portType === PORT_TYPE_OUTPUT)?1:-1;
var dy = mousePos[1]-(drag_line.node.y+portY); drag_line.el.attr("d",generateLinkPath(drag_line.node.x+sc*drag_line.node.w/2,drag_line.node.y+portY,mousePos[0],mousePos[1],sc));
var dx = mousePos[0]-(drag_line.node.x+sc*drag_line.node.w/2);
var delta = Math.sqrt(dy*dy+dx*dx);
var scale = lineCurveScale;
var scaleY = 0;
if (delta < node_width) {
scale = 0.75-0.75*((node_width-delta)/node_width);
}
if (dx*sc < 0) {
scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
if (Math.abs(dy) < 3*node_height) {
scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
}
}
drag_line.el.attr("d",
"M "+(drag_line.node.x+sc*drag_line.node.w/2)+" "+(drag_line.node.y+portY)+
" C "+(drag_line.node.x+sc*(drag_line.node.w/2+node_width*scale))+" "+(drag_line.node.y+portY+scaleY*node_height)+" "+
(mousePos[0]-sc*(scale)*node_width)+" "+(mousePos[1]-scaleY*node_height)+" "+
mousePos[0]+" "+mousePos[1]
);
} }
d3.event.preventDefault(); d3.event.preventDefault();
} else if (mouse_mode == RED.state.MOVING) { } else if (mouse_mode == RED.state.MOVING) {
@ -1060,17 +1120,20 @@ RED.view = (function() {
function zoomIn() { function zoomIn() {
if (scaleFactor < 2) { if (scaleFactor < 2) {
scaleFactor += 0.1; scaleFactor += 0.1;
RED.view.navigator.resize();
redraw(); redraw();
} }
} }
function zoomOut() { function zoomOut() {
if (scaleFactor > 0.3) { if (scaleFactor > 0.3) {
scaleFactor -= 0.1; scaleFactor -= 0.1;
RED.view.navigator.resize();
redraw(); redraw();
} }
} }
function zoomZero() { function zoomZero() {
scaleFactor = 1; scaleFactor = 1;
RED.view.navigator.resize();
redraw(); redraw();
} }
@ -1407,6 +1470,9 @@ RED.view = (function() {
function disableQuickJoinEventHandler(evt) { function disableQuickJoinEventHandler(evt) {
// Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari) // Check for ctrl (all browsers), "Meta" (Chrome/FF), keyCode 91 (Safari)
if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) { if (evt.keyCode === 17 || evt.key === "Meta" || evt.keyCode === 91) {
if (quickAddActive && drag_lines.length > 0) {
quickAddLink = drag_lines[0];
}
resetMouseVars(); resetMouseVars();
hideDragLines(); hideDragLines();
redraw(); redraw();
@ -1418,6 +1484,10 @@ RED.view = (function() {
//console.log(d,portType,portIndex); //console.log(d,portType,portIndex);
// disable zoom // disable zoom
//vis.call(d3.behavior.zoom().on("zoom"), null); //vis.call(d3.behavior.zoom().on("zoom"), null);
if (d3.event.button === 1) {
return;
}
mousedown_node = d; mousedown_node = d;
mousedown_port_type = portType; mousedown_port_type = portType;
mousedown_port_index = portIndex || 0; mousedown_port_index = portIndex || 0;
@ -1649,6 +1719,9 @@ RED.view = (function() {
function nodeMouseDown(d) { function nodeMouseDown(d) {
focusView(); focusView();
if (d3.event.button === 1) {
return;
}
//var touch0 = d3.event; //var touch0 = d3.event;
//var pos = [touch0.pageX,touch0.pageY]; //var pos = [touch0.pageX,touch0.pageY];
//RED.touch.radialMenu.show(d3.select(this),pos); //RED.touch.radialMenu.show(d3.select(this),pos);
@ -1692,7 +1765,9 @@ RED.view = (function() {
clickElapsed = now-clickTime; clickElapsed = now-clickTime;
clickTime = now; clickTime = now;
dblClickPrimed = (lastClickNode == mousedown_node); dblClickPrimed = (lastClickNode == mousedown_node &&
d3.event.buttons === 1 &&
!d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey);
lastClickNode = mousedown_node; lastClickNode = mousedown_node;
var i; var i;
@ -1963,7 +2038,7 @@ RED.view = (function() {
.attr("ry",4) .attr("ry",4)
.attr("width",16) .attr("width",16)
.attr("height",node_height-12) .attr("height",node_height-12)
.attr("fill",function(d) { return d._def.color;}) .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def); /*d._def.color;*/})
.attr("cursor","pointer") .attr("cursor","pointer")
.on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}}) .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("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
@ -1984,7 +2059,7 @@ RED.view = (function() {
.classed("node_unknown",function(d) { return d.type == "unknown"; }) .classed("node_unknown",function(d) { return d.type == "unknown"; })
.attr("rx", 5) .attr("rx", 5)
.attr("ry", 5) .attr("ry", 5)
.attr("fill",function(d) { return d._def.color;}) .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def); /*d._def.color;*/})
.on("mouseup",nodeMouseUp) .on("mouseup",nodeMouseUp)
.on("mousedown",nodeMouseDown) .on("mousedown",nodeMouseDown)
.on("touchstart",function(d) { .on("touchstart",function(d) {
@ -2381,32 +2456,17 @@ RED.view = (function() {
var numOutputs = d.source.outputs || 1; var numOutputs = d.source.outputs || 1;
var sourcePort = d.sourcePort || 0; var sourcePort = d.sourcePort || 0;
var y = -((numOutputs-1)/2)*13 +13*sourcePort; var y = -((numOutputs-1)/2)*13 +13*sourcePort;
var dy = d.target.y-(d.source.y+y);
var dx = (d.target.x-d.target.w/2)-(d.source.x+d.source.w/2);
var delta = Math.sqrt(dy*dy+dx*dx);
var scale = lineCurveScale;
var scaleY = 0;
if (delta < node_width) {
scale = 0.75-0.75*((node_width-delta)/node_width);
}
if (dx < 0) {
scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
if (Math.abs(dy) < 3*node_height) {
scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
}
}
d.x1 = d.source.x+d.source.w/2; d.x1 = d.source.x+d.source.w/2;
d.y1 = d.source.y+y; d.y1 = d.source.y+y;
d.x2 = d.target.x-d.target.w/2; d.x2 = d.target.x-d.target.w/2;
d.y2 = d.target.y; d.y2 = d.target.y;
return "M "+d.x1+" "+d.y1+ // return "M "+d.x1+" "+d.y1+
" C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+ // " C "+(d.x1+scale*node_width)+" "+(d.y1+scaleY*node_height)+" "+
(d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+ // (d.x2-scale*node_width)+" "+(d.y2-scaleY*node_height)+" "+
d.x2+" "+d.y2; // d.x2+" "+d.y2;
return generateLinkPath(d.x1,d.y1,d.x2,d.y2,1);
}); });
} }
}) })
@ -2542,7 +2602,7 @@ RED.view = (function() {
} }
).classed("link_selected", false); ).classed("link_selected", false);
} }
RED.view.navigator.refresh();
if (d3.event) { if (d3.event) {
d3.event.preventDefault(); d3.event.preventDefault();
} }
@ -2816,7 +2876,9 @@ RED.view = (function() {
gridSize = Math.max(5,v); gridSize = Math.max(5,v);
updateGrid(); updateGrid();
} }
},
getActiveNodes: function() {
return activeNodes;
} }
}; };
})(); })();

View File

@ -19,3 +19,7 @@
div.btn-group, a.btn { div.btn-group, a.btn {
@include disable-selection; @include disable-selection;
} }
.dropdown-menu>li>a {
color: #444;
}

View File

@ -64,7 +64,8 @@
display: inline-block; display: inline-block;
} }
} }
.debug-message-row { }
.debug-message-row {
.debug-message-tools-pin { .debug-message-tools-pin {
display: none; display: none;
} }
@ -82,7 +83,6 @@
} }
} }
} }
}
} }
.debug-message-meta .debug-message-tools { .debug-message-meta .debug-message-tools {
.editor-button-small { .editor-button-small {

View File

@ -353,3 +353,64 @@
} }
} }
#node-settings-icon {
margin-left: 10px;
width: calc(100% - 163px);
}
.red-ui-icon-picker {
position: absolute;
border: 1px solid $primary-border-color;
box-shadow: 0 1px 6px -3px black;
background: white;
z-Index: 21;
display: none;
select {
box-sizing: border-box;
margin: 3px;
width: calc(100% - 6px);
}
}
.red-ui-icon-list {
width: 308px;
height: 200px;
overflow-y: scroll;
line-height: 0px;
}
.red-ui-icon-list-icon {
display: inline-block;
margin: 2px;
padding: 4px;
cursor: pointer;
border-radius: 4px;
&:hover {
background: lighten($node-selected-color,20%);
}
&.selected {
background: lighten($node-selected-color,20%);
.red-ui-search-result-node {
border-color: white;
}
}
}
.red-ui-icon-list-module {
background: $palette-header-background;
font-size: 0.9em;
padding: 3px;
color: #666;
clear: both;
i {
margin-right: 5px;
}
}
.red-ui-icon-meta {
border-top: 1px solid $secondary-border-color;
span {
padding: 4px;
color: #666;
font-size: 0.9em;
}
button {
float: right;
margin: 2px;
}
}

View File

@ -248,7 +248,7 @@
.link_outline { .link_outline {
stroke: #fff; stroke: #fff;
stroke-width: 4; stroke-width: 5;
cursor: crosshair; cursor: crosshair;
fill: none; fill: none;
pointer-events: none; pointer-events: none;

View File

@ -134,15 +134,18 @@
color: $workspace-button-toggle-color !important; color: $workspace-button-toggle-color !important;
background:$workspace-button-background-active; background:$workspace-button-background-active;
margin-bottom: 1px; margin-bottom: 1px;
&.selected:not(.disabled) {
&.selected:not(.disabled):not(:disabled) {
color: $workspace-button-toggle-color-selected !important; color: $workspace-button-toggle-color-selected !important;
background: $workspace-button-background; background: $workspace-button-background;
border-bottom-width: 2px; border-bottom-width: 2px;
border-bottom-color: $form-input-border-selected-color; border-bottom-color: $form-input-border-selected-color;
margin-bottom: 0; margin-bottom: 0;
&:not(.single) {
cursor: default; cursor: default;
} }
&.disabled { }
&.disabled,&:disabled {
color: $workspace-button-toggle-color-disabled !important; color: $workspace-button-toggle-color-disabled !important;
} }
} }
@ -203,7 +206,7 @@
height: 25px; height: 25px;
line-height: 23px; line-height: 23px;
padding: 0 10px; padding: 0 10px;
user-select: none;
.button-group:not(:last-child) { .button-group:not(:last-child) {
margin-right: 5px; margin-right: 5px;
@ -227,6 +230,7 @@
font-size: 11px; font-size: 11px;
line-height: 17px; line-height: 17px;
height: 18px; height: 18px;
width: 18px;
&.text-button { &.text-button {
width: auto; width: auto;
padding: 0 5px; padding: 0 5px;

View File

@ -90,13 +90,14 @@
text-align: left; text-align: left;
padding: 9px; padding: 9px;
font-weight: bold; font-weight: bold;
padding-left: 30px;
overflow: hidden; overflow: hidden;
white-space: nowrap; user-select: none;
text-overflow: ellipsis;
} }
.palette-header > i { .palette-header > i {
margin: 3px 10px 3px 3px; position: absolute;
left: 11px;
top: 12px;
-webkit-transition: all 0.2s ease-in-out; -webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out;

View File

@ -40,6 +40,7 @@
@import "panels"; @import "panels";
@import "tabs"; @import "tabs";
@import "tab-config"; @import "tab-config";
@import "tab-context";
@import "tab-info"; @import "tab-info";
@import "popover"; @import "popover";
@import "flow"; @import "flow";

View File

@ -0,0 +1,54 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
.sidebar-context-stack {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-y: scroll;
.palette-category {
&:not(.palette-category-expanded) button {
display: none;
}
}
}
.sidebar-context-property {
position: relative;
.debug-message-tools {
right: 0px;
margin-right: 5px;
display: none;
}
&:hover .debug-message-tools {
display: inline-block;
}
}
.sidebar-context-updated {
text-align: right;
font-size: 11px;
color: #bbb;
padding: 1px 3px;
}
.sidebar-context-property-storename {
display: block;
font-size: 0.8em;
font-style: italic;
color: #aaa;
}

View File

@ -214,12 +214,11 @@
border-bottom: 1px solid $primary-border-color; border-bottom: 1px solid $primary-border-color;
z-index: 2; z-index: 2;
a { a {
@include workspace-button; @include workspace-button-toggle;
line-height: 26px; line-height: 26px;
height: 28px; height: 28px;
width: 28px; width: 28px;
margin: 4px 3px 3px; margin: 4px 3px 3px;
border: 1px solid $primary-border-color;
z-index: 2; z-index: 2;
&.red-ui-tab-link-button { &.red-ui-tab-link-button {
&:not(.active) { &:not(.active) {

View File

@ -23,7 +23,7 @@
margin: 0; margin: 0;
vertical-align: middle; vertical-align: middle;
box-sizing: border-box; box-sizing: border-box;
overflow:hidden; overflow:visible;
position: relative; position: relative;
.red-ui-typedInput-input { .red-ui-typedInput-input {
position: absolute; position: absolute;
@ -43,6 +43,7 @@
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
box-shadow: none; box-shadow: none;
vertical-align: middle; vertical-align: middle;
// backgroun/d: #f0fff0;
} }
&.red-ui-typedInput-focus:not(.input-error) { &.red-ui-typedInput-focus:not(.input-error) {
@ -63,7 +64,7 @@
line-height: 32px; line-height: 32px;
vertical-align: middle; vertical-align: middle;
color: #555; color: #555;
i { i.red-ui-typedInput-icon {
position: relative; position: relative;
top: -3px; top: -3px;
margin-left: 1px; margin-left: 1px;
@ -76,11 +77,11 @@
} }
&.disabled { &.disabled {
cursor: default; cursor: default;
i { i.red-ui-typedInput-icon {
color: #bbb; color: #bbb;
} }
} }
span { .red-ui-typedInput-type-label,.red-ui-typedInput-option-label {
display: inline-block; display: inline-block;
height: 100%; height: 100%;
padding: 0 1px 0 5px; padding: 0 1px 0 5px;
@ -121,26 +122,25 @@
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
padding: 0 0 0 0; padding: 0 0 0 0;
position:absolute; position:absolute;
width: calc( 100% ); right: 0;
i {
position:absolute;
right: 4px;
top: 7px;
}
.red-ui-typedInput-option-label { .red-ui-typedInput-option-label {
background:#fff; background:$typedInput-button-background;
position:absolute; position:absolute;
left:0; left:0;
right:23px; right:23px;
top: 0; top: 0;
padding: 0 5px 0 5px; padding: 0 5px 0 8px;
i.red-ui-typedInput-icon {
margin-right: 4px;
margin-top: 4px;
}
} }
.red-ui-typedInput-option-caret { .red-ui-typedInput-option-caret {
top: 0; top: 0;
position: absolute; position: absolute;
right: 0; right: 0;
width: 17px; width: 17px;
padding-left: 6px;
} }
&:focus { &:focus {
box-shadow: none; box-shadow: none;
@ -175,4 +175,7 @@
background: $typedInput-button-background-active; background: $typedInput-button-background-active;
} }
} }
.red-ui-typedInput-icon {
margin-right: 4px;
}
} }

View File

@ -47,7 +47,9 @@
.workspace-footer-button { .workspace-footer-button {
@include component-footer-button; @include component-footer-button;
} }
.workspace-footer-button-toggle {
@include component-footer-button-toggle;
}
#workspace-footer { #workspace-footer {
@include component-footer; @include component-footer;
} }

View File

@ -51,6 +51,7 @@
<a class="workspace-footer-button" id="btn-zoom-out" href="#"><i class="fa fa-minus"></i></a> <a class="workspace-footer-button" id="btn-zoom-out" href="#"><i class="fa fa-minus"></i></a>
<a class="workspace-footer-button" id="btn-zoom-zero" href="#"><i class="fa fa-circle-o"></i></a> <a class="workspace-footer-button" id="btn-zoom-zero" href="#"><i class="fa fa-circle-o"></i></a>
<a class="workspace-footer-button" id="btn-zoom-in" href="#"><i class="fa fa-plus"></i></a> <a class="workspace-footer-button" id="btn-zoom-in" href="#"><i class="fa fa-plus"></i></a>
<a class="workspace-footer-button-toggle single" id="btn-navigate" href="#"><i class="fa fa-map-o"></i></a>
</div> </div>
<div id="editor-shade" class="hide"></div> <div id="editor-shade" class="hide"></div>
</div> </div>

View File

@ -237,10 +237,9 @@ If you want every 20 minutes from now - use the <i>"interval"</i> option.</p>
} else { } else {
return this._("inject.timestamp")+suffix; return this._("inject.timestamp")+suffix;
} }
} else if (this.payloadType === 'flow' && this.payload.length < 19) { } else if (this.payloadType === 'flow' || this.payloadType === 'global') {
return 'flow.'+this.payload+suffix; var key = RED.utils.parseContextKey(this.payload);
} else if (this.payloadType === 'global' && this.payload.length < 17) { return this.payloadType+"."+key.key+suffix;
return 'global.'+this.payload+suffix;
} else { } else {
return this._("inject.inject")+suffix; return this._("inject.inject")+suffix;
} }
@ -502,7 +501,13 @@ If you want every 20 minutes from now - use the <i>"interval"</i> option.</p>
if (this.changed) { if (this.changed) {
return RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.undeployedChanges")}),"warning"); return RED.notify(RED._("notification.warning", {message:RED._("notification.warnings.undeployedChanges")}),"warning");
} }
var label = (this.name||this.payload); var payload = this.payload;
if ((this.payloadType === 'flow') ||
(this.payloadType === 'global')) {
var key = RED.utils.parseContextKey(payload);
payload = this.payloadType+"."+key.key;
}
var label = (this.name||payload);
if (label.length > 30) { if (label.length > 30) {
label = label.substring(0,50)+"..."; label = label.substring(0,50)+"...";
} }

View File

@ -63,8 +63,9 @@ module.exports = function(RED) {
} }
this.on("input",function(msg) { this.on("input",function(msg) {
try {
msg.topic = this.topic; msg.topic = this.topic;
if (this.payloadType !== 'flow' && this.payloadType !== 'global') {
try {
if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") { if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") {
msg.payload = Date.now(); msg.payload = Date.now();
} else if (this.payloadType == null) { } else if (this.payloadType == null) {
@ -79,6 +80,17 @@ module.exports = function(RED) {
} catch(err) { } catch(err) {
this.error(err,msg); this.error(err,msg);
} }
} else {
RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg, function(err,res) {
if (err) {
node.error(err,msg);
} else {
msg.payload = res;
node.send(msg);
}
});
}
}); });
} }

View File

@ -156,7 +156,7 @@
toolbar: uiComponents.footer, toolbar: uiComponents.footer,
enableOnEdit: true, enableOnEdit: true,
pinned: true, pinned: true,
iconClass: "fa fa-list-alt" iconClass: "fa fa-bug"
}); });
RED.actions.add("core:show-debug-tab",function() { RED.sidebar.show('debug'); }); RED.actions.add("core:show-debug-tab",function() { RED.sidebar.show('debug'); });

View File

@ -4,7 +4,6 @@ module.exports = function(RED) {
var util = require("util"); var util = require("util");
var events = require("events"); var events = require("events");
var path = require("path"); var path = require("path");
var safeJSONStringify = require("json-stringify-safe");
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";
@ -104,111 +103,7 @@ module.exports = function(RED) {
function sendDebug(msg) { function sendDebug(msg) {
// don't put blank errors in sidebar (but do add to logs) // don't put blank errors in sidebar (but do add to logs)
//if ((msg.msg === "") && (msg.hasOwnProperty("level")) && (msg.level === 20)) { return; } //if ((msg.msg === "") && (msg.hasOwnProperty("level")) && (msg.level === 20)) { return; }
if (msg.msg instanceof Error) { msg = RED.util.encodeObject(msg,{maxLength:debuglength});
msg.format = "error";
var errorMsg = {};
if (msg.msg.name) {
errorMsg.name = msg.msg.name;
}
if (msg.msg.hasOwnProperty('message')) {
errorMsg.message = msg.msg.message;
} else {
errorMsg.message = msg.msg.toString();
}
msg.msg = JSON.stringify(errorMsg);
} else if (msg.msg instanceof Buffer) {
msg.format = "buffer["+msg.msg.length+"]";
msg.msg = msg.msg.toString('hex');
if (msg.msg.length > debuglength) {
msg.msg = msg.msg.substring(0,debuglength);
}
} else if (msg.msg && typeof msg.msg === 'object') {
try {
msg.format = msg.msg.constructor.name || "Object";
// Handle special case of msg.req/res objects from HTTP In node
if (msg.format === "IncomingMessage" || msg.format === "ServerResponse") {
msg.format = "Object";
}
} catch(err) {
msg.format = "Object";
}
if (/error/i.test(msg.format)) {
msg.msg = JSON.stringify({
name: msg.msg.name,
message: msg.msg.message
});
} else {
var isArray = util.isArray(msg.msg);
if (isArray) {
msg.format = "array["+msg.msg.length+"]";
if (msg.msg.length > debuglength) {
// msg.msg = msg.msg.slice(0,debuglength);
msg.msg = {
__encoded__: true,
type: "array",
data: msg.msg.slice(0,debuglength),
length: msg.msg.length
}
}
}
if (isArray || (msg.format === "Object")) {
msg.msg = safeJSONStringify(msg.msg, function(key, value) {
if (key === '_req' || key === '_res') {
value = "[internal]"
} else if (value instanceof Error) {
value = value.toString()
} else if (util.isArray(value) && value.length > debuglength) {
value = {
__encoded__: true,
type: "array",
data: value.slice(0,debuglength),
length: value.length
}
} else if (typeof value === 'string') {
if (value.length > debuglength) {
value = value.substring(0,debuglength)+"...";
}
} else if (value && value.constructor) {
if (value.type === "Buffer") {
value.__encoded__ = true;
value.length = value.data.length;
if (value.length > debuglength) {
value.data = value.data.slice(0,debuglength);
}
} else if (value.constructor.name === "ServerResponse") {
value = "[internal]"
} else if (value.constructor.name === "Socket") {
value = "[internal]"
}
}
return value;
}," ");
} else {
try { msg.msg = msg.msg.toString(); }
catch(e) { msg.msg = "[Type not printable]"; }
}
}
} else if (typeof msg.msg === "boolean") {
msg.format = "boolean";
msg.msg = msg.msg.toString();
} else if (typeof msg.msg === "number") {
msg.format = "number";
msg.msg = msg.msg.toString();
} else if (msg.msg === 0) {
msg.format = "number";
msg.msg = "0";
} else if (msg.msg === null || typeof msg.msg === "undefined") {
msg.format = (msg.msg === null)?"null":"undefined";
msg.msg = "(undefined)";
} else {
msg.format = "string["+msg.msg.length+"]";
if (msg.msg.length > debuglength) {
msg.msg = msg.msg.substring(0,debuglength)+"...";
}
}
// if (msg.msg.length > debuglength) {
// msg.msg = msg.msg.substr(0,debuglength) +" ....";
// }
RED.comms.publish("debug",msg); RED.comms.publish("debug",msg);
} }

View File

@ -9,7 +9,8 @@
<input type="hidden" id="node-input-func" autofocus="autofocus"> <input type="hidden" id="node-input-func" autofocus="autofocus">
<input type="hidden" id="node-input-noerr"> <input type="hidden" id="node-input-noerr">
</div> </div>
<div class="form-row node-text-editor-row"> <div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom:calc(100% + 3px);"><button id="node-function-expand-js" class="editor-button editor-button-small"><i class="fa fa-expand"></i></button></div>
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div> <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
</div> </div>
<div class="form-row"> <div class="form-row">
@ -119,6 +120,22 @@
fields:['name','outputs'] fields:['name','outputs']
}); });
this.editor.focus(); this.editor.focus();
$("#node-function-expand-js").click(function(e) {
e.preventDefault();
var value = that.editor.getValue();
RED.editor.editJavaScript({
value: value,
cursor: that.editor.getCursorPosition(),
complete: function(v,cursor) {
that.editor.setValue(v, -1);
that.editor.gotoLine(cursor.row+1,cursor.column,false);
setTimeout(function() {
that.editor.focus();
},300);
}
})
})
}, },
oneditsave: function() { oneditsave: function() {
var annot = this.editor.getSession().getAnnotations(); var annot = this.editor.getSession().getAnnotations();

View File

@ -77,7 +77,7 @@
}</pre> }</pre>
<p>The resulting property will be: <p>The resulting property will be:
<pre>Hello Fred. Today is Monday</pre> <pre>Hello Fred. Today is Monday</pre>
<p>It is possible to use a property from the flow context or global context. Just use <code>{{flow.name}}</code> or <code>{{global.name}}</code>. <p>It is possible to use a property from the flow context or global context. Just use <code>{{flow.name}}</code> or <code>{{global.name}}</code>, or for persistable store <code>store</code> use <code>{{flow[store].name}}</code> or <code>{{flobal[store].name}}</code>.
<p><b>Note: </b>By default, <i>mustache</i> will escape any HTML entities in the values it substitutes. <p><b>Note: </b>By default, <i>mustache</i> will escape any HTML entities in the values it substitutes.
To prevent this, use <code>{{{triple}}}</code> braces. To prevent this, use <code>{{{triple}}}</code> braces.
</script> </script>

View File

@ -19,20 +19,39 @@ module.exports = function(RED) {
var mustache = require("mustache"); var mustache = require("mustache");
var yaml = require("js-yaml"); var yaml = require("js-yaml");
function parseContext(key) {
var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key);
if (match) {
var parts = {};
parts.type = match[1];
parts.store = (match[3] === '') ? "default" : match[3];
parts.field = match[4];
return parts;
}
return undefined;
}
/** /**
* Custom Mustache Context capable to resolve message property and node * Custom Mustache Context capable to collect message property and node
* flow and global context * flow and global context
*/ */
function NodeContext(msg, nodeContext, parent, escapeStrings) {
function NodeContext(msg, nodeContext, parent, escapeStrings, promises, results) {
this.msgContext = new mustache.Context(msg,parent); this.msgContext = new mustache.Context(msg,parent);
this.nodeContext = nodeContext; this.nodeContext = nodeContext;
this.escapeStrings = escapeStrings; this.escapeStrings = escapeStrings;
this.promises = promises;
this.results = results;
} }
NodeContext.prototype = new mustache.Context(); NodeContext.prototype = new mustache.Context();
NodeContext.prototype.lookup = function (name) { NodeContext.prototype.lookup = function (name) {
var results = this.results;
if (results) {
var val = results.shift();
return val;
}
// try message first: // try message first:
try { try {
var value = this.msgContext.lookup(name); var value = this.msgContext.lookup(name);
@ -45,22 +64,39 @@ module.exports = function(RED) {
value = value.replace(/\f/g, "\\f"); value = value.replace(/\f/g, "\\f");
value = value.replace(/[\b]/g, "\\b"); value = value.replace(/[\b]/g, "\\b");
} }
this.promises.push(Promise.resolve(value));
return value; return value;
} }
// try node context: // try flow/global context:
var dot = name.indexOf("."); var context = parseContext(name);
/* istanbul ignore else */ if (context) {
if (dot > 0) { var type = context.type;
var contextName = name.substr(0, dot); var store = context.store;
var variableName = name.substr(dot + 1); var field = context.field;
var target = this.nodeContext[type];
if (contextName === "flow" && this.nodeContext.flow) { if (target) {
return this.nodeContext.flow.get(variableName); var promise = new Promise((resolve, reject) => {
var callback = (err, val) => {
if (err) {
reject(err);
} else {
resolve(val);
} }
else if (contextName === "global" && this.nodeContext.global) { };
return this.nodeContext.global.get(variableName); target.get(field, store, callback);
});
this.promises.push(promise);
return '';
} }
else {
this.promises.push(Promise.resolve(''));
return '';
}
}
else {
this.promises.push(Promise.resolve(''));
return '';
} }
} }
catch(err) { catch(err) {
@ -69,7 +105,7 @@ module.exports = function(RED) {
} }
NodeContext.prototype.push = function push (view) { NodeContext.prototype.push = function push (view) {
return new NodeContext(view, this.nodeContext,this.msgContext); return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.promises, this.results);
}; };
function TemplateNode(n) { function TemplateNode(n) {
@ -83,8 +119,33 @@ module.exports = function(RED) {
var node = this; var node = this;
node.on("input", function(msg) { node.on("input", function(msg) {
function output(value) {
/* istanbul ignore else */
if (node.outputFormat === "json") {
value = JSON.parse(value);
}
/* istanbul ignore else */
if (node.outputFormat === "yaml") {
value = yaml.load(value);
}
if (node.fieldType === 'msg') {
RED.util.setMessageProperty(msg, node.field, value);
node.send(msg);
} else if ((node.fieldType === 'flow') ||
(node.fieldType === 'global')) {
var context = RED.util.parseContextStore(node.field);
var target = node.context()[node.fieldType];
target.set(context.key, value, context.store, function (err) {
if (err) {
node.error(err, msg);
} else {
node.send(msg);
}
});
}
}
try { try {
var value;
/*** /***
* Allow template contents to be defined externally * Allow template contents to be defined externally
* through inbound msg.template IFF node.template empty * through inbound msg.template IFF node.template empty
@ -97,31 +158,18 @@ module.exports = function(RED) {
} }
if (node.syntax === "mustache") { if (node.syntax === "mustache") {
if (node.outputFormat === "json") { var is_json = (node.outputFormat === "json");
value = mustache.render(template,new NodeContext(msg, node.context(), null, true)); var promises = [];
mustache.render(template, new NodeContext(msg, node.context(), null, is_json, promises, null));
Promise.all(promises).then(function (values) {
var value = mustache.render(template, new NodeContext(msg, node.context(), null, is_json, null, values));
output(value);
}).catch(function (err) {
node.error(err.message);
});
} else { } else {
value = mustache.render(template,new NodeContext(msg, node.context(), null, false)); output(template);
} }
} else {
value = template;
}
/* istanbul ignore else */
if (node.outputFormat === "json") {
value = JSON.parse(value);
}
/* istanbul ignore else */
if (node.outputFormat === "yaml") {
value = yaml.load(value);
}
if (node.fieldType === 'msg') {
RED.util.setMessageProperty(msg,node.field,value);
} else if (node.fieldType === 'flow') {
node.context().flow.set(node.field,value);
} else if (node.fieldType === 'global') {
node.context().global.set(node.field,value);
}
node.send(msg);
} }
catch(err) { catch(err) {
node.error(err.message); node.error(err.message);

View File

@ -162,7 +162,7 @@
$("#node-input-op1").typedInput({ $("#node-input-op1").typedInput({
default: 'str', default: 'str',
typeField: $("#node-input-op1type"), typeField: $("#node-input-op1type"),
types:['flow','global','str','num','bool','json', types:['flow','global','str','num','bool','json','bin','date','env',
optionPayload, optionPayload,
optionNothing optionNothing
] ]
@ -170,7 +170,7 @@
$("#node-input-op2").typedInput({ $("#node-input-op2").typedInput({
default: 'str', default: 'str',
typeField: $("#node-input-op2type"), typeField: $("#node-input-op2type"),
types:['flow','global','str','num','bool','json', types:['flow','global','str','num','bool','json','bin','date','env',
optionOriginalPayload, optionOriginalPayload,
optionLatestPayload, optionLatestPayload,
optionNothing optionNothing

View File

@ -76,8 +76,43 @@ module.exports = function(RED) {
var node = this; var node = this;
node.topics = {}; node.topics = {};
this.on("input", function(msg) { var pendingMessages = [];
var activeMessagePromise = null;
var processMessageQueue = function(msg) {
if (msg) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
if (activeMessagePromise !== null) {
// The node is currently processing a message, so do nothing
// more with this message
return;
}
}
if (pendingMessages.length === 0) {
// There are no more messages to process, clear the active flag
// and return
activeMessagePromise = null;
return;
}
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
activeMessagePromise = processMessage(nextMsg)
.then(processMessageQueue)
.catch((err) => {
node.error(err,nextMsg);
return processMessageQueue();
});
}
this.on('input', function(msg) {
processMessageQueue(msg);
});
var processMessage = function(msg) {
var topic = msg.topic || "_none"; var topic = msg.topic || "_none";
var promise;
if (node.bytopic === "all") { topic = "_none"; } if (node.bytopic === "all") { topic = "_none"; }
node.topics[topic] = node.topics[topic] || {}; node.topics[topic] = node.topics[topic] || {};
if (msg.hasOwnProperty("reset") || ((node.reset !== '') && msg.hasOwnProperty("payload") && (msg.payload !== null) && msg.payload.toString && (msg.payload.toString() == node.reset)) ) { if (msg.hasOwnProperty("reset") || ((node.reset !== '') && msg.hasOwnProperty("payload") && (msg.payload !== null) && msg.payload.toString && (msg.payload.toString() == node.reset)) ) {
@ -88,18 +123,39 @@ module.exports = function(RED) {
} }
else { else {
if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) { if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) {
promise = Promise.resolve();
if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); } else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); }
else if (node.op2type !== "nul") { else if (node.op2type !== "nul") {
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) {
reject(err);
} else {
node.topics[topic].m2 = value;
resolve();
}
});
});
} }
return promise.then(() => {
promise = Promise.resolve();
if (node.op1type === "pay") { } if (node.op1type === "pay") { }
else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); } else if (node.op1Templated) { msg.payload = mustache.render(node.op1,msg); }
else if (node.op1type !== "nul") { else if (node.op1type !== "nul") {
msg.payload = RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg); promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg,(err,value) => {
if (err) {
reject(err);
} else {
msg.payload = value;
resolve();
} }
});
});
}
return promise.then(() => {
if (node.duration === 0) { node.topics[topic].tout = 0; } if (node.duration === 0) { node.topics[topic].tout = 0; }
else if (node.loop === true) { else if (node.loop === true) {
/* istanbul ignore else */ /* istanbul ignore else */
@ -115,21 +171,40 @@ module.exports = function(RED) {
node.topics[topic].tout = setTimeout(function() { node.topics[topic].tout = setTimeout(function() {
var msg2 = null; var msg2 = null;
if (node.op2type !== "nul") { if (node.op2type !== "nul") {
var promise = Promise.resolve();
msg2 = RED.util.cloneMessage(msg); msg2 = RED.util.cloneMessage(msg);
if (node.op2type === "flow" || node.op2type === "global") { if (node.op2type === "flow" || node.op2type === "global") {
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) {
reject(err);
} else {
node.topics[topic].m2 = value;
resolve();
} }
});
});
}
promise.then(() => {
msg2.payload = node.topics[topic].m2; msg2.payload = node.topics[topic].m2;
delete node.topics[topic]; delete node.topics[topic];
node.send(msg2); node.send(msg2);
}
else { delete node.topics[topic]; }
node.status({}); node.status({});
}).catch(err => {
node.error(err);
});
} else {
delete node.topics[topic];
node.status({});
}
}, node.duration); }, node.duration);
} }
} }
node.status({fill:"blue",shape:"dot",text:" "}); node.status({fill:"blue",shape:"dot",text:" "});
if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); }
});
});
} }
else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) { else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) {
/* istanbul ignore else */ /* istanbul ignore else */
@ -138,10 +213,24 @@ module.exports = function(RED) {
if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); } if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); }
node.topics[topic].tout = setTimeout(function() { node.topics[topic].tout = setTimeout(function() {
var msg2 = null; var msg2 = null;
var promise = Promise.resolve();
if (node.op2type !== "nul") { if (node.op2type !== "nul") {
if (node.op2type === "flow" || node.op2type === "global") { if (node.op2type === "flow" || node.op2type === "global") {
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); promise = new Promise((resolve,reject) => {
RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg,(err,value) => {
if (err) {
reject(err);
} else {
node.topics[topic].m2 = value;
resolve();
} }
});
});
}
}
promise.then(() => {
if (node.op2type !== "nul") {
if (node.topics[topic] !== undefined) { if (node.topics[topic] !== undefined) {
msg2 = RED.util.cloneMessage(msg); msg2 = RED.util.cloneMessage(msg);
msg2.payload = node.topics[topic].m2; msg2.payload = node.topics[topic].m2;
@ -150,13 +239,17 @@ module.exports = function(RED) {
delete node.topics[topic]; delete node.topics[topic];
node.status({}); node.status({});
node.send(msg2); node.send(msg2);
}).catch(err => {
node.error(err);
});
}, node.duration); }, node.duration);
} }
else { else {
if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
} }
} }
}); return Promise.resolve();
}
this.on("close", function() { this.on("close", function() {
for (var t in node.topics) { for (var t in node.topics) {
/* istanbul ignore else */ /* istanbul ignore else */

View File

@ -455,24 +455,8 @@ RED.debug = (function() {
$('<span class="debug-message-name">'+name+'</span>').appendTo(metaRow); $('<span class="debug-message-name">'+name+'</span>').appendTo(metaRow);
} }
if ((format === 'number') && (payload === "NaN")) { payload = RED.utils.decodeObject(payload,format);
payload = Number.NaN;
} else if (format === 'Object' || /^array/.test(format) || format === 'boolean' || format === 'number' ) {
payload = JSON.parse(payload);
} else if (/error/i.test(format)) {
payload = JSON.parse(payload);
payload = (payload.name?payload.name+": ":"")+payload.message;
} else if (format === 'null') {
payload = null;
} else if (format === 'undefined') {
payload = undefined;
} else if (/^buffer/.test(format)) {
var buffer = payload;
payload = [];
for (var c = 0; c < buffer.length; c += 2) {
payload.push(parseInt(buffer.substr(c, 2), 16));
}
}
var el = $('<span class="debug-message-payload"></span>').appendTo(msg); var el = $('<span class="debug-message-payload"></span>').appendTo(msg);
var path = o.property||''; var path = o.property||'';
var debugMessage = RED.utils.createObjectElement(payload, { var debugMessage = RED.utils.createObjectElement(payload, {

View File

@ -6,35 +6,36 @@ module.exports = function(RED) {
var fs = require('fs'); var fs = require('fs');
var gpioCommand = __dirname+'/nrgpio'; var gpioCommand = __dirname+'/nrgpio';
var allOK = true;
try { try {
var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString();
if (cpuinfo.indexOf(": BCM") === -1) { throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); } if (cpuinfo.indexOf(": BCM") === -1) {
} catch(err) { allOK = false;
throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.ignorenode"));
} }
try { try {
fs.statSync("/usr/share/doc/python-rpi.gpio"); // test on Raspbian fs.statSync("/usr/share/doc/python-rpi.gpio"); // test on Raspbian
// /usr/lib/python2.7/dist-packages/RPi/GPIO // /usr/lib/python2.7/dist-packages/RPi/GPIO
} catch(err) { } catch(err) {
try { try {
fs.statSync("/usr/lib/python2.7/site-packages/RPi/GPIO"); // test on Arch fs.statSync("/usr/lib/python2.7/site-packages/RPi/GPIO"); // test on Arch
} } catch(err) {
catch(err) {
try { try {
fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot
} } catch(err) {
catch(err) { RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.libnotfound"));
RED.log.warn(RED._("rpi-gpio.errors.libnotfound")); allOK = false;
throw "Warning : "+RED._("rpi-gpio.errors.libnotfound");
} }
} }
} }
if ( !(1 & parseInt((fs.statSync(gpioCommand).mode & parseInt("777", 8)).toString(8)[0]) )) { if ( !(1 & parseInt((fs.statSync(gpioCommand).mode & parseInt("777", 8)).toString(8)[0]) )) {
RED.log.error(RED._("rpi-gpio.errors.needtobeexecutable",{command:gpioCommand})); RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.needtobeexecutable",{command:gpioCommand}));
throw "Error : "+RED._("rpi-gpio.errors.mustbeexecutable"); allOK = false;
}
} catch(err) {
allOK = false;
RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.ignorenode"));
} }
// the magic to make python print stuff immediately // the magic to make python print stuff immediately
@ -61,6 +62,7 @@ module.exports = function(RED) {
} }
} }
if (allOK === true) {
if (node.pin !== undefined) { if (node.pin !== undefined) {
node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]); node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]);
node.running = true; node.running = true;
@ -104,6 +106,19 @@ module.exports = function(RED) {
else { else {
node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin);
} }
}
else {
node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"});
if (node.read === true) {
var val;
if (node.intype == "up") { val = 1; }
if (node.intype == "down") { val = 0; }
setTimeout(function(){
node.send({ topic:"pi/"+node.pin, payload:val });
node.status({fill:"grey",shape:"dot",text:RED._("rpi-gpio.status.na",{value:val})});
},250);
}
}
node.on("close", function(done) { node.on("close", function(done) {
node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"});
@ -155,6 +170,7 @@ module.exports = function(RED) {
else { node.warn(RED._("rpi-gpio.errors.invalidinput")+": "+out); } else { node.warn(RED._("rpi-gpio.errors.invalidinput")+": "+out); }
} }
if (allOK === true) {
if (node.pin !== undefined) { if (node.pin !== undefined) {
if (node.set && (node.out === "out")) { if (node.set && (node.out === "out")) {
node.child = spawn(gpioCommand, [node.out,node.pin,node.level]); node.child = spawn(gpioCommand, [node.out,node.pin,node.level]);
@ -196,6 +212,13 @@ module.exports = function(RED) {
else { else {
node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin);
} }
}
else {
node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"});
node.on("input", function(msg){
node.status({fill:"grey",shape:"dot",text:RED._("rpi-gpio.status.na",{value:msg.payload.toString()})});
});
}
node.on("close", function(done) { node.on("close", function(done) {
node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"});
@ -216,12 +239,13 @@ module.exports = function(RED) {
this.butt = n.butt || 7; this.butt = n.butt || 7;
var node = this; var node = this;
if (allOK === true) {
node.child = spawn(gpioCommand+".py", ["mouse",node.butt]); node.child = spawn(gpioCommand+".py", ["mouse",node.butt]);
node.status({fill:"green",shape:"dot",text:"common.status.ok"}); node.status({fill:"green",shape:"dot",text:"common.status.ok"});
node.child.stdout.on('data', function (data) { node.child.stdout.on('data', function (data) {
data = Number(data); data = Number(data);
if (data === 1) { node.send({ topic:"pi/mouse", button:data, payload:1 }); } if (data !== 0) { node.send({ topic:"pi/mouse", button:data, payload:1 }); }
else { node.send({ topic:"pi/mouse", button:data, payload:0 }); } else { node.send({ topic:"pi/mouse", button:data, payload:0 }); }
}); });
@ -256,12 +280,17 @@ module.exports = function(RED) {
else { done(); } else { done(); }
}); });
} }
else {
node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"});
}
}
RED.nodes.registerType("rpi-mouse",PiMouseNode); RED.nodes.registerType("rpi-mouse",PiMouseNode);
function PiKeyboardNode(n) { function PiKeyboardNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
var node = this; var node = this;
if (allOK === true) {
node.child = spawn(gpioCommand+".py", ["kbd","0"]); node.child = spawn(gpioCommand+".py", ["kbd","0"]);
node.status({fill:"green",shape:"dot",text:"common.status.ok"}); node.status({fill:"green",shape:"dot",text:"common.status.ok"});
@ -304,9 +333,14 @@ module.exports = function(RED) {
else { done(); } else { done(); }
}); });
} }
else {
node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"});
}
}
RED.nodes.registerType("rpi-keyboard",PiKeyboardNode); RED.nodes.registerType("rpi-keyboard",PiKeyboardNode);
var pitype = { type:"" }; var pitype = { type:"" };
if (allOK === true) {
exec(gpioCommand+" info", function(err,stdout,stderr) { exec(gpioCommand+" info", function(err,stdout,stderr) {
if (err) { if (err) {
RED.log.info(RED._("rpi-gpio.errors.version")); RED.log.info(RED._("rpi-gpio.errors.version"));
@ -321,6 +355,7 @@ module.exports = function(RED) {
} }
} }
}); });
}
RED.httpAdmin.get('/rpi-gpio/:id', RED.auth.needsPermission('rpi-gpio.read'), function(req,res) { RED.httpAdmin.get('/rpi-gpio/:id', RED.auth.needsPermission('rpi-gpio.read'), function(req,res) {
res.json(pitype); res.json(pitype);

View File

@ -23,10 +23,6 @@ from time import sleep
bounce = 25; bounce = 25;
if sys.version_info >= (3,0):
print("Sorry - currently only configured to work with python 2.x")
sys.exit(1)
if len(sys.argv) > 2: if len(sys.argv) > 2:
cmd = sys.argv[1].lower() cmd = sys.argv[1].lower()
pin = int(sys.argv[2]) pin = int(sys.argv[2])
@ -34,7 +30,7 @@ if len(sys.argv) > 2:
GPIO.setwarnings(False) GPIO.setwarnings(False)
if cmd == "pwm": if cmd == "pwm":
#print "Initialised pin "+str(pin)+" to PWM" #print("Initialised pin "+str(pin)+" to PWM")
try: try:
freq = int(sys.argv[3]) freq = int(sys.argv[3])
except: except:
@ -54,10 +50,10 @@ if len(sys.argv) > 2:
GPIO.cleanup(pin) GPIO.cleanup(pin)
sys.exit(0) sys.exit(0)
except Exception as ex: except Exception as ex:
print "bad data: "+data print("bad data: "+data)
elif cmd == "buzz": elif cmd == "buzz":
#print "Initialised pin "+str(pin)+" to Buzz" #print("Initialised pin "+str(pin)+" to Buzz")
GPIO.setup(pin,GPIO.OUT) GPIO.setup(pin,GPIO.OUT)
p = GPIO.PWM(pin, 100) p = GPIO.PWM(pin, 100)
p.stop() p.stop()
@ -76,10 +72,10 @@ if len(sys.argv) > 2:
GPIO.cleanup(pin) GPIO.cleanup(pin)
sys.exit(0) sys.exit(0)
except Exception as ex: except Exception as ex:
print "bad data: "+data print("bad data: "+data)
elif cmd == "out": elif cmd == "out":
#print "Initialised pin "+str(pin)+" to OUT" #print("Initialised pin "+str(pin)+" to OUT")
GPIO.setup(pin,GPIO.OUT) GPIO.setup(pin,GPIO.OUT)
if len(sys.argv) == 4: if len(sys.argv) == 4:
GPIO.output(pin,int(sys.argv[3])) GPIO.output(pin,int(sys.argv[3]))
@ -103,11 +99,11 @@ if len(sys.argv) > 2:
GPIO.output(pin,data) GPIO.output(pin,data)
elif cmd == "in": elif cmd == "in":
#print "Initialised pin "+str(pin)+" to IN" #print("Initialised pin "+str(pin)+" to IN")
bounce = float(sys.argv[4]) bounce = float(sys.argv[4])
def handle_callback(chan): def handle_callback(chan):
sleep(bounce/1000.0) sleep(bounce/1000.0)
print GPIO.input(chan) print(GPIO.input(chan))
if sys.argv[3].lower() == "up": if sys.argv[3].lower() == "up":
GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP) GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP)
@ -116,7 +112,7 @@ if len(sys.argv) > 2:
else: else:
GPIO.setup(pin,GPIO.IN) GPIO.setup(pin,GPIO.IN)
print GPIO.input(pin) print(GPIO.input(pin))
GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=int(bounce)) GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=int(bounce))
while True: while True:
@ -129,7 +125,7 @@ if len(sys.argv) > 2:
sys.exit(0) sys.exit(0)
elif cmd == "byte": elif cmd == "byte":
#print "Initialised BYTE mode - "+str(pin)+ #print("Initialised BYTE mode - "+str(pin)+)
list = [7,11,13,12,15,16,18,22] list = [7,11,13,12,15,16,18,22]
GPIO.setup(list,GPIO.OUT) GPIO.setup(list,GPIO.OUT)
@ -152,7 +148,7 @@ if len(sys.argv) > 2:
GPIO.output(list[bit], data & mask) GPIO.output(list[bit], data & mask)
elif cmd == "borg": elif cmd == "borg":
#print "Initialised BORG mode - "+str(pin)+ #print("Initialised BORG mode - "+str(pin)+)
GPIO.setup(11,GPIO.OUT) GPIO.setup(11,GPIO.OUT)
GPIO.setup(13,GPIO.OUT) GPIO.setup(13,GPIO.OUT)
GPIO.setup(15,GPIO.OUT) GPIO.setup(15,GPIO.OUT)
@ -190,7 +186,7 @@ if len(sys.argv) > 2:
button = ord( buf[0] ) & pin # mask out just the required button(s) button = ord( buf[0] ) & pin # mask out just the required button(s)
if button != oldbutt: # only send if changed if button != oldbutt: # only send if changed
oldbutt = button oldbutt = button
print button print(button)
while True: while True:
try: try:
@ -215,7 +211,7 @@ if len(sys.argv) > 2:
# type,code,value # type,code,value
print("%u,%u" % (code, value)) print("%u,%u" % (code, value))
event = file.read(EVENT_SIZE) event = file.read(EVENT_SIZE)
print "0,0" print("0,0")
file.close() file.close()
sys.exit(0) sys.exit(0)
except: except:
@ -225,14 +221,14 @@ if len(sys.argv) > 2:
elif len(sys.argv) > 1: elif len(sys.argv) > 1:
cmd = sys.argv[1].lower() cmd = sys.argv[1].lower()
if cmd == "rev": if cmd == "rev":
print GPIO.RPI_REVISION print(GPIO.RPI_REVISION)
elif cmd == "ver": elif cmd == "ver":
print GPIO.VERSION print(GPIO.VERSION)
elif cmd == "info": elif cmd == "info":
print GPIO.RPI_INFO print(GPIO.RPI_INFO)
else: else:
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}")
print " only ver (gpio version) and info (board information) accept no pin parameter." print(" only ver (gpio version) and info (board information) accept no pin parameter.")
else: else:
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}" print("Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}")

View File

@ -63,6 +63,11 @@
<input type="checkbox" id="node-config-input-verifyservercert" style="display: inline-block; width: auto; vertical-align: top;"> <input type="checkbox" id="node-config-input-verifyservercert" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-verifyservercert" style="width: calc(100% - 170px);" data-i18n="tls.label.verify-server-cert"></label> <label for="node-config-input-verifyservercert" style="width: calc(100% - 170px);" data-i18n="tls.label.verify-server-cert"></label>
</div> </div>
<div class="form-row">
<label style="width: 120px;" for="node-config-input-servername"><i class="fa fa-server"></i> <span data-i18n="tls.label.servername"></span></label>
<input style="width: calc(100% - 170px);" type="text" id="node-config-input-servername" data-i18n="[placeholder]tls.placeholder.servername">
</div>
<hr>
<div class="form-row"> <div class="form-row">
<label style="width: 120px;" for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> <label style="width: 120px;" for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input style="width: calc(100% - 170px);" type="text" id="node-config-input-name" data-i18n="[placeholder]common.label.name"> <input style="width: calc(100% - 170px);" type="text" id="node-config-input-name" data-i18n="[placeholder]common.label.name">
@ -96,6 +101,7 @@
certname: {value:""}, certname: {value:""},
keyname: {value:""}, keyname: {value:""},
caname: {value:""}, caname: {value:""},
servername: {value:""},
verifyservercert: {value: true} verifyservercert: {value: true}
}, },
credentials: { credentials: {

View File

@ -25,6 +25,7 @@ module.exports = function(RED) {
var certPath = n.cert.trim(); var certPath = n.cert.trim();
var keyPath = n.key.trim(); var keyPath = n.key.trim();
var caPath = n.ca.trim(); var caPath = n.ca.trim();
this.servername = (n.servername||"").trim();
if ((certPath.length > 0) || (keyPath.length > 0)) { if ((certPath.length > 0) || (keyPath.length > 0)) {
@ -102,6 +103,9 @@ module.exports = function(RED) {
if (this.credentials && this.credentials.passphrase) { if (this.credentials && this.credentials.passphrase) {
opts.passphrase = this.credentials.passphrase; opts.passphrase = this.credentials.passphrase;
} }
if (this.servername) {
opts.servername = this.servername;
}
opts.rejectUnauthorized = this.verifyservercert; opts.rejectUnauthorized = this.verifyservercert;
} }
return opts; return opts;

View File

@ -212,11 +212,11 @@ module.exports = function(RED) {
if (this.serverConfig) { if (this.serverConfig) {
this.serverConfig.registerInputNode(this); this.serverConfig.registerInputNode(this);
// TODO: nls // TODO: nls
this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:"connected "+n}); }); this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); });
this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"error"}); }); this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"common.status.error"}); });
this.serverConfig.on('closed', function(n) { this.serverConfig.on('closed', function(n) {
if (n > 0) { node.status({fill:"green",shape:"dot",text:"connected "+n}); } if (n > 0) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); }
else { node.status({fill:"red",shape:"ring",text:"disconnected"}); } else { node.status({fill:"red",shape:"ring",text:"common.status.disconnected"}); }
}); });
} else { } else {
this.error(RED._("websocket.errors.missing-conf")); this.error(RED._("websocket.errors.missing-conf"));
@ -240,11 +240,11 @@ module.exports = function(RED) {
} }
else { else {
// TODO: nls // TODO: nls
this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:"connected "+n}); }); this.serverConfig.on('opened', function(n) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); });
this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"error"}); }); this.serverConfig.on('erro', function() { node.status({fill:"red",shape:"ring",text:"common.status.error"}); });
this.serverConfig.on('closed', function(n) { this.serverConfig.on('closed', function(n) {
if (n > 0) { node.status({fill:"green",shape:"dot",text:"connected "+n}); } if (n > 0) { node.status({fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:n})}); }
else { node.status({fill:"red",shape:"ring",text:"disconnected"}); } else { node.status({fill:"red",shape:"ring",text:"common.status.disconnected"}); }
}); });
} }
this.on("input", function(msg) { this.on("input", function(msg) {

View File

@ -18,10 +18,36 @@ module.exports = function(RED) {
"use strict"; "use strict";
var reconnectTime = RED.settings.socketReconnectTime||10000; var reconnectTime = RED.settings.socketReconnectTime||10000;
var socketTimeout = RED.settings.socketTimeout||null; var socketTimeout = RED.settings.socketTimeout||null;
const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000;
const Denque = require('denque');
var net = require('net'); var net = require('net');
var connectionPool = {}; var connectionPool = {};
/**
* Enqueue `item` in `queue`
* @param {Denque} queue - Queue
* @param {*} item - Item to enqueue
* @private
* @returns {Denque} `queue`
*/
const enqueue = (queue, item) => {
// drop msgs from front of queue if size is going to be exceeded
if (queue.size() === msgQueueSize) {
queue.shift();
}
queue.push(item);
return queue;
};
/**
* Shifts item off front of queue
* @param {Deque} queue - Queue
* @private
* @returns {*} Item previously at front of queue
*/
const dequeue = queue => queue.shift();
function TcpIn(n) { function TcpIn(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
this.host = n.host; this.host = n.host;
@ -435,11 +461,14 @@ module.exports = function(RED) {
// the clients object will have: // the clients object will have:
// clients[id].client, clients[id].msg, clients[id].timeout // clients[id].client, clients[id].msg, clients[id].timeout
var connection_id = host + ":" + port; var connection_id = host + ":" + port;
clients[connection_id] = clients[connection_id] || {}; clients[connection_id] = clients[connection_id] || {
clients[connection_id].msg = msg; msgQueue: new Denque(),
clients[connection_id].connected = clients[connection_id].connected || false; connected: false,
connecting: false
};
enqueue(clients[connection_id].msgQueue, msg);
if (!clients[connection_id].connected) { if (!clients[connection_id].connecting && !clients[connection_id].connected) {
var buf; var buf;
if (this.out == "count") { if (this.out == "count") {
if (this.splitc === 0) { buf = Buffer.alloc(1); } if (this.splitc === 0) { buf = Buffer.alloc(1); }
@ -451,14 +480,19 @@ module.exports = function(RED) {
if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);} if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);}
if (host && port) { if (host && port) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(port, host, function() { clients[connection_id].client.connect(port, host, function() {
//node.log(RED._("tcpin.errors.client-connected")); //node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"}); node.status({fill:"green",shape:"dot",text:"common.status.connected"});
if (clients[connection_id] && clients[connection_id].client) { if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = true; clients[connection_id].connected = true;
clients[connection_id].client.write(clients[connection_id].msg.payload); clients[connection_id].connecting = false;
let msg;
while (msg = dequeue(clients[connection_id].msgQueue)) {
clients[connection_id].client.write(msg.payload);
}
if (node.out === "time" && node.splitc < 0) { if (node.out === "time" && node.splitc < 0) {
clients[connection_id].connected = false; clients[connection_id].connected = clients[connection_id].connecting = false;
clients[connection_id].client.end(); clients[connection_id].client.end();
delete clients[connection_id]; delete clients[connection_id];
node.status({}); node.status({});
@ -473,9 +507,10 @@ module.exports = function(RED) {
clients[connection_id].client.on('data', function(data) { clients[connection_id].client.on('data', function(data) {
if (node.out === "sit") { // if we are staying connected just send the buffer if (node.out === "sit") { // if we are staying connected just send the buffer
if (clients[connection_id]) { if (clients[connection_id]) {
if (!clients[connection_id].hasOwnProperty("msg")) { clients[connection_id].msg = {}; } let msg = dequeue(clients[connection_id].msgQueue) || {};
clients[connection_id].msg.payload = data; clients[connection_id].msgQueue.unshift(msg);
node.send(RED.util.cloneMessage(clients[connection_id].msg)); msg.payload = data;
node.send(RED.util.cloneMessage(msg));
} }
} }
// else if (node.splitc === 0) { // else if (node.splitc === 0) {
@ -495,9 +530,11 @@ module.exports = function(RED) {
clients[connection_id].timeout = setTimeout(function () { clients[connection_id].timeout = setTimeout(function () {
if (clients[connection_id]) { if (clients[connection_id]) {
clients[connection_id].timeout = null; clients[connection_id].timeout = null;
clients[connection_id].msg.payload = Buffer.alloc(i+1); let msg = dequeue(clients[connection_id].msgQueue) || {};
buf.copy(clients[connection_id].msg.payload,0,0,i+1); clients[connection_id].msgQueue.unshift(msg);
node.send(clients[connection_id].msg); msg.payload = Buffer.alloc(i+1);
buf.copy(msg.payload,0,0,i+1);
node.send(msg);
if (clients[connection_id].client) { if (clients[connection_id].client) {
node.status({}); node.status({});
clients[connection_id].client.destroy(); clients[connection_id].client.destroy();
@ -516,9 +553,11 @@ module.exports = function(RED) {
i += 1; i += 1;
if ( i >= node.splitc) { if ( i >= node.splitc) {
if (clients[connection_id]) { if (clients[connection_id]) {
clients[connection_id].msg.payload = Buffer.alloc(i); let msg = dequeue(clients[connection_id].msgQueue) || {};
buf.copy(clients[connection_id].msg.payload,0,0,i); clients[connection_id].msgQueue.unshift(msg);
node.send(clients[connection_id].msg); msg.payload = Buffer.alloc(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
if (clients[connection_id].client) { if (clients[connection_id].client) {
node.status({}); node.status({});
clients[connection_id].client.destroy(); clients[connection_id].client.destroy();
@ -534,9 +573,11 @@ module.exports = function(RED) {
i += 1; i += 1;
if (data[j] == node.splitc) { if (data[j] == node.splitc) {
if (clients[connection_id]) { if (clients[connection_id]) {
clients[connection_id].msg.payload = Buffer.alloc(i); let msg = dequeue(clients[connection_id].msgQueue) || {};
buf.copy(clients[connection_id].msg.payload,0,0,i); clients[connection_id].msgQueue.unshift(msg);
node.send(clients[connection_id].msg); msg.payload = Buffer.alloc(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
if (clients[connection_id].client) { if (clients[connection_id].client) {
node.status({}); node.status({});
clients[connection_id].client.destroy(); clients[connection_id].client.destroy();
@ -554,7 +595,7 @@ module.exports = function(RED) {
//console.log("END"); //console.log("END");
node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"}); node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"});
if (clients[connection_id] && clients[connection_id].client) { if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = false; clients[connection_id].connected = clients[connection_id].connecting = false;
clients[connection_id].client = null; clients[connection_id].client = null;
} }
}); });
@ -562,7 +603,7 @@ module.exports = function(RED) {
clients[connection_id].client.on('close', function() { clients[connection_id].client.on('close', function() {
//console.log("CLOSE"); //console.log("CLOSE");
if (clients[connection_id]) { if (clients[connection_id]) {
clients[connection_id].connected = false; clients[connection_id].connected = clients[connection_id].connecting = false;
} }
var anyConnected = false; var anyConnected = false;
@ -592,21 +633,23 @@ module.exports = function(RED) {
clients[connection_id].client.on('timeout',function() { clients[connection_id].client.on('timeout',function() {
//console.log("TIMEOUT"); //console.log("TIMEOUT");
if (clients[connection_id]) { if (clients[connection_id]) {
clients[connection_id].connected = false; clients[connection_id].connected = clients[connection_id].connecting = false;
node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"}); node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"});
//node.warn(RED._("tcpin.errors.connect-timeout")); //node.warn(RED._("tcpin.errors.connect-timeout"));
if (clients[connection_id].client) { if (clients[connection_id].client) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(port, host, function() { clients[connection_id].client.connect(port, host, function() {
clients[connection_id].connected = true; clients[connection_id].connected = true;
clients[connection_id].connecting = false;
node.status({fill:"green",shape:"dot",text:"common.status.connected"}); node.status({fill:"green",shape:"dot",text:"common.status.connected"});
}); });
} }
} }
}); });
} }
else { else if (!clients[connection_id].connecting && clients[connection_id].connected) {
if (clients[connection_id] && clients[connection_id].client) { if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].client.write(clients[connection_id].msg.payload); clients[connection_id].client.write(dequeue(clients[connection_id].msgQueue));
} }
} }
}); });

View File

@ -63,7 +63,7 @@ module.exports = function(RED) {
udpInputPortsInUse[this.port] = server; udpInputPortsInUse[this.port] = server;
} }
else { else {
node.warn(RED._("udp.errors.alreadyused",{port:node.port})); node.log(RED._("udp.errors.alreadyused",{port:node.port}));
server = udpInputPortsInUse[this.port]; // re-use existing server = udpInputPortsInUse[this.port]; // re-use existing
} }
@ -172,8 +172,7 @@ module.exports = function(RED) {
if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; } if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; }
var sock; var sock;
var p = this.port; var p = this.outport || this.port || "0";
if (node.multicast != "false") { p = this.outport||"0"; }
if (udpInputPortsInUse[p]) { if (udpInputPortsInUse[p]) {
sock = udpInputPortsInUse[p]; sock = udpInputPortsInUse[p];
node.log(RED._("udp.status.re-use",{outport:node.outport,host:node.addr,port:node.port})); node.log(RED._("udp.status.re-use",{outport:node.outport,host:node.addr,port:node.port}));

View File

@ -153,13 +153,15 @@
"key": "Private Key", "key": "Private Key",
"passphrase": "Passphrase", "passphrase": "Passphrase",
"ca": "CA Certificate", "ca": "CA Certificate",
"verify-server-cert":"Verify server certificate" "verify-server-cert":"Verify server certificate",
"servername": "Server Name"
}, },
"placeholder": { "placeholder": {
"cert":"path to certificate (PEM format)", "cert":"path to certificate (PEM format)",
"key":"path to private key (PEM format)", "key":"path to private key (PEM format)",
"ca":"path to CA certificate (PEM format)", "ca":"path to CA certificate (PEM format)",
"passphrase":"private key passphrase (optional)" "passphrase":"private key passphrase (optional)",
"servername":"for use with SNI"
}, },
"error": { "error": {
"missing-file": "No certificate/key file provided" "missing-file": "No certificate/key file provided"
@ -420,6 +422,10 @@
"url1": "URL should use ws:&#47;&#47; or wss:&#47;&#47; scheme and point to an existing websocket listener.", "url1": "URL should use ws:&#47;&#47; or wss:&#47;&#47; scheme and point to an existing websocket listener.",
"url2": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The client can be configured to send or receive the entire message object as a JSON formatted string." "url2": "By default, <code>payload</code> will contain the data to be sent over, or received from a websocket. The client can be configured to send or receive the entire message object as a JSON formatted string."
}, },
"status": {
"connected": "connected __count__",
"connected_plural": "connected __count__"
},
"errors": { "errors": {
"connect-error": "An error occured on the ws connection: ", "connect-error": "An error occured on the ws connection: ",
"send-error": "An error occurred while sending: ", "send-error": "An error occurred while sending: ",
@ -575,6 +581,8 @@
"null":"is null", "null":"is null",
"nnull":"is not null", "nnull":"is not null",
"istype":"is of type", "istype":"is of type",
"empty":"is empty",
"nempty":"is not empty",
"head":"head", "head":"head",
"tail":"tail", "tail":"tail",
"index":"index between", "index":"index between",
@ -697,7 +705,9 @@
"errors": { "errors": {
"dropped-object": "Ignored non-object payload", "dropped-object": "Ignored non-object payload",
"dropped": "Ignored unsupported payload type", "dropped": "Ignored unsupported payload type",
"dropped-error": "Failed to convert payload" "dropped-error": "Failed to convert payload",
"schema-error": "JSON Schema error",
"schema-error-compile": "JSON Schema error: failed to compile schema"
}, },
"label": { "label": {
"o2j": "Object to JSON options", "o2j": "Object to JSON options",
@ -786,8 +796,8 @@
"na": "N/A : __value__" "na": "N/A : __value__"
}, },
"errors": { "errors": {
"ignorenode": "Ignoring Raspberry Pi specific node", "ignorenode": "Raspberry Pi specific node set inactive",
"version": "Version command failed", "version": "Failed to get version from Pi",
"sawpitype": "Saw Pi Type", "sawpitype": "Saw Pi Type",
"libnotfound": "Cannot find Pi RPi.GPIO python library", "libnotfound": "Cannot find Pi RPi.GPIO python library",
"alreadyset": "GPIO pin __pin__ already set as type: __type__", "alreadyset": "GPIO pin __pin__ already set as type: __type__",
@ -924,8 +934,8 @@
"ascending" : "ascending", "ascending" : "ascending",
"descending" : "descending", "descending" : "descending",
"as-number" : "as number", "as-number" : "as number",
"invalid-exp" : "invalid JSONata expression in sort node", "invalid-exp" : "Invalid JSONata expression in sort node: __message__",
"too-many" : "too many pending messages in sort node", "too-many" : "Too many pending messages in sort node",
"clear" : "clear pending message in sort node" "clear" : "clear pending message in sort node"
}, },
"batch" : { "batch" : {

View File

@ -26,7 +26,7 @@
<dt class="optional">kill <span class="property-type">文字列</span></dt> <dt class="optional">kill <span class="property-type">文字列</span></dt>
<dd>execードのプロセスに対して送るシグナルの種別を指定します</dd> <dd>execードのプロセスに対して送るシグナルの種別を指定します</dd>
<dt class="optional">pid <span class="property-type">数値|文字列</span></dt> <dt class="optional">pid <span class="property-type">数値|文字列</span></dt>
<dd>シグナル送信対象のexecードのプロセスID</dd> <dd>シグナル送信対象のexecードのプロセスIDを指定します</dd>
</dl> </dl>
<h3>出力</h3> <h3>出力</h3>
@ -60,13 +60,12 @@
</ol> </ol>
<h3>詳細</h3> <h3>詳細</h3>
<p>デフォルトでは、<code>exec</code>システムコールを用いてコマンドを呼び出してその完了を待ち、出力を返します。例えば、コマンドの実行が成功した場合には、<code>{ code: 0 }</code>と言う返却値を返します。</p> <p>デフォルトでは、<code>exec</code>システムコールを用いてコマンドを呼び出してその完了を待ち、出力を返します。例えば、コマンドの実行が成功した場合には、<code>{ code: 0 }</code>と言う返却値を返します。</p>
<p><code>spawn</code>を使ってコマンドを実行し、 <p><code>spawn</code>を使ってコマンドを実行し、標準出力および標準エラー出力へ出力を返すようにすることもできます。この場合、通常1行毎に値を返します。コマンドの実行が完了すると、3番目の端子にオブジェクトを出力します。例えば、コマンドの実行が成功した場合には、<code>{ code: 0 }</code>という返却値を返します。</p>
標準出力および標準エラー出力へ出力を返すようにすることもできます。この場合、通常1行毎に値を返します。コマンドの実行が完了すると、3番目の端子にオブジェクトを出力します。例えば、コマンドの実行が成功した場合には、<code>{ code: 0 }</code>と言う返却値を返します。</p>
<p>エラー発生時には、3番目の端子の<code>msg.payload</code><code>message</code><code>signal</code>など付加情報を返します。</p> <p>エラー発生時には、3番目の端子の<code>msg.payload</code><code>message</code><code>signal</code>など付加情報を返します。</p>
<p>実行対象のコマンドはノード設定で定義します。<code>msg.payload</code>や追加引数をコマンドに追加することもできます。</p> <p>実行対象のコマンドはノード設定で定義します。<code>msg.payload</code>や追加引数をコマンドに追加することもできます。</p>
<p>コマンドもしくはパラメータが空白を含む場合には、引用符で囲みます。- <code>"This is a single parameter"</code></p> <p>コマンドもしくはパラメータが空白を含む場合には、引用符で囲みます。- <code>"これは一つのパラメータです"</code></p>
<p>返却する<code>payload</code>は通常<i>文字列</i>ですが、UTF8文字以外が存在すると<i>バッファ</i>となります。</p> <p>返却する<code>payload</code>は通常<i>文字列</i>ですが、UTF8文字以外が存在すると<i>バッファ</i>となります。</p>
<p>ードが実行中の場合、ステータスアイコンとPIDを表示します。この状態変化は<code>status</code>ノードで検知できます。</p> <p>ードが実行中の場合、ステータスアイコンとPIDを表示します。この状態変化は<code>Status</code>ノードで検知できます。</p>
<h4>プロセスの停止</h4> <h4>プロセスの停止</h4>
<p><code>msg.kill</code>を受信すると、実行中のプロセスを停止することができます。<code>msg.kill</code>には送出するシグナルの種別を指定します。例えば、<code>SIGINT</code><code>SIGQUIT</code><code>SIGHUP</code>などです。空の文字列を指定した場合には、<code>SIGTERM</code>を指定したものとみなします。</p> <p><code>msg.kill</code>を受信すると、実行中のプロセスを停止することができます。<code>msg.kill</code>には送出するシグナルの種別を指定します。例えば、<code>SIGINT</code><code>SIGQUIT</code><code>SIGHUP</code>などです。空の文字列を指定した場合には、<code>SIGTERM</code>を指定したものとみなします。</p>
<p>ードが1つ以上のプロセスを実行している場合、<code>msg.pid</code>に停止対象のPIDを指定しなければなりません。</p> <p>ードが1つ以上のプロセスを実行している場合、<code>msg.pid</code>に停止対象のPIDを指定しなければなりません。</p>

View File

@ -18,7 +18,7 @@
<p>受信メッセージに対して処理を行うJavaScriptコード(関数の本体)を定義します。</p> <p>受信メッセージに対して処理を行うJavaScriptコード(関数の本体)を定義します。</p>
<p>入力メッセージは<code>msg</code>という名称のJavaScriptオブジェクトで受け渡されます。</p> <p>入力メッセージは<code>msg</code>という名称のJavaScriptオブジェクトで受け渡されます。</p>
<p><code>msg</code>オブジェクトは<code>msg.payload</code>プロパティにメッセージ本体を保持するのが慣例です。</p> <p><code>msg</code>オブジェクトは<code>msg.payload</code>プロパティにメッセージ本体を保持するのが慣例です。</p>
<p>通常、コードはメッセージオブジェクト(もしくは複数のメッセージオブジェクト)を返却します。何も返却しない場合には、フロー実行を停止します</p> <p>通常、コードはメッセージオブジェクト(もしくは複数のメッセージオブジェクト)を返却します。後続フローの実行を停止したい場合は、オブジェクトを返却しなくてもかまいません</p>
<h3>詳細</h3> <h3>詳細</h3>
<p>コードの書き方の詳細については、<a target="_blank" href="http://nodered.org/docs/writing-functions.html">オンラインドキュメント</a>を参照してください。</p> <p>コードの書き方の詳細については、<a target="_blank" href="http://nodered.org/docs/writing-functions.html">オンラインドキュメント</a>を参照してください。</p>
<h4>メッセージの送信</h4> <h4>メッセージの送信</h4>
@ -40,4 +40,10 @@
</p> </p>
<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>
<p>コード中ではードのIDおよび名前を以下のプロパティで参照できます:</p>
<ul>
<li><code>node.id</code> - ードのID</li>
<li><code>node.name</code> - ノードの名称</li>
</ul>
</script> </script>

View File

@ -63,6 +63,8 @@
<p>ードにクライアントIDを設定しておらずセッションの初期化を設定している場合、ランダムなクライアントIDを生成します。クライアントIDを設定する場合、接続先のブローカで一意となるようにしてください。</p> <p>ードにクライアントIDを設定しておらずセッションの初期化を設定している場合、ランダムなクライアントIDを生成します。クライアントIDを設定する場合、接続先のブローカで一意となるようにしてください。</p>
<h4>Birthメッセージ</h4> <h4>Birthメッセージ</h4>
<p>接続を確立した際に、設定したトピックに対して発行するメッセージ</p> <p>接続を確立した際に、設定したトピックに対して発行するメッセージ</p>
<h4>Closeメッセージ</h4>
<p>接続が正常に終了する前に、ノードの再デプロイまたはシャットダウンした場合に、設定したトピックに対して発行するメッセージ</p>
<h4>Willメッセージ</h4> <h4>Willメッセージ</h4>
<p>予期せず接続が切断された場合にブローカが発行するメッセージ</p> <p>予期せず接続が切断された場合にブローカが発行するメッセージ</p>
<h4>WebSocket</h4> <h4>WebSocket</h4>

View File

@ -30,7 +30,9 @@
<dt class="optional">payload</dt> <dt class="optional">payload</dt>
<dd>リクエストボディとして送るデータ</dd> <dd>リクエストボディとして送るデータ</dd>
<dt class="optional">rejectUnauthorized</dt> <dt class="optional">rejectUnauthorized</dt>
<dd><code>true</code>をセットすると、自己署名証明書を使用するhttpsサイトへのリクエストを許可します。</dd> <dd><code>false</code>をセットすると、自己署名証明書を使用するhttpsサイトへのリクエストを許可します。</dd>
<dt class="optional">followRedirects</dt>
<dd><code>false</code>をセットすると、リダイレクトを行いません。デフォルトは<code>true</code>です。</dd>
</dl> </dl>
<h3>出力</h3> <h3>出力</h3>
<dl class="message-properties"> <dl class="message-properties">

View File

@ -29,7 +29,11 @@
<li><b>その他</b> - これより前のルールにマッチするものがなかった場合に適用</li> <li><b>その他</b> - これより前のルールにマッチするものがなかった場合に適用</li>
</ol> </ol>
<h3>注釈</h3>
<p><code>is true/false</code><code>is null</code>のルールは、型に対して厳密な比較を行います。型変換した上での比較はしません。</p>
<p><code>is empty</code>のルールは、長さ0の文字列・配列・バッファ、またはプロパティを持たないオブジェクトを出力します。<code>null</code><code>undefined</code>は出力しません。</p>
<h3>メッセージ列の扱い</h3> <h3>メッセージ列の扱い</h3>
<p>switchードは入力メッセージの列に関する情報を保持する<code>msg.parts</code>をデフォルトでは変更しません。</p> <p>switchードは入力メッセージの列に関する情報を保持する<code>msg.parts</code>をデフォルトでは変更しません。</p>
<p><b>メッセージ列の補正</b>」オプションを指定すると、マッチした各ルールに対して新しいメッセージ列を生成します。このモードでは、switchードは新たなメッセージ列を送信する前に、入力メッセージ列全体を内部に蓄積します。<code>nodeMessageBufferMaxLength</code>を設定すると、蓄積するメッセージ数を制限できます。</p> <p><b>メッセージ列の補正</b>」オプションを指定すると、マッチした各ルールに対して新しいメッセージ列を生成します。このモードでは、switchードは新たなメッセージ列を送信する前に、入力メッセージ列全体を内部に蓄積します。<b>settings.js</b><code>nodeMessageBufferMaxLength</code>を設定すると、蓄積するメッセージ数を制限できます。</p>
</script> </script>

View File

@ -30,5 +30,5 @@
</dd> </dd>
</dl> </dl>
<h4>メッセージの蓄積</h4> <h4>メッセージの蓄積</h4>
<p>このノードの処理ではメッセージ列の処理のためメッセージを内部に蓄積します。<code>nodeMessageBufferMaxLength</code>を指定することで蓄積するメッセージの最大値を制限することができます。</p> <p>このノードの処理ではメッセージ列の処理のためメッセージを内部に蓄積します。<b>settings.js</b><code>nodeMessageBufferMaxLength</code>を指定することで蓄積するメッセージの最大値を制限することができます。</p>
</script> </script>

View File

@ -153,19 +153,23 @@
"key": "秘密鍵", "key": "秘密鍵",
"passphrase": "パスフレーズ", "passphrase": "パスフレーズ",
"ca": "CA証明書", "ca": "CA証明書",
"verify-server-cert": "サーバ証明書を確認" "verify-server-cert": "サーバ証明書を確認",
"servername": "サーバ名"
}, },
"placeholder": { "placeholder": {
"cert": "証明書(PEM形式)のパス", "cert": "証明書(PEM形式)のパス",
"key": "秘密鍵(PEM形式)のパス", "key": "秘密鍵(PEM形式)のパス",
"ca": "CA証明書(PEM形式)のパス", "ca": "CA証明書(PEM形式)のパス",
"passphrase":"秘密鍵のパスフレーズ (任意)" "passphrase": "秘密鍵のパスフレーズ (任意)",
"servername": "SNIで使用"
}, },
"error": { "error": {
"missing-file": "証明書と秘密鍵のファイルが設定されていません" "missing-file": "証明書と秘密鍵のファイルが設定されていません"
} }
}, },
"exec": { "exec": {
"exec": "exec",
"spawn": "spawn",
"label": { "label": {
"command": "コマンド", "command": "コマンド",
"append": "引数", "append": "引数",
@ -184,6 +188,7 @@
"oldrc": "旧型式の出力を使用(互換モード)" "oldrc": "旧型式の出力を使用(互換モード)"
}, },
"function": { "function": {
"function": "",
"label": { "label": {
"function": "コード", "function": "コード",
"outputs": "出力数" "outputs": "出力数"
@ -195,6 +200,7 @@
"tip": "コードの記述方法はノードの「情報」を参照してください。" "tip": "コードの記述方法はノードの「情報」を参照してください。"
}, },
"template": { "template": {
"template": "template",
"label": { "label": {
"template": "テンプレート", "template": "テンプレート",
"property": "設定先", "property": "設定先",
@ -301,6 +307,7 @@
} }
}, },
"comment": { "comment": {
"comment": "comment",
"label": { "label": {
"title": "タイトル", "title": "タイトル",
"body": "本文" "body": "本文"
@ -318,6 +325,7 @@
"broker": "サーバ", "broker": "サーバ",
"example": "例) localhost", "example": "例) localhost",
"qos": "QoS", "qos": "QoS",
"retain": "保持",
"clientid": "クライアント", "clientid": "クライアント",
"port": "ポート", "port": "ポート",
"keepalive": "キープアライブ時間", "keepalive": "キープアライブ時間",
@ -327,10 +335,10 @@
"verify-server-cert": "サーバの証明書を確認", "verify-server-cert": "サーバの証明書を確認",
"compatmode": "旧MQTT 3.1のサポート" "compatmode": "旧MQTT 3.1のサポート"
}, },
"sections-label":{ "sections-label": {
"birth-message": "接続時の送信メッセージ(Birthメッセージ)", "birth-message": "接続時の送信メッセージ(Birthメッセージ)",
"will-message":"予期しない切断時の送信メッセージ(Willメッセージ)", "will-message": "予期しない切断時の送信メッセージ(Willメッセージ)",
"close-message":"切断前の送信メッセージ(Closeメッセージ)" "close-message": "切断前の送信メッセージ(Closeメッセージ)"
}, },
"tabs-label": { "tabs-label": {
"connection": "接続", "connection": "接続",
@ -414,6 +422,10 @@
"url1": "URLには ws:&#47;&#47; または wss:&#47;&#47; スキーマを使用して、存在するwebsocketリスナを設定してください。", "url1": "URLには ws:&#47;&#47; または wss:&#47;&#47; スキーマを使用して、存在するwebsocketリスナを設定してください。",
"url2": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。" "url2": "標準では <code>payload</code> がwebsocketから送信、受信されるデータを持ちます。クライアントはJSON形式の文字列としてメッセージ全体を送信、受信するよう設定できます。"
}, },
"status": {
"connected": "接続数 __count__",
"connected_plural": "接続数 __count__"
},
"errors": { "errors": {
"connect-error": "ws接続でエラーが発生しました: ", "connect-error": "ws接続でエラーが発生しました: ",
"send-error": "送信中にエラーが発生しました: ", "send-error": "送信中にエラーが発生しました: ",
@ -421,6 +433,7 @@
} }
}, },
"watch": { "watch": {
"watch": "watch",
"label": { "label": {
"files": "ファイル", "files": "ファイル",
"recursive": "サブディレクトリを再帰的に監視" "recursive": "サブディレクトリを再帰的に監視"
@ -543,15 +556,15 @@
"port-notset": "udp: ポートが設定されていません", "port-notset": "udp: ポートが設定されていません",
"port-invalid": "udp: ポート番号が不正です", "port-invalid": "udp: ポート番号が不正です",
"alreadyused": "udp: 既に__port__番ポートが使用されています", "alreadyused": "udp: 既に__port__番ポートが使用されています",
"ifnotfound": "udp: インターフェイス __iface__ がありません", "ifnotfound": "udp: インターフェイス __iface__ がありません"
"alreadyused": "udp: 既にポートが使用されています"
} }
}, },
"switch": { "switch": {
"switch": "switch",
"label": { "label": {
"property": "プロパティ", "property": "プロパティ",
"rule": "条件", "rule": "条件",
"repair" : "メッセージ列の補正" "repair": "メッセージ列の補正"
}, },
"and": "", "and": "",
"checkall": "全ての条件を適用", "checkall": "全ての条件を適用",
@ -565,15 +578,18 @@
"false": "is false", "false": "is false",
"null": "is null", "null": "is null",
"nnull": "is not null", "nnull": "is not null",
"head":"head", "istype": "is of type",
"tail":"tail", "empty": "is empty",
"index":"index between", "nempty": "is not empty",
"exp":"JSONata式", "head": "head",
"tail": "tail",
"index": "index between",
"exp": "JSONata式",
"else": "その他" "else": "その他"
}, },
"errors": { "errors": {
"invalid-expr": "不正な表現: __error__", "invalid-expr": "不正な表現: __error__",
"too-many" : "switchード内で保持しているメッセージが多すぎます" "too-many": "switchード内で保持しているメッセージが多すぎます"
} }
}, },
"change": { "change": {
@ -603,6 +619,7 @@
} }
}, },
"range": { "range": {
"range": "range",
"label": { "label": {
"action": "動作", "action": "動作",
"inputrange": "入力値の範囲", "inputrange": "入力値の範囲",
@ -686,7 +703,9 @@
"errors": { "errors": {
"dropped-object": "オブジェクト形式でないペイロードを無視しました", "dropped-object": "オブジェクト形式でないペイロードを無視しました",
"dropped": "対応していない形式のペイロードを無視しました", "dropped": "対応していない形式のペイロードを無視しました",
"dropped-error": "ペイロードの変換処理が失敗しました" "dropped-error": "ペイロードの変換処理が失敗しました",
"schema-error": "JSONスキーマエラー",
"schema-error-compile": "JSONスキーマエラー: スキーマのコンパイルが失敗しました"
}, },
"label": { "label": {
"o2j": "オブジェクトからJSONへ変換", "o2j": "オブジェクトからJSONへ変換",
@ -695,8 +714,8 @@
"property": "プロパティ", "property": "プロパティ",
"actions": { "actions": {
"toggle": "JSON文字列とオブジェクト間の相互変換", "toggle": "JSON文字列とオブジェクト間の相互変換",
"str":"常にJSON文字列に変換", "str": "常にJSON文字列に変換",
"obj":"常にJavaScriptオブジェクトに変換" "obj": "常にJavaScriptオブジェクトに変換"
} }
} }
}, },
@ -791,6 +810,7 @@
} }
}, },
"tail": { "tail": {
"tail": "tail",
"label": { "label": {
"filename": "ファイル名", "filename": "ファイル名",
"type": "ファイル形式", "type": "ファイル形式",
@ -844,6 +864,7 @@
"tip": "注釈: 「ファイル名」はフルパスを設定する必要があります。" "tip": "注釈: 「ファイル名」はフルパスを設定する必要があります。"
}, },
"split": { "split": {
"split": "split",
"intro": "型に基づいて <code>msg.payload</code> を分割:", "intro": "型に基づいて <code>msg.payload</code> を分割:",
"object": "<b>オブジェクト</b>", "object": "<b>オブジェクト</b>",
"objectSend": "各key/valueペアのメッセージを送信", "objectSend": "各key/valueペアのメッセージを送信",
@ -855,11 +876,12 @@
"addname": " keyのコピー先" "addname": " keyのコピー先"
}, },
"join": { "join": {
"join": "join",
"mode": { "mode": {
"mode": "動作", "mode": "動作",
"auto": "自動", "auto": "自動",
"merge":"列のマージ", "merge": "列のマージ",
"reduce":"列の集約", "reduce": "列の集約",
"custom": "手動" "custom": "手動"
}, },
"combine": "結合", "combine": "結合",
@ -882,12 +904,12 @@
"seconds": "秒", "seconds": "秒",
"complete": "<code>msg.complete</code> プロパティが設定されたメッセージ受信後", "complete": "<code>msg.complete</code> プロパティが設定されたメッセージ受信後",
"tip": "このモードでは、本ノードが <i>split</i> ノードと組となるか、 <code>msg.parts</code> プロパティが設定されたメッセージを受け取ることが前提となります。", "tip": "このモードでは、本ノードが <i>split</i> ノードと組となるか、 <code>msg.parts</code> プロパティが設定されたメッセージを受け取ることが前提となります。",
"too-many" : "joinード内部で保持しているメッセージが多すぎます", "too-many": "joinード内部で保持しているメッセージが多すぎます",
"merge": { "merge": {
"topics-label":"対象トピック", "topics-label": "対象トピック",
"topics":"トピック", "topics": "トピック",
"topic" : "トピック", "topic": "トピック",
"on-change":"新規トピックを受け取るとメッセージを送信する" "on-change": "新規トピックを受け取るとメッセージを送信する"
}, },
"reduce": { "reduce": {
"exp": "集約式", "exp": "集約式",
@ -900,43 +922,45 @@
"invalid-expr": "JSONata式が不正: __error__" "invalid-expr": "JSONata式が不正: __error__"
} }
}, },
"sort" : { "sort": {
"target" : "対象", "sort": "sort",
"seq" : "メッセージ列", "target": "対象",
"key" : "キー", "seq": "メッセージ列",
"elem" : "要素の値", "key": "キー",
"order" : "順序", "elem": "要素の値",
"ascending" : "昇順", "order": "順序",
"descending" : "降順", "ascending": "昇順",
"as-number" : "数値として比較", "descending": "降順",
"invalid-exp" : "sortードで不正なJSONata式が指定されました", "as-number": "数値として比較",
"too-many" : "sortードの未処理メッセージの数が許容数を超えました", "invalid-exp": "sortードで不正なJSONata式が指定されました",
"clear" : "sortードの未処理メッセージを破棄しました" "too-many": "sortードの未処理メッセージの数が許容数を超えました",
"clear": "sortードの未処理メッセージを破棄しました"
}, },
"batch" : { "batch": {
"batch": "batch",
"mode": { "mode": {
"label" : "モード", "label": "モード",
"num-msgs" : "メッセージ数でグループ化", "num-msgs": "メッセージ数でグループ化",
"interval" : "時間間隔でグループ化", "interval": "時間間隔でグループ化",
"concat" : "列の結合" "concat": "列の結合"
}, },
"count": { "count": {
"label" : "メッセージ数", "label": "メッセージ数",
"overlap" : "オーバラップ", "overlap": "オーバラップ",
"count" : "数", "count": "数",
"invalid" : "メッセージ数とオーバラップ数が不正" "invalid": "メッセージ数とオーバラップ数が不正"
}, },
"interval": { "interval": {
"label" : "時間間隔", "label": "時間間隔",
"seconds" : "秒", "seconds": "秒",
"empty" : "メッセージを受信しない場合、空のメッセージを送信" "empty": "メッセージを受信しない場合、空のメッセージを送信"
}, },
"concat": { "concat": {
"topics-label": "トピック", "topics-label": "トピック",
"topic" : "トピック" "topic": "トピック"
}, },
"too-many" : "batchード内で保持しているメッセージが多すぎます", "too-many": "batchード内で保持しているメッセージが多すぎます",
"unexpected" : "想定外のモード", "unexpected": "想定外のモード",
"no-parts" : "メッセージにpartsプロパティがありません" "no-parts": "メッセージにpartsプロパティがありません"
} }
} }

View File

@ -20,6 +20,8 @@
<dl class="message-properties"> <dl class="message-properties">
<dt>payload<span class="property-type">オブジェクト | 文字列</span></dt> <dt>payload<span class="property-type">オブジェクト | 文字列</span></dt>
<dd>JavaScriptオブジェクトもしくはJSON文字列</dd> <dd>JavaScriptオブジェクトもしくはJSON文字列</dd>
<dt>schema<span class="property-type">オブジェクト</span></dt>
<dd>JSONの検証に利用するJSONスキーマ。設定されていない場合は検証を行いません。</dd>
</dl> </dl>
<h3>出力</h3> <h3>出力</h3>
<dl class="message-properties"> <dl class="message-properties">
@ -30,9 +32,12 @@
<li>入力がJavaScriptオブジェクトの場合、JSON文字列に変換します。JSON文字列は整形することも可能です。</li> <li>入力がJavaScriptオブジェクトの場合、JSON文字列に変換します。JSON文字列は整形することも可能です。</li>
</ul> </ul>
</dd> </dd>
<dt>schemaError<span class="property-type">配列</span></dt>
<dd>JSONの検証でエラーが発生した場合、Catchードを利用し、エラーを配列として<code>schemaError</code>プロパティから取得することができます。</dd>
</dl> </dl>
<h3>詳細</h3> <h3>詳細</h3>
<p>デフォルトの変換対象は<code>msg.payload</code>ですが、他のメッセージプロパティを変換対象とすることも可能です。</p> <p>デフォルトの変換対象は<code>msg.payload</code>ですが、他のメッセージプロパティを変換対象とすることも可能です。</p>
<p>双方向の変換を自動選択するのではなく、特定の変換のみ行うように設定できます。この機能は、例えば、<code>HTTP In</code>ードに対するリクエストがcontent-typeを正しく設定していない場合であっても、JSONードによる変換結果がJavaScriptオブジェクトであることを保証するために利用します。</p> <p>双方向の変換を自動選択するのではなく、特定の変換のみ行うように設定できます。この機能は、例えば、<code>HTTP In</code>ードに対するリクエストがcontent-typeを正しく設定していない場合であっても、JSONードによる変換結果がJavaScriptオブジェクトであることを保証するために利用します。</p>
<p>JSON文字列への変換が指定されている場合、受信した文字列に対してさらなるチェックは行いません。すなわち、文字列がJSONとして正しいかどうかの検査や、整形オプションを指定していたとしても整形処理を実施しません。</p> <p>JSON文字列への変換が指定されている場合、受信した文字列に対してさらなるチェックは行いません。すなわち、文字列がJSONとして正しいかどうかの検査や、整形オプションを指定していたとしても整形処理を実施しません。</p>
<p>JSONスキーマの詳細については、<a href="http://json-schema.org/latest/json-schema-validation.html">こちら</a>を参照してください。</p>
</script> </script>

View File

@ -21,6 +21,8 @@
<dt class="optional">filename <span class="property-type">文字列</span></dt> <dt class="optional">filename <span class="property-type">文字列</span></dt>
<dd>対象ファイル名をノードに設定していない場合、このプロパティでファイルを指定できます</dd> <dd>対象ファイル名をノードに設定していない場合、このプロパティでファイルを指定できます</dd>
</dl> </dl>
<h3>出力</h3>
<p>書き込みの完了時、入力メッセージを出力端子に送出します。</p>
<h3>詳細</h3> <h3>詳細</h3>
<p>入力メッセージのペイロードをファイルの最後に追記します。改行(\n)を各データの最後に追加することもできます。</p> <p>入力メッセージのペイロードをファイルの最後に追記します。改行(\n)を各データの最後に追加することもできます。</p>
<p><code>msg.filename</code>を使う場合、書き込みを行う毎にファイルをクローズします。より良い性能を得るためにはファイル名をノードに設定してください。</p> <p><code>msg.filename</code>を使う場合、書き込みを行う毎にファイルをクローズします。より良い性能を得るためにはファイル名をノードに設定してください。</p>

View File

@ -60,6 +60,12 @@
<li>An <b>Otherwise</b> rule can be used to match if none of the preceeding <li>An <b>Otherwise</b> rule can be used to match if none of the preceeding
rules have matched.</li> rules have matched.</li>
</ol> </ol>
<h4>Notes</h4>
<p>The <code>is true/false</code> and <code>is null</code> rules perform strict
comparisons against those types. They do not convert between types.</p>
<p>The <code>is empty</code> rule passes for Strings, Arrays and Buffers that have
a length of 0, or Objects that have no properties. It does not pass for <code>null</code>
or <code>undefined</code> values.</p>
<h4>Handling message sequences</h4> <h4>Handling message sequences</h4>
<p>By default, the node does not modify the <code>msg.parts</code> property of messages <p>By default, the node does not modify the <code>msg.parts</code> property of messages
that are part of a sequence.</p> that are part of a sequence.</p>
@ -86,6 +92,8 @@
{v:"null",t:"switch.rules.null",kind:'V'}, {v:"null",t:"switch.rules.null",kind:'V'},
{v:"nnull",t:"switch.rules.nnull",kind:'V'}, {v:"nnull",t:"switch.rules.nnull",kind:'V'},
{v:"istype",t:"switch.rules.istype",kind:'V'}, {v:"istype",t:"switch.rules.istype",kind:'V'},
{v:"empty",t:"switch.rules.empty",kind:'V'},
{v:"nempty",t:"switch.rules.nempty",kind:'V'},
{v:"head",t:"switch.rules.head",kind:'S'}, {v:"head",t:"switch.rules.head",kind:'S'},
{v:"index",t:"switch.rules.index",kind:'S'}, {v:"index",t:"switch.rules.index",kind:'S'},
{v:"tail",t:"switch.rules.tail",kind:'S'}, {v:"tail",t:"switch.rules.tail",kind:'S'},
@ -99,11 +107,17 @@
} }
return v; return v;
} }
function prop2name(key) {
var result = RED.utils.parseContextKey(key);
return result.key;
}
function getValueLabel(t,v) { function getValueLabel(t,v) {
if (t === 'str') { if (t === 'str') {
return '"'+clipValueLength(v)+'"'; return '"'+clipValueLength(v)+'"';
} else if (t === 'msg' || t==='flow' || t==='global') { } else if (t === 'msg') {
return t+"."+clipValueLength(v); return t+"."+clipValueLength(v);
} else if (t === 'flow' || t === 'global') {
return t+"."+clipValueLength(prop2name(v));
} }
return clipValueLength(v); return clipValueLength(v);
} }
@ -133,7 +147,7 @@
} }
if ((rule.t === 'btwn') || (rule.t === 'index')) { if ((rule.t === 'btwn') || (rule.t === 'index')) {
label += " "+getValueLabel(rule.vt,rule.v)+" & "+getValueLabel(rule.v2t,rule.v2); label += " "+getValueLabel(rule.vt,rule.v)+" & "+getValueLabel(rule.v2t,rule.v2);
} else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'else' ) { } else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'empty' && rule.t !== 'nempty' && rule.t !== 'else' ) {
label += " "+getValueLabel(rule.vt,rule.v); label += " "+getValueLabel(rule.vt,rule.v);
} }
return label; return label;
@ -185,7 +199,7 @@
} else if (type === "istype") { } else if (type === "istype") {
typeField.typedInput("width",(newWidth-selectWidth-70)); typeField.typedInput("width",(newWidth-selectWidth-70));
} else { } else {
if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") { if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else") {
// valueField.hide(); // valueField.hide();
} else { } else {
valueField.typedInput("width",(newWidth-selectWidth-70)); valueField.typedInput("width",(newWidth-selectWidth-70));
@ -281,7 +295,7 @@
numValueField.typedInput('hide'); numValueField.typedInput('hide');
typeValueField.typedInput('hide'); typeValueField.typedInput('hide');
valueField.typedInput('hide'); valueField.typedInput('hide');
if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") { if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else") {
valueField.typedInput('hide'); valueField.typedInput('hide');
typeValueField.typedInput('hide'); typeValueField.typedInput('hide');
} }
@ -382,7 +396,7 @@
var rule = $(this); var rule = $(this);
var type = rule.find("select").val(); var type = rule.find("select").val();
var r = {t:type}; var r = {t:type};
if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else")) { if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else")) {
if ((type === "btwn") || (type === "index")) { if ((type === "btwn") || (type === "index")) {
r.v = rule.find(".node-input-rule-btwn-value").typedInput('value'); r.v = rule.find(".node-input-rule-btwn-value").typedInput('value');
r.vt = rule.find(".node-input-rule-btwn-value").typedInput('type'); r.vt = rule.find(".node-input-rule-btwn-value").typedInput('type');

View File

@ -31,6 +31,23 @@ module.exports = function(RED) {
'false': function(a) { return a === false; }, 'false': function(a) { return a === false; },
'null': function(a) { return (typeof a == "undefined" || a === null); }, 'null': function(a) { return (typeof a == "undefined" || a === null); },
'nnull': function(a) { return (typeof a != "undefined" && a !== null); }, 'nnull': function(a) { return (typeof a != "undefined" && a !== null); },
'empty': function(a) {
if (typeof a === 'string' || Array.isArray(a) || Buffer.isBuffer(a)) {
return a.length === 0;
} else if (typeof a === 'object' && a !== null) {
return Object.keys(a).length === 0;
}
return false;
},
'nempty': function(a) {
if (typeof a === 'string' || Array.isArray(a) || Buffer.isBuffer(a)) {
return a.length !== 0;
} else if (typeof a === 'object' && a !== null) {
return Object.keys(a).length !== 0;
}
return false;
},
'istype': function(a, b) { 'istype': function(a, b) {
if (b === "array") { return Array.isArray(a); } if (b === "array") { return Array.isArray(a); }
else if (b === "buffer") { return Buffer.isBuffer(a); } else if (b === "buffer") { return Buffer.isBuffer(a); }
@ -59,21 +76,157 @@ module.exports = function(RED) {
'else': function(a) { return a === true; } 'else': function(a) { return a === true; }
}; };
var _max_kept_msgs_count = undefined; var _maxKeptCount;
function max_kept_msgs_count(node) { function getMaxKeptCount() {
if (_max_kept_msgs_count === undefined) { if (_maxKeptCount === undefined) {
var name = "nodeMessageBufferMaxLength"; var name = "nodeMessageBufferMaxLength";
if (RED.settings.hasOwnProperty(name)) { if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name]; _maxKeptCount = RED.settings[name];
} }
else { else {
_max_kept_msgs_count = 0; _maxKeptCount = 0;
} }
} }
return _max_kept_msgs_count; return _maxKeptCount;
} }
function getProperty(node,msg) {
return new Promise((resolve,reject) => {
if (node.propertyType === 'jsonata') {
RED.util.evaluateJSONataExpression(node.property,msg,(err,value) => {
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else {
RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg,(err,value) => {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
}
});
}
function getV1(node,msg,rule,hasParts) {
return new Promise( (resolve,reject) => {
if (rule.vt === 'prev') {
resolve(node.previousValue);
} else if (rule.vt === 'jsonata') {
var exp = rule.v;
if (rule.t === 'jsonata_exp') {
if (hasParts) {
exp.assign("I", msg.parts.index);
exp.assign("N", msg.parts.count);
}
}
RED.util.evaluateJSONataExpression(exp,msg,(err,value) => {
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else if (rule.vt === 'json') {
resolve("json");
} else if (rule.vt === 'null') {
resolve("null");
} else {
RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg, function(err,value) {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
}
});
}
function getV2(node,msg,rule) {
return new Promise((resolve,reject) => {
var v2 = rule.v2;
if (rule.v2t === 'prev') {
resolve(node.previousValue);
} else if (rule.v2t === 'jsonata') {
RED.util.evaluateJSONataExpression(rule.v2,msg,(err,value) => {
if (err) {
reject(RED._("switch.errors.invalid-expr",{error:err.message}));
} else {
resolve(value);
}
});
} else if (typeof v2 !== 'undefined') {
RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg, function(err,value) {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
} else {
resolve(v2);
}
})
}
function applyRule(node, msg, property, state) {
return new Promise((resolve,reject) => {
var rule = node.rules[state.currentRule];
var v1,v2;
getV1(node,msg,rule,state.hasParts).then(value => {
v1 = value;
}).then(()=>getV2(node,msg,rule)).then(value => {
v2 = value;
}).then(() => {
if (rule.t == "else") {
property = state.elseflag;
state.elseflag = true;
}
if (operators[rule.t](property,v1,v2,rule.case,msg.parts)) {
state.onward.push(msg);
state.elseflag = false;
if (node.checkall == "false") {
return resolve(false);
}
} else {
state.onward.push(null);
}
resolve(state.currentRule < node.rules.length - 1);
});
})
}
function applyRules(node, msg, property,state) {
if (!state) {
state = {
currentRule: 0,
elseflag: true,
onward: [],
hasParts: msg.hasOwnProperty("parts") &&
msg.parts.hasOwnProperty("id") &&
msg.parts.hasOwnProperty("index")
}
}
return applyRule(node,msg,property,state).then(hasMore => {
if (hasMore) {
state.currentRule++;
return applyRules(node,msg,property,state);
} else {
node.previousValue = property;
return state.onward;
}
});
}
function SwitchNode(n) { function SwitchNode(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
this.rules = n.rules || []; this.rules = n.rules || [];
@ -94,10 +247,10 @@ module.exports = function(RED) {
var node = this; var node = this;
var valid = true; var valid = true;
var repair = n.repair; var repair = n.repair;
var needs_count = repair; var needsCount = repair;
for (var i=0; i<this.rules.length; i+=1) { for (var i=0; i<this.rules.length; i+=1) {
var rule = this.rules[i]; var rule = this.rules[i];
needs_count = needs_count || ((rule.t === "tail") || (rule.t === "jsonata_exp")); needsCount = needsCount || ((rule.t === "tail") || (rule.t === "jsonata_exp"));
if (!rule.vt) { if (!rule.vt) {
if (!isNaN(Number(rule.v))) { if (!isNaN(Number(rule.v))) {
rule.vt = 'num'; rule.vt = 'num';
@ -142,26 +295,26 @@ module.exports = function(RED) {
return; return;
} }
var pending_count = 0; var pendingCount = 0;
var pending_id = 0; var pendingId = 0;
var pending_in = {}; var pendingIn = {};
var pending_out = {}; var pendingOut = {};
var received = {}; var received = {};
function add2group_in(id, msg, parts) { function addMessageToGroup(id, msg, parts) {
if (!(id in pending_in)) { if (!(id in pendingIn)) {
pending_in[id] = { pendingIn[id] = {
count: undefined, count: undefined,
msgs: [], msgs: [],
seq_no: pending_id++ seq_no: pendingId++
}; };
} }
var group = pending_in[id]; var group = pendingIn[id];
group.msgs.push(msg); group.msgs.push(msg);
pending_count++; pendingCount++;
var max_msgs = max_kept_msgs_count(node); var max_msgs = getMaxKeptCount();
if ((max_msgs > 0) && (pending_count > max_msgs)) { if ((max_msgs > 0) && (pendingCount > max_msgs)) {
clear_pending(); clearPending();
node.error(RED._("switch.errors.too-many"), msg); node.error(RED._("switch.errors.too-many"), msg);
} }
if (parts.hasOwnProperty("count")) { if (parts.hasOwnProperty("count")) {
@ -170,32 +323,29 @@ module.exports = function(RED) {
return group; return group;
} }
function del_group_in(id, group) {
pending_count -= group.msgs.length;
delete pending_in[id];
}
function add2pending_in(msg) { function addMessageToPending(msg) {
var parts = msg.parts; var parts = msg.parts;
if (parts.hasOwnProperty("id") && // We've already checked the msg.parts has the require bits
parts.hasOwnProperty("index")) { var group = addMessageToGroup(parts.id, msg, parts);
var group = add2group_in(parts.id, msg, parts);
var msgs = group.msgs; var msgs = group.msgs;
var count = group.count; var count = group.count;
if (count === msgs.length) { if (count === msgs.length) {
for (var i = 0; i < msgs.length; i++) { // We have a complete group - send the individual parts
var msg = msgs[i]; return msgs.reduce((promise, msg) => {
return promise.then((result) => {
msg.parts.count = count; msg.parts.count = count;
process_msg(msg, false); return processMessage(msg, false);
})
}, Promise.resolve()).then( () => {
pendingCount -= group.msgs.length;
delete pendingIn[parts.id];
});
} }
del_group_in(parts.id, group); return Promise.resolve();
}
return true;
}
return false;
} }
function send_group(onwards, port_count) { function sendGroup(onwards, port_count) {
var counts = new Array(port_count).fill(0); var counts = new Array(port_count).fill(0);
for (var i = 0; i < onwards.length; i++) { for (var i = 0; i < onwards.length; i++) {
var onward = onwards[i]; var onward = onwards[i];
@ -230,141 +380,104 @@ module.exports = function(RED) {
} }
} }
function send2ports(onward, msg) { function sendGroupMessages(onward, msg) {
var parts = msg.parts; var parts = msg.parts;
var gid = parts.id; var gid = parts.id;
received[gid] = ((gid in received) ? received[gid] : 0) +1; received[gid] = ((gid in received) ? received[gid] : 0) +1;
var send_ok = (received[gid] === parts.count); var send_ok = (received[gid] === parts.count);
if (!(gid in pending_out)) { if (!(gid in pendingOut)) {
pending_out[gid] = { pendingOut[gid] = {
onwards: [] onwards: []
}; };
} }
var group = pending_out[gid]; var group = pendingOut[gid];
var onwards = group.onwards; var onwards = group.onwards;
onwards.push(onward); onwards.push(onward);
pending_count++; pendingCount++;
if (send_ok) { if (send_ok) {
send_group(onwards, onward.length, msg); sendGroup(onwards, onward.length, msg);
pending_count -= onward.length; pendingCount -= onward.length;
delete pending_out[gid]; delete pendingOut[gid];
delete received[gid]; delete received[gid];
} }
var max_msgs = max_kept_msgs_count(node); var max_msgs = getMaxKeptCount();
if ((max_msgs > 0) && (pending_count > max_msgs)) { if ((max_msgs > 0) && (pendingCount > max_msgs)) {
clear_pending(); clearPending();
node.error(RED._("switch.errors.too-many"), msg); node.error(RED._("switch.errors.too-many"), msg);
} }
} }
function msg_has_parts(msg) {
if (msg.hasOwnProperty("parts")) {
var parts = msg.parts;
return (parts.hasOwnProperty("id") &&
parts.hasOwnProperty("index"));
}
return false;
}
function process_msg(msg, check_parts) {
var has_parts = msg_has_parts(msg);
if (needs_count && check_parts && has_parts &&
add2pending_in(msg)) { function processMessage(msg, checkParts) {
return; var hasParts = msg.hasOwnProperty("parts") &&
msg.parts.hasOwnProperty("id") &&
msg.parts.hasOwnProperty("index");
if (needsCount && checkParts && hasParts) {
return addMessageToPending(msg);
} }
var onward = []; return getProperty(node,msg)
try { .then(property => applyRules(node,msg,property))
var prop; .then(onward => {
if (node.propertyType === 'jsonata') { if (!repair || !hasParts) {
prop = RED.util.evaluateJSONataExpression(node.property,msg);
} else {
prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg);
}
var elseflag = true;
for (var i=0; i<node.rules.length; i+=1) {
var rule = node.rules[i];
var test = prop;
var v1,v2;
if (rule.vt === 'prev') {
v1 = node.previousValue;
} else if (rule.vt === 'jsonata') {
try {
var exp = rule.v;
if (rule.t === 'jsonata_exp') {
if (has_parts) {
exp.assign("I", msg.parts.index);
exp.assign("N", msg.parts.count);
}
}
v1 = RED.util.evaluateJSONataExpression(exp,msg);
} catch(err) {
node.error(RED._("switch.errors.invalid-expr",{error:err.message}));
return;
}
} else if (rule.vt === 'json') {
v1 = "json";
} else if (rule.vt === 'null') {
v1 = "null";
} else {
try {
v1 = RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg);
} catch(err) {
v1 = undefined;
}
}
v2 = rule.v2;
if (rule.v2t === 'prev') {
v2 = node.previousValue;
} else if (rule.v2t === 'jsonata') {
try {
v2 = RED.util.evaluateJSONataExpression(rule.v2,msg);
} catch(err) {
node.error(RED._("switch.errors.invalid-expr",{error:err.message}));
return;
}
} else if (typeof v2 !== 'undefined') {
try {
v2 = RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg);
} catch(err) {
v2 = undefined;
}
}
if (rule.t == "else") { test = elseflag; elseflag = true; }
if (operators[rule.t](test,v1,v2,rule.case,msg.parts)) {
onward.push(msg);
elseflag = false;
if (node.checkall == "false") { break; }
} else {
onward.push(null);
}
}
node.previousValue = prop;
if (!repair || !has_parts) {
node.send(onward); node.send(onward);
} }
else { else {
send2ports(onward, msg); sendGroupMessages(onward, msg);
} }
} catch(err) { }).catch(err => {
node.warn(err); node.warn(err);
} });
} }
function clear_pending() { function clearPending() {
pending_count = 0; pendingCount = 0;
pending_id = 0; pendingId = 0;
pending_in = {}; pendingIn = {};
pending_out = {}; pendingOut = {};
received = {}; received = {};
} }
var pendingMessages = [];
var activeMessagePromise = null;
var processMessageQueue = function(msg) {
if (msg) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
if (activeMessagePromise !== null) {
// The node is currently processing a message, so do nothing
// more with this message
return;
}
}
if (pendingMessages.length === 0) {
// There are no more messages to process, clear the active flag
// and return
activeMessagePromise = null;
return;
}
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
activeMessagePromise = processMessage(nextMsg,true)
.then(processMessageQueue)
.catch((err) => {
node.error(err,nextMsg);
return processMessageQueue();
});
}
this.on('input', function(msg) { this.on('input', function(msg) {
process_msg(msg, true); processMessageQueue(msg);
}); });
this.on('close', function() { this.on('close', function() {
clear_pending(); clearPending();
}); });
} }

View File

@ -54,6 +54,10 @@
outputs: 1, outputs: 1,
icon: "swap.png", icon: "swap.png",
label: function() { label: function() {
function prop2name(type, key) {
var result = RED.utils.parseContextKey(key);
return type +"." +result.key;
}
if (this.name) { if (this.name) {
return this.name; return this.name;
} }
@ -70,13 +74,13 @@
} else { } else {
if (this.rules.length == 1) { if (this.rules.length == 1) {
if (this.rules[0].t === "set") { if (this.rules[0].t === "set") {
return this._("change.label.set",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.set",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} else if (this.rules[0].t === "change") { } else if (this.rules[0].t === "change") {
return this._("change.label.change",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.change",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} else if (this.rules[0].t === "move") { } else if (this.rules[0].t === "move") {
return this._("change.label.move",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.move",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} else { } else {
return this._("change.label.delete",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p}); return this._("change.label.delete",{property:prop2name((this.rules[0].pt||"msg"), this.rules[0].p)});
} }
} else { } else {
return this._("change.label.changeCount",{count:this.rules.length}); return this._("change.label.changeCount",{count:this.rules.length});

View File

@ -98,44 +98,61 @@ module.exports = function(RED) {
} }
} }
function applyRule(msg,rule) { function getToValue(msg,rule) {
try {
var property = rule.p;
var value = rule.to; var value = rule.to;
if (rule.tot === 'json') { if (rule.tot === 'json') {
value = JSON.parse(rule.to); value = JSON.parse(rule.to);
} else if (rule.tot === 'bin') { } else if (rule.tot === 'bin') {
value = Buffer.from(JSON.parse(rule.to)) value = Buffer.from(JSON.parse(rule.to))
} }
var current;
var fromValue;
var fromType;
var fromRE;
if (rule.tot === "msg") { if (rule.tot === "msg") {
value = RED.util.getMessageProperty(msg,rule.to); value = RED.util.getMessageProperty(msg,rule.to);
} else if (rule.tot === 'flow') { } else if ((rule.tot === 'flow') ||
value = node.context().flow.get(rule.to); (rule.tot === 'global')) {
} else if (rule.tot === 'global') { return new Promise((resolve,reject) => {
value = node.context().global.get(rule.to); RED.util.evaluateNodeProperty(rule.to, rule.tot, node, msg, (err,value) => {
if (err) {
resolve(undefined);
} else {
resolve(value);
}
});
});
} else if (rule.tot === 'date') { } else if (rule.tot === 'date') {
value = Date.now(); value = Date.now();
} else if (rule.tot === 'jsonata') { } else if (rule.tot === 'jsonata') {
try{ return new Promise((resolve,reject) => {
value = RED.util.evaluateJSONataExpression(rule.to,msg); RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => {
} catch(err) { if (err) {
node.error(RED._("change.errors.invalid-expr",{error:err.message})); reject(RED._("change.errors.invalid-expr",{error:err.message}))
return; } else {
resolve(value);
} }
});
});
} }
return Promise.resolve(value);
}
function getFromValue(msg,rule) {
var fromValue;
var fromType;
var fromRE;
if (rule.t === 'change') { if (rule.t === 'change') {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') { if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
return new Promise((resolve,reject) => {
if (rule.fromt === "msg") { if (rule.fromt === "msg") {
fromValue = RED.util.getMessageProperty(msg,rule.from); resolve(RED.util.getMessageProperty(msg,rule.from));
} else if (rule.fromt === 'flow') { } else if (rule.fromt === 'flow' || rule.fromt === 'global') {
fromValue = node.context().flow.get(rule.from); var contextKey = RED.util.parseContextStore(rule.from);
} else if (rule.fromt === 'global') { node.context()[rule.fromt].get(contextKey.key, contextKey.store, (err,fromValue) => {
fromValue = node.context().global.get(rule.from); if (err) {
reject(err);
} else {
resolve(fromValue);
} }
});
}
}).then(fromValue => {
if (typeof fromValue === 'number' || fromValue instanceof Number) { if (typeof fromValue === 'number' || fromValue instanceof Number) {
fromType = 'num'; fromType = 'num';
} else if (typeof fromValue === 'boolean') { } else if (typeof fromValue === 'boolean') {
@ -149,21 +166,43 @@ module.exports = function(RED) {
try { try {
fromRE = new RegExp(fromRE, "g"); fromRE = new RegExp(fromRE, "g");
} catch (e) { } catch (e) {
valid = false; return Promise.reject(new Error(RED._("change.errors.invalid-from",{error:e.message})));
node.error(RED._("change.errors.invalid-from",{error:e.message}));
return;
} }
} else { } else {
node.error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)})); return Promise.reject(new Error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)})));
return
} }
return {
fromType,
fromValue,
fromRE
}
});
} else { } else {
fromType = rule.fromt; fromType = rule.fromt;
fromValue = rule.from; fromValue = rule.from;
fromRE = rule.fromRE; fromRE = rule.fromRE;
} }
} }
return Promise.resolve({
fromType,
fromValue,
fromRE
});
}
function applyRule(msg,rule) {
var property = rule.p;
var current;
var fromValue;
var fromType;
var fromRE;
try {
return getToValue(msg,rule).then(value => {
return getFromValue(msg,rule).then(fromParts => {
fromValue = fromParts.fromValue;
fromType = fromParts.fromType;
fromRE = fromParts.fromRE;
if (rule.pt === 'msg') { if (rule.pt === 'msg') {
try {
if (rule.t === 'delete') { if (rule.t === 'delete') {
RED.util.setMessageProperty(msg,property,undefined); RED.util.setMessageProperty(msg,property,undefined);
} else if (rule.t === 'set') { } else if (rule.t === 'set') {
@ -189,68 +228,98 @@ module.exports = function(RED) {
} }
} }
} }
} catch(err) {}
return msg;
} else if (rule.pt === 'flow' || rule.pt === 'global') {
var contextKey = RED.util.parseContextStore(property);
return new Promise((resolve,reject) => {
var target = node.context()[rule.pt];
var callback = err => {
if (err) {
reject(err);
} else {
resolve(msg);
} }
else {
var target;
if (rule.pt === 'flow') {
target = node.context().flow;
} else if (rule.pt === 'global') {
target = node.context().global;
} }
if (target) {
if (rule.t === 'delete') { if (rule.t === 'delete') {
target.set(property,undefined); target.set(contextKey.key,undefined,contextKey.store,callback);
} else if (rule.t === 'set') { } else if (rule.t === 'set') {
target.set(property,value); target.set(contextKey.key,value,contextKey.store,callback);
} else if (rule.t === 'change') { } else if (rule.t === 'change') {
current = target.get(property); target.get(contextKey.key,contextKey.store,(err,current) => {
if (err) {
reject(err);
return;
}
if (typeof current === 'string') { if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) { if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) {
// str representation of exact from number/boolean // str representation of exact from number/boolean
// only replace if they match exactly // only replace if they match exactly
target.set(property,value); target.set(contextKey.key,value,contextKey.store,callback);
} else { } else {
current = current.replace(fromRE,value); current = current.replace(fromRE,value);
target.set(property,current); target.set(contextKey.key,current,contextKey.store,callback);
} }
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { } else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) { if (current == Number(fromValue)) {
target.set(property,value); target.set(contextKey.key,value,contextKey.store,callback);
} }
} else if (typeof current === 'boolean' && fromType === 'bool') { } else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) { if (current.toString() === fromValue) {
target.set(property,value); target.set(contextKey.key,value,contextKey.store,callback);
} }
} }
});
}
});
}
});
}).catch(err => {
node.error(err, msg);
return null;
});
} catch(err) {
return Promise.resolve(msg);
} }
} }
function applyRules(msg, currentRule) {
var r = node.rules[currentRule];
var rulePromise;
if (r.t === "move") {
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) {
rulePromise = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt}).then(
msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt})
);
} }
} catch(err) {/*console.log(err.stack)*/} else { // 2 step move if we are moving from a child
rulePromise = applyRule(msg,{t:"set", p:"_temp_move", pt:r.tot, to:r.p, tot:r.pt}).then(
msg => applyRule(msg,{t:"delete", p:r.p, pt:r.pt})
).then(
msg => applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:"_temp_move", tot:r.pt})
).then(
msg => applyRule(msg,{t:"delete", p:"_temp_move", pt:r.pt})
)
}
} else {
rulePromise = applyRule(msg,r);
}
return rulePromise.then(
msg => {
if (!msg) {
return
} else if (currentRule === node.rules.length - 1) {
return msg; return msg;
} else {
return applyRules(msg, currentRule+1);
}
}
);
} }
if (valid) { if (valid) {
this.on('input', function(msg) { this.on('input', function(msg) {
for (var i=0; i<this.rules.length; i++) { applyRules(msg, 0)
if (this.rules[i].t === "move") { .then( msg => { if (msg) { node.send(msg) }} )
var r = this.rules[i]; .catch( err => node.error(err, msg))
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) {
msg = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt});
applyRule(msg,{t:"delete", p:r.p, pt:r.pt});
}
else { // 2 step move if we are moving from a child
msg = applyRule(msg,{t:"set", p:"_temp_move", pt:r.tot, to:r.p, tot:r.pt});
applyRule(msg,{t:"delete", p:r.p, pt:r.pt});
msg = applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:"_temp_move", tot:r.pt});
applyRule(msg,{t:"delete", p:"_temp_move", pt:r.pt});
}
} else {
msg = applyRule(msg,this.rules[i]);
}
if (msg === null) {
return;
}
}
node.send(msg);
}); });
} }
} }

View File

@ -415,7 +415,7 @@
$("#node-input-reduceExp").typedInput({types:[jsonata_or_empty]}); $("#node-input-reduceExp").typedInput({types:[jsonata_or_empty]});
$("#node-input-reduceInit").typedInput({ $("#node-input-reduceInit").typedInput({
default: 'num', default: 'num',
types:['flow','global','str','num','bool','json','bin','date','jsonata'], types:['flow','global','str','num','bool','json','bin','date','jsonata','env'],
typeField: $("#node-input-reduceInitType") typeField: $("#node-input-reduceInitType")
}); });
$("#node-input-reduceFixup").typedInput({types:[jsonata_or_empty]}); $("#node-input-reduceFixup").typedInput({types:[jsonata_or_empty]});

View File

@ -233,7 +233,7 @@ module.exports = function(RED) {
RED.nodes.registerType("split",SplitNode); RED.nodes.registerType("split",SplitNode);
var _max_kept_msgs_count = undefined; var _max_kept_msgs_count;
function max_kept_msgs_count(node) { function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) { if (_max_kept_msgs_count === undefined) {
@ -252,13 +252,29 @@ module.exports = function(RED) {
exp.assign("I", index); exp.assign("I", index);
exp.assign("N", count); exp.assign("N", count);
exp.assign("A", accum); exp.assign("A", accum);
return RED.util.evaluateJSONataExpression(exp, msg); return new Promise((resolve,reject) => {
RED.util.evaluateJSONataExpression(exp, msg, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
} }
function apply_f(exp, accum, count) { function apply_f(exp, accum, count) {
exp.assign("N", count); exp.assign("N", count);
exp.assign("A", accum); exp.assign("A", accum);
return RED.util.evaluateJSONataExpression(exp, {}); return new Promise((resolve,reject) => {
return RED.util.evaluateJSONataExpression(exp, {}, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
} }
function exp_or_undefined(exp) { function exp_or_undefined(exp) {
@ -269,32 +285,40 @@ module.exports = function(RED) {
return exp return exp
} }
function reduce_and_send_group(node, group) { function reduceAndSendGroup(node, group) {
var is_right = node.reduce_right; var is_right = node.reduce_right;
var flag = is_right ? -1 : 1; var flag = is_right ? -1 : 1;
var msgs = group.msgs; var msgs = group.msgs;
var accum = eval_exp(node, node.exp_init, node.exp_init_type); return getInitialReduceValue(node, node.exp_init, node.exp_init_type).then(accum => {
var reduce_exp = node.reduce_exp; var reduce_exp = node.reduce_exp;
var reduce_fixup = node.reduce_fixup; var reduce_fixup = node.reduce_fixup;
var count = group.count; var count = group.count;
msgs.sort(function(x,y) { msgs.sort(function(x,y) {
var ix = x.parts.index; var ix = x.parts.index;
var iy = y.parts.index; var iy = y.parts.index;
if (ix < iy) return -flag; if (ix < iy) {return -flag;}
if (ix > iy) return flag; if (ix > iy) {return flag;}
return 0; return 0;
}); });
for(var msg of msgs) {
accum = apply_r(reduce_exp, accum, msg, msg.parts.index, count); return msgs.reduce((promise, msg) => promise.then(accum => apply_r(reduce_exp, accum, msg, msg.parts.index, count)), Promise.resolve(accum))
} .then(accum => {
if(reduce_fixup !== undefined) { if(reduce_fixup !== undefined) {
accum = apply_f(reduce_fixup, accum, count); return apply_f(reduce_fixup, accum, count).then(accum => {
}
node.send({payload: accum}); node.send({payload: accum});
});
} else {
node.send({payload: accum});
}
});
}).catch(err => {
throw new Error(RED._("join.errors.invalid-expr",{error:err.message}));
});
} }
function reduce_msg(node, msg) { function reduce_msg(node, msg) {
if(msg.hasOwnProperty('parts')) { var promise;
if (msg.hasOwnProperty('parts')) {
var parts = msg.parts; var parts = msg.parts;
var pending = node.pending; var pending = node.pending;
var pending_count = node.pending_count; var pending_count = node.pending_count;
@ -311,66 +335,51 @@ module.exports = function(RED) {
} }
var group = pending[gid]; var group = pending[gid];
var msgs = group.msgs; var msgs = group.msgs;
if(parts.hasOwnProperty('count') && if(parts.hasOwnProperty('count') && (group.count === undefined)) {
(group.count === undefined)) { group.count = parts.count;
group.count = count;
} }
msgs.push(msg); msgs.push(msg);
pending_count++; pending_count++;
if(msgs.length === group.count) { var completeProcess = function() {
delete pending[gid];
try {
pending_count -= msgs.length;
reduce_and_send_group(node, group);
} catch(e) {
node.error(RED._("join.errors.invalid-expr",{error:e.message})); }
}
node.pending_count = pending_count; node.pending_count = pending_count;
var max_msgs = max_kept_msgs_count(node); var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) { if ((max_msgs > 0) && (pending_count > max_msgs)) {
node.pending = {}; node.pending = {};
node.pending_count = 0; node.pending_count = 0;
node.error(RED._("join.too-many"), msg); var promise = Promise.reject(RED._("join.too-many"));
promise.catch(()=>{});
return promise;
} }
return Promise.resolve();
} }
else { if(msgs.length === group.count) {
delete pending[gid];
pending_count -= msgs.length;
promise = reduceAndSendGroup(node, group).then(completeProcess);
} else {
promise = completeProcess();
}
} else {
node.send(msg); node.send(msg);
} }
if (!promise) {
promise = Promise.resolve();
}
return promise;
} }
function eval_exp(node, exp, exp_type) { function getInitialReduceValue(node, exp, exp_type) {
if(exp_type === "flow") { return new Promise((resolve, reject) => {
return node.context().flow.get(exp); RED.util.evaluateNodeProperty(exp, exp_type, node, {},
(err, result) => {
if(err) {
return reject(err);
} }
else if(exp_type === "global") { else {
return node.context().global.get(exp); return resolve(result);
} }
else if(exp_type === "str") { });
return exp; });
}
else if(exp_type === "num") {
return Number(exp);
}
else if(exp_type === "bool") {
if (exp === 'true') {
return true;
}
else if (exp === 'false') {
return false;
}
}
else if ((exp_type === "bin") ||
(exp_type === "json")) {
return JSON.parse(exp);
}
else if(exp_type === "date") {
return Date.now();
}
else if(exp_type === "jsonata") {
var jexp = RED.util.prepareJSONataExpression(exp, node);
return RED.util.evaluateJSONataExpression(jexp, {});
}
throw new Error("unexpected initial value type");
} }
function JoinNode(n) { function JoinNode(n) {
@ -399,6 +408,7 @@ module.exports = function(RED) {
this.reduce_fixup = (exp_fixup !== undefined) ? RED.util.prepareJSONataExpression(exp_fixup, this) : undefined; this.reduce_fixup = (exp_fixup !== undefined) ? RED.util.prepareJSONataExpression(exp_fixup, this) : undefined;
} catch(e) { } catch(e) {
this.error(RED._("join.errors.invalid-expr",{error:e.message})); this.error(RED._("join.errors.invalid-expr",{error:e.message}));
return;
} }
} }
@ -437,7 +447,8 @@ module.exports = function(RED) {
newArray = newArray.concat(n); newArray = newArray.concat(n);
}) })
group.payload = newArray; group.payload = newArray;
} else if (group.type === 'buffer') { }
else if (group.type === 'buffer') {
var buffers = []; var buffers = [];
var bufferLen = 0; var bufferLen = 0;
if (group.joinChar !== undefined) { if (group.joinChar !== undefined) {
@ -450,7 +461,8 @@ module.exports = function(RED) {
buffers.push(group.payload[i]); buffers.push(group.payload[i]);
bufferLen += group.payload[i].length; bufferLen += group.payload[i].length;
} }
} else { }
else {
bufferLen = group.bufferLen; bufferLen = group.bufferLen;
buffers = group.payload; buffers = group.payload;
} }
@ -463,7 +475,8 @@ module.exports = function(RED) {
groupJoinChar = group.joinChar.toString(); groupJoinChar = group.joinChar.toString();
} }
RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar)); RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar));
} else { }
else {
if (node.propertyType === 'full') { if (node.propertyType === 'full') {
group.msg = RED.util.cloneMessage(group.msg); group.msg = RED.util.cloneMessage(group.msg);
} }
@ -471,13 +484,48 @@ module.exports = function(RED) {
} }
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) { if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
group.msg.parts = group.msg.parts.parts; group.msg.parts = group.msg.parts.parts;
} else { }
else {
delete group.msg.parts; delete group.msg.parts;
} }
delete group.msg.complete; delete group.msg.complete;
node.send(group.msg); node.send(group.msg);
} }
var pendingMessages = [];
var activeMessagePromise = null;
// In reduce mode, we must process messages fully in order otherwise
// groups may overlap and cause unexpected results. The use of JSONata
// means some async processing *might* occur if flow/global context is
// accessed.
var processReduceMessageQueue = function(msg) {
if (msg) {
// A new message has arrived - add it to the message queue
pendingMessages.push(msg);
if (activeMessagePromise !== null) {
// The node is currently processing a message, so do nothing
// more with this message
return;
}
}
if (pendingMessages.length === 0) {
// There are no more messages to process, clear the active flag
// and return
activeMessagePromise = null;
return;
}
// There are more messages to process. Get the next message and
// start processing it. Recurse back in to check for any more
var nextMsg = pendingMessages.shift();
activeMessagePromise = reduce_msg(node, nextMsg)
.then(processReduceMessageQueue)
.catch((err) => {
node.error(err,nextMsg);
return processReduceMessageQueue();
});
}
this.on("input", function(msg) { this.on("input", function(msg) {
try { try {
var property; var property;
@ -516,8 +564,7 @@ module.exports = function(RED) {
propertyIndex = msg.parts.index; propertyIndex = msg.parts.index;
} }
else if (node.mode === 'reduce') { else if (node.mode === 'reduce') {
reduce_msg(node, msg); return processReduceMessageQueue(msg);
return;
} }
else { else {
// Use the node configuration to identify all of the group information // Use the node configuration to identify all of the group information
@ -525,7 +572,7 @@ module.exports = function(RED) {
payloadType = node.build; payloadType = node.build;
targetCount = node.count; targetCount = node.count;
joinChar = node.joiner; joinChar = node.joiner;
if (targetCount === 0 && msg.hasOwnProperty('parts')) { if (n.count === "" && msg.hasOwnProperty('parts')) {
targetCount = msg.parts.count || 0; targetCount = msg.parts.count || 0;
} }
if (node.build === 'object') { if (node.build === 'object') {
@ -554,7 +601,7 @@ module.exports = function(RED) {
payload:{}, payload:{},
targetCount:targetCount, targetCount:targetCount,
type:"object", type:"object",
msg:msg msg:RED.util.cloneMessage(msg)
}; };
} }
else if (node.accumulate === true) { else if (node.accumulate === true) {
@ -564,7 +611,7 @@ module.exports = function(RED) {
payload:{}, payload:{},
targetCount:targetCount, targetCount:targetCount,
type:payloadType, type:payloadType,
msg:msg msg:RED.util.cloneMessage(msg)
} }
if (payloadType === 'string' || payloadType === 'array' || payloadType === 'buffer') { if (payloadType === 'string' || payloadType === 'array' || payloadType === 'buffer') {
inflight[partId].payload = []; inflight[partId].payload = [];
@ -576,7 +623,7 @@ module.exports = function(RED) {
payload:[], payload:[],
targetCount:targetCount, targetCount:targetCount,
type:payloadType, type:payloadType,
msg:msg msg:RED.util.cloneMessage(msg)
}; };
if (payloadType === 'string') { if (payloadType === 'string') {
inflight[partId].joinChar = joinChar; inflight[partId].joinChar = joinChar;
@ -619,19 +666,22 @@ module.exports = function(RED) {
} else { } else {
if (!isNaN(propertyIndex)) { if (!isNaN(propertyIndex)) {
group.payload[propertyIndex] = property; group.payload[propertyIndex] = property;
group.currentCount++;
} else { } else {
if (property !== undefined) {
group.payload.push(property); group.payload.push(property);
}
group.currentCount++; group.currentCount++;
} }
// TODO: currently reuse the last received - add option to pick first received }
group.msg = msg; }
group.msg = Object.assign(group.msg, msg);
var tcnt = group.targetCount; var tcnt = group.targetCount;
if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; } if (msg.hasOwnProperty("parts")) { tcnt = group.targetCount || msg.parts.count; }
if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) { if ((tcnt > 0 && group.currentCount >= tcnt) || msg.hasOwnProperty('complete')) {
completeSend(partId); completeSend(partId);
} }
} catch(err) { }
catch(err) {
console.log(err.stack); console.log(err.stack);
} }
}); });

View File

@ -17,7 +17,7 @@
module.exports = function(RED) { module.exports = function(RED) {
"use strict"; "use strict";
var _max_kept_msgs_count = undefined; var _max_kept_msgs_count;
function max_kept_msgs_count(node) { function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) { if (_max_kept_msgs_count === undefined) {
@ -32,30 +32,20 @@ module.exports = function(RED) {
return _max_kept_msgs_count; return _max_kept_msgs_count;
} }
function eval_jsonata(node, code, val) { // function get_context_val(node, name, dval) {
try { // var context = node.context();
return RED.util.evaluateJSONataExpression(code, val); // var val = context.get(name);
} // if (val === undefined) {
catch (e) { // context.set(name, dval);
node.error(RED._("sort.invalid-exp")); // return dval;
throw e; // }
} // return val;
} // }
function get_context_val(node, name, dval) {
var context = node.context();
var val = context.get(name);
if (val === undefined) {
context.set(name, dval);
return dval;
}
return val;
}
function SortNode(n) { function SortNode(n) {
RED.nodes.createNode(this, n); RED.nodes.createNode(this, n);
var node = this; var node = this;
var pending = get_context_val(node, 'pending', {}) var pending = {};//get_context_val(node, 'pending', {})
var pending_count = 0; var pending_count = 0;
var pending_id = 0; var pending_id = 0;
var order = n.order || "ascending"; var order = n.order || "ascending";
@ -71,16 +61,15 @@ module.exports = function(RED) {
key_exp = RED.util.prepareJSONataExpression(key_exp, this); key_exp = RED.util.prepareJSONataExpression(key_exp, this);
} }
catch (e) { catch (e) {
node.error(RED._("sort.invalid-exp")); node.error(RED._("sort.invalid-exp",{message:e.toString()}));
return; return;
} }
} }
var dir = (order === "ascending") ? 1 : -1; var dir = (order === "ascending") ? 1 : -1;
var conv = as_num var conv = as_num ? function(x) { return Number(x); }
? function(x) { return Number(x); }
: function(x) { return x; }; : function(x) { return x; };
function gen_comp(key) { function generateComparisonFunction(key) {
return function(x, y) { return function(x, y) {
var xp = conv(key(x)); var xp = conv(key(x));
var yp = conv(key(y)); var yp = conv(key(y));
@ -90,69 +79,99 @@ module.exports = function(RED) {
}; };
} }
function send_group(group) { function sortMessageGroup(group) {
var key = key_is_exp var promise;
? function(msg) {
return eval_jsonata(node, key_exp, msg);
}
: function(msg) {
return RED.util.getMessageProperty(msg, key_prop);
};
var comp = gen_comp(key);
var msgs = group.msgs; var msgs = group.msgs;
if (key_is_exp) {
var evaluatedDataPromises = msgs.map(msg => {
return new Promise((resolve,reject) => {
RED.util.evaluateJSONataExpression(key_exp, msg, (err, result) => {
if (err) {
reject(RED._("sort.invalid-exp",{message:err.toString()}));
} else {
resolve({
item: msg,
sortValue: result
})
}
});
})
});
promise = Promise.all(evaluatedDataPromises).then(evaluatedElements => {
// Once all of the sort keys are evaluated, sort by them
var comp = generateComparisonFunction(elem=>elem.sortValue);
return evaluatedElements.sort(comp).map(elem=>elem.item);
});
} else {
var key = function(msg) {
return ;
}
var comp = generateComparisonFunction(msg => RED.util.getMessageProperty(msg, key_prop));
try { try {
msgs.sort(comp); msgs.sort(comp);
} }
catch (e) { catch (e) {
return; // not send when error return; // not send when error
} }
promise = Promise.resolve(msgs);
}
return promise.then(msgs => {
for (var i = 0; i < msgs.length; i++) { for (var i = 0; i < msgs.length; i++) {
var msg = msgs[i]; var msg = msgs[i];
msg.parts.index = i; msg.parts.index = i;
node.send(msg); node.send(msg);
} }
});
} }
function sort_payload(msg) { function sortMessageProperty(msg) {
var data = RED.util.getMessageProperty(msg, target_prop); var data = RED.util.getMessageProperty(msg, target_prop);
if (Array.isArray(data)) { if (Array.isArray(data)) {
var key = key_is_exp if (key_is_exp) {
? function(elem) { // key is an expression. Evaluated the expression for each item
return eval_jsonata(node, key_exp, elem); // to get its sort value. As this could be async, need to do
// it first.
var evaluatedDataPromises = data.map(elem => {
return new Promise((resolve,reject) => {
RED.util.evaluateJSONataExpression(key_exp, elem, (err, result) => {
if (err) {
reject(RED._("sort.invalid-exp",{message:err.toString()}));
} else {
resolve({
item: elem,
sortValue: result
})
} }
: function(elem) { return elem; }; });
var comp = gen_comp(key); })
})
return Promise.all(evaluatedDataPromises).then(evaluatedElements => {
// Once all of the sort keys are evaluated, sort by them
// and reconstruct the original message item with the newly
// sorted values.
var comp = generateComparisonFunction(elem=>elem.sortValue);
data = evaluatedElements.sort(comp).map(elem=>elem.item);
RED.util.setMessageProperty(msg, target_prop,data);
return true;
})
} else {
var comp = generateComparisonFunction(elem=>elem);
try { try {
data.sort(comp); data.sort(comp);
} catch (e) {
return Promise.resolve(false);
} }
catch (e) { return Promise.resolve(true);
return false;
} }
return true;
} }
return false; return Promise.resolve(false);
} }
function check_parts(parts) { function removeOldestPending() {
if (parts.hasOwnProperty("id") && var oldest;
parts.hasOwnProperty("index")) { var oldest_key;
return true;
}
return false;
}
function clear_pending() {
for(var key in pending) {
node.log(RED._("sort.clear"), pending[key].msgs[0]);
delete pending[key];
}
pending_count = 0;
}
function remove_oldest_pending() {
var oldest = undefined;
var oldest_key = undefined;
for(var key in pending) { for(var key in pending) {
if (pending.hasOwnProperty(key)) {
var item = pending[key]; var item = pending[key];
if((oldest === undefined) || if((oldest === undefined) ||
(oldest.seq_no > item.seq_no)) { (oldest.seq_no > item.seq_no)) {
@ -160,6 +179,7 @@ module.exports = function(RED) {
oldest_key = key; oldest_key = key;
} }
} }
}
if(oldest !== undefined) { if(oldest !== undefined) {
delete pending[oldest_key]; delete pending[oldest_key];
return oldest.msgs.length; return oldest.msgs.length;
@ -167,15 +187,18 @@ module.exports = function(RED) {
return 0; return 0;
} }
function process_msg(msg) { function processMessage(msg) {
if (target_is_prop) { if (target_is_prop) {
if (sort_payload(msg)) { sortMessageProperty(msg).then(send => {
if (send) {
node.send(msg); node.send(msg);
} }
return; }).catch(err => {
node.error(err,msg);
});
} }
var parts = msg.parts; var parts = msg.parts;
if (!check_parts(parts)) { if (!parts.hasOwnProperty("id") || !parts.hasOwnProperty("index")) {
return; return;
} }
var gid = parts.id; var gid = parts.id;
@ -195,23 +218,31 @@ module.exports = function(RED) {
pending_count++; pending_count++;
if (group.count === msgs.length) { if (group.count === msgs.length) {
delete pending[gid] delete pending[gid]
send_group(group); sortMessageGroup(group).catch(err => {
node.error(err,msg);
});
pending_count -= msgs.length; pending_count -= msgs.length;
} } else {
var max_msgs = max_kept_msgs_count(node); var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) { if ((max_msgs > 0) && (pending_count > max_msgs)) {
pending_count -= remove_oldest_pending(); pending_count -= removeOldestPending();
node.error(RED._("sort.too-many"), msg); node.error(RED._("sort.too-many"), msg);
} }
} }
}
this.on("input", function(msg) { this.on("input", function(msg) {
process_msg(msg); processMessage(msg);
}); });
this.on("close", function() { this.on("close", function() {
clear_pending(); for(var key in pending) {
}) if (pending.hasOwnProperty(key)) {
node.log(RED._("sort.clear"), pending[key].msgs[0]);
delete pending[key];
}
}
pending_count = 0; })
} }
RED.nodes.registerType("sort", SortNode); RED.nodes.registerType("sort", SortNode);

View File

@ -31,6 +31,8 @@
<dl class="message-properties"> <dl class="message-properties">
<dt>payload<span class="property-type">object | string</span></dt> <dt>payload<span class="property-type">object | string</span></dt>
<dd>A JavaScript object or JSON string.</dd> <dd>A JavaScript object or JSON string.</dd>
<dt>schema<span class="property-type">object</span></dt>
<dd>An optional JSON Schema object to validate the payload against.</dd>
</dl> </dl>
<h3>Outputs</h3> <h3>Outputs</h3>
<dl class="message-properties"> <dl class="message-properties">
@ -41,6 +43,9 @@
<li>If the input is a JavaScript object it creates a JSON string. The string can optionally be well-formatted.</li> <li>If the input is a JavaScript object it creates a JSON string. The string can optionally be well-formatted.</li>
</ul> </ul>
</dd> </dd>
<dt>schemaError<span class="property-type">array</span></dt>
<dd>If JSON schema validation fails, the catch node will have a <code>schemaError</code> property
containing an array of errors.</dd>
</dl> </dl>
<h3>Details</h3> <h3>Details</h3>
<p>By default, the node operates on <code>msg.payload</code>, but can be configured <p>By default, the node operates on <code>msg.payload</code>, but can be configured
@ -53,6 +58,8 @@
receives a String, no further checks will be made of the property. It will receives a String, no further checks will be made of the property. It will
not check the String is valid JSON nor will it reformat it if the format option not check the String is valid JSON nor will it reformat it if the format option
is selected.</p> is selected.</p>
<p>For more details about JSON Schema you can consult the specification
<a href="http://json-schema.org/latest/json-schema-validation.html">here</a>.</p>
</script> </script>
<script type="text/javascript"> <script type="text/javascript">

View File

@ -16,21 +16,52 @@
module.exports = function(RED) { module.exports = function(RED) {
"use strict"; "use strict";
const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true, schemaId: 'auto'});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
function JSONNode(n) { function JSONNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
this.indent = n.pretty ? 4 : 0; this.indent = n.pretty ? 4 : 0;
this.action = n.action||""; this.action = n.action||"";
this.property = n.property||"payload"; this.property = n.property||"payload";
this.schema = null;
this.compiledSchema = null;
var node = this; var node = this;
this.on("input", function(msg) { this.on("input", function(msg) {
var validate = false;
if (msg.schema) {
// If input schema is different, re-compile it
if (JSON.stringify(this.schema) != JSON.stringify(msg.schema)) {
try {
this.compiledSchema = ajv.compile(msg.schema);
this.schema = msg.schema;
} catch(e) {
this.schema = null;
this.compiledSchema = null;
node.error(RED._("json.errors.schema-error-compile"), msg);
return;
}
}
validate = true;
}
var value = RED.util.getMessageProperty(msg,node.property); var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) { if (value !== undefined) {
if (typeof value === "string") { if (typeof value === "string") {
if (node.action === "" || node.action === "obj") { if (node.action === "" || node.action === "obj") {
try { try {
RED.util.setMessageProperty(msg,node.property,JSON.parse(value)); RED.util.setMessageProperty(msg,node.property,JSON.parse(value));
if (validate) {
if (this.compiledSchema(msg[node.property])) {
node.send(msg); node.send(msg);
} else {
msg.schemaError = this.compiledSchema.errors;
node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg);
}
} else {
node.send(msg);
}
} }
catch(e) { node.error(e.message,msg); } catch(e) { node.error(e.message,msg); }
} else { } else {
@ -41,8 +72,19 @@ module.exports = function(RED) {
if (node.action === "" || node.action === "str") { if (node.action === "" || node.action === "str") {
if (!Buffer.isBuffer(value)) { if (!Buffer.isBuffer(value)) {
try { try {
if (validate) {
if (this.compiledSchema(value)) {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent)); RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
node.send(msg); node.send(msg);
} else {
msg.schemaError = this.compiledSchema.errors;
node.error(`${RED._("json.errors.schema-error")}: ${ajv.errorsText(this.compiledSchema.errors)}`, msg);
}
} else {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
node.send(msg);
}
} }
catch(e) { node.error(RED._("json.errors.dropped-error")); } catch(e) { node.error(RED._("json.errors.dropped-error")); }
} }

View File

@ -37,6 +37,8 @@
<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>
</dl> </dl>
<h3>Output</h3>
<p>On completion of write, input message is sent to output port.</p>
<h3>Details</h3> <h3>Details</h3>
<p>Each message payload will be added to the end of the file, optionally appending <p>Each message payload will be added to the end of the file, optionally appending
a newline (\n) character between each one.</p> a newline (\n) character between each one.</p>
@ -123,9 +125,8 @@
}, },
color:"BurlyWood", color:"BurlyWood",
inputs:1, inputs:1,
outputs:0, outputs:1,
icon: "file.png", icon: "file-out.png",
align: "right",
label: function() { label: function() {
if (this.overwriteFile === "delete") { if (this.overwriteFile === "delete") {
return this.name||this._("file.label.deletelabel",{file:this.filename}); return this.name||this._("file.label.deletelabel",{file:this.filename});
@ -159,7 +160,7 @@
outputLabels: function(i) { outputLabels: function(i) {
return (this.format === "utf8") ? "UTF8 string" : "binary buffer"; return (this.format === "utf8") ? "UTF8 string" : "binary buffer";
}, },
icon: "file.png", icon: "file-in.png",
label: function() { label: function() {
return this.name||this.filename||this._("file.label.filelabel"); return this.name||this.filename||this._("file.label.filelabel");
}, },

View File

@ -39,14 +39,20 @@ module.exports = function(RED) {
node.tout = null; node.tout = null;
},333); },333);
} }
if (filename === "") { node.warn(RED._("file.errors.nofilename")); } if (filename === "") {
else if (node.overwriteFile === "delete") { node.warn(RED._("file.errors.nofilename"));
} else if (node.overwriteFile === "delete") {
fs.unlink(filename, function (err) { fs.unlink(filename, function (err) {
if (err) { node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg); } if (err) {
else if (RED.settings.verbose) { node.log(RED._("file.status.deletedfile",{file:filename})); } node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg);
}); } else {
if (RED.settings.verbose) {
node.log(RED._("file.status.deletedfile",{file:filename}));
} }
else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) { node.send(msg);
}
});
} else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) {
var dir = path.dirname(filename); var dir = path.dirname(filename);
if (node.createDir) { if (node.createDir) {
try { try {
@ -64,15 +70,21 @@ 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; }
node.data.push(Buffer.from(data)); node.data.push({msg:msg,data:Buffer.from(data)});
while (node.data.length > 0) { while (node.data.length > 0) {
if (node.overwriteFile === "true") { if (node.overwriteFile === "true") {
(function(packet) {
node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true }); node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true });
node.wstream.on("error", function(err) { node.wstream.on("error", function(err) {
node.error(RED._("file.errors.writefail",{error:err.toString()}),msg); node.error(RED._("file.errors.writefail",{error:err.toString()}),msg);
}); });
node.wstream.end(node.data.shift()); node.wstream.on("open", function() {
node.wstream.end(packet.data, function() {
node.send(packet.msg);
});
})
})(node.data.shift());
} }
else { else {
// Append mode // Append mode
@ -115,10 +127,17 @@ module.exports = function(RED) {
} }
if (node.filename) { if (node.filename) {
// Static filename - write and reuse the stream next time // Static filename - write and reuse the stream next time
node.wstream.write(node.data.shift()); var packet = node.data.shift()
node.wstream.write(packet.data, function() {
node.send(packet.msg);
});
} else { } else {
// Dynamic filename - write and close the stream // Dynamic filename - write and close the stream
node.wstream.end(node.data.shift()); var packet = node.data.shift()
node.wstream.end(packet.data, function() {
node.send(packet.msg);
});
delete node.wstream; delete node.wstream;
delete node.wstreamIno; delete node.wstreamIno;
} }

View File

@ -34,6 +34,7 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"ajv": "6.5.2",
"basic-auth": "2.0.0", "basic-auth": "2.0.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.18.3", "body-parser": "1.18.3",
@ -43,25 +44,25 @@
"cookie-parser": "1.4.3", "cookie-parser": "1.4.3",
"cors": "2.8.4", "cors": "2.8.4",
"cron": "1.3.0", "cron": "1.3.0",
"denque": "1.3.0",
"express": "4.16.3", "express": "4.16.3",
"express-session": "1.15.6", "express-session": "1.15.6",
"fs-extra": "5.0.0", "fs-extra": "5.0.0",
"fs.notify": "0.0.4", "fs.notify": "0.0.4",
"hash-sum": "1.0.2", "hash-sum": "1.0.2",
"i18next": "1.10.6", "i18next": "^11.4.0",
"ink-docstrap": "^1.3.2",
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
"js-yaml": "3.11.0", "js-yaml": "3.12.0",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"jsonata": "1.5.4", "jsonata": "1.5.4",
"media-typer": "0.3.0", "media-typer": "0.3.0",
"memorystore": "1.6.0", "memorystore": "1.6.0",
"mime": "1.4.1", "mime": "1.4.1",
"mqtt": "2.18.0", "mqtt": "2.18.3",
"multer": "1.3.0", "multer": "1.3.1",
"mustache": "2.3.0", "mustache": "2.3.0",
"node-red-node-email": "0.1.*", "node-red-node-email": "0.1.*",
"node-red-node-feedparser": "0.1.*", "node-red-node-feedparser": "^0.1.12",
"node-red-node-rbe": "0.2.*", "node-red-node-rbe": "0.2.*",
"node-red-node-twitter": "*", "node-red-node-twitter": "*",
"nopt": "4.0.1", "nopt": "4.0.1",
@ -74,17 +75,17 @@
"request": "2.87.0", "request": "2.87.0",
"semver": "5.5.0", "semver": "5.5.0",
"sentiment": "2.1.0", "sentiment": "2.1.0",
"uglify-js": "3.3.25", "uglify-js": "3.4.5",
"when": "3.7.8", "when": "3.7.8",
"ws": "1.1.5", "ws": "1.1.5",
"xml2js": "0.4.19" "xml2js": "0.4.19"
}, },
"optionalDependencies": { "optionalDependencies": {
"bcrypt": "~1.0.3" "bcrypt": "~2.0.0"
}, },
"devDependencies": { "devDependencies": {
"chromedriver": "^2.33.2", "chromedriver": "^2.40.0",
"grunt": "~1.0.1", "grunt": "~1.0.3",
"grunt-chmod": "~1.1.1", "grunt-chmod": "~1.1.1",
"grunt-cli": "~1.2.0", "grunt-cli": "~1.2.0",
"grunt-concurrent": "~2.3.1", "grunt-concurrent": "~2.3.1",
@ -94,7 +95,7 @@
"grunt-contrib-copy": "~1.0.0", "grunt-contrib-copy": "~1.0.0",
"grunt-contrib-jshint": "~1.1.0", "grunt-contrib-jshint": "~1.1.0",
"grunt-contrib-uglify": "~3.3.0", "grunt-contrib-uglify": "~3.3.0",
"grunt-contrib-watch": "~1.0.0", "grunt-contrib-watch": "~1.1.0",
"grunt-jsdoc": "^2.2.1", "grunt-jsdoc": "^2.2.1",
"grunt-jsdoc-to-markdown": "^4.0.0", "grunt-jsdoc-to-markdown": "^4.0.0",
"grunt-jsonlint": "~1.1.0", "grunt-jsonlint": "~1.1.0",
@ -106,15 +107,15 @@
"http-proxy": "^1.16.2", "http-proxy": "^1.16.2",
"ink-docstrap": "^1.3.2", "ink-docstrap": "^1.3.2",
"istanbul": "0.4.5", "istanbul": "0.4.5",
"mocha": "^5.1.1", "mocha": "^5.2.0",
"should": "^8.4.0", "should": "^8.4.0",
"sinon": "1.17.7", "sinon": "1.17.7",
"stoppable": "^1.0.6", "stoppable": "^1.0.6",
"supertest": "3.0.0", "supertest": "3.1.0",
"wdio-chromedriver-service": "^0.1.1", "wdio-chromedriver-service": "^0.1.3",
"wdio-mocha-framework": "^0.5.11", "wdio-mocha-framework": "^0.6.2",
"wdio-spec-reporter": "^0.1.3", "wdio-spec-reporter": "^0.1.5",
"webdriverio": "^4.9.11", "webdriverio": "^4.13.1",
"node-red-node-test-helper": "^0.1.7" "node-red-node-test-helper": "^0.1.7"
}, },
"engines": { "engines": {

41
red/api/admin/context.js Normal file
View File

@ -0,0 +1,41 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var apiUtils = require("../util");
var runtimeAPI;
module.exports = {
init: function(_runtimeAPI) {
runtimeAPI = _runtimeAPI;
},
get: function(req,res) {
var opts = {
user: req.user,
scope: req.params.scope,
id: req.params.id,
key: req.params[0],
store: req.query['store']
}
runtimeAPI.context.getValue(opts).then(function(result) {
res.json(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
}
}

View File

@ -19,6 +19,7 @@ var express = require("express");
var nodes = require("./nodes"); var nodes = require("./nodes");
var flows = require("./flows"); var flows = require("./flows");
var flow = require("./flow"); var flow = require("./flow");
var context = require("./context");
var auth = require("../auth"); var auth = require("../auth");
var apiUtil = require("../util"); var apiUtil = require("../util");
@ -28,6 +29,7 @@ module.exports = {
flows.init(runtimeAPI); flows.init(runtimeAPI);
flow.init(runtimeAPI); flow.init(runtimeAPI);
nodes.init(runtimeAPI); nodes.init(runtimeAPI);
context.init(runtimeAPI);
var needsPermission = auth.needsPermission; var needsPermission = auth.needsPermission;
@ -54,6 +56,12 @@ module.exports = {
adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler); adminApp.get(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler);
adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler); adminApp.put(/\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler);
// Context
adminApp.get("/context/:scope(global)",needsPermission("context.read"),context.get,apiUtil.errorHandler);
adminApp.get("/context/:scope(global)/*",needsPermission("context.read"),context.get,apiUtil.errorHandler);
adminApp.get("/context/:scope(node|flow)/:id",needsPermission("context.read"),context.get,apiUtil.errorHandler);
adminApp.get("/context/:scope(node|flow)/:id/*",needsPermission("context.read"),context.get,apiUtil.errorHandler);
return adminApp; return adminApp;
} }
} }

View File

@ -144,6 +144,7 @@ module.exports = {
runtimeAPI.nodes.getModuleCatalog(opts).then(function(result) { runtimeAPI.nodes.getModuleCatalog(opts).then(function(result) {
res.json(result); res.json(result);
}).catch(function(err) { }).catch(function(err) {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err); apiUtils.rejectHandler(req,res,err);
}) })
}, },
@ -156,6 +157,7 @@ module.exports = {
runtimeAPI.nodes.getModuleCatalogs(opts).then(function(result) { runtimeAPI.nodes.getModuleCatalogs(opts).then(function(result) {
res.json(result); res.json(result);
}).catch(function(err) { }).catch(function(err) {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err); apiUtils.rejectHandler(req,res,err);
}) })
}, },

View File

@ -30,13 +30,12 @@ module.exports = {
var lngs = req.query.lng; var lngs = req.query.lng;
namespace = namespace.replace(/\.json$/,""); namespace = namespace.replace(/\.json$/,"");
var lang = req.query.lng; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []); var lang = req.query.lng; //apiUtil.determineLangFromHeaders(req.acceptsLanguages() || []);
var prevLang = i18n.i.lng(); var prevLang = i18n.i.language;
// Trigger a load from disk of the language if it is not the default // Trigger a load from disk of the language if it is not the default
i18n.i.setLng(lang, function(){ i18n.i.changeLanguage(lang, function(){
var catalog = i18n.catalog(namespace,lang); var catalog = i18n.i.getResourceBundle(lang, namespace);
res.json(catalog||{}); res.json(catalog||{});
}); });
i18n.i.setLng(prevLang); i18n.i.changeLanguage(prevLang);
} }
} }

View File

@ -240,6 +240,7 @@
"output": "outputs:", "output": "outputs:",
"deleteSubflow": "delete subflow", "deleteSubflow": "delete subflow",
"info": "Description", "info": "Description",
"category": "Category",
"format":"markdown format", "format":"markdown format",
"errors": { "errors": {
"noNodesSelected": "<strong>Cannot create subflow</strong>: no nodes selected", "noNodesSelected": "<strong>Cannot create subflow</strong>: no nodes selected",
@ -264,6 +265,8 @@
"settingIcon": "Icon", "settingIcon": "Icon",
"noDefaultLabel": "none", "noDefaultLabel": "none",
"defaultLabel": "use default label", "defaultLabel": "use default label",
"searchIcons": "Search icons",
"useDefault": "use default",
"errors": { "errors": {
"scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it" "scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it"
} }
@ -305,19 +308,18 @@
"savedNodes": "Saved nodes", "savedNodes": "Saved nodes",
"savedType": "Saved __type__", "savedType": "Saved __type__",
"saveFailed": "Save failed: __message__", "saveFailed": "Save failed: __message__",
"filename": "Filename", "filename": "Filename",
"folder": "Folder", "folder": "Folder",
"filenamePlaceholder": "file", "filenamePlaceholder": "file",
"fullFilenamePlaceholder": "a/b/file", "fullFilenamePlaceholder": "a/b/file",
"folderPlaceholder": "a/b", "folderPlaceholder": "a/b",
"breadcrumb": "Library" "breadcrumb": "Library"
}, },
"palette": { "palette": {
"noInfo": "no information available", "noInfo": "no information available",
"filter": "filter nodes", "filter": "filter nodes",
"search": "search modules", "search": "search modules",
"addCategory": "Add new...",
"label": { "label": {
"subflows": "subflows", "subflows": "subflows",
"input": "input", "input": "input",
@ -356,7 +358,6 @@
"monthsV_plural": "__count__ months ago", "monthsV_plural": "__count__ months ago",
"yearsV": "__count__ year ago", "yearsV": "__count__ year ago",
"yearsV_plural": "__count__ years ago", "yearsV_plural": "__count__ years ago",
"yearMonthsV": "__y__ year, __count__ month ago", "yearMonthsV": "__y__ year, __count__ month ago",
"yearMonthsV_plural": "__y__ year, __count__ months ago", "yearMonthsV_plural": "__y__ year, __count__ months ago",
"yearsMonthsV": "__y__ years, __count__ month ago", "yearsMonthsV": "__y__ years, __count__ month ago",
@ -457,6 +458,16 @@
"filterAll":"all", "filterAll":"all",
"filtered": "__count__ hidden" "filtered": "__count__ hidden"
}, },
"context": {
"name":"Context Data",
"label":"context",
"none": "none selected",
"refresh": "refresh to load",
"empty": "empty",
"node": "Node",
"flow": "Flow",
"global": "Global"
},
"palette": { "palette": {
"name": "Palette management", "name": "Palette management",
"label": "palette" "label": "palette"
@ -477,15 +488,14 @@
"install": "install", "install": "install",
"removeFromProject": "remove from project", "removeFromProject": "remove from project",
"addToProject": "add to project", "addToProject": "add to project",
"none": "None",
"files": "Files", "files": "Files",
"flow": "Flow", "flow": "Flow",
"credentials": "Credentials", "credentials": "Credentials",
"invalidEncryptionKey": "Invalid encryption key", "invalidEncryptionKey": "Invalid encryption key",
"encryptionEnabled": "Encryption enabled", "encryptionEnabled": "Encryption enabled",
"encryptionDisabled": "Encryption disabled", "encryptionDisabled": "Encryption disabled",
"resetTheEncryptionKey": "Reset the encryption key:",
"setTheEncryptionKey": "Set the encryption key:", "setTheEncryptionKey": "Set the encryption key:",
"resetTheEncryptionKey": "Reset the encryption key:",
"changeTheEncryptionKey": "Change the encryption key:", "changeTheEncryptionKey": "Change the encryption key:",
"currentKey": "Current key", "currentKey": "Current key",
"newKey": "New key", "newKey": "New key",
@ -608,7 +618,9 @@
"bool": "boolean", "bool": "boolean",
"json": "JSON", "json": "JSON",
"bin": "buffer", "bin": "buffer",
"date": "timestamp" "date": "timestamp",
"jsonata": "expression",
"env": "env variable"
} }
}, },
"editableList": { "editableList": {
@ -637,6 +649,9 @@
"eval": "Error evaluating expression:\n __message__" "eval": "Error evaluating expression:\n __message__"
} }
}, },
"jsEditor": {
"title": "JavaScript editor"
},
"jsonEditor": { "jsonEditor": {
"title": "JSON editor", "title": "JSON editor",
"format": "format JSON" "format": "format JSON"
@ -699,7 +714,7 @@
"ssh-key-add": "Add an ssh key", "ssh-key-add": "Add an ssh key",
"credential-key": "Credentials encryption key", "credential-key": "Credentials encryption key",
"cant-get-ssh-key": "Error! Can't get selected SSH key path.", "cant-get-ssh-key": "Error! Can't get selected SSH key path.",
"already-exists": "already exists", "already-exists2": "already exists",
"git-error": "git error", "git-error": "git error",
"connection-failed": "Connection failed", "connection-failed": "Connection failed",
"not-git-repo": "Not a git repository", "not-git-repo": "Not a git repository",
@ -793,6 +808,7 @@
"username": "Username", "username": "Username",
"password": "Password", "password": "Password",
"passphrase": "Passphrase", "passphrase": "Passphrase",
"retry": "Retry",
"update-failed": "Failed to update auth", "update-failed": "Failed to update auth",
"unhandled": "Unhandled error response" "unhandled": "Unhandled error response"
}, },

View File

@ -189,11 +189,11 @@
"desc":"Returns an aggregated value derived from applying the `function` parameter successively to each value in `array` in combination with the result of the previous application of the function.\n\nThe function must accept two arguments, and behaves like an infix operator between each value within the `array`.\n\nThe optional `init` parameter is used as the initial value in the aggregation." "desc":"Returns an aggregated value derived from applying the `function` parameter successively to each value in `array` in combination with the result of the previous application of the function.\n\nThe function must accept two arguments, and behaves like an infix operator between each value within the `array`.\n\nThe optional `init` parameter is used as the initial value in the aggregation."
}, },
"$flowContext": { "$flowContext": {
"args": "string", "args": "string[, string]",
"desc": "Retrieves a flow context property.\n\nThis is a Node-RED defined function." "desc": "Retrieves a flow context property.\n\nThis is a Node-RED defined function."
}, },
"$globalContext": { "$globalContext": {
"args": "string", "args": "string[, string]",
"desc": "Retrieves a global context property.\n\nThis is a Node-RED defined function." "desc": "Retrieves a global context property.\n\nThis is a Node-RED defined function."
}, },
"$pad": { "$pad": {

View File

@ -98,10 +98,13 @@
"nodeActionDisabled": "ノードのアクションは、サブフロー内で無効になっています", "nodeActionDisabled": "ノードのアクションは、サブフロー内で無効になっています",
"missing-types": "不明なノードが存在するため、フローを停止しました。詳細はログを確認してください。", "missing-types": "不明なノードが存在するため、フローを停止しました。詳細はログを確認してください。",
"restartRequired": "更新されたモジュールを有効化するため、Node-REDを再起動する必要があります", "restartRequired": "更新されたモジュールを有効化するため、Node-REDを再起動する必要があります",
"credentials_load_failed": "<p>認証情報を復号できないため、フローを停止しました</p><p>フローの認証情報は暗号化されています。しかし、プロジェクトの暗号鍵が存在しない、または不正です</p>", "credentials_load_failed": "<p>認証情報を復号できないため、フローを停止しました</p><p>フローの認証情報ファイルは暗号化されています。しかし、プロジェクトの暗号鍵が存在しない、または不正です</p>",
"credentials_load_failed_reset": "<p>認証情報を復号できません</p><p>フローの認証情報ファイルは暗号化されています。しかし、プロジェクトの暗号鍵が存在しない、または不正です。</p><p>次回のデプロイでフローの認証情報ファイルがリセットされます。既存フローの認証情報は削除されます。</p>",
"missing_flow_file": "<p>プロジェクトのフローファイルが存在しません</p><p>本プロジェクトにフローファイルが登録されていません</p>", "missing_flow_file": "<p>プロジェクトのフローファイルが存在しません</p><p>本プロジェクトにフローファイルが登録されていません</p>",
"missing_package_file": "<p>プロジェクトのパッケージファイルが存在しません</p><p>本プロジェクトにはpackage.jsonファイルがありません</p>",
"project_empty": "<p>空のプロジェクトです</p><p>デフォルトのプロジェクトファイルを作成しますか?<br/>作成しない場合、エディタの外でファイルをプロジェクトへ手動で追加する必要があります</p>", "project_empty": "<p>空のプロジェクトです</p><p>デフォルトのプロジェクトファイルを作成しますか?<br/>作成しない場合、エディタの外でファイルをプロジェクトへ手動で追加する必要があります</p>",
"project_not_found": "<p>プロジェクト '__project__' が存在しません</p>" "project_not_found": "<p>プロジェクト '__project__' が存在しません</p>",
"git_merge_conflict": "<p>変更の自動マージが失敗しました</p><p>マージされていない競合を解決し、コミットしてください</p>"
}, },
"error": "<strong>エラー</strong>: __message__", "error": "<strong>エラー</strong>: __message__",
"errors": { "errors": {
@ -236,6 +239,7 @@
"output": "出力:", "output": "出力:",
"deleteSubflow": "サブフローを削除", "deleteSubflow": "サブフローを削除",
"info": "詳細", "info": "詳細",
"category": "カテゴリ",
"format": "マークダウン形式", "format": "マークダウン形式",
"errors": { "errors": {
"noNodesSelected": "<strong>サブフローを作成できません</strong>: ノードが選択されていません", "noNodesSelected": "<strong>サブフローを作成できません</strong>: ノードが選択されていません",
@ -260,6 +264,8 @@
"settingIcon": "アイコン", "settingIcon": "アイコン",
"noDefaultLabel": "なし", "noDefaultLabel": "なし",
"defaultLabel": "既定の名前を使用", "defaultLabel": "既定の名前を使用",
"searchIcons": "アイコンを検索",
"useDefault": "デフォルトを使用",
"errors": { "errors": {
"scopeChange": "スコープの変更は、他のフローで使われているノードを無効にします" "scopeChange": "スコープの変更は、他のフローで使われているノードを無効にします"
} }
@ -312,6 +318,7 @@
"noInfo": "情報がありません", "noInfo": "情報がありません",
"filter": "ノードを検索", "filter": "ノードを検索",
"search": "ノードを検索", "search": "ノードを検索",
"addCategory": "新規追加...",
"label": { "label": {
"subflows": "サブフロー", "subflows": "サブフロー",
"input": "入力", "input": "入力",
@ -449,6 +456,16 @@
"filterAll": "全て", "filterAll": "全て",
"filtered": "__count__ 個が無効" "filtered": "__count__ 個が無効"
}, },
"context": {
"name": "コンテキストデータ",
"label": "コンテキストデータ",
"none": "選択されていません",
"refresh": "読み込みのため更新してください",
"empty": "データが存在しません",
"node": "Node",
"flow": "Flow",
"global": "Global"
},
"palette": { "palette": {
"name": "パレットの管理", "name": "パレットの管理",
"label": "パレット" "label": "パレット"
@ -568,7 +585,6 @@
"pullUnrelatedHistory": "<p>リモートに関連のないコミット履歴があります。</p><p>本当に変更をプルしてローカルリポジトリに反映しますか?</p>", "pullUnrelatedHistory": "<p>リモートに関連のないコミット履歴があります。</p><p>本当に変更をプルしてローカルリポジトリに反映しますか?</p>",
"pullChanges": "プル変更", "pullChanges": "プル変更",
"history": "履歴", "history": "履歴",
"plural": "",
"daysAgo": "__count__ 日前", "daysAgo": "__count__ 日前",
"daysAgo_plural": "__count__ 日前", "daysAgo_plural": "__count__ 日前",
"hoursAgo": "__count__ 時間前", "hoursAgo": "__count__ 時間前",
@ -600,7 +616,9 @@
"bool": "真偽", "bool": "真偽",
"json": "JSON", "json": "JSON",
"bin": "バッファ", "bin": "バッファ",
"date": "日時" "date": "日時",
"jsonata": "JSONata式",
"env": "環境変数"
} }
}, },
"editableList": { "editableList": {
@ -629,6 +647,9 @@
"eval": "表現評価エラー:\n __message__" "eval": "表現評価エラー:\n __message__"
} }
}, },
"jsEditor": {
"title": "JavaScriptエディタ"
},
"jsonEditor": { "jsonEditor": {
"title": "JSONエディタ", "title": "JSONエディタ",
"format": "JSONフォーマット" "format": "JSONフォーマット"
@ -691,7 +712,7 @@
"ssh-key-add": "SSHキーの追加", "ssh-key-add": "SSHキーの追加",
"credential-key": "認証情報の暗号化キー", "credential-key": "認証情報の暗号化キー",
"cant-get-ssh-key": "エラー! 選択したSSHキーのパスを取得できません。", "cant-get-ssh-key": "エラー! 選択したSSHキーのパスを取得できません。",
"already-exists": "既に存在します", "already-exists2": "既に存在します",
"git-error": "Gitエラー", "git-error": "Gitエラー",
"connection-failed": "接続に失敗しました", "connection-failed": "接続に失敗しました",
"not-git-repo": "Gitリポジトリではありません", "not-git-repo": "Gitリポジトリではありません",
@ -785,6 +806,7 @@
"username": "ユーザ名", "username": "ユーザ名",
"password": "パスワード", "password": "パスワード",
"passphrase": "パスフレーズ", "passphrase": "パスフレーズ",
"retry": "リトライ",
"update-failed": "認証の更新に失敗しました", "update-failed": "認証の更新に失敗しました",
"unhandled": "エラー応答が処理されませんでした" "unhandled": "エラー応答が処理されませんでした"
}, },
@ -798,7 +820,7 @@
"no-empty": "デフォルトのファイル群を空でないプロジェクトに作成することはできません。", "no-empty": "デフォルトのファイル群を空でないプロジェクトに作成することはできません。",
"git-error": "Gitエラー" "git-error": "Gitエラー"
}, },
"errors" : { "errors": {
"no-username-email": "Gitクライアントのユーザ名/emailが設定されていません。", "no-username-email": "Gitクライアントのユーザ名/emailが設定されていません。",
"unexpected": "予期しないエラーが発生しました", "unexpected": "予期しないエラーが発生しました",
"code": "コード" "code": "コード"

View File

@ -214,5 +214,9 @@
"$toMillis": { "$toMillis": {
"args": "timestamp", "args": "timestamp",
"desc": "ISO 8601形式の文字列 `timestamp` を、Unixエポック(1 January, 1970 UTC)からの経過ミリ秒を表す数値へ変換します。 文字列が正しい形式でない場合、エラーとなります。" "desc": "ISO 8601形式の文字列 `timestamp` を、Unixエポック(1 January, 1970 UTC)からの経過ミリ秒を表す数値へ変換します。 文字列が正しい形式でない場合、エラーとなります。"
},
"$env": {
"args": "arg",
"desc": "環境変数の値を返します。\n\n本関数はNode-REDの定義関数です。"
} }
} }

156
red/runtime-api/context.js Normal file
View File

@ -0,0 +1,156 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* @namespace RED.context
*/
var runtime;
// TODO: move runtime/util to util/index
var util = require("../runtime/util");
function exportContextStore(scope,ctx, store, result, callback) {
ctx.keys(store,function(err, keys) {
if (err) {
return callback(err);
}
result[store] = {};
var c = keys.length;
if (c === 0) {
callback(null);
} else {
keys.forEach(function(key) {
ctx.get(key,store,function(err, v) {
if (err) {
return callback(err);
}
if (scope !== 'global' ||
store === runtime.nodes.listContextStores().default ||
!runtime.settings.hasOwnProperty("functionGlobalContext") ||
!runtime.settings.functionGlobalContext.hasOwnProperty(key) ||
runtime.settings.functionGlobalContext[key] !== v) {
result[store][key] = util.encodeObject({msg:v});
}
c--;
if (c === 0) {
callback(null);
}
});
});
}
});
}
var api = module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
/**
* Gets the info of an individual node set
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.scope - the scope of the context
* @param {String} opts.id - the id of the context
* @param {String} opts.store - the context store
* @param {String} opts.key - the context key
* @return {Promise} - the node information
* @memberof RED.nodes
*/
getValue: function(opts) {
return new Promise(function(resolve,reject) {
var scope = opts.scope;
var id = opts.id;
var store = opts.store;
var key = opts.key;
var availableStores = runtime.nodes.listContextStores();
//{ default: 'default', stores: [ 'default', 'file' ] }
if (store && availableStores.stores.indexOf(store) === -1) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"not_found"});
var err = new Error();
err.code = "not_found";
err.status = 404;
return reject(err);
}
var ctx;
if (scope === 'global') {
ctx = runtime.nodes.getContext('global');
} else if (scope === 'flow') {
ctx = runtime.nodes.getContext(id);
} else if (scope === 'node') {
var node = runtime.nodes.getNode(id);
if (node) {
ctx = node.context();
}
}
if (ctx) {
if (key) {
store = store || availableStores.default;
ctx.get(key,store,function(err, v) {
var encoded = util.encodeObject({msg:v});
if (store !== availableStores.default) {
encoded.store = store;
}
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key});
resolve(encoded);
});
return;
} else {
var stores;
if (!store) {
stores = availableStores.stores;
} else {
stores = [store];
}
var result = {};
var c = stores.length;
var errorReported = false;
stores.forEach(function(store) {
exportContextStore(scope,ctx,store,result,function(err) {
if (err) {
// TODO: proper error reporting
if (!errorReported) {
errorReported = true;
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key,error:"unexpected_error"});
var err = new Error();
err.code = "unexpected_error";
err.status = 400;
return reject(err);
}
return;
}
c--;
if (c === 0) {
if (!errorReported) {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key});
resolve(result);
}
}
});
})
}
} else {
runtime.log.audit({event: "context.get",scope:scope,id:id,store:store,key:key});
resolve({});
}
})
}
}

View File

@ -33,6 +33,7 @@ var api = module.exports = {
api.settings.init(runtime); api.settings.init(runtime);
api.library.init(runtime); api.library.init(runtime);
api.projects.init(runtime); api.projects.init(runtime);
api.context.init(runtime);
}, },
comms: require("./comms"), comms: require("./comms"),
@ -41,6 +42,7 @@ var api = module.exports = {
nodes: require("./nodes"), nodes: require("./nodes"),
settings: require("./settings"), settings: require("./settings"),
projects: require("./projects"), projects: require("./projects"),
context: require("./context"),
/** /**
* Returns whether the runtime is started * Returns whether the runtime is started

View File

@ -361,19 +361,19 @@ var api = module.exports = {
return new Promise(function(resolve,reject) { return new Promise(function(resolve,reject) {
var namespace = opts.module; var namespace = opts.module;
var lang = opts.lang; var lang = opts.lang;
var prevLang = runtime.i18n.i.lng(); var prevLang = runtime.i18n.i.language;
// Trigger a load from disk of the language if it is not the default // Trigger a load from disk of the language if it is not the default
runtime.i18n.i.setLng(lang, function(){ runtime.i18n.i.changeLanguage(lang, function(){
var nodeList = runtime.nodes.getNodeList(); var nodeList = runtime.nodes.getNodeList();
var result = {}; var result = {};
nodeList.forEach(function(n) { nodeList.forEach(function(n) {
if (n.module !== "node-red") { if (n.module !== "node-red") {
result[n.id] = runtime.i18n.catalog(n.id,lang)||{}; result[n.id] = runtime.i18n.i.getResourceBundle(lang, n.id)||{};
} }
}); });
resolve(result); resolve(result);
}); });
runtime.i18n.i.setLng(prevLang); runtime.i18n.i.changeLanguage(prevLang);
}); });
}, },
@ -392,11 +392,11 @@ var api = module.exports = {
var lang = opts.lang; var lang = opts.lang;
var prevLang = runtime.i18n.i.lng(); var prevLang = runtime.i18n.i.lng();
// Trigger a load from disk of the language if it is not the default // Trigger a load from disk of the language if it is not the default
runtime.i18n.i.setLng(lang, function(){ runtime.i18n.i.changeLanguage(lang, function(){
var catalog = runtime.i18n.catalog(namespace,lang); var catalog = runtime.i18n.getResourceBundle(lang, namespace);
resolve(catalog||{}); resolve(catalog||{});
}); });
runtime.i18n.i.setLng(prevLang); runtime.i18n.i.changeLanguage(prevLang);
}); });
}, },

View File

@ -79,6 +79,9 @@ var api = module.exports = {
} }
}) })
} }
safeSettings.context = runtime.nodes.listContextStores();
if (util.isArray(runtime.settings.paletteCategories)) { if (util.isArray(runtime.settings.paletteCategories)) {
safeSettings.paletteCategories = runtime.settings.paletteCategories; safeSettings.paletteCategories = runtime.settings.paletteCategories;
} }

View File

@ -185,7 +185,7 @@ function addModule(module) {
if (!set.err) { if (!set.err) {
set.types.forEach(function(t) { set.types.forEach(function(t) {
if (nodeTypeToId.hasOwnProperty(t)) { if (nodeTypeToId.hasOwnProperty(t)) {
set.err = new Error("Type already registered"); set.err = "Type already registered";
set.err.code = "type_already_registered"; set.err.code = "type_already_registered";
set.err.details = { set.err.details = {
type: t, type: t,

View File

@ -88,6 +88,7 @@ function getVersion() {
} }
function start() { function start() {
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"locales")),"runtime.json") return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"locales")),"runtime.json")
.then(function() { return storage.init(runtime)}) .then(function() { return storage.init(runtime)})
.then(function() { return settings.load(storage)}) .then(function() { return settings.load(storage)})
@ -163,8 +164,10 @@ function start() {
if (settings.httpStatic) { if (settings.httpStatic) {
log.info(log._("runtime.paths.httpStatic",{path:path.resolve(settings.httpStatic)})); log.info(log._("runtime.paths.httpStatic",{path:path.resolve(settings.httpStatic)}));
} }
redNodes.loadContextsPlugin().then(function () {
redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {}); redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {});
started = true; started = true;
});
}).catch(function(err) { }).catch(function(err) {
console.log(err); console.log(err);
}); });
@ -230,7 +233,9 @@ function stop() {
clearTimeout(reinstallTimeout); clearTimeout(reinstallTimeout);
} }
started = false; started = false;
return redNodes.stopFlows(); return redNodes.stopFlows().then(function(){
return redNodes.closeContextsPlugin();
});
} }
var runtime = module.exports = { var runtime = module.exports = {

View File

@ -155,5 +155,15 @@
"readme": "### About\n\nThis is your project's README.md file. It helps users understand what your\nproject does, how to use it and anything else they may need to know." "readme": "### About\n\nThis is your project's README.md file. It helps users understand what your\nproject does, how to use it and anything else they may need to know."
} }
} }
},
"context": {
"log-store-init": "Context store : '__name__' [__info__]",
"error-loading-module": "Error loading context store '__module__': __message__ ",
"error-module-not-defined": "Context store '__storage__' missing 'module' option",
"error-invalid-module-name": "Invalid context store name: '__name__'",
"error-invalid-default-module": "Default context store unknown: '__storage__'",
"unknown-store": "Unknown context store '__name__' specified. Using default store."
} }
} }

View File

@ -1,10 +1,169 @@
{ {
"storage": { "runtime": {
"welcome": "Welcome to Node-RED",
"version": "__component__ バージョン: __version__",
"unsupported_version": "__component__ は未サポートのバージョンです。必要: __requires__ 検知: __version__",
"paths": {
"settings": "設定ファイル: __path__",
"httpStatic": "HTTP Static : __path__"
}
},
"server": {
"loading": "パレットノードのロード",
"palette-editor": {
"disabled": "パレットエディタを無効化 : ユーザ設定",
"npm-not-found": "バレットエディタを無効化 : npmコマンドが見つかりません"
},
"errors": "__count__ 個のノードの登録に失敗しました",
"errors_plural": "__count__ 個のノードの登録に失敗しました",
"errors-help": "詳細は -v を指定して実行してください",
"missing-modules": "不足しているノードモジュール:",
"node-version-mismatch": "ノードモジュールはこのバージョンではロードできません。必要なバージョン: __version__ ",
"type-already-registered": "'__type__' はモジュール __module__ で登録済みです",
"removing-modules": "設定からモジュールを削除します",
"added-types": "追加したノード:",
"removed-types": "削除したノード:",
"install": {
"invalid": "不正なモジュール名",
"installing": "モジュール__name__, バージョン: __version__をインスートールします",
"installed": "モジュール __name__ をインストールしました",
"install-failed": "インストールに失敗しました",
"install-failed-long": "モジュール __name__ のインストールに失敗しました:",
"install-failed-not-found": "$t(install-failed-long) モジュールが見つかりません",
"upgrading": "モジュール __name__ をバージョン __version__ に更新します",
"upgraded": "モジュール __name__ を更新しました。新しいバージョンを使うには、Node-REDを再起動してください。",
"upgrade-failed-not-found": "$t(server.install.install-failed-long) バージョンが見つかりません",
"install-failed-not-found": "$t(server.install.install-failed-long) モジュールが見つかりません",
"uninstalling": "モジュールをアンインストールします: __name__",
"uninstall-failed": "アンインストールに失敗しました",
"uninstall-failed-long": "モジュール __name__ のアンインストールに失敗しました:",
"uninstalled": "モジュール __name__ をアンインストールしました"
},
"unable-to-listen": "__listenpath__ に対してlistenできません",
"port-in-use": "エラー: ポートが使用中です",
"uncaught-exception": "未補足の例外:",
"admin-ui-disabled": "管理UIを無効化しました",
"now-running": "サーバは __listenpath__ で実行中です",
"failed-to-start": "サーバの起動に失敗しました:",
"headless-mode": "ヘッドレスモードで実行中です",
"httpadminauth-deprecated": "httpAdminAuthは非推奨です。代わりに adminAuth を使用してください"
},
"api": {
"flows": {
"error-save": "フローの保存エラー: __message__",
"error-reload": "フローの読み込みエラー: __message__"
},
"library": {
"error-load-entry": "ライブラリエントリ '__path__' の読み込みエラー: __message__",
"error-save-entry": "ライブラリエントリ '__path__' の保存エラー: __message__",
"error-load-flow": "フロー '__path__' の読み込みエラー: __message__",
"error-save-flow": "フロー '__path__' の保存エラー: __message__"
},
"nodes": {
"enabled": "ノードを有効化しました:",
"disabled": "ノードを無効化しました:",
"error-enable": "ノードの有効化に失敗しました:"
}
},
"comms": {
"error": "通信チャネルエラー: __message__",
"error-server": "サーバエラー: __message__",
"error-send": "送信エラー: __message__"
},
"settings": {
"user-not-available": "ユーザ設定を保存できません: __message__",
"not-available": "設定が利用できません",
"property-read-only": "プロパティ '__prop__' は読み出し専用です"
},
"nodes": {
"credentials": {
"error":"クレデンシャルの読み込みエラー: __message__",
"error-saving":"クレデンシャルの保存エラー: __message__",
"not-registered": "クレデンシャル '__type__' は登録されていません",
"system-key-warning": "\n\n---------------------------------------------------------------------\nフローのクレデンシャルファイルはシステム生成キーで暗号化されています。\n\nシステム生成キーを何らかの理由で失った場合、クレデンシャルファイルを\n復元することはできません。その場合、ファイルを削除してクレデンシャルを\n再入力しなければなりません。\n\n設定ファイル内で 'credentialSecret' オプションを使って独自キーを設定\nします。変更を次にデプロイする際、Node-REDは選択したキーを用いてクレ\nデンシャルを再暗号化します。 \n\n---------------------------------------------------------------------\n"
},
"flows": {
"registered-missing": "欠落しているノードを登録します: __type__",
"error": "フローの読み込みエラー: __message__",
"starting-modified-nodes": "更新されたノードを開始します",
"starting-modified-flows": "更新されたフローを開始します",
"starting-flows": "フローを開始します",
"started-modified-nodes": "更新されたノードを開始しました",
"started-modified-flows": "更新されたフローを開始しました",
"started-flows": "フローを開始しました",
"stopping-modified-nodes": "更新されたノードを停止します",
"stopping-modified-flows": "更新されたフローを停止します",
"stopping-flows": "フローを停止します",
"stopped-modified-nodes": "更新されたノードを停止しました",
"stopped-modified-flows": "更新されたフローを停止しました",
"stopped-flows": "フローを停止しました",
"stopped": "停止しました",
"stopping-error": "ノードの停止に失敗しました: __message__",
"added-flow": "フローを追加します: __label__",
"updated-flow": "フローを更新しました: __label__",
"removed-flow": "フローを削除しました: __label__",
"missing-types": "欠落しているノードが登録されるのを待っています:",
"missing-type-provided": " - __type__ (npmモジュール __module__ からインストールされました)",
"missing-type-install-1": "欠落しているモジュールをインストールするには次のコマンドを実行してください:",
"missing-type-install-2": "コマンドの実行は次のディレクトリで行います:"
},
"flow": {
"unknown-type": "不明なノード: __type__",
"missing-types": "欠落したノード",
"error-loop": "メッセージの例外補足回数が最大値を超えました"
},
"index": {
"unrecognised-id": "不明なID: __id__",
"type-in-use": "ノードは使用中です: __msg__",
"unrecognised-module": "不明なモジュール: __module__"
},
"registry": {
"localfilesystem": { "localfilesystem": {
"module-not-found": "モジュール '__module__' が見つかりません"
}
}
},
"storage": {
"index": {
"forbidden-flow-name": "不正なフロー名"
},
"localfilesystem": {
"user-dir": "ユーザディレクトリ : __path__",
"flows-file": "フローファイル : __path__",
"create": "__type__ ファイルを作成します",
"empty": "既存の __type__ ファイルが空です",
"invalid": "既存の __type__ ファイルはJSON形式ではありません",
"restore": " __type__ ファイルをバックアップ __path__ から復元します",
"restore-fail": "__type__ ファイルをバックアップから復元するのに失敗しました : __message__",
"fsync-fail": "ファイル __path__ のディスクへの書き出しに失敗しました : __message__",
"projects": { "projects": {
"changing-project": "プロジェクトを設定します : __project__",
"active-project": "選択中のプロジェクト : __project__",
"project-not-found": "プロジェクトが見つかりません : __project__",
"no-active-project": "プロジェクトが選択されていません : デフォルトのフローファイルを使用します",
"disabled": "プロジェクトは無効化されています : editorTheme.projects.enabled=false",
"disabledNoFlag": "プロジェクトは無効化されています : 有効にするには editorTheme.projects.enabled=true を設定してください",
"git-not-found": "プロジェクトは無効化されています : gitコマンドが見つかりません",
"git-version-old": "プロジェクトは無効化されています : git __version__ はサポートされていません。2.xが必要です。",
"summary": "Node-REDプロジェクト", "summary": "Node-REDプロジェクト",
"readme": "### 説明\nこれはプロジェクトのREADME.mdファイルです。このファイルには、\nプロジェクトの説明、利用方法、その他の情報を記載します。" "readme": "### 説明\nこれはプロジェクトのREADME.mdファイルです。このファイルには、\nプロジェクトの説明、利用方法、その他の情報を記載します。"
} }
} }
},
"context": {
"log-store-init": "コンテクストストア : '__name__' [__info__]",
"error-loading-module": "コンテクストストア '__module__' のロードでエラーが発生しました: __message__ ",
"error-module-not-defined": "コンテクストストア '__storage__' に 'module' オプションが指定されていません",
"error-invalid-module-name": "不正なコンテクストストア名: '__name__'",
"error-invalid-default-module": "デフォルトコンテクストストアが不明: '__storage__'",
"unknown-store": "不明なコンテクストストア '__name__' が指定されました。デフォルトストアを使用します。"
} }
} }

View File

@ -105,12 +105,12 @@ Node.prototype.close = function(removed) {
if (promises.length > 0) { if (promises.length > 0) {
return when.settle(promises).then(function() { return when.settle(promises).then(function() {
if (this._context) { if (this._context) {
context.delete(this._alias||this.id,this.z); return context.delete(this._alias||this.id,this.z);
} }
}); });
} else { } else {
if (this._context) { if (this._context) {
context.delete(this._alias||this.id,this.z); return context.delete(this._alias||this.id,this.z);
} }
return; return;
} }

View File

@ -1,91 +0,0 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var clone = require("clone");
var when = require("when");
var util = require("../util");
function createContext(id,seed) {
var data = seed || {};
var obj = seed || {};
obj.get = function get(key) {
return util.getMessageProperty(data,key);
};
obj.set = function set(key, value) {
util.setMessageProperty(data,key,value);
}
obj.keys = function() {
var keysData = Object.keys(data);
if (seed == null) {
return keysData;
} else {
return keysData.filter(function (key) {
return key !== "set" && key !== "get" && key !== "keys";
});
}
}
return obj;
}
var contexts = {};
var globalContext = null;
function getContext(localId,flowId) {
var contextId = localId;
if (flowId) {
contextId = localId+":"+flowId;
}
if (contexts.hasOwnProperty(contextId)) {
return contexts[contextId];
}
var newContext = createContext(contextId);
if (flowId) {
newContext.flow = getContext(flowId);
}
if (globalContext) {
newContext.global = globalContext;
}
contexts[contextId] = newContext;
return newContext;
}
function deleteContext(id,flowId) {
var contextId = id;
if (flowId) {
contextId = id+":"+flowId;
}
delete contexts[contextId];
}
function clean(flowConfig) {
var activeIds = {};
var contextId;
var node;
for (var id in contexts) {
if (contexts.hasOwnProperty(id)) {
var idParts = id.split(":");
if (!flowConfig.allNodes.hasOwnProperty(idParts[0])) {
delete contexts[id];
}
}
}
}
module.exports = {
init: function(settings) {
globalContext = createContext("global",settings.functionGlobalContext || {});
},
get: getContext,
delete: deleteContext,
clean:clean
};

View File

@ -0,0 +1,374 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var clone = require("clone");
var log = require("../../../util").log; // TODO: separate module
var memory = require("./memory");
var settings;
// A map of scope id to context instance
var contexts = {};
// A map of store name to instance
var stores = {};
var storeList = [];
var defaultStore;
// Whether there context storage has been configured or left as default
var hasConfiguredStore = false;
// Unknown Stores
var unknownStores = {};
function logUnknownStore(name) {
if (name) {
var count = unknownStores[name] || 0;
if (count == 0) {
log.warn(log._("context.unknown-store", {name: name}));
count++;
unknownStores[name] = count;
}
}
}
function init(_settings) {
settings = _settings;
contexts = {};
stores = {};
storeList = [];
hasConfiguredStore = false;
var seed = settings.functionGlobalContext || {};
contexts['global'] = createContext("global",seed);
// create a default memory store - used by the unit tests that skip the full
// `load()` initialisation sequence.
// If the user has any stores configured, this will be disgarded
stores["_"] = new memory();
defaultStore = "memory";
}
function load() {
return new Promise(function(resolve,reject) {
// load & init plugins in settings.contextStorage
var plugins = settings.contextStorage || {};
var defaultIsAlias = false;
var promises = [];
if (plugins && Object.keys(plugins).length > 0) {
var hasDefault = plugins.hasOwnProperty('default');
var defaultName;
for (var pluginName in plugins) {
if (plugins.hasOwnProperty(pluginName)) {
// "_" is a reserved name - do not allow it to be overridden
if (pluginName === "_") {
continue;
}
if (!/^[a-zA-Z0-9_]+$/.test(pluginName)) {
return reject(new Error(log._("context.error-invalid-module-name", {name:pluginName})));
}
// Check if this is setting the 'default' context to be a named plugin
if (pluginName === "default" && typeof plugins[pluginName] === "string") {
// Check the 'default' alias exists before initialising anything
if (!plugins.hasOwnProperty(plugins[pluginName])) {
return reject(new Error(log._("context.error-invalid-default-module", {storage:plugins["default"]})));
}
defaultIsAlias = true;
continue;
}
if (!hasDefault && !defaultName) {
defaultName = pluginName;
}
var plugin;
if (plugins[pluginName].hasOwnProperty("module")) {
// Get the provided config and copy in the 'approved' top-level settings (eg userDir)
var config = plugins[pluginName].config || {};
copySettings(config, settings);
if (typeof plugins[pluginName].module === "string") {
// This config identifies the module by name - assume it is a built-in one
// TODO: check it exists locally, if not, try to require it as-is
try {
plugin = require("./"+plugins[pluginName].module);
} catch(err) {
return reject(new Error(log._("context.error-loading-module", {module:plugins[pluginName].module,message:err.toString()})));
}
} else {
// Assume `module` is an already-required module we can use
plugin = plugins[pluginName].module;
}
try {
// Create a new instance of the plugin by calling its module function
stores[pluginName] = plugin(config);
log.info(log._("context.log-store-init", {name:pluginName, info:"module="+plugins[pluginName].module}));
} catch(err) {
return reject(new Error(log._("context.error-loading-module",{module:pluginName,message:err.toString()})));
}
} else {
// Plugin does not specify a 'module'
return reject(new Error(log._("context.error-module-not-defined", {storage:pluginName})));
}
}
}
// Open all of the configured contexts
for (var plugin in stores) {
if (stores.hasOwnProperty(plugin)) {
promises.push(stores[plugin].open());
}
}
// There is a 'default' listed in the configuration
if (hasDefault) {
// If 'default' is an alias, point it at the right module - we have already
// checked that it exists. If it isn't an alias, then it will
// already be set to a configured store
if (defaultIsAlias) {
stores["_"] = stores[plugins["default"]];
defaultStore = plugins["default"];
} else {
stores["_"] = stores["default"];
defaultStore = "default";
}
} else if (defaultName) {
// No 'default' listed, so pick first in list as the default
stores["_"] = stores[defaultName];
defaultStore = defaultName;
defaultIsAlias = true;
} else {
// else there were no stores list the config object - fall through
// to below where we default to a memory store
storeList = ["memory"];
defaultStore = "memory";
}
hasConfiguredStore = true;
storeList = Object.keys(stores).filter(n=>!(defaultIsAlias && n==="default") && n!== "_");
} else {
// No configured plugins
log.info(log._("context.log-store-init", {name:"default", info:"module=memory"}));
promises.push(stores["_"].open())
storeList = ["memory"];
defaultStore = "memory";
}
return resolve(Promise.all(promises));
});
}
function copySettings(config, settings){
var copy = ["userDir"]
config.settings = {};
copy.forEach(function(setting){
config.settings[setting] = clone(settings[setting]);
});
}
function getContextStorage(storage) {
if (stores.hasOwnProperty(storage)) {
// A known context
return stores[storage];
} else if (stores.hasOwnProperty("_")) {
// Not known, but we have a default to fall back to
if (storage !== defaultStore) {
// It isn't the default store either, so log it
logUnknownStore(storage);
}
return stores["_"];
}
}
function createContext(id,seed) {
// Seed is only set for global context - sourced from functionGlobalContext
var scope = id;
var obj = seed || {};
var seedKeys;
var insertSeedValues;
if (seed) {
seedKeys = Object.keys(seed);
insertSeedValues = function(keys,values) {
if (!Array.isArray(keys)) {
if (values[0] === undefined) {
values[0] = seed[keys];
}
} else {
for (var i=0;i<keys.length;i++) {
if (values[i] === undefined) {
values[i] = seed[keys[i]];
}
}
}
}
}
obj.get = function(key, storage, callback) {
var context;
if (!storage && !callback) {
context = stores["_"];
} else {
if (typeof storage === 'function') {
callback = storage;
storage = "_";
}
if (callback && typeof callback !== 'function'){
throw new Error("Callback must be a function");
}
context = getContextStorage(storage);
}
if (callback) {
if (!seed) {
context.get(scope,key,callback);
} else {
context.get(scope,key,function() {
if (arguments[0]) {
callback(arguments[0]);
return;
}
var results = Array.prototype.slice.call(arguments,[1]);
insertSeedValues(key,results);
// Put the err arg back
results.unshift(undefined);
callback.apply(null,results);
});
}
} else {
// No callback, attempt to do this synchronously
var results = context.get(scope,key);
if (seed) {
if (Array.isArray(key)) {
insertSeedValues(key,results);
} else if (results === undefined){
results = seed[key];
}
}
return results;
}
};
obj.set = function(key, value, storage, callback) {
var context;
if (!storage && !callback) {
context = stores["_"];
} else {
if (typeof storage === 'function') {
callback = storage;
storage = "_";
}
if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
}
context = getContextStorage(storage);
}
context.set(scope, key, value, callback);
};
obj.keys = function(storage, callback) {
var context;
if (!storage && !callback) {
context = stores["_"];
} else {
if (typeof storage === 'function') {
callback = storage;
storage = "_";
}
if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
}
context = getContextStorage(storage);
}
if (seed) {
if (callback) {
context.keys(scope, function(err,keys) {
callback(err,Array.from(new Set(seedKeys.concat(keys)).keys()));
});
} else {
var keys = context.keys(scope);
return Array.from(new Set(seedKeys.concat(keys)).keys())
}
} else {
return context.keys(scope, callback);
}
};
return obj;
}
function getContext(localId,flowId) {
var contextId = localId;
if (flowId) {
contextId = localId+":"+flowId;
}
if (contexts.hasOwnProperty(contextId)) {
return contexts[contextId];
}
var newContext = createContext(contextId);
if (flowId) {
newContext.flow = getContext(flowId);
}
newContext.global = contexts['global'];
contexts[contextId] = newContext;
return newContext;
}
function deleteContext(id,flowId) {
if(!hasConfiguredStore){
// only delete context if there's no configured storage.
var contextId = id;
if (flowId) {
contextId = id+":"+flowId;
}
delete contexts[contextId];
return stores["_"].delete(contextId);
}else{
return Promise.resolve();
}
}
function clean(flowConfig) {
var promises = [];
for(var plugin in stores){
if(stores.hasOwnProperty(plugin)){
promises.push(stores[plugin].clean(Object.keys(flowConfig.allNodes)));
}
}
for (var id in contexts) {
if (contexts.hasOwnProperty(id) && id !== "global") {
var idParts = id.split(":");
if (!flowConfig.allNodes.hasOwnProperty(idParts[0])) {
delete contexts[id];
}
}
}
return Promise.all(promises);
}
function close() {
var promises = [];
for(var plugin in stores){
if(stores.hasOwnProperty(plugin)){
promises.push(stores[plugin].close());
}
}
return Promise.all(promises);
}
function listStores() {
return {default:defaultStore,stores:storeList};
}
module.exports = {
init: init,
load: load,
listStores: listStores,
get: getContext,
delete: deleteContext,
clean: clean,
close: close
};

View File

@ -0,0 +1,291 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* Local file-system based context storage
*
* Configuration options:
* {
* base: "contexts", // the base directory to use
* // default: "contexts"
* dir: "/path/to/storage", // the directory to create the base directory in
* // default: settings.userDir
* cache: true // whether to cache contents in memory
* // default: true
* }
*
*
* $HOME/.node-red/contexts
* global
* global_context.json
* <id of Flow 1>
* flow_context.json
* <id of Node a>.json
* <id of Node b>.json
* <id of Flow 2>
* flow_context.json
* <id of Node x>.json
* <id of Node y>.json
*/
var fs = require('fs-extra');
var path = require("path");
var util = require("../../util");
var MemoryStore = require("./memory");
function getStoragePath(storageBaseDir, scope) {
if(scope.indexOf(":") === -1){
if(scope === "global"){
return path.join(storageBaseDir,"global",scope);
}else{ // scope:flow
return path.join(storageBaseDir,scope,"flow");
}
}else{ // scope:local
var ids = scope.split(":")
return path.join(storageBaseDir,ids[1],ids[0]);
}
}
function getBasePath(config) {
var base = config.base || "contexts";
var storageBaseDir;
if (!config.dir) {
if(config.settings && config.settings.userDir){
storageBaseDir = path.join(config.settings.userDir, base);
}else{
try {
fs.statSync(path.join(process.env.NODE_RED_HOME,".config.json"));
storageBaseDir = path.join(process.env.NODE_RED_HOME, base);
} catch(err) {
try {
// Consider compatibility for older versions
if (process.env.HOMEPATH) {
fs.statSync(path.join(process.env.HOMEPATH,".node-red",".config.json"));
storageBaseDir = path.join(process.env.HOMEPATH, ".node-red", base);
}
} catch(err) {
}
if (!storageBaseDir) {
storageBaseDir = path.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || process.env.NODE_RED_HOME,".node-red", base);
}
}
}
}else{
storageBaseDir = path.join(config.dir, base);
}
return storageBaseDir;
}
function loadFile(storagePath){
return fs.pathExists(storagePath).then(function(exists){
if(exists === true){
return fs.readFile(storagePath, "utf8");
}else{
return Promise.resolve(undefined);
}
}).catch(function(err){
throw Promise.reject(err);
});
}
function LocalFileSystem(config){
this.config = config;
this.storageBaseDir = getBasePath(this.config);
if (config.hasOwnProperty('cache')?config.cache:true) {
this.cache = MemoryStore({});
}
}
LocalFileSystem.prototype.open = function(){
var self = this;
if (this.cache) {
var scopes = [];
var promises = [];
var subdirs = [];
var subdirPromises = [];
return fs.readdir(self.storageBaseDir).then(function(dirs){
dirs.forEach(function(fn) {
var p = getStoragePath(self.storageBaseDir ,fn)+".json";
scopes.push(fn);
promises.push(loadFile(p));
subdirs.push(path.join(self.storageBaseDir,fn));
subdirPromises.push(fs.readdir(path.join(self.storageBaseDir,fn)));
})
return Promise.all(subdirPromises);
}).then(function(dirs) {
dirs.forEach(function(files,i) {
files.forEach(function(fn) {
if (fn !== 'flow.json' && fn !== 'global.json') {
scopes.push(fn.substring(0,fn.length-5)+":"+scopes[i]);
promises.push(loadFile(path.join(subdirs[i],fn)))
}
});
})
return Promise.all(promises);
}).then(function(res) {
scopes.forEach(function(scope,i) {
var data = res[i]?JSON.parse(res[i]):{};
Object.keys(data).forEach(function(key) {
self.cache.set(scope,key,data[key]);
})
});
}).catch(function(err){
if(err.code == 'ENOENT') {
return fs.ensureDir(self.storageBaseDir);
}else{
return Promise.reject(err);
}
});
} else {
return Promise.resolve();
}
}
LocalFileSystem.prototype.close = function(){
return Promise.resolve();
}
LocalFileSystem.prototype.get = function(scope, key, callback) {
if (this.cache) {
return this.cache.get(scope,key,callback);
}
if(typeof callback !== "function"){
throw new Error("Callback must be a function");
}
var storagePath = getStoragePath(this.storageBaseDir ,scope);
loadFile(storagePath + ".json").then(function(data){
if(data){
data = JSON.parse(data);
if (!Array.isArray(key)) {
callback(null, util.getObjectProperty(data,key));
} else {
var results = [undefined];
for (var i=0;i<key.length;i++) {
results.push(util.getObjectProperty(data,key[i]))
}
callback.apply(null,results);
}
}else{
callback(null, undefined);
}
}).catch(function(err){
callback(err);
});
};
LocalFileSystem.prototype.set = function(scope, key, value, callback) {
var storagePath = getStoragePath(this.storageBaseDir ,scope);
if (this.cache) {
this.cache.set(scope,key,value,callback);
// With cache enabled, no need to re-read the file prior to writing.
var newContext = this.cache._export()[scope];
fs.outputFile(storagePath + ".json", JSON.stringify(newContext, undefined, 4), "utf8").catch(function(err) {
});
} else if (callback && typeof callback !== 'function') {
throw new Error("Callback must be a function");
} else {
loadFile(storagePath + ".json").then(function(data){
var obj = data ? JSON.parse(data) : {}
if (!Array.isArray(key)) {
key = [key];
value = [value];
} else if (!Array.isArray(value)) {
// key is an array, but value is not - wrap it as an array
value = [value];
}
for (var i=0;i<key.length;i++) {
var v = null;
if (i<value.length) {
v = value[i];
}
util.setObjectProperty(obj,key[i],v);
}
return fs.outputFile(storagePath + ".json", JSON.stringify(obj, undefined, 4), "utf8");
}).then(function(){
if(typeof callback === "function"){
callback(null);
}
}).catch(function(err){
if(typeof callback === "function"){
callback(err);
}
});
}
};
LocalFileSystem.prototype.keys = function(scope, callback){
if (this.cache) {
return this.cache.keys(scope,callback);
}
if(typeof callback !== "function"){
throw new Error("Callback must be a function");
}
var storagePath = getStoragePath(this.storageBaseDir ,scope);
loadFile(storagePath + ".json").then(function(data){
if(data){
callback(null, Object.keys(JSON.parse(data)));
}else{
callback(null, []);
}
}).catch(function(err){
callback(err);
});
};
LocalFileSystem.prototype.delete = function(scope){
var cachePromise;
if (this.cache) {
cachePromise = this.cache.delete(scope);
} else {
cachePromise = Promise.resolve();
}
var that = this;
return cachePromise.then(function() {
var storagePath = getStoragePath(that.storageBaseDir,scope);
return fs.remove(storagePath + ".json");
});
}
LocalFileSystem.prototype.clean = function(activeNodes){
var self = this;
var cachePromise;
if (this.cache) {
cachePromise = this.cache.clean(activeNodes);
} else {
cachePromise = Promise.resolve();
}
return cachePromise.then(function() {
return fs.readdir(self.storageBaseDir).then(function(dirs){
return Promise.all(dirs.reduce(function(result, item){
if(item !== "global" && activeNodes.indexOf(item) === -1){
result.push(fs.remove(path.join(self.storageBaseDir,item)));
}
return result;
},[]));
}).catch(function(err){
if(err.code == 'ENOENT') {
return Promise.resolve();
}else{
return Promise.reject(err);
}
});
});
}
module.exports = function(config){
return new LocalFileSystem(config);
};

View File

@ -0,0 +1,166 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var util = require("../../util");
function Memory(config){
this.data = {};
}
Memory.prototype.open = function(){
return Promise.resolve();
};
Memory.prototype.close = function(){
return Promise.resolve();
};
Memory.prototype._getOne = function(scope, key) {
var value;
var error;
if(this.data[scope]){
value = util.getObjectProperty(this.data[scope], key);
}
return value;
}
Memory.prototype.get = function(scope, key, callback) {
var value;
var error;
if (!Array.isArray(key)) {
try {
value = this._getOne(scope,key);
} catch(err) {
if (!callback) {
throw err;
}
error = err;
}
if (callback) {
callback(error,value);
return;
} else {
return value;
}
}
value = [];
for (var i=0; i<key.length; i++) {
try {
value.push(this._getOne(scope,key[i]));
} catch(err) {
if (!callback) {
throw err;
} else {
callback(err);
return;
}
}
}
if (callback) {
callback.apply(null, [undefined].concat(value));
} else {
return value;
}
};
Memory.prototype.set = function(scope, key, value, callback) {
if(!this.data[scope]){
this.data[scope] = {};
}
var error;
if (!Array.isArray(key)) {
key = [key];
value = [value];
} else if (!Array.isArray(value)) {
// key is an array, but value is not - wrap it as an array
value = [value];
}
try {
for (var i=0; i<key.length; i++) {
var v = null;
if (i < value.length) {
v = value[i];
}
util.setObjectProperty(this.data[scope],key[i],v);
}
} catch(err) {
if (callback) {
error = err;
} else {
throw err;
}
}
if(callback){
callback(error);
}
};
Memory.prototype.keys = function(scope, callback){
var values = [];
var error;
try{
if(this.data[scope]){
if (scope !== "global") {
values = Object.keys(this.data[scope]);
} else {
values = Object.keys(this.data[scope]).filter(function (key) {
return key !== "set" && key !== "get" && key !== "keys";
});
}
}
}catch(err){
if(callback){
error = err;
}else{
throw err;
}
}
if(callback){
if(error){
callback(error);
} else {
callback(null, values);
}
} else {
return values;
}
};
Memory.prototype.delete = function(scope){
delete this.data[scope];
return Promise.resolve();
};
Memory.prototype.clean = function(activeNodes){
for(var id in this.data){
if(this.data.hasOwnProperty(id) && id !== "global"){
var idParts = id.split(":");
if(activeNodes.indexOf(idParts[0]) === -1){
delete this.data[id];
}
}
}
return Promise.resolve();
}
Memory.prototype._export = function() {
return this.data;
}
module.exports = function(config){
return new Memory(config);
};

View File

@ -163,11 +163,12 @@ function setFlows(_config,type,muteLog,forceStart) {
activeFlowConfig = newFlowConfig; activeFlowConfig = newFlowConfig;
if (forceStart || started) { if (forceStart || started) {
return stop(type,diff,muteLog).then(function() { return stop(type,diff,muteLog).then(function() {
context.clean(activeFlowConfig); return context.clean(activeFlowConfig).then(function() {
start(type,diff,muteLog).then(function() { start(type,diff,muteLog).then(function() {
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true});
}); });
return flowRevision; return flowRevision;
});
}).catch(function(err) { }).catch(function(err) {
}) })
} else { } else {

View File

@ -187,6 +187,8 @@ module.exports = {
createNode: createNode, createNode: createNode,
getNode: flows.get, getNode: flows.get,
eachNode: flows.eachNode, eachNode: flows.eachNode,
getContext: context.get,
paletteEditorEnabled: registry.paletteEditorEnabled, paletteEditorEnabled: registry.paletteEditorEnabled,
installModule: installModule, installModule: installModule,
@ -236,5 +238,10 @@ module.exports = {
setCredentialSecret: credentials.setKey, setCredentialSecret: credentials.setKey,
clearCredentials: credentials.clear, clearCredentials: credentials.clear,
exportCredentials: credentials.export, exportCredentials: credentials.export,
getCredentialKeyType: credentials.getKeyType getCredentialKeyType: credentials.getKeyType,
// Contexts
loadContextsPlugin: context.load,
closeContextsPlugin: context.close,
listContextStores: context.listStores
}; };

View File

@ -48,7 +48,9 @@ function runGitCommand(args,cwd,env) {
var err = new Error(stderr); var err = new Error(stderr);
err.stdout = stdout; err.stdout = stdout;
err.stderr = stderr; err.stderr = stderr;
if(/Connection refused/i.test(stderr)) { if (/Connection refused/i.test(stderr)) {
err.code = "git_connection_failed";
} else if (/Connection timed out/i.test(stderr)) {
err.code = "git_connection_failed"; err.code = "git_connection_failed";
} else if (/fatal: could not read/i.test(stderr)) { } else if (/fatal: could not read/i.test(stderr)) {
// Username/Password // Username/Password

Some files were not shown because too many files have changed in this diff Show More