Merge branch '0.18' into projects

This commit is contained in:
Nick O'Leary
2018-01-16 11:21:54 +00:00
146 changed files with 6584 additions and 1498 deletions

View File

@@ -26,6 +26,14 @@
<input type="text" id="node-input-topic">
</div>
<div class="form-row" id="node-once">
<label for="node-input-once">&nbsp;</label>
<input type="checkbox" id="node-input-once" style="display:inline-block;width:15px;vertical-align:baseline;">
<span data-i18n="inject.onstart"></span>&nbsp;
<input type="text" id="node-input-onceDelay" placeholder="0.1" style="width:45px">&nbsp;
<span data-i18n="inject.onceDelay"></span>
</div>
<div class="form-row">
<label for=""><i class="fa fa-repeat"></i> <span data-i18n="inject.label.repeat"></span></label>
<select id="inject-time-type-select">
@@ -106,13 +114,6 @@
</div>
</div>
</div>
<div class="form-row" id="node-once">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-once" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-once" style="width: 70%;" data-i18n="inject.onstart"></label>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@@ -181,9 +182,10 @@ If you want every 20 minutes from now - use the <i>"interval"</i> option.</p>
topic: {value:""},
payload: {value:"", validate: RED.validators.typedInput("payloadType")},
payloadType: {value:"date"},
repeat: {value:""},
repeat: {value:"", validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0))) }},
crontab: {value:""},
once: {value:false}
once: {value:false},
onceDelay: {value:0.1}
},
icon: "inject.png",
inputs:0,

View File

@@ -26,27 +26,35 @@ module.exports = function(RED) {
this.repeat = n.repeat;
this.crontab = n.crontab;
this.once = n.once;
this.onceDelay = (n.onceDelay || 0.1) * 1000;
var node = this;
this.interval_id = null;
this.cronjob = null;
if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) {
node.repeaterSetup = function () {
if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) {
this.repeat = this.repeat * 1000;
if (RED.settings.verbose) { this.log(RED._("inject.repeat",this)); }
this.interval_id = setInterval( function() {
node.emit("input",{});
}, this.repeat );
} else if (this.crontab) {
if (RED.settings.verbose) { this.log(RED._("inject.crontab",this)); }
this.cronjob = new cron.CronJob(this.crontab,
function() {
node.emit("input",{});
},
null,true);
}
if (RED.settings.verbose) {
this.log(RED._("inject.repeat", this));
}
this.interval_id = setInterval(function() {
node.emit("input", {});
}, this.repeat);
} else if (this.crontab) {
if (RED.settings.verbose) {
this.log(RED._("inject.crontab", this));
}
this.cronjob = new cron.CronJob(this.crontab, function() { node.emit("input", {}); }, null, true);
}
};
if (this.once) {
setTimeout( function() { node.emit("input",{}); }, 100 );
this.onceTimeout = setTimeout( function() {
node.emit("input",{});
node.repeaterSetup();
}, this.onceDelay);
} else {
node.repeaterSetup();
}
this.on("input",function(msg) {
@@ -56,7 +64,7 @@ module.exports = function(RED) {
msg.payload = Date.now();
} else if (this.payloadType == null) {
msg.payload = this.payload;
} else if (this.payloadType == 'none') {
} else if (this.payloadType === 'none') {
msg.payload = "";
} else {
msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg);
@@ -72,6 +80,9 @@ module.exports = function(RED) {
RED.nodes.registerType("inject",InjectNode);
InjectNode.prototype.close = function() {
if (this.onceTimeout) {
clearTimeout(this.onceTimeout);
}
if (this.interval_id != null) {
clearInterval(this.interval_id);
if (RED.settings.verbose) { this.log(RED._("inject.stopped")); }
@@ -80,7 +91,7 @@ module.exports = function(RED) {
if (RED.settings.verbose) { this.log(RED._("inject.stopped")); }
delete this.cronjob;
}
}
};
RED.httpAdmin.post("/inject/:id", RED.auth.needsPermission("inject.write"), function(req,res) {
var node = RED.nodes.getNode(req.params.id);

View File

@@ -6,11 +6,22 @@
<input id="node-input-complete" type="hidden">
</div>
<div class="form-row">
<label for="node-input-console"><i class="fa fa-random"></i> <span data-i18n="debug.to"></span></label>
<select type="text" id="node-input-console" style="display: inline-block; width: 250px; vertical-align: top;">
<option value="false" data-i18n="debug.debtab"></option>
<option value="true" data-i18n="debug.tabcon"></option>
</select>
<label for="node-input-tosidebar"><i class="fa fa-random"></i> <span data-i18n="debug.to"></span></label>
<label for="node-input-tosidebar" style="width:70%">
<input type="checkbox" id="node-input-tosidebar" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="debug.toSidebar"></span>
</label>
</div>
<div class="form-row">
<label for="node-input-console"> </label>
<label for="node-input-console" style="width:70%">
<input type="checkbox" id="node-input-console" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="debug.toConsole"></span>
</label>
</div>
<div class="form-row" id="node-tostatus-line">
<label for="node-input-tostatus"> </label>
<label for="node-input-tostatus" style="width:70%">
<input type="checkbox" id="node-input-tostatus" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="debug.toStatus"></span>
</label>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
@@ -26,7 +37,7 @@
<p>Alongside each message, the debug sidebar includes information about the time the message was received, the node that sent it and the type of the message.
Clicking on the source node id will reveal that node within the workspace.</p>
<p>The button on the node can be used to enable or disable its output. It is recommended to disable or remove any Debug nodes that are not being used.</p>
<p>The node can also be configured to send all messages to the runtime log.</p>
<p>The node can also be configured to send all messages to the runtime log, or to send short (32 characters) to the status text under the debug node.</p>
</script>
<script src="debug/view/debug-utils.js"></script>
@@ -38,7 +49,9 @@
defaults: {
name: {value:""},
active: {value:true},
console: {value:"false"},
tosidebar: {value:true},
console: {value:false},
tostatus: {value:false},
complete: {value:"false", required:true}
},
label: function() {
@@ -100,7 +113,6 @@
}
},
onpaletteadd: function() {
var options = {
messageMouseEnter: function(sourceId) {
if (sourceId) {
@@ -148,7 +160,7 @@
var that = this;
RED._debug = function(msg) {
that.handleDebugMessage("",{
that.handleDebugMessage("", {
name:"debug",
msg:msg
});
@@ -170,7 +182,6 @@
var sourceNode = RED.nodes.node(o.id) || RED.nodes.node(o.z);
if (sourceNode) {
o._source = {id:sourceNode.id,z:sourceNode.z,name:sourceNode.name};
}
RED.debug.handleDebugMessage(o);
if (subWindow) {
@@ -237,6 +248,15 @@
delete RED._debug;
},
oneditprepare: function() {
if (this.tosidebar === undefined) {
this.tosidebar = true;
$("#node-input-tosidebar").prop('checked', true);
}
if (typeof this.console === "string") {
this.console = (this.console == 'true');
$("#node-input-console").prop('checked', this.console);
$("#node-input-tosidebar").prop('checked', true);
}
$("#node-input-typed-complete").typedInput({types:['msg', {value:"full",label:RED._("node-red:debug.msgobj"),hasValue:false}]});
if (this.complete === "true" || this.complete === true) {
// show complete message object
@@ -252,6 +272,16 @@
) {
$("#node-input-typed-complete").typedInput('value','payload');
}
if ($("#node-input-typed-complete").typedInput('type') === 'msg') {
$("#node-tostatus-line").show();
}
else { $("#node-tostatus-line").hide(); }
});
$("#node-input-complete").on('change',function() {
if ($("#node-input-typed-complete").typedInput('type') === 'msg') {
$("#node-tostatus-line").show();
}
else { $("#node-tostatus-line").hide(); }
});
},
oneditsave: function() {
@@ -262,7 +292,6 @@
$("#node-input-complete").val($("#node-input-typed-complete").typedInput('value'));
}
}
});
})();
</script>

View File

@@ -5,7 +5,7 @@ module.exports = function(RED) {
var events = require("events");
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;
util.inspect.styles.boolean = "red";
@@ -13,26 +13,49 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
this.name = n.name;
this.complete = (n.complete||"payload").toString();
if (this.complete === "false") {
this.complete = "payload";
}
this.console = n.console;
if (this.complete === "false") { this.complete = "payload"; }
this.console = ""+(n.console || false);
this.tostatus = n.tostatus || false;
this.tosidebar = n.tosidebar;
if (this.tosidebar === undefined) { this.tosidebar = true; }
this.severity = n.severity || 40;
this.active = (n.active === null || typeof n.active === "undefined") || n.active;
this.status({});
var node = this;
var levels = {
off: 1,
fatal: 10,
error: 20,
warn: 30,
info: 40,
debug: 50,
trace: 60,
audit: 98,
metric: 99
};
var colors = {
"0": "grey",
"10": "grey",
"20": "red",
"30": "yellow",
"40": "grey",
"50": "green",
"60": "blue"
};
this.on("input",function(msg) {
if (this.complete === "true") {
// debug complete msg object
// debug complete msg object
if (this.console === "true") {
node.log("\n"+util.inspect(msg, {colors:useColors, depth:10}));
}
if (this.active) {
sendDebug({id:node.id,name:node.name,topic:msg.topic,msg:msg,_path:msg._path});
if (this.active && this.tosidebar) {
sendDebug({id:node.id, name:node.name, topic:msg.topic, msg:msg, _path:msg._path});
}
} else {
// debug user defined msg property
}
else {
// debug user defined msg property
var property = "payload";
var output = msg[property];
if (this.complete !== "false" && typeof this.complete !== "undefined") {
@@ -53,7 +76,15 @@ module.exports = function(RED) {
}
}
if (this.active) {
sendDebug({id:node.id,z:node.z,name:node.name,topic:msg.topic,property:property,msg:output,_path:msg._path});
if (this.tosidebar == true) {
sendDebug({id:node.id, z:node.z, name:node.name, topic:msg.topic, property:property, msg:output, _path:msg._path});
}
if (this.tostatus === true) {
var st = util.inspect(output);
if (st.length > 32) { st = st.substr(0,32) + "..."; }
node.oldStatus = {fill:colors[node.severity], shape:"dot", text:st};
node.status(node.oldStatus);
}
}
}
});
@@ -138,7 +169,7 @@ module.exports = function(RED) {
value = value.substring(0,debuglength)+"...";
}
} else if (value && value.constructor) {
if (value.constructor.name === "Buffer") {
if (value.type === "Buffer") {
value.__encoded__ = true;
value.length = value.data.length;
if (value.length > debuglength) {
@@ -196,9 +227,14 @@ module.exports = function(RED) {
if (state === "enable") {
node.active = true;
res.sendStatus(200);
if (node.tostatus) { node.status({}); }
} else if (state === "disable") {
node.active = false;
res.sendStatus(201);
if (node.tostatus && node.hasOwnProperty("oldStatus")) {
node.oldStatus.shape = "ring";
node.status(node.oldStatus);
}
} else {
res.sendStatus(404);
}

View File

@@ -75,12 +75,20 @@
<dt>payload <span class="property-type">string</span></dt>
<dd>the standard output of the command.</dd>
</dl>
<dl class="message-properties">
<dt>rc <span class="property-type">object</span></dt>
<dd>exec mode only, a copy of the return code object (also available on port 3)</dd>
</dl>
</li>
<li>Standard error
<dl class="message-properties">
<dt>payload <span class="property-type">string</span></dt>
<dd>the standard error of the command.</dd>
</dl>
<dl class="message-properties">
<dt>rc <span class="property-type">object</span></dt>
<dd>exec mode only, a copy of the return code object (also available on port 3)</dd>
</dl>
</li>
<li>Return code
<dl class="message-properties">

View File

@@ -125,14 +125,15 @@ module.exports = function(RED) {
if (node.append.trim() !== "") { cl += " "+node.append; }
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(cl); }
child = exec(cl, {encoding: 'binary', maxBuffer:10000000}, function (error, stdout, stderr) {
child = exec(cl, {encoding:'binary', maxBuffer:10000000}, function (error, stdout, stderr) {
var msg2, msg3;
delete msg.payload;
if (stderr) {
msg2 = RED.util.cloneMessage(msg);
msg2.payload = stderr;
}
msg.payload = Buffer.from(stdout,"binary");
if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); }
var msg2 = null;
if (stderr) {
msg2 = {payload: stderr};
}
var msg3 = null;
node.status({});
//console.log('[exec] stdout: ' + stdout);
//console.log('[exec] stderr: ' + stderr);
@@ -142,10 +143,15 @@ module.exports = function(RED) {
if (error.code === null) { node.status({fill:"red",shape:"dot",text:"killed"}); }
else { node.status({fill:"red",shape:"dot",text:"error:"+error.code}); }
node.log('error:' + error);
} else if (node.oldrc === "false") {
}
else if (node.oldrc === "false") {
msg3 = {payload:{code:0}};
}
if (!msg3) { node.status({}); }
else {
msg.rc = msg3.payload;
if (msg2) { msg2.rc = msg3.payload; }
}
node.send([msg,msg2,msg3]);
if (child.tout) { clearTimeout(child.tout); }
delete node.activeProcesses[child.pid];

View File

@@ -70,10 +70,19 @@
label: function() {
return this.name;
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var that = this;
$( "#node-input-outputs" ).spinner({
min:1
min:1,
change: function(event, ui) {
var value = this.value;
if (!value.match(/^\d+$/)) { value = 1; }
else if (value < this.min) { value = this.min; }
if (value !== this.value) { $(this).spinner("value", value); }
}
});
this.editor = RED.editor.createEditor({

View File

@@ -187,6 +187,13 @@ module.exports = function(RED) {
}
}
};
if (util.hasOwnProperty('promisify')) {
sandbox.setTimeout[util.promisify.custom] = function(after, value) {
return new Promise(function(resolve, reject) {
sandbox.setTimeout(function(){ resolve(value) }, after);
});
}
}
var context = vm.createContext(sandbox);
try {
this.script = vm.createScript(functionText);

View File

@@ -42,6 +42,7 @@
<select id="node-input-output" style="width:180px;">
<option value="str" data-i18n="template.label.plain"></option>
<option value="json" data-i18n="template.label.json"></option>
<option value="yaml" data-i18n="template.label.yaml"></option>
</select>
</div>
@@ -53,6 +54,9 @@
<dl class="message-properties">
<dt>msg <span class="property-type">object</span></dt>
<dd>A msg object containing information to populate the template.</dd>
<dt class="optional">template <span class="property-type">string</span></dt>
<dd>A template to be populated from msg.payload. If not configured in the edit panel,
this can be set as a property of msg.</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">
@@ -97,6 +101,9 @@
label: function() {
return this.name;
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var that = this;
if (!this.fieldType) {

View File

@@ -17,14 +17,17 @@
module.exports = function(RED) {
"use strict";
var mustache = require("mustache");
var yaml = require("js-yaml");
/**
* Custom Mustache Context capable to resolve message property and node
* flow and global context
*/
function NodeContext(msg, nodeContext,parent) {
function NodeContext(msg, nodeContext, parent, escapeStrings) {
this.msgContext = new mustache.Context(msg,parent);
this.nodeContext = nodeContext;
this.escapeStrings = escapeStrings;
}
NodeContext.prototype = new mustache.Context();
@@ -34,6 +37,14 @@ module.exports = function(RED) {
try {
var value = this.msgContext.lookup(name);
if (value !== undefined) {
if (this.escapeStrings && typeof value === "string") {
value = value.replace(/\\/g, "\\\\");
value = value.replace(/\n/g, "\\n");
value = value.replace(/\t/g, "\\t");
value = value.replace(/\r/g, "\\r");
value = value.replace(/\f/g, "\\f");
value = value.replace(/[\b]/g, "\\b");
}
return value;
}
@@ -72,14 +83,31 @@ module.exports = function(RED) {
node.on("input", function(msg) {
try {
var value;
/***
* Allow template contents to be defined externally
* through inbound msg.template IFF node.template empty
*/
if (msg.hasOwnProperty("template")) {
if (node.template == "" || node.template === null) {
node.template = msg.template;
}
}
if (node.syntax === "mustache") {
value = mustache.render(node.template,new NodeContext(msg, node.context()));
if (node.outputFormat === "json") {
value = mustache.render(node.template,new NodeContext(msg, node.context(), null, true));
} else {
value = mustache.render(node.template,new NodeContext(msg, node.context(), null, false));
}
} else {
value = node.template;
}
if (node.outputFormat === "json") {
value = JSON.parse(value);
}
if (node.outputFormat === "yaml") {
value = yaml.load(value);
}
if (node.fieldType === 'msg') {
RED.util.setMessageProperty(msg,node.field,value);

View File

@@ -102,7 +102,7 @@
<dt class="optional">delay <span class="property-type">number</span></dt>
<dd>Sets the delay, in milliseconds, to be applied to the message. This
option only applies if the node is configured to allow the message to
provide the delay interval.</dd>
override the configured default delay interval.</dd>
<dt class="optional">reset</dt>
<dd>If the received message has this property set to any value, all
outstanding messages held by the node are cleared without being sent.</dd>
@@ -129,13 +129,13 @@
defaults: {
name: {value:""},
pauseType: {value:"delay", required:true},
timeout: {value:"5", required:true, validate:RED.validators.number()},
timeout: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
timeoutUnits: {value:"seconds"},
rate: {value:"1", required:true, validate:RED.validators.number()},
rate: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
nbRateUnits: {value:"1", required:false, validate:RED.validators.regex(/\d+|/)},
rateUnits: {value: "second"},
randomFirst: {value:"1", required:true, validate:RED.validators.number()},
randomLast: {value:"5", required:true, validate:RED.validators.number()},
randomFirst: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomLast: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomUnits: {value: "seconds"},
drop: {value:false}
},
@@ -260,7 +260,7 @@
$("#delay-details-for").show();
$("#random-details").hide();
} else if (this.value === "delayv") {
$("#delay-details-for").hide();
$("#delay-details-for").show();
$("#random-details").hide();
} else if (this.value === "random") {
$("#delay-details-for").hide();

View File

@@ -105,7 +105,10 @@ module.exports = function(RED) {
}
else if (node.pauseType === "delayv") {
node.on("input", function(msg) {
var delayvar = Number(msg.delay || 0);
var delayvar = Number(node.timeout);
if (msg.hasOwnProperty("delay") && !isNaN(parseFloat(msg.delay))) {
delayvar = parseFloat(msg.delay);
}
if (delayvar < 0) { delayvar = 0; }
var id = setTimeout(function() {
node.idList.splice(node.idList.indexOf(id),1);
@@ -113,7 +116,7 @@ module.exports = function(RED) {
node.send(msg);
}, delayvar);
node.idList.push(id);
if ((delayvar >= 1) && (node.idList.length !== 0)) {
if ((delayvar >= 0) && (node.idList.length !== 0)) {
node.status({fill:"blue",shape:"dot",text:delayvar/1000+"s"});
}
if (msg.hasOwnProperty("reset")) { clearDelayList(); }

View File

@@ -56,6 +56,13 @@
</ul>
</div>
<br/>
<div class="form-row">
<label data-i18n="trigger.for" for="node-input-bytopic"></label>
<select id="node-input-bytopic">
<option value="all" data-i18n="trigger.alltopics"></option>
<option value="topic" data-i18n="trigger.bytopics"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></input>
@@ -85,7 +92,7 @@
<p>If set to a <i>string</i> type, the node supports the mustache template syntax.</p>
<p>If the node receives a message with a <code>reset</code> property, or a <code>payload</code>
that matches that configured in the node, any timeout or repeat currently in
progress will be cleared and no message triggered.</o>
progress will be cleared and no message triggered.</p>
<p>The node can be configured to resend a message at a regular interval until it
is reset by a received message.</p>
</script>
@@ -103,6 +110,7 @@
extend: {value:"false"},
units: {value:"ms"},
reset: {value:""},
bytopic: {value: "all"},
name: {value:""}
},
inputs:1,

View File

@@ -19,6 +19,7 @@ module.exports = function(RED) {
var mustache = require("mustache");
function TriggerNode(n) {
RED.nodes.createNode(this,n);
this.bytopic = n.bytopic || "all";
this.op1 = n.op1 || "1";
this.op2 = n.op2 || "0";
this.op1type = n.op1type || "str";
@@ -47,7 +48,7 @@ module.exports = function(RED) {
this.extend = n.extend || "false";
this.units = n.units || "ms";
this.reset = n.reset || '';
this.duration = parseInt(n.duration);
this.duration = parseFloat(n.duration);
if (isNaN(this.duration)) {
this.duration = 250;
}
@@ -65,29 +66,32 @@ module.exports = function(RED) {
this.op2Templated = (this.op2type === 'str' && this.op2.indexOf("{{") != -1);
if ((this.op1type === "num") && (!isNaN(this.op1))) { this.op1 = Number(this.op1); }
if ((this.op2type === "num") && (!isNaN(this.op2))) { this.op2 = Number(this.op2); }
if (this.op1 == "null") { this.op1 = null; }
if (this.op2 == "null") { this.op2 = null; }
//if (this.op1 == "null") { this.op1 = null; }
//if (this.op2 == "null") { this.op2 = null; }
//try { this.op1 = JSON.parse(this.op1); }
//catch(e) { this.op1 = this.op1; }
//try { this.op2 = JSON.parse(this.op2); }
//catch(e) { this.op2 = this.op2; }
var node = this;
var tout = null;
var m2;
node.topics = {};
this.on("input", function(msg) {
var topic = msg.topic || "_none";
if (node.bytopic === "all") { topic = "_none"; }
node.topics[topic] = node.topics[topic] || {};
if (msg.hasOwnProperty("reset") || ((node.reset !== '') && (msg.payload == node.reset)) ) {
if (node.loop === true) { clearInterval(tout); }
else { clearTimeout(tout); }
tout = null;
if (node.loop === true) { clearInterval(node.topics[topic].tout); }
else { clearTimeout(node.topics[topic].tout); }
delete node.topics[topic];
node.status({});
}
else {
if (((!tout) && (tout !== 0)) || (node.loop === true)) {
if (node.op2type === "pay" || node.op2type === "payl") { m2 = msg.payload; }
else if (node.op2Templated) { m2 = mustache.render(node.op2,msg); }
if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) {
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.op2type !== "nul") {
m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg);
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg);
}
if (node.op1type === "pay") { }
@@ -96,58 +100,64 @@ module.exports = function(RED) {
msg.payload = RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg);
}
if (node.op1type !== "nul") { node.send(msg); }
if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); }
if (node.duration === 0) { tout = 0; }
if (node.duration === 0) { node.topics[topic].tout = 0; }
else if (node.loop === true) {
if (tout) { clearInterval(tout); }
if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); }
if (node.op1type !== "nul") {
var msg2 = RED.util.cloneMessage(msg);
tout = setInterval(function() { node.send(msg2); },node.duration);
node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration);
}
}
else {
tout = setTimeout(function() {
node.topics[topic].tout = setTimeout(function() {
var msg2 = null;
if (node.op2type !== "nul") {
var msg2 = RED.util.cloneMessage(msg);
msg2 = RED.util.cloneMessage(msg);
if (node.op2type === "flow" || node.op2type === "global") {
m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg);
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg);
}
msg2.payload = m2;
node.send(msg2);
msg2.payload = node.topics[topic].m2;
}
tout = null;
delete node.topics[topic];
node.status({});
},node.duration);
node.send(msg2);
}, node.duration);
}
node.status({fill:"blue",shape:"dot",text:" "});
}
else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) {
if (tout) { clearTimeout(tout); }
if (node.op2type === "payl") { m2 = msg.payload; }
tout = setTimeout(function() {
if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); }
node.topics[topic].tout = setTimeout(function() {
var msg2 = null;
if (node.op2type !== "nul") {
var msg2 = RED.util.cloneMessage(msg);
if (node.op2type === "flow" || node.op2type === "global") {
m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg);
node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg);
}
if (node.topics[topic] !== undefined) {
msg2 = RED.util.cloneMessage(msg);
msg2.payload = node.topics[topic].m2;
}
msg2.payload = m2;
node.send(msg2);
}
tout = null;
delete node.topics[topic];
node.status({});
},node.duration);
node.send(msg2);
}, node.duration);
}
else {
if (node.op2type === "payl") { m2 = msg.payload; }
if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
}
}
});
this.on("close", function() {
if (tout) {
if (node.loop === true) { clearInterval(tout); }
else { clearTimeout(tout); }
tout = null;
for (var t in node.topics) {
if (node.topics[t]) {
if (node.loop === true) { clearInterval(node.topics[t].tout); }
else { clearTimeout(node.topics[t].tout); }
delete node.topics[t];
}
}
node.status({});
});

View File

@@ -29,6 +29,7 @@ RED.debug = (function() {
var messagesByNode = {};
var sbc;
var activeWorkspace;
var numMessages = 100; // Hardcoded number of message to show in debug window scrollback
var filterVisible = false;
@@ -367,9 +368,24 @@ RED.debug = (function() {
})
menuOptionMenu.show();
}
function handleDebugMessage(o) {
var msg = document.createElement("div");
var stack = [];
var busy = false;
function handleDebugMessage(o) {
if (o) { stack.push(o); }
if (!busy && (stack.length > 0)) {
busy = true;
processDebugMessage(stack.shift());
setTimeout(function() {
busy = false;
handleDebugMessage();
}, 15); // every 15mS = 66 times a second
if (stack.length > numMessages) { stack = stack.splice(-numMessages); }
}
}
function processDebugMessage(o) {
var msg = document.createElement("div");
var sourceNode = o._source;
msg.onmouseenter = function() {
@@ -421,7 +437,9 @@ RED.debug = (function() {
$('<span class="debug-message-name">'+name+'</span>').appendTo(metaRow);
}
if (format === 'Object' || /^array/.test(format) || format === 'boolean' || format === 'number' ) {
if ((format === 'number') && (payload === "NaN")) {
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);
@@ -495,7 +513,7 @@ RED.debug = (function() {
}
}
if (messages.length === 100) {
if (messages.length === numMessages) {
m = messages.shift();
if (view === "list") {
m.el.remove();
@@ -528,6 +546,6 @@ RED.debug = (function() {
init: init,
refreshMessageList:refreshMessageList,
handleDebugMessage: handleDebugMessage,
clearMessageList: clearMessageList,
clearMessageList: clearMessageList
}
})();

View File

@@ -24,44 +24,48 @@
<span class="tls-config-input-data">
<label class="editor-button" for="node-config-input-certfile"><i class="fa fa-upload"></i> <span data-i18n="tls.label.upload"></span></label>
<input class="hide" type="file" id="node-config-input-certfile">
<span id="tls-config-certname" style="width: 180px; overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
<span id="tls-config-certname" style="width: calc(100% - 280px); overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
<button class="editor-button editor-button-small" id="tls-config-button-cert-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
</span>
<input type="hidden" id="node-config-input-certname">
<input type="hidden" id="node-config-input-certdata">
<input class="hide tls-config-input-path" style="width: 60%;" type="text" id="node-config-input-cert" data-i18n="[placeholder]tls.placeholder.cert">
<input class="hide tls-config-input-path" style="width: calc(100% - 170px);" type="text" id="node-config-input-cert" data-i18n="[placeholder]tls.placeholder.cert">
</div>
<div class="form-row">
<label style="width: 120px;" for="node-config-input-key"><i class="fa fa-file-text-o"></i> <span data-i18n="tls.label.key"></span></label>
<span class="tls-config-input-data">
<label class="editor-button" for="node-config-input-keyfile"><i class="fa fa-upload"></i> <span data-i18n="tls.label.upload"></span></label>
<input class="hide" type="file" id="node-config-input-keyfile">
<span id="tls-config-keyname" style="width: 180px; overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
<span id="tls-config-keyname" style="width: calc(100% - 280px); overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
<button class="editor-button editor-button-small" id="tls-config-button-key-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
</span>
<input type="hidden" id="node-config-input-keyname">
<input type="hidden" id="node-config-input-keydata">
<input class="hide tls-config-input-path" style="width: 60%;" type="text" id="node-config-input-key" data-i18n="[placeholder]tls.placeholder.key">
<input class="hide tls-config-input-path" style="width: calc(100% - 170px);" type="text" id="node-config-input-key" data-i18n="[placeholder]tls.placeholder.key">
</div>
<div class="form-row">
<label style="width: 100px; margin-left: 20px;" for="node-config-input-passphrase"> <span data-i18n="tls.label.passphrase"></span></label>
<input type="password" style="width: calc(100% - 170px);" id="node-config-input-passphrase" data-i18n="[placeholder]tls.placeholder.passphrase">
</div>
<div class="form-row">
<label style="width: 120px;" for="node-config-input-ca"><i class="fa fa-file-text-o"></i> <span data-i18n="tls.label.ca"></span></label>
<span class="tls-config-input-data">
<label class="editor-button" for="node-config-input-cafile"><i class="fa fa-upload"></i> <span data-i18n="tls.label.upload"></span></label>
<input class="hide" type="file" title=" " id="node-config-input-cafile">
<span id="tls-config-caname" style="width: 180px; overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
<span id="tls-config-caname" style="width: calc(100% - 280px); overflow: hidden; line-height:34px; height:34px; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: middle;"> </span>
<button class="editor-button editor-button-small" id="tls-config-button-ca-clear" style="margin-left: 10px"><i class="fa fa-times"></i></button>
</span>
<input type="hidden" id="node-config-input-caname">
<input type="hidden" id="node-config-input-cadata">
<input class="hide tls-config-input-path" style="width: 60%;" type="text" id="node-config-input-ca" data-i18n="[placeholder]tls.placeholder.ca">
<input class="hide tls-config-input-path" style="width: calc(100% - 170px);" type="text" id="node-config-input-ca" data-i18n="[placeholder]tls.placeholder.ca">
</div>
<div class="form-row">
<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: 70%;" 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 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>
<input style="width: 60%;" 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">
</div>
</script>
@@ -97,7 +101,8 @@
credentials: {
certdata: {type:"text"},
keydata: {type:"text"},
cadata: {type:"text"}
cadata: {type:"text"},
passphrase: {type:"password"}
},
label: function() {
return this.name || this._("tls.tls");

View File

@@ -77,7 +77,8 @@ module.exports = function(RED) {
credentials: {
certdata: {type:"text"},
keydata: {type:"text"},
cadata: {type:"text"}
cadata: {type:"text"},
passphrase: {type:"password"}
},
settings: {
tlsConfigDisableLocalFiles: {
@@ -98,6 +99,9 @@ module.exports = function(RED) {
if (this.ca) {
opts.ca = this.ca;
}
if (this.credentials && this.credentials.passphrase) {
opts.passphrase = this.credentials.passphrase;
}
opts.rejectUnauthorized = this.verifyservercert;
}
return opts;

View File

@@ -165,6 +165,10 @@
</script>
<script type="text/x-red" data-template-name="mqtt-broker">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-config-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<ul style="background: #fff; min-width: 600px; margin-bottom: 20px;" id="node-config-mqtt-broker-tabs"></ul>
</div>
@@ -256,27 +260,41 @@
</script>
<script type="text/x-red" data-help-name="mqtt-broker">
<p>A minimum MQTT broker connection requires only a broker server address to be added to the default configuration.</p>
<p>To secure the connection with SSL/TLS, a TLS Configuration must also be configured and selected.</p>
<p>If you create a Client ID it must be unique to the broker you are connecting to.</p>
<p>For more information about MQTT see the <a href="http://www.eclipse.org/paho/" target="_blank">Eclipse Paho</a> site.</p>
<p>Configuration for a connection to an MQTT broker.</p>
<p>This configuration will create a single connection to the broker which can
then be reused by <code>MQTT In</code> and <code>MQTT Out</code> nodes.</p>
<p>The node will generate a random Client ID if one is not set and the
node is configured to use a Clean Session connection. If a Client ID is set,
it must be unique to the broker you are connecting to.</p>
<h4>Birth Message</h4>
<p>This is a message that will be published on the configured topic whenever the
connection is established.</p>
<h4>Will Message</h4>
<p>This is a message that will be published by the broker in the event the node
unexpectedly loses its connection.</p>
<h4>WebSockets</h4>
<p>The node can be configured to use a WebSocket connection. To do so, the Server
field should be configured with a full URI for the connection. For example:</p>
<pre>ws://example.com:4000/mqtt</pre>
</script>
<script type="text/javascript">
RED.nodes.registerType('mqtt-broker',{
category: 'config',
defaults: {
name: {value:""},
broker: {value:"",required:true},
port: {value:1883,required:true,validate:RED.validators.number()},
port: {value:1883,required:false,validate:RED.validators.number(true)},
tls: {type:"tls-config",required: false},
clientid: {value:"", validate: function(v) {
if ($("#node-config-input-clientid").length) {
// Currently editing the node
return $("#node-config-input-cleansession").is(":checked") || (v||"").length > 0;
} else {
return (this.cleansession===undefined || this.cleansession) || (v||"").length > 0;
}
}},
if ($("#node-config-input-clientid").length) {
// Currently editing the node
return $("#node-config-input-cleansession").is(":checked") || (v||"").length > 0;
} else {
return (this.cleansession===undefined || this.cleansession) || (v||"").length > 0;
}
}},
usetls: {value: false},
verifyservercert: { value: false},
compatmode: { value: true},
@@ -296,9 +314,18 @@
password: {type: "password"}
},
label: function() {
if (this.name) {
return this.name;
}
var b = this.broker;
if (b === "") { b = "undefined"; }
return (this.clientid?this.clientid+"@":"")+b+":"+this.port;
var lab = "";
lab = (this.clientid?this.clientid+"@":"")+b;
if (b.indexOf("://") === -1){
if (!this.port){ lab = lab + ":1883"; }
else { lab = lab + ":" + this.port; }
}
return lab;
},
oneditprepare: function () {
var tabs = RED.tabs.create({
@@ -374,6 +401,28 @@
$("#node-config-input-cleansession").on("click",function() {
updateClientId();
});
function updatePortEntry(){
var disabled = $("#node-config-input-port").prop("disabled");
if ($("#node-config-input-broker").val().indexOf("://") === -1){
if (disabled){
$("#node-config-input-port").prop("disabled", false);
}
}
else {
if (!disabled){
$("#node-config-input-port").prop("disabled", true);
}
}
}
$("#node-config-input-broker").change(function() {
updatePortEntry();
});
$("#node-config-input-broker").on( "keyup", function() {
updatePortEntry();
});
setTimeout(updatePortEntry,50);
},
oneditsave: function() {
if (!$("#node-config-input-usetls").is(':checked')) {

View File

@@ -36,6 +36,7 @@ module.exports = function(RED) {
this.port = n.port;
this.clientid = n.clientid;
this.usetls = n.usetls;
this.usews = n.usews;
this.verifyservercert = n.verifyservercert;
this.compatmode = n.compatmode;
this.keepalive = n.keepalive;
@@ -69,6 +70,9 @@ module.exports = function(RED) {
if (typeof this.usetls === 'undefined') {
this.usetls = false;
}
if (typeof this.usews === 'undefined') {
this.usews = false;
}
if (typeof this.compatmode === 'undefined') {
this.compatmode = true;
}
@@ -86,15 +90,27 @@ module.exports = function(RED) {
// Create the URL to pass in to the MQTT.js library
if (this.brokerurl === "") {
if (this.usetls) {
this.brokerurl="mqtts://";
// if the broker may be ws:// or wss:// or even tcp://
if (this.broker.indexOf("://") > -1) {
this.brokerurl = this.broker;
} else {
this.brokerurl="mqtt://";
}
if (this.broker !== "") {
this.brokerurl = this.brokerurl+this.broker+":"+this.port;
} else {
this.brokerurl = this.brokerurl+"localhost:1883";
// construct the std mqtt:// url
if (this.usetls) {
this.brokerurl="mqtts://";
} else {
this.brokerurl="mqtt://";
}
if (this.broker !== "") {
this.brokerurl = this.brokerurl+this.broker+":";
// port now defaults to 1883 if unset.
if (!this.port){
this.brokerurl = this.brokerurl+"1883";
} else {
this.brokerurl = this.brokerurl+this.port;
}
} else {
this.brokerurl = this.brokerurl+"localhost:1883";
}
}
}
@@ -278,7 +294,9 @@ module.exports = function(RED) {
this.publish = function (msg) {
if (node.connected) {
if (!Buffer.isBuffer(msg.payload)) {
if (msg.payload === null || msg.payload === undefined) {
msg.payload = "";
} else if (!Buffer.isBuffer(msg.payload)) {
if (typeof msg.payload === "object") {
msg.payload = JSON.stringify(msg.payload);
} else if (typeof msg.payload !== "string") {

View File

@@ -85,6 +85,9 @@
<dd>If set, can be used to send cookies with the request.</dd>
<dt class="optional">payload</dt>
<dd>Sent as the body of the request.</dd>
<dt class="optional">rejectUnauthorized</dt>
<dd>If set to <code>true</code>, allows requests to be made to https sites that use
self signed certificates.</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">

View File

@@ -135,7 +135,7 @@ module.exports = function(RED) {
}
var payload = null;
if (typeof msg.payload !== "undefined" && (method == "POST" || method == "PUT" || method == "PATCH" ) ) {
if (typeof msg.payload !== "undefined") {
if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
payload = msg.payload;
} else if (typeof msg.payload == "number") {
@@ -196,6 +196,10 @@ module.exports = function(RED) {
}
if (tlsNode) {
tlsNode.addTLSOptions(opts);
} else {
if (msg.hasOwnProperty('rejectUnauthorized')) {
opts.rejectUnauthorized = msg.rejectUnauthorized;
}
}
var req = ((/^https/.test(urltotest))?https:http).request(opts,function(res) {
// Force NodeJs to return a Buffer (instead of a string)

View File

@@ -59,7 +59,8 @@
"Sunday"
],
"on": "on",
"onstart": "Inject once at start?",
"onstart": "Inject once after",
"onceDelay": "seconds, then",
"tip": "<b>Note:</b> \"interval between times\" and \"at a specific time\" will use cron.<br/>See info box for details.",
"success": "Successfully injected: __label__",
"errors": {
@@ -102,9 +103,13 @@
"output": "Output",
"msgprop": "message property",
"msgobj": "complete msg object",
"to": "to",
"to": "To",
"debtab": "debug tab",
"tabcon": "debug tab and console",
"toSidebar": "debug window",
"toConsole": "system console",
"toStatus": "node status (32 characters)",
"severity": "Level",
"notification": {
"activated": "Successfully activated: __label__",
"deactivated": "Successfully deactivated: __label__"
@@ -144,13 +149,15 @@
"upload": "Upload",
"cert": "Certificate",
"key": "Private Key",
"passphrase": "Passphrase",
"ca": "CA Certificate",
"verify-server-cert":"Verify server certificate"
},
"placeholder": {
"cert":"path to certificate (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)"
},
"error": {
"missing-file": "No certificate/key file provided"
@@ -195,6 +202,7 @@
"mustache": "Mustache template",
"plain": "Plain text",
"json": "Parsed JSON",
"yaml": "Parsed YAML",
"none": "none"
},
"templatevalue": "This is the payload: {{payload}} !"
@@ -204,7 +212,7 @@
"for": "For",
"delaymsg": "Delay each message",
"delayfixed": "Fixed delay",
"delayvarmsg": "Set delay with msg.delay",
"delayvarmsg": "Override delay with msg.delay",
"randomdelay": "Random delay",
"limitrate": "Rate Limit",
"limitall": "All messages",
@@ -270,6 +278,9 @@
"wait-reset": "wait to be reset",
"wait-for": "wait for",
"wait-loop": "resend it every",
"for": "Handling",
"bytopics": "each msg.topic independently",
"alltopics": "all messages",
"duration": {
"ms": "Milliseconds",
"s": "Seconds",
@@ -607,6 +618,8 @@
"c2o": "CSV to Object options",
"o2c": "Object to CSV options",
"input": "Input",
"skip-s": "Skip first",
"skip-e": "lines",
"firstrow": "first row contains column names",
"output": "Output",
"includerow": "include column name row",
@@ -661,7 +674,14 @@
},
"label": {
"o2j": "Object to JSON options",
"pretty": "Format JSON string"
"pretty": "Format JSON string",
"action": "Action",
"property": "Property",
"actions": {
"toggle": "Convert between JSON String & Object",
"str":"Always convert to JSON String",
"obj":"Always convert to JavaScript Object"
}
}
},
"yaml": {
@@ -842,5 +862,18 @@
"seconds":"seconds",
"complete":"After a message with the <code>msg.complete</code> property set",
"tip":"This mode assumes this node is either paired with a <i>split</i> node or the received messages will have a properly configured <code>msg.parts</code> property."
},
"sort" : {
"key-type" : "Key type",
"payload" : "payload or element",
"exp" : "expression",
"key-exp" : "Key exp.",
"order" : "Order",
"ascending" : "ascending",
"descending" : "descending",
"as-number" : "as number",
"invalid-exp" : "invalid JSONata expression in sort node",
"too-many" : "too many pending messages in sort node",
"clear" : "clear pending message in sort node"
}
}

View File

@@ -0,0 +1,846 @@
{
"common": {
"label": {
"payload": "内容",
"topic": "主题",
"name": "名称",
"username": "用户名",
"password": "密码"
},
"status": {
"connected": "已连接",
"not-connected": "未连接",
"disconnected": "已断开",
"connecting": "连接中",
"error": "错误",
"ok": "确认"
},
"notification": {
"error": "<strong>错误</strong>: __message__",
"errors": {
"not-deployed": "节点未部署",
"no-response": "服务器无反应",
"unexpected": "发生意外错误 (__status__) __message__"
}
},
"errors": {
"nooverride": "警告: 信息的属性已经不可以改写节点的属性. 详情参考 bit.ly/nr-override-msg-props"
}
},
"inject": {
"inject": "注入",
"repeat": "重复 = __repeat__",
"crontab": "crontab = __crontab__",
"stopped": "停止",
"failed": "注入失败: __error__",
"label": {
"repeat": "重复"
},
"timestamp": "时间戳",
"none": "空白",
"interval": "间隔",
"interval-time": "特定时间内间隔",
"time": "特定时间",
"seconds": "秒",
"minutes": "分钟",
"hours": "小时",
"between": "介于",
"previous": "之前数值",
"at": "在",
"and": "之间",
"every": "每个",
"days": [
"星期一",
"星期二",
"星期三",
"星期四",
"星期五",
"星期六",
"星期天"
],
"on": "在",
"onstart": "运行时注入?",
"tip": "<b>注意:</b> \"特定时间内间隔\" 和 \"特定时间\" 会用cron系统.<br/> 详情擦看信息页.",
"success": "成功注入: __label__",
"errors": {
"failed": "注入失败, 请查看日志"
}
},
"catch": {
"catch": "检测异常",
"catchNodes": "检测到 (__number__)",
"label": {
"source": "检测错误来自",
"node": "节点",
"type": "类型",
"selectAll": "全选",
"sortByLabel": "按名称排序",
"sortByType": "按类型排序"
},
"scope": {
"all": "所有节点",
"selected": "已选节点"
}
},
"status": {
"status": "状态 (所有)",
"statusNodes": "状态显示 (__number__)",
"label": {
"source": "状态报告来自",
"node": "节点",
"type": "类型",
"selectAll": "全选",
"sortByLabel": "按名称排序",
"sortByType": "按类型排序"
},
"scope": {
"all": "所有节点",
"selected": "已选节点"
}
},
"debug": {
"output": "输出",
"msgprop": "信息属性",
"msgobj": "完整信息",
"to": "目标",
"debtab": "调试窗口",
"tabcon": "调试窗口及终端控制台",
"notification": {
"activated": "成功激活: __label__",
"deactivated": "成功取消: __label__"
},
"sidebar": {
"label": "调试窗口",
"name": "名称",
"filterAll": "所有节点",
"filterSelected": "已选节点",
"filterCurrent": "目前流程",
"debugNodes": "调试节点",
"clearLog": "清理日志",
"openWindow": "在新窗口打开"
},
"messageMenu": {
"collapseAll": "折叠所有路径",
"clearPinned": "清理已固定路径",
"filterNode": "过滤此节点",
"clearFilter": "清除已设过滤"
}
},
"link": {
"linkIn": "连接入口",
"linkOut": "连接出口",
"label": {
"event": "事件名称",
"node": "节点名称",
"type": "流程",
"sortByFlow":"根据流程排序",
"sortByLabel": "根据名称排序"
}
},
"tls": {
"tls": "TLS设置",
"label": {
"use-local-files": "使用本地密匙及证书文件",
"upload": "上传",
"cert": "证书",
"key": "私钥",
"ca": "CA证书",
"verify-server-cert":"验证服务器证书"
},
"placeholder": {
"cert":"证书路径 (PEM 格式)",
"key":"私匙路径 (PEM 格式)",
"ca":"CA证书路径 (PEM 格式)"
},
"error": {
"missing-file": "无证书/密匙文件提供"
}
},
"exec": {
"label": {
"command": "命令",
"append": "追加",
"timeout": "超时",
"timeoutplace": "可选填",
"return": "输出",
"seconds": "秒"
},
"placeholder": {
"extraparams": "额外的输入参数"
},
"opt": {
"exec": "当命令任务完成时 - exec 模式",
"spawn": "当命令任务进行时 - spawn 模式"
},
"oldrc": "使用旧式输出模式 (传统模式)"
},
"function": {
"label": {
"function": "函数",
"outputs": "输出"
},
"error": {
"inputListener":"无法在函数里面加入对‘注入’事件的监视",
"non-message-returned":"函数节点尝试返回类型为 __type__ 的信息"
},
"tip": "可从信息页面查看更多关于如何编写函数的帮助"
},
"template": {
"label": {
"template": "模版",
"property": "设定属性",
"format": "语法高亮",
"syntax": "格式",
"output": "输出为",
"mustache": "Mustache 模版",
"plain": "纯文本",
"json": "解析JSON",
"none": "无"
},
"templatevalue": "This is the payload: {{payload}} !"
},
"delay": {
"action": "行为设置",
"for": "时长",
"delaymsg": "延迟每一条信息",
"delayfixed": "固定延迟时间",
"delayvarmsg": "用 msg.delay 改写延迟时长",
"randomdelay": "随机延迟",
"limitrate": "信息速度限制",
"limitall": "所有信息",
"limittopic": "每一个 msg.topic",
"fairqueue": "轮流发每一个主题",
"timedqueue": "发所有主题",
"milisecs": "毫秒",
"secs": "秒",
"sec": "秒",
"mins": "分",
"min": "分",
"hours": "小时",
"hour": "小时",
"days": "日",
"day": "日",
"between": "介于",
"and": "和",
"rate": "速度",
"msgper": "信息 每",
"dropmsg": "不传输中间信息",
"label": {
"delay": "延迟",
"variable": "变量",
"limit": "限制",
"limitTopic": "限制主题",
"random": "随机",
"units" : {
"second": {
"plural" : "秒",
"singular": "秒"
},
"minute": {
"plural" : "分钟",
"singular": "分钟"
},
"hour": {
"plural" : "小时",
"singular": "小时"
},
"day": {
"plural" : "日",
"singular": "日"
}
}
},
"error": {
"buffer": "缓冲超过了 1000 条信息",
"buffer1": "缓冲超过了 10000 条信息"
}
},
"trigger": {
"send": "发送",
"then": "然后",
"then-send": "然后发送",
"output": {
"string": "字符串",
"number": "数字",
"existing": "现有信息对象",
"original": "原本信息对象",
"latest": "最新信息对象",
"nothing": "无"
},
"wait-reset": "等待至重置",
"wait-for": "等待",
"wait-loop": "重发每",
"duration": {
"ms": "毫秒",
"s": "秒",
"m": "分钟",
"h": "小时"
},
"extend": " 如有新信息,延长延迟",
"label": {
"trigger": "触发",
"trigger-block": "出发并阻止",
"trigger-loop": "重发每",
"reset": "重置触发节点条件 如果:",
"resetMessage":"msg.reset 已设置",
"resetPayload":"msg.payload 等于",
"resetprompt": "可选填"
}
},
"comment": {
"label": {
"title": "标题",
"body": "主体"
},
"tip": "提示: 主题内容可以添加格式化为 <a href=\"https://help.github.com/articles/markdown-basics/\" target=\"_blank\">Github 风格 Markdown</a>"
},
"unknown": {
"label": {
"unknown": "未知"
},
"tip": "<p>此节点是您安装但Node-RED所不知道的类型。</p><p><i>如果在此状态下部署节点,那么它的配置将被保留,但是流程将不会启动,直到安装缺少的类型。</i></p><p>有关更多帮助,请参阅信息侧栏</p>"
},
"mqtt": {
"label": {
"broker": "服务端",
"example": "e.g. localhost",
"qos": "QoS",
"clientid": "客户端ID",
"port": "端口",
"keepalive": "存货定时器(秒)",
"cleansession": "使用新的会话",
"use-tls": "使用安全连接 (SSL/TLS)",
"tls-config":"TLS 设置",
"verify-server-cert":"验证服务器证书",
"compatmode": "使用旧式 MQTT 3.1 支持"
},
"tabs-label": {
"connection": "连接",
"security": "安全",
"will": "终结信息",
"birth": "初始信息"
},
"placeholder": {
"clientid": "留空白将会自动生成",
"clientid-nonclean":"如非新会话必须设置客户端ID",
"will-topic": "留空白将禁止终止信息",
"birth-topic": "留空白将禁止初始信息"
},
"state": {
"connected": "已连接到服务端: __broker__",
"disconnected": "已断开与服务端 __broker__ 的链接",
"connect-failed": "与服务端 __broker__ 的连接失败"
},
"retain": "保留",
"true": "是",
"false": "否",
"tip": "提示: 如果你想用msg属性来设置主题qos 或者是否保存,请将这几个区域留空",
"errors": {
"not-defined": "主题未设置",
"missing-config": "未设置服务端",
"invalid-topic": "主题无效",
"nonclean-missingclientid": "客户端ID未设定使用新会话"
}
},
"httpin": {
"label": {
"method": "方法",
"url": "URL",
"doc": "文档",
"return": "返回",
"upload": "接受文件上传?",
"status": "状态码",
"headers": "头子段",
"other": "其他"
},
"setby": "- 用 msg.method 设定 -",
"basicauth": "基本认证",
"use-tls": "使用安全连接 (SSL/TLS) ",
"tls-config":"TLS 设置",
"utf8": "UTF-8 字符串",
"binary": "二进制缓冲模块",
"json": "解析JSON对象",
"tip": {
"in": "相对URL",
"res": "发送到此节点的消息<b>必须</b>来自 <i>http input</i> 节点",
"req": "提示如果JSON解析失败则获取的字符串将按原样返回."
},
"httpreq": "http 请求",
"errors": {
"not-created": "当httpNodeRoot为否时无法创建 http-in 节点",
"missing-path": "无路径",
"no-response": "无响应对象",
"json-error": "JSON 解析错误",
"no-url": "未设定 URL",
"deprecated-call":"__method__ 方法已弃用",
"invalid-transport":"非HTTP传输请求"
},
"status": {
"requesting": "请求中"
}
},
"websocket": {
"label": {
"type": "类型",
"path": "路径",
"url": "URL"
},
"listenon": "监听",
"connectto": "连接",
"payload": "发送/接受 有效载荷",
"message": "发送/接受 完整信息",
"tip": {
"path1": "默认情况下,<code> payload </code>将包含要发送或从Websocket接收的数据。侦听器可以配置为以JSON格式的字符串发送或接收整个消息对象.",
"path2": "这条路径将相对于 ",
"url1": "URL 应该使用 ws:&#47;&#47; 或者 wss:&#47;&#47; 方案并指向现有的websocket侦听器.",
"url2": "默认情况下,<code> payload </code>将包含要发送或从Websocket接收的数据。可以将客户端配置为以JSON格式的字符串发送或接收整个消息对象."
},
"errors": {
"connect-error": "ws连接发生了错误: ",
"send-error": "发送时发生了错误: ",
"missing-conf": "未设置服务器"
}
},
"watch": {
"label": {
"files": "文件(s)",
"recursive": "递归查看文件夹"
},
"placeholder": {
"files": "逗号分开文件或文件夹"
},
"tip": "在Windows上请务必使用双斜杠 \\\\ 来隔开文件夹名字"
},
"tcpin": {
"label": {
"type": "类型",
"output": "输出",
"port": "端口",
"host": "主服务器",
"payload": "有效载荷(s)",
"delimited": "分隔符号",
"close-connection": "是否在成功发送每条信息后断开连接?",
"decode-base64": "用 Base64 解码信息?",
"server": "服务器",
"return": "返回",
"ms": "毫秒",
"chars": "字符"
},
"type": {
"listen": "监听",
"connect": "连接",
"reply": "回应到 TCP"
},
"output": {
"stream": "字串流",
"single": "单一",
"buffer": "缓冲模块",
"string": "字符串",
"base64": "Base64 字符串"
},
"return": {
"timeout": "在固定时间超时后",
"character": "当收到某个字符时",
"number": "固定数目的字符",
"never": "永不 - 保持连接",
"immed": "马上 - 不需要等待回复"
},
"status": {
"connecting": "正在连接到 __host__:__port__",
"connected": "已经连接到 __host__:__port__",
"listening-port": "监听端口 __port__",
"stopped-listening": "已停止监听端口",
"connection-from": "连接来自 __host__:__port__",
"connection-closed": "连接已关闭 __host__:__port__",
"connections": "__count__ 段连接",
"connections_plural": "__count__ 段连接"
},
"errors": {
"connection-lost": "连接中断 __host__:__port__",
"timeout": "超时关闭套接字连接,端口 __port__",
"cannot-listen": "无法监听端口 __port__, 错误: __error__",
"error": "错误: __error__",
"socket-error": "套接字连接错误来自 __host__:__port__",
"no-host": "主服务器和/或者端口未设定",
"connect-timeout": "连接超时",
"connect-fail": "连接失败"
}
},
"udp": {
"label": {
"listen": "监听",
"onport": "端口",
"using": "使用",
"output": "输出",
"group": "组",
"interface": "本地IP",
"interfaceprompt": "(可选填)本地 IP 绑定到",
"send": "发送一个",
"toport": "到端口",
"address": "地址",
"decode-base64": "是否解码编码为Base64的信息?"
},
"placeholder": {
"interface": "可选填eth0 的 ip 地址",
"address": "目的地 ip 地址"
},
"udpmsgs": "udp 信息",
"mcmsgs": "组播信息",
"udpmsg": "udp 信息",
"bcmsg": "广播信息",
"mcmsg": "组播信息",
"output": {
"buffer": "缓冲模块",
"string": "字符串",
"base64": "Base64编码字符串"
},
"bind": {
"random": "绑定到任意本地端口",
"local": "绑定到本地端口",
"target": "绑定到目标端口"
},
"tip": {
"in": "提示:确保您的防火墙将允许数据进入",
"out": "提示:如果要使用<code> msg.ip </code>和<code> msg.port </code>设置,请将地址和端口留空",
"port": "端口已在使用: "
},
"status": {
"listener-at": "udp 监听器正在监听 __host__:__port__",
"mc-group": "udp 组播到 __group__",
"listener-stopped": "udp 监听器已停止",
"output-stopped": "udp 输出已停止",
"mc-ready": "udp 组播已准备好: __outport__ -> __host__:__port__",
"bc-ready": "udp 广播已准备好: __outport__ -> __host__:__port__",
"ready": "udp 已准备好: __outport__ -> __host__:__port__",
"ready-nolocal": "udp 已准备好: __host__:__port__"
},
"errors": {
"access-error": "UDP 访问错误, 你可能需要root权限才能接入1024以下的端口",
"error": "错误: __error__",
"bad-mcaddress": "无效的组播地址",
"interface": "必须是需要接口的 ip 地址",
"ip-notset": "udp: ip 地址未设定",
"port-notset": "udp: 端口未设定",
"port-invalid": "udp: 无效端口号码",
"alreadyused": "udp: 端口已经在使用"
}
},
"switch": {
"label": {
"property": "属性",
"rule": "规矩"
},
"and": "和",
"checkall": "全选所有规则",
"stopfirst": "接受第一条匹配信息后停止",
"ignorecase": "忽视大小写",
"rules": {
"btwn":"在之间",
"cont":"包含",
"regex":"匹配正则表达式",
"true":"为真",
"false":"为假",
"null":"为空值",
"nnull":"非空值",
"else":"除此以外"
},
"errors": {
"invalid-expr": "无效 JSONata 表达: __error__"
}
},
"change": {
"label": {
"rules": "规矩",
"rule": "规矩",
"set": "设定 __property__",
"change": "改变 __property__",
"delete": "删除 __property__",
"move": "移动 __property__",
"changeCount": "改变: __count__ 条规矩",
"regex": "用正则表达式"
},
"action": {
"set": "设定",
"change": "更改",
"delete": "删除",
"move": "转移",
"to": "到",
"search": "搜索",
"replace": "更改为"
},
"errors": {
"invalid-from": "无效来源 from 属性: __error__",
"invalid-json": "无效 to 属性",
"invalid-expr": "无效 JSONata 表示: __error__"
}
},
"range": {
"label": {
"action": "行为作用",
"inputrange": "映射输入数据范围",
"resultrange": "至目标范围",
"from": "从",
"to": "到",
"roundresult": "取最接近整数?"
},
"placeholder": {
"min": "e.g. 0",
"maxin": "e.g. 99",
"maxout": "e.g. 255"
},
"scale": {
"payload": "按比例 msg.payload",
"limit": "按比例并设定界限至目标范围",
"wrap": "按比例并包含在目标范围内"
},
"tip": "提示: 此节点仅对数字有效",
"errors": {
"notnumber": "不是一个数"
}
},
"csv": {
"label": {
"columns": "列",
"separator": "分隔符号",
"c2o": "CSV 至对象选项",
"o2c": "对象至 to CSV 选项",
"input": "输入",
"firstrow": "第一行包含列名",
"output": "输出",
"includerow": "包含列名行",
"newline": "新的一行"
},
"placeholder": {
"columns": "用逗号分割列名"
},
"separator": {
"comma": "逗号",
"tab": "Tab",
"space": "空格",
"semicolon": "分号",
"colon": "冒号",
"hashtag": "井号",
"other": "其他..."
},
"output": {
"row": "每行包含一条信息",
"array": "一条单独信息 [数组]"
},
"newline": {
"linux": "Linux (\\n)",
"mac": "Mac (\\r)",
"windows": "Windows (\\r\\n)"
},
"errors": {
"csv_js": "此节点仅处理 CSV 字符串或 js 对象",
"obj_csv": "对象 -> CSV 转换未设定列模版"
}
},
"html": {
"label": {
"select": "选取项",
"output": "输出"
},
"output": {
"html": "选定元素的 html 内容",
"text": "选定元素的纯文本内容",
"attr": "选定元素的所有属性对象"
},
"format": {
"single": "由一个单独信息包含一个数组",
"multi": "由多条信息,每一条包含一个元素"
}
},
"json": {
"errors": {
"dropped-object": "忽略非对象格式的有效负载",
"dropped": "忽略不支持格式的有效负载类型",
"dropped-error": "转换有效负载失败"
},
"label": {
"o2j": "对象至 JSON 选项",
"pretty": "格式化 JSON 字符串"
}
},
"yaml": {
"errors": {
"dropped-object": "忽略非对象格式的有效负载",
"dropped": "忽略不支持格式的有效负载类型",
"dropped-error": "转换有效负载失败"
}
},
"xml": {
"label": {
"represent": "XML标签属性的属性名称",
"prefix": "标签文本内容的属性名称",
"advanced": "高级选项",
"x2o": "XML到对象选项"
},
"errors": {
"xml_js": "此节点仅处理XML字符串或JS对象."
}
},
"rpi-gpio": {
"label": {
"gpiopin": "GPIO",
"selectpin": "选择引脚",
"resistor": "电阻?",
"readinitial": "在部署/重新启动时读取引脚的初始状态?",
"type": "类型",
"initpin": "初始化引脚状态?",
"debounce": "去抖动",
"freq": "频率",
"button": "按钮",
"pimouse": "Pi 鼠标",
"pikeyboard": "Pi 键盘",
"left": "左",
"right": "右",
"middle": "中"
},
"resistor": {
"none": "无",
"pullup": "上拉电阻",
"pulldown": "下拉电阻"
},
"digout": "数字输出",
"pwmout": "PWM 输出",
"servo": "伺服输出",
"initpin0": "初始引脚电平 - 低 (0)",
"initpin1": "初始引脚电平 - 高 (1)",
"left": "左",
"right": "右",
"middle": "中",
"any": "任何",
"pinname": "引脚",
"alreadyuse": "已经在用",
"alreadyset": "已经设定为",
"tip": {
"pin": "<b>引脚在使用</b>: ",
"in": "提示: 仅接受数字输入 - 输出必须为 0 或 1.",
"dig": "提示: 如用数字输出 - 输入必须为 0 或 1.",
"pwm": "提示: 如用PWM输出 - 输入必须为0至100之间; 如用高频率可能会比预期占用更多CPU资源.",
"ser": "<b>提示</b>: 如用伺服输出 - 输入必须为0至100之间. 50为中间值."
},
"types": {
"digout": "数字输出",
"input": "输入",
"pullup": "含有上拉电阻的输入",
"pulldown": "含有下拉电阻的输入",
"pwmout": "PWM 输出",
"servo": "伺服输出"
},
"status": {
"stopped": "已停止",
"closed": "已关闭",
"not-running": "不运行"
},
"errors": {
"ignorenode": "忽略树莓派的特定节点",
"version": "版本命令失败",
"sawpitype": "查看Pi类型",
"libnotfound": "找不到树莓派 RPi.GPIO python库",
"alreadyset": "GPIO 引脚 __pin__ 已经被设定为类型: __type__",
"invalidpin": "无效 GPIO 引脚",
"invalidinput": "无效输入",
"needtobeexecutable": "__command__ 需要为可运行命令",
"mustbeexecutable": "nrgpio 需要为可运行",
"commandnotfound": "nrgpio 命令不存在",
"commandnotexecutable": "nrgpio 命令无法运行",
"error": "错误: __error__",
"pythoncommandnotfound": "nrpgio python 命令不运行"
}
},
"tail": {
"label": {
"filename": "文件名",
"type": "文件类型",
"splitlines": "拆分线 \\n?"
},
"action": {
"text": "文本 - 返回字符串",
"binary": "二进制 - 返回缓冲区"
},
"errors": {
"windowsnotsupport": "Windows目前不支持."
}
},
"file": {
"label": {
"filename": "文件名",
"action": "行为",
"addnewline": "向每个有效载荷添加换行符(\\ n?",
"createdir": "创建目录(如果不存在)?",
"outputas": "输出",
"breakchunks": "分拆成块",
"breaklines": "分拆成行",
"filelabel": "文件",
"sendError": "发生错误时发送消息(传统模式)",
"deletelabel": "删除 __file__"
},
"action": {
"append": "追加至文件",
"overwrite": "改写文件",
"delete": "删除文件"
},
"output": {
"utf8": "一条单独 utf8 字符串",
"buffer": "一条单独缓冲区对象",
"lines": "每行一条信息",
"stream": "缓冲区流"
},
"status": {
"wrotefile": "写入至文件: __file__",
"deletedfile": "删除文件: __file__",
"appendedfile": "追加至文件: __file__"
},
"errors": {
"nofilename": "未指定文件名",
"invaliddelete": "警告:无效删除。请在配置对话框中使用特定的删除选项",
"deletefail": "无法删除文件: __error__",
"writefail": "无法写入文件: __error__",
"appendfail": "无法追加到文件: __error__",
"createfail": "文件创建失败: __error__"
},
"tip": "提示: 文件名应该是绝对路径否则它将相对于Node-RED进程的工作目录。"
},
"split": {
"intro":"分裂 <code>msg.payload</code> 基于类型:",
"object":"<b>对象</b>",
"objectSend":"发送每个键/值对的消息",
"strBuff":"<b>字符串</b> / <b>缓冲区</b>",
"array":"<b>数组</b>",
"splitUsing":"拆分使用",
"splitLength":"固定长度",
"stream":"处理为消息流",
"addname":" 复制键到 "
},
"join":{
"mode":{
"mode":"模式",
"auto":"自动",
"custom":"手动"
},
"combine":"结合每一个",
"create":"创建输出",
"type":{
"string":"字符串",
"array":"数组",
"buffer":"缓冲区",
"object":"键/值对象",
"merged":"合并对象"
},
"using":"使用数值",
"key":"当作键",
"joinedUsing":"合并符号",
"send":"发送信息:",
"afterCount":"当达到一定数目的信息部件时",
"count":"数目",
"subsequent":"和每个后续的消息",
"afterTimeout":"第一条消息的超时后",
"seconds":"秒",
"complete":"在使用<code> msg.complete </ code>属性设置的消息后",
"tip":"此模式假定此节点与 <i>split</i> 或者接收到的消息将具有正确配置的 <code>msg.parts</code> 属性."
}
}

View File

@@ -114,6 +114,9 @@
label: function() {
return this.name||"switch";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var node = this;
var previousValueType = {value:"prev",label:this._("inject.previous"),hasValue:false};
@@ -199,6 +202,7 @@
} else if (type === "btwn") {
row2.hide();
row3.show();
btwnValue2Field.typedInput('show');
} else {
row2.hide();
row3.hide();

View File

@@ -29,7 +29,7 @@
<dt>Delete</dt>
<dd>delete a property.</dd>
<dt>Move</dt>
<dd>move or rename a property.</dt>
<dd>move or rename a property.</dd>
</dl>
<p>The "expression" type uses the <a href="http://jsonata.org/" target="_new">JSONata</a>
query and expression language.

View File

@@ -129,9 +129,9 @@ module.exports = function(RED) {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
if (rule.fromt === "msg") {
fromValue = RED.util.getMessageProperty(msg,rule.from);
} else if (rule.tot === 'flow') {
} else if (rule.fromt === 'flow') {
fromValue = node.context().flow.get(rule.from);
} else if (rule.tot === 'global') {
} else if (rule.fromt === 'global') {
fromValue = node.context().global.get(rule.from);
}
if (typeof fromValue === 'number' || fromValue instanceof Number) {
@@ -201,7 +201,7 @@ module.exports = function(RED) {
} else if (rule.t === 'set') {
target.set(property,value);
} else if (rule.t === 'change') {
current = target.get(msg,property);
current = target.get(property);
if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool' || fromType === 'str') && current === fromValue) {
// str representation of exact from number/boolean

View File

@@ -54,7 +54,7 @@
<dt>payload<span class="property-type">object | string | array | buffer</span></dt>
<dd>The behaviour of the node is determined by the type of <code>msg.payload</code>:
<ul>
<li><b>string</b>/<b>buffer</b> - the message is split using the specified character (default: <code>\n</code>), buffer sequence or into fixed lengths.
<li><b>string</b>/<b>buffer</b> - the message is split using the specified character (default: <code>\n</code>), buffer sequence or into fixed lengths.</li>
<li><b>array</b> - the message is split into either individual array elements, or arrays of a fixed-length.</li>
<li><b>object</b> - a message is sent for each key/value pair of the object.</li>
</ul>

View File

@@ -300,6 +300,9 @@ module.exports = function(RED) {
}
RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar));
} else {
if (node.propertyType === 'full') {
group.msg = RED.util.cloneMessage(group.msg);
}
RED.util.setMessageProperty(group.msg,node.property,group.payload);
}
if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) {
@@ -438,7 +441,7 @@ module.exports = function(RED) {
}
} else {
for (propertyKey in property) {
if (property.hasOwnProperty(propertyKey)) {
if (property.hasOwnProperty(propertyKey) && propertyKey !== '_msgid') {
group.payload[propertyKey] = property[propertyKey];
}
}

View File

@@ -0,0 +1,112 @@
<!--
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.
-->
<!DOCTYPE html>
<script type="text/x-red" data-template-name="sort">
<div class="form-row">
<label><i class="fa fa-dot-circle-o"></i> <span data-i18n="sort.key-type"></span></label>
<select id="node-input-keyType" style="width:200px;">
<option value="payload" data-i18n="sort.payload"></option>
<option value="exp" data-i18n="sort.exp"></option>
</select>
</div>
<div class="node-row-sort-key">
<div class="form-row">
<label><i class="fa fa-filter"></i> <span data-i18n="sort.key-exp"></span></label>
<input type="text" id="node-input-key" style="width:70%;">
</div>
</div>
<div class="form-row">
<label><i class="fa fa-random"></i> <span data-i18n="sort.order"></span></label>
<select id="node-input-order" style="width:200px;">
<option value="ascending" data-i18n="sort.ascending"></option>
<option value="descending" data-i18n="sort.descending"></option>
</select>
</div>
<div class="form-row" id="node-as_num">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-as_num" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-as_num" style="width: 70%;" data-i18n="sort.as-number"></label>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</script>
<script type="text/x-red" data-help-name="sort">
<p>A function that sorts a sequence of messages or payload of array type.</p>
<p>When paired with the <b>split</b> node, it will reorder the
messages.</p>
<p>The sorting order can be:</p>
<ul>
<li><b>ascending</b>,</li>
<li><b>descending</b>.</li>
</ul>
<p>For numbers, numerical ordering can be specified by a checkbox.</p>
<p>Sort key can be <code>payload</code> or any JSONata expression for sorting messages, element value or any JSONata expression for sorting payload of array type.</p>
<p>The sort node relies on the received messages to have <code>msg.parts</code> set for sorting messages. The split node generates this property, but can be manually created. It has the following properties:</p>
<p>
<ul>
<li><code>id</code> - an identifier for the group of messages</li>
<li><code>index</code> - the position within the group</li>
<li><code>count</code> - the total number of messages in the group</li>
</ul>
</p>
<p><b>Note:</b> This node internally keeps messages for its operation. In order to prevent unexpected memory usage, maximum number of messages kept can be specified. Default is no limit on number of messages.
<ul>
<li><code>sortMaxKeptMsgsCount</code> property set in <b>settings.js</b>.</li>
</ul>
</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('sort',{
category: 'function',
color:"#E2D96E",
defaults: {
name: { value:"" },
order: { value:"ascending" },
as_num : { value:false },
keyType : { value:"payload" },
key : { value:"" }
},
inputs:1,
outputs:1,
icon: "sort.png",
label: function() {
return this.name || "sort";
},
labelStyle: function() {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function() {
$("#node-input-key").typedInput({default:'jsonata', types:['jsonata']});
$("#node-input-keyType").change(function(e) {
var val = $(this).val();
$(".node-row-sort-key").toggle(val === 'exp');
});
$("#node-input-keyType").change();
$("#node-input-order").change();
}
});
</script>

212
nodes/core/logic/18-sort.js Normal file
View File

@@ -0,0 +1,212 @@
/**
* 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.
**/
module.exports = function(RED) {
"use strict";
var _max_kept_msgs_count = undefined;
function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) {
var name = "sortMaxKeptMsgsCount";
if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name];
}
else {
_max_kept_msgs_count = 0;
}
}
return _max_kept_msgs_count;
}
function eval_jsonata(node, code, val) {
try {
return RED.util.evaluateJSONataExpression(code, val);
}
catch (e) {
node.error(RED._("sort.invalid-exp"));
throw e;
}
}
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) {
RED.nodes.createNode(this, n);
var node = this;
var pending = get_context_val(node, 'pending', {})
var pending_count = 0;
var pending_id = 0;
var order = n.order || "ascending";
var as_num = n.as_num || false;
var key_is_payload = (n.keyType === 'payload');
var key_exp = undefined;
if (!key_is_payload) {
try {
key_exp = RED.util.prepareJSONataExpression(n.key, this);
}
catch (e) {
node.error(RED._("sort.invalid-exp"));
return;
}
}
var dir = (order === "ascending") ? 1 : -1;
var conv = as_num
? function(x) { return Number(x); }
: function(x) { return x; };
function gen_comp(key) {
return function(x, y) {
var xp = conv(key(x));
var yp = conv(key(y));
if (xp === yp) { return 0; }
if (xp > yp) { return dir; }
return -dir;
};
}
function send_group(group) {
var key = key_is_payload
? function(msg) { return msg.payload; }
: function(msg) {
return eval_jsonata(node, key_exp, msg);
};
var comp = gen_comp(key);
var msgs = group.msgs;
try {
msgs.sort(comp);
}
catch (e) {
return; // not send when error
}
for (var i = 0; i < msgs.length; i++) {
var msg = msgs[i];
msg.parts.index = i;
node.send(msg);
}
}
function sort_payload(msg) {
var payload = msg.payload;
if (Array.isArray(payload)) {
var key = key_is_payload
? function(elem) { return elem; }
: function(elem) {
return eval_jsonata(node, key_exp, elem);
};
var comp = gen_comp(key);
try {
payload.sort(comp);
}
catch (e) {
return false;
}
return true;
}
return false;
}
function check_parts(parts) {
if (parts.hasOwnProperty("id") &&
parts.hasOwnProperty("index")) {
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) {
var item = pending[key];
if((oldest === undefined) ||
(oldest.seq_no > item.seq_no)) {
oldest = item;
oldest_key = key;
}
}
if(oldest !== undefined) {
delete pending[oldest_key];
return oldest.msgs.length;
}
return 0;
}
function process_msg(msg) {
if (!msg.hasOwnProperty("parts")) {
if (sort_payload(msg)) {
node.send(msg);
}
return;
}
var parts = msg.parts;
if (!check_parts(parts)) {
return;
}
var gid = parts.id;
if (!pending.hasOwnProperty(gid)) {
pending[gid] = {
count: undefined,
msgs: [],
seq_no: pending_id++
};
}
var group = pending[gid];
var msgs = group.msgs;
msgs.push(msg);
if (parts.hasOwnProperty("count")) {
group.count = parts.count;
}
pending_count++;
if (group.count === msgs.length) {
delete pending[gid]
send_group(group);
pending_count -= msgs.length;
}
var max_msgs = max_kept_msgs_count(node);
if ((max_msgs > 0) && (pending_count > max_msgs)) {
pending_count -= remove_oldest_pending();
node.error(RED._("sort.too-many"), msg);
}
}
this.on("input", function(msg) {
process_msg(msg);
});
this.on("close", function() {
clear_pending();
})
}
RED.nodes.registerType("sort", SortNode);
}

View File

@@ -24,29 +24,31 @@
</div>
<hr align="middle"/>
<div class="form-row">
<label style="width:100%; border-bottom: 1px solid #eee;"><span data-i18n="csv.label.c2o"></span></label>
<label style="width:100%; border-bottom:1px solid #eee;"><span data-i18n="csv.label.c2o"></span></label>
</div>
<div class="form-row" style="padding-left: 20px;">
<div class="form-row" style="padding-left:20px;">
<label><i class="fa fa-sign-in"></i> <span data-i18n="csv.label.input"></span></label>
<input style="width:20px; vertical-align:top; margin-right: 5px;" type="checkbox" id="node-input-hdrin"><label style="width: auto;" for="node-input-hdrin"><span data-i18n="csv.label.firstrow"></span>
<span data-i18n="csv.label.skip-s"></span>&nbsp;<input type="text" id="node-input-skip" style="width:30px; height:25px;"/>&nbsp;<span data-i18n="csv.label.skip-e"></span><br/>
<label>&nbsp;</label>
<input style="width:20px; vertical-align:baseline; margin-right:5px;" type="checkbox" id="node-input-hdrin"><label style="width:auto; margin-top:7px;" for="node-input-hdrin"><span data-i18n="csv.label.firstrow"></span>
</div>
<div class="form-row" style="padding-left: 20px;">
<div class="form-row" style="padding-left:20px;">
<label><i class="fa fa-sign-out"></i> <span data-i18n="csv.label.output"></span></label>
<select type="text" id="node-input-multi" style="width: 250px;">
<select type="text" id="node-input-multi" style="width:250px;">
<option value="one" data-i18n="csv.output.row"></option>
<option value="mult" data-i18n="csv.output.array"></option>
</select>
</div>
<div class="form-row" style="margin-top: 20px">
<label style="width:100%; border-bottom: 1px solid #eee;"><span data-i18n="csv.label.o2c"></span></label>
<div class="form-row" style="margin-top:20px">
<label style="width:100%; border-bottom:1px solid #eee;"><span data-i18n="csv.label.o2c"></span></label>
</div>
<div class="form-row" style="padding-left: 20px;">
<div class="form-row" style="padding-left:20px;">
<label><i class="fa fa-sign-in"></i> <span data-i18n="csv.label.output"></span></label>
<input style="width:20px; vertical-align:top; margin-right: 5px;" type="checkbox" id="node-input-hdrout"><label style="width:auto;" for="node-input-hdrout"><span data-i18n="csv.label.includerow"></span></span>
<input style="width:20px; vertical-align:top; margin-right:5px;" type="checkbox" id="node-input-hdrout"><label style="width:auto;" for="node-input-hdrout"><span data-i18n="csv.label.includerow"></span></span>
</div>
<div class="form-row" style="padding-left: 20px;">
<div class="form-row" style="padding-left:20px;">
<label></label>
<label style="width: auto; margin-right: 10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
<label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label>
<select style="width:150px;" id="node-input-ret">
<option value='\n' data-i18n="csv.newline.linux"></option>
<option value='\r' data-i18n="csv.newline.mac"></option>
@@ -95,7 +97,8 @@
hdrout: {value:""},
multi: {value:"one",required:true},
ret: {value:'\\n'},
temp: {value:""}
temp: {value:""},
skip: {value:"0"}
},
inputs:1,
outputs:1,
@@ -107,6 +110,9 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
console.log(this.skip,$("#node-input-skip").val());
if (this.skip === undefined) { this.skip = 0; $("#node-input-skip").val("0");}
$("#node-input-skip").spinner({ min:0 });
if (this.sep == "," || this.sep == "\\t" || this.sep == ";" || this.sep == ":" || this.sep == " " || this.sep == "#") {
$("#node-input-select-sep").val(this.sep);
$("#node-input-sep").hide();

View File

@@ -28,6 +28,8 @@ module.exports = function(RED) {
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || false;
this.goodtmpl = true;
this.skip = parseInt(n.skip || 0);
this.store = [];
var tmpwarn = true;
var node = this;
@@ -72,15 +74,18 @@ module.exports = function(RED) {
}
else {
if ((node.template.length === 1) && (node.template[0] === '')) {
/* istanbul ignore else */
if (tmpwarn === true) { // just warn about missing template once
node.warn(RED._("csv.errors.obj_csv"));
tmpwarn = false;
}
ou = "";
for (var p in msg.payload[0]) {
/* istanbul ignore else */
if (msg.payload[0].hasOwnProperty(p)) {
/* istanbul ignore else */
if (typeof msg.payload[0][p] !== "object") {
var q = msg.payload[0][p];
var q = "" + msg.payload[0][p];
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
ou += node.quo + q + node.quo + node.sep;
@@ -100,9 +105,8 @@ module.exports = function(RED) {
ou += node.sep;
}
else {
// aaargh - resorting to eval here - but fairly contained front and back.
var p = RED.util.ensureString(eval("msg.payload[s]."+node.template[t]));
var p = RED.util.ensureString(RED.util.getMessageProperty(msg,"payload["+s+"]['"+node.template[t]+"']"));
/* istanbul ignore else */
if (p === "undefined") { p = ""; }
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
p = p.replace(/"/g, '""');
@@ -132,16 +136,26 @@ module.exports = function(RED) {
var a = []; // output array is needed for multiline option
var first = true; // is this the first line
var line = msg.payload;
var linecount = 0;
var tmp = "";
var reg = /^[-]?[0-9]*\.?[0-9]+$/;
if (msg.hasOwnProperty("parts")) {
linecount = msg.parts.index;
if (msg.parts.index > node.skip) { first = false; }
}
// For now we are just going to assume that any \r or \n means an end of line...
// got to be a weird csv that has singleton \r \n in it for another reason...
// Now process the whole file/line
for (var i = 0; i < line.length; i++) {
if (first && (linecount < node.skip)) {
if (line[i] === "\n") { linecount += 1; }
continue;
}
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")) { // look for first line break
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
node.template = clean(tmp.split(node.sep));
first = false;
}
@@ -173,12 +187,7 @@ module.exports = function(RED) {
o[node.template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
if (node.multi === "one") {
var newMessage = RED.util.cloneMessage(msg);
newMessage.payload = o;
node.send(newMessage); // either send
}
else { a.push(o); } // or add to the array
a.push(o); // add to the array
}
j = 0;
k = [""];
@@ -199,17 +208,50 @@ module.exports = function(RED) {
o[node.template[j]] = k[j];
}
if (JSON.stringify(o) !== "{}") { // don't send empty objects
if (node.multi === "one") {
var newMessage = RED.util.cloneMessage(msg);
newMessage.payload = o;
node.send(newMessage); // either send
}
else { a.push(o); } // or add to the aray
a.push(o); // add to the array
}
var has_parts = msg.hasOwnProperty("parts");
if (node.multi !== "one") {
msg.payload = a;
node.send(msg); // finally send the array
if (has_parts) {
if (JSON.stringify(o) !== "{}") {
node.store.push(o);
}
if (msg.parts.index + 1 === msg.parts.count) {
msg.payload = node.store;
delete msg.parts;
node.send(msg);
node.store = [];
}
}
else {
node.send(msg); // finally send the array
}
}
else {
var len = a.length;
for (var i = 0; i < len; i++) {
var newMessage = RED.util.cloneMessage(msg);
newMessage.payload = a[i];
if (!has_parts) {
newMessage.parts = {
id: msg._msgid,
index: i,
count: len
};
}
else {
newMessage.parts.index -= node.skip;
newMessage.parts.count -= node.skip;
if (node.hdrin) { // if we removed the header line then shift the counts by 1
newMessage.parts.index -= 1;
newMessage.parts.count -= 1;
}
}
node.send(newMessage);
}
}
node.linecount = 0;
}
catch(e) { node.error(e,msg); }
}

View File

@@ -31,6 +31,11 @@ module.exports = function(RED) {
try {
var $ = cheerio.load(msg.payload);
var pay = [];
var count = 0;
$(tag).each(function() {
count++;
});
var index = 0;
$(tag).each(function() {
if (node.as === "multi") {
var pay2 = null;
@@ -41,6 +46,13 @@ module.exports = function(RED) {
/* istanbul ignore else */
if (pay2) {
msg.payload = pay2;
msg.parts = {
id: msg._msgid,
index: index,
count: count,
type: "string",
ch: ""
};
node.send(msg);
}
}
@@ -50,6 +62,7 @@ module.exports = function(RED) {
if (node.ret === "attr") { pay.push( this.attribs ); }
//if (node.ret === "val") { pay.push( $(this).val() ); }
}
index++;
});
if ((node.as === "single") && (pay.length !== 0)) {
msg.payload = pay;

View File

@@ -4,11 +4,25 @@
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<hr align="middle"/>
<div class="form-row">
<label for="node-input-action"><span data-i18n="json.label.action"></span></label>
<select style="width:70%" id="node-input-action">
<option value="" data-i18n="json.label.actions.toggle"></option>
<option value="str" data-i18n="json.label.actions.str"></option>
<option value="obj" data-i18n="json.label.actions.obj"></option>
</select>
</div>
<div class="form-row">
<label data-i18n="json.label.property"></label>
<input type="text" id="node-input-property" style="width: 70%"/>
</div>
<hr align="middle"/>
<div class="form-row node-json-to-json-options">
<label style="width:100%; border-bottom: 1px solid #eee;"><span data-i18n="json.label.o2j"></span></label>
</div>
<div class="form-row" style="padding-left: 20px;">
<div class="form-row node-json-to-json-options" style="padding-left: 20px;">
<input style="width:20px; vertical-align:top; margin-right: 5px;" type="checkbox" id="node-input-pretty"><label style="width: auto;" for="node-input-pretty" data-i18n="json.label.pretty"></span>
</div>
</script>
@@ -30,6 +44,18 @@
</ul>
</dd>
</dl>
<h3>Details</h3>
<p>By default, the node operates on <code>msg.payload</code>, but can be configured
to convert any message property.</p>
<p>The node can also be configured to ensure a particular encoding instead of toggling
between the two. This can be used, for example, with the <code>HTTP In</code>
node to ensure the payload is a parsed object even if an incoming request
did not set its content-type correctly for the HTTP In node to do the conversion.</p>
<p>If the node is configured to ensure the property is encoded as a String and it
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
is selected.</p>
</script>
<script type="text/javascript">
@@ -38,6 +64,8 @@
color:"#DEBD5C",
defaults: {
name: {value:""},
property: { value:"payload" },
action: { value:"" },
pretty: {value:false}
},
inputs:1,
@@ -48,6 +76,20 @@
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
$("#node-input-action").change(function() {
if (this.value === "" || this.value === "str") {
$(".node-json-to-json-options").show();
} else {
$(".node-json-to-json-options").hide();
}
});
$("#node-input-action").change();
}
});
</script>

View File

@@ -20,29 +20,40 @@ module.exports = function(RED) {
function JSONNode(n) {
RED.nodes.createNode(this,n);
this.indent = n.pretty ? 4 : 0;
this.action = n.action||"";
this.property = n.property||"payload";
var node = this;
this.on("input", function(msg) {
if (msg.hasOwnProperty("payload")) {
if (typeof msg.payload === "string") {
try {
msg.payload = JSON.parse(msg.payload);
node.send(msg);
}
catch(e) { node.error(e.message,msg); }
}
else if (typeof msg.payload === "object") {
if (!Buffer.isBuffer(msg.payload)) {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (typeof value === "string") {
if (node.action === "" || node.action === "obj") {
try {
msg.payload = JSON.stringify(msg.payload,null,node.indent);
RED.util.setMessageProperty(msg,node.property,JSON.parse(value));
node.send(msg);
}
catch(e) { node.error(RED._("json.errors.dropped-error")); }
catch(e) { node.error(e.message,msg); }
} else {
node.send(msg);
}
}
else if (typeof value === "object") {
if (node.action === "" || node.action === "str") {
if (!Buffer.isBuffer(value)) {
try {
RED.util.setMessageProperty(msg,node.property,JSON.stringify(value,null,node.indent));
node.send(msg);
}
catch(e) { node.error(RED._("json.errors.dropped-error")); }
}
else { node.warn(RED._("json.errors.dropped-object")); }
} else {
node.send(msg);
}
else { node.warn(RED._("json.errors.dropped-object")); }
}
else { node.warn(RED._("json.errors.dropped")); }
}
else { node.send(msg); } // If no payload - just pass it on.
else { node.send(msg); } // If no property - just pass it on.
});
}
RED.nodes.registerType("json",JSONNode);

View File

@@ -49,9 +49,12 @@ module.exports = function(RED) {
else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) {
var dir = path.dirname(filename);
if (node.createDir) {
fs.ensureDir(dir, function(err) {
if (err) { node.error(RED._("file.errors.createfail",{error:err.toString()}),msg); }
});
try {
fs.ensureDirSync(dir);
} catch(err) {
node.error(RED._("file.errors.createfail",{error:err.toString()}),msg);
return;
}
}
var data = msg.payload;
@@ -60,11 +63,11 @@ module.exports = function(RED) {
}
if (typeof data === "boolean") { data = data.toString(); }
if (typeof data === "number") { data = data.toString(); }
if ((this.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; }
if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; }
node.data.push(Buffer.from(data));
while (node.data.length > 0) {
if (this.overwriteFile === "true") {
if (node.overwriteFile === "true") {
node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true });
node.wstream.on("error", function(err) {
node.error(RED._("file.errors.writefail",{error:err.toString()}),msg);