Merge branch 'dev' into tcp-node-better-split

This commit is contained in:
Nick O'Leary
2022-04-20 09:34:16 +01:00
committed by GitHub
181 changed files with 26968 additions and 21057 deletions

View File

@@ -0,0 +1,5 @@
<!--
05-junction.html
This file exists so that the runtime loads the Junction node into the registry,
but it is empty so it doesn't appear in the editor palette
-->

View File

@@ -0,0 +1,12 @@
module.exports = function(RED) {
"use strict";
function JunctionNode(n) {
RED.nodes.createNode(this,n);
this.on("input",function(msg, send, done) {
send(msg);
done();
});
}
RED.nodes.registerType("junction",JunctionNode);
}

View File

@@ -370,13 +370,6 @@
var id = $("#inject-time-type-select").val();
$(".inject-time-row").hide();
$("#inject-time-row-"+id).show();
if ((id == "none") || (id == "interval") || (id == "interval-time")) {
$("#node-once").show();
}
else {
$("#node-once").hide();
$("#node-input-once").prop('checked', false);
}
// Scroll down
var scrollDiv = $("#dialog-form").parent();

View File

@@ -160,6 +160,10 @@
},
messageSourceClick: function(sourceId, aliasId, path) {
// Get all of the nodes that could have logged this message
if (RED.nodes.workspace(sourceId)) {
RED.view.reveal(sourceId);
return
}
var candidateNodes = [RED.nodes.node(sourceId)]
if (path) {
for (var i=2;i<path.length;i++) {
@@ -235,10 +239,11 @@
// sourceNode should be the top-level node - one that is on a flow.
var sourceNode;
var pathParts;
var pathHierarchy;
if (o.path) {
// Path is a `/`-separated list of ids that identifies the
// complete parentage of the node that generated this message.
// flow-id/subflow-A-instance/subflow-A-type/subflow-B-instance/subflow-B-type/node-id
// flow-id/subflow-A-instance/subflow-B-instance
// If it has one id, that is a top level flow
// each subsequent id is the instance id of a subflow node
@@ -251,11 +256,41 @@
// Highlight the subflow instance node.
sourceNode = RED.nodes.node(pathParts[1]);
}
pathHierarchy = pathParts.map((id,index) => {
if (index === 0) {
return {
id: id,
label: RED.nodes.workspace(id).label
}
} else {
var instanceNode = RED.nodes.node(id)
return {
id: id,
label: (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8)).name)
}
}
})
if (pathParts.length === 1) {
pathHierarchy.push({
id: o.id,
label: sourceNode.name || sourceNode.type+":"+sourceNode.id
})
}
if (o._alias) {
let aliasNode = RED.nodes.node(o._alias)
if (aliasNode) {
pathHierarchy.push({
id: o._alias,
label: aliasNode.name || aliasNode.type+":"+aliasNode.id
})
}
}
} else {
// This is probably redundant...
sourceNode = RED.nodes.node(o.id) || RED.nodes.node(o.z);
}
if (sourceNode) {
var sourceFlow = RED.nodes.workspace(sourceNode.z)
o._source = {
id:sourceNode.id,
z:sourceNode.z,
@@ -266,7 +301,9 @@
// the top-level subflow instance node.
// This means the node's name is displayed in the sidebar.
_alias:o._alias,
path: pathParts
flowName: sourceFlow?(sourceFlow.label||sourceNode.z):sourceNode.z,
path: pathParts,
pathHierarchy: pathHierarchy
};
}
RED.debug.handleDebugMessage(o);
@@ -507,6 +544,9 @@
$("#node-input-complete").val($("#node-input-typed-complete").typedInput('value'));
}
$("#node-input-statusVal").val($("#node-input-typed-status").typedInput('value'));
},
onadd: function() {
RED.actions.invoke("core:generate-node-names",this)
}
});
})();

View File

@@ -32,8 +32,17 @@
<label for="node-input-timeout"><span data-i18n="exec.label.timeout"></span></label>
<input type="text" id="node-input-timeout" placeholder="30" style="width: 70px; margin-right: 5px;"><span data-i18n="inject.seconds"></span>
</div>
<div style="position:relative; height: 30px; text-align: right;"><div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div></div>
<div class="form-row node-input-link-row"></div>
<div class="form-row">
<label for="node-input-linkType" data-i18n="link.linkCallType"></label>
<select id="node-input-linkType" style="width: 70%">
<option value="static" data-i18n="link.staticLinkCall"></option>
<option value="dynamic" data-i18n="link.dynamicLinkCall"></option>
</select>
</div>
<div class="link-call-target-tree" style="position:relative; height: 30px; text-align: right;">
<div style="display:inline-block"><input type="text" id="node-input-link-target-filter"></div>
</div>
<div class="form-row node-input-link-row link-call-target-tree"></div>
</script>
<script type="text/javascript">
@@ -209,6 +218,8 @@
}
function onAdd() {
RED.actions.invoke("core:generate-node-names",this)
for (var i=0;i<this.links.length;i++) {
var n = RED.nodes.node(this.links[i]);
if (n && n.links.indexOf(this.id) === -1) {
@@ -259,6 +270,7 @@
defaults: {
name: {value:""},
links: { value: [], type:"link in[]"},
linkType: {value:"static"},
timeout: { value: "30", validate:RED.validators.number(true) }
},
inputs: 1,
@@ -271,7 +283,9 @@
if (this.name) {
return this.name;
}
if (this.links.length > 0) {
if (this.linkType === "dynamic") {
return this._("link.dynamicLinkLabel");
} else if (this.links.length > 0) {
var targetNode = RED.nodes.node(this.links[0]);
return targetNode && (targetNode.name || this._("link.linkCall"));
}
@@ -281,12 +295,31 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
console.log("link call oneditprepare")
const updateVisibility = function() {
const static = $('#node-input-linkType').val() !== "dynamic";
if(static) {
$("div.link-call-target-tree").show();
} else {
$("div.link-call-target-tree").hide();
}
}
$("#node-input-linkType").on("change",function(d){
updateVisibility();
});
if (["static","dynamic"].indexOf(this.linkType) < 0) {
$("#node-input-linkType").val('static');
}
updateVisibility();
onEditPrepare(this,"link in");
},
oneditsave: function() {
onEditSave(this);
},
oneditresize: resizeNodeList
oneditresize: resizeNodeList,
onadd: function() {
RED.actions.invoke("core:generate-node-names",this)
}
});
RED.nodes.registerType('link out',{

View File

@@ -14,10 +14,119 @@
* limitations under the License.
**/
/**
* @typedef LinkTarget
* @type {object}
* @property {string} id - ID of the target node.
* @property {string} name - Name of target Node
* @property {number} flowId - ID of flow where the target node exists
* @property {string} flowName - Name of flow where the target node exists
* @property {boolean} isSubFlow - True if the link-in node exists in a subflow instance
*/
module.exports = function(RED) {
"use strict";
const crypto = require("crypto");
const targetCache = (function () {
const registry = { id: {}, name: {} };
function getIndex(/** @type {[LinkTarget]}*/ targets, id) {
for (let index = 0; index < (targets || []).length; index++) {
const element = targets[index];
if (element.id === id) {
return index;
}
}
return -1;
}
/**
* Generate a target object from a node
* @param {LinkInNode} node
* @returns {LinkTarget} a link target object
*/
function generateTarget(node) {
const isSubFlow = node._flow.TYPE === "subflow";
return {
id: node.id,
name: node.name || node.id,
flowId: node._flow.flow.id,
flowName: isSubFlow ? node._flow.subflowDef.name : node._flow.flow.label,
isSubFlow: isSubFlow
}
}
return {
/**
* Get a list of targets registerd to this name
* @param {string} name Name of the target
* @param {boolean} [excludeSubflows] set `true` to exclude
* @returns {[LinkTarget]} Targets registerd to this name.
*/
getTargets(name, excludeSubflows) {
const targets = registry.name[name] || [];
if (excludeSubflows) {
return targets.filter(e => e.isSubFlow != true);
}
return targets;
},
/**
* Get a single target by registered name.
* To restrict to a single flow, include the `flowId`
* If there is no targets OR more than one target, null is returned
* @param {string} name Name of the node
* @param {string} [flowId]
* @returns {LinkTarget} target
*/
getTarget(name, flowId) {
/** @type {[LinkTarget]}*/
let possibleTargets = this.getTargets(name);
/** @type {LinkTarget}*/
let target;
if (possibleTargets.length && flowId) {
possibleTargets = possibleTargets.filter(e => e.flowId == flowId);
}
if (possibleTargets.length === 1) {
target = possibleTargets[0];
}
return target;
},
/**
* Get a target by node ID
* @param {string} nodeId ID of the node
* @returns {LinkTarget} target
*/
getTargetById(nodeId) {
return registry.id[nodeId];
},
register(/** @type {LinkInNode} */ node) {
const target = generateTarget(node);
const tByName = this.getTarget(target.name, target.flowId);
if (!tByName || tByName.id !== target.id) {
registry.name[target.name] = registry.name[target.name] || [];
registry.name[target.name].push(target)
}
registry.id[target.id] = target;
return target;
},
remove(node) {
const target = generateTarget(node);
const tn = this.getTarget(target.name, target.flowId);
if (tn) {
const targs = this.getTargets(tn.name);
const idx = getIndex(targs, tn.id);
if (idx > -1) {
targs.splice(idx, 1);
}
if (targs.length === 0) {
delete registry.name[tn.name];
}
}
delete registry.id[target.id];
},
clear() {
registry = { id: {}, name: {} };
}
}
})();
function LinkInNode(n) {
RED.nodes.createNode(this,n);
@@ -27,12 +136,14 @@ module.exports = function(RED) {
msg._event = n.event;
node.receive(msg);
}
targetCache.register(node);
RED.events.on(event,handler);
this.on("input", function(msg, send, done) {
send(msg);
done();
});
this.on("close",function() {
targetCache.remove(node);
RED.events.removeListener(event,handler);
});
}
@@ -74,31 +185,69 @@ module.exports = function(RED) {
function LinkCallNode(n) {
RED.nodes.createNode(this,n);
const node = this;
const target = n.links[0];
const staticTarget = typeof n.links === "string" ? n.links : n.links[0];
const linkType = n.linkType;
const messageEvents = {};
let timeout = parseFloat(n.timeout || "30")*1000;
let timeout = parseFloat(n.timeout || "30") * 1000;
if (isNaN(timeout)) {
timeout = 30000;
}
function getTargetNode(msg) {
const dynamicMode = linkType === "dynamic";
const target = dynamicMode ? msg.target : staticTarget
this.on("input", function(msg, send, done) {
msg._linkSource = msg._linkSource || [];
const messageEvent = {
id: crypto.randomBytes(14).toString('hex'),
node: node.id,
////1st see if the target is a direct node id
let foundNode;
if (targetCache.getTargetById(target)) {
foundNode = RED.nodes.getNode(target)
}
messageEvents[messageEvent.id] = {
msg: RED.util.cloneMessage(msg),
send,
done,
ts: setTimeout(function() {
timeoutMessage(messageEvent.id)
}, timeout )
};
msg._linkSource.push(messageEvent);
var targetNode = RED.nodes.getNode(target);
if (targetNode) {
targetNode.receive(msg);
if (target && !foundNode && dynamicMode) {
//next, look in **this flow only** for the node
let cachedTarget = targetCache.getTarget(target, node._flow.flow.id);
if (!cachedTarget) {
//single target node not found in registry!
//get all possible targets from regular flows (exclude subflow instances)
const possibleTargets = targetCache.getTargets(target, true);
if (possibleTargets.length === 1) {
//only 1 link-in found with this name - good, lets use it
cachedTarget = possibleTargets[0];
} else if (possibleTargets.length > 1) {
//more than 1 link-in has this name, raise an error
throw new Error(`Multiple link-in nodes named '${target}' found`);
}
}
if (cachedTarget) {
foundNode = RED.nodes.getNode(cachedTarget.id);
}
}
if (foundNode instanceof LinkInNode) {
return foundNode;
}
throw new Error(`target link-in node '${target || ""}' not found`);
}
this.on("input", function (msg, send, done) {
try {
const targetNode = getTargetNode(msg);
if (targetNode instanceof LinkInNode) {
msg._linkSource = msg._linkSource || [];
const messageEvent = {
id: crypto.randomBytes(14).toString('hex'),
node: node.id,
}
messageEvents[messageEvent.id] = {
msg: RED.util.cloneMessage(msg),
send,
done,
ts: setTimeout(function () {
timeoutMessage(messageEvent.id)
}, timeout)
};
msg._linkSource.push(messageEvent);
targetNode.receive(msg);
}
} catch (error) {
node.error(error, msg);
}
});

View File

@@ -581,12 +581,45 @@ RED.debug = (function() {
var metaRow = $('<div class="red-ui-debug-msg-meta"></div>').appendTo(msg);
$('<span class="red-ui-debug-msg-date">'+ getTimestamp()+'</span>').appendTo(metaRow);
if (sourceNode) {
$('<a>',{href:"#",class:"red-ui-debug-msg-name"}).text('node: '+(o.name||sourceNode.name||sourceNode.id))
var nodeLink = $('<a>',{href:"#",class:"red-ui-debug-msg-name"}).text("node: "+(o.name||sourceNode.name||sourceNode.id))
.appendTo(metaRow)
.on("click", function(evt) {
evt.preventDefault();
config.messageSourceClick(sourceNode.id, sourceNode._alias, sourceNode.path);
});
if (sourceNode.pathHierarchy) {
RED.popover.create({
tooltip: true,
target:nodeLink,
trigger: "hover",
size: "small",
direction: "bottom",
interactive: true,
content: function() {
const content = $("<div>")
sourceNode.pathHierarchy.forEach((pathPart,idx) => {
const link = $("<a>", {href:"#" ,style:'display: block'})
.css({
paddingLeft:((idx*10)+((idx === sourceNode.pathHierarchy.length - 1)?10:0))+"px",
paddingRight:'2px'
})
.text(pathPart.label)
.appendTo(content)
.on("click", function(evt) {
evt.preventDefault();
config.messageSourceClick(pathPart.id);
})
if (idx < sourceNode.pathHierarchy.length - 1) {
$('<i class="fa fa-angle-down" style="margin-right: 3px"></i>').prependTo(link)
}
})
return content
},
delay: { show: 50, hide: 150 }
});
}
} else if (name) {
$('<span class="red-ui-debug-msg-name">'+name+'</span>').appendTo(metaRow);
}

View File

@@ -399,7 +399,7 @@
$("#func-tabs-content").children().hide();
$("#" + tab.id).show();
let editor = $("#" + tab.id).find('.monaco-editor').first();
if(editor.length) {
if(editor.length) {
if(that.editor.nodered && that.editor.type == "monaco") {
that.editor.nodered.refreshModuleLibs(getLibsList());
}

View File

@@ -103,7 +103,7 @@ module.exports = function(RED) {
if(src[propName] === "true" || src[propName] === true) {
dst[propName] = true;
} else if(src[propName] === "false" || src[propName] === false) {
dst[propName] = true;
dst[propName] = false;
}
} else {
if(def != undefined) dst[propName] = def;
@@ -465,7 +465,7 @@ module.exports = function(RED) {
};
if(hasProperty(opts, "willTopic")) {
//will v5 properties must be set in the "properties" sub object
node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properies");
node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properties");
};
} else {
//update options

View File

@@ -100,14 +100,107 @@
<option value="obj" data-i18n="httpin.json"></option>
</select>
</div>
<div class="form-row form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="httpin.label.headers"></span></label>
</div>
<div class="form-row node-input-headers-container-row">
<ol id="node-input-headers-container"></ol>
</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">
</div>
<div class="form-tips" id="tip-json" hidden><span data-i18n="httpin.tip.req"></span></div>
</script>
<script type="text/javascript">
(function() {
const headerTypes = [
{ value: "Accept", label: "Accept", hasValue: false },
{ value: "Accept-Encoding", label: "Accept-Encoding", hasValue: false },
{ value: "Accept-Language", label: "Accept-Language", hasValue: false },
{ value: "Authorization", label: "Authorization", hasValue: false },
{ value: "Content-Type", label: "Content-Type", hasValue: false },
{ value: "Cache-Control", label: "Cache-Control", hasValue: false },
{ value: "User-Agent", label: "User-Agent", hasValue: false },
{ value: "Location", label: "Location", hasValue: false },
{ value: "other", label: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
{ value: "msg", label: "msg.", hasValue: true },
]
const headerOptions = {};
const defaultOptions = [
{ value: "other", label: "other", hasValue: true, icon: "red/images/typedInput/az.png" },
{ value: "msg", label: "msg.", hasValue: true },
];
headerOptions["accept"] = [
{ value: "text/plain", label: "text/plain", hasValue: false },
{ value: "text/html", label: "text/html", hasValue: false },
{ value: "application/json", label: "application/json", hasValue: false },
{ value: "application/xml", label: "application/xml", hasValue: false },
...defaultOptions,
];
headerOptions["accept-encoding"] = [
{ value: "gzip", label: "gzip", hasValue: false },
{ value: "deflate", label: "deflate", hasValue: false },
{ value: "compress", label: "compress", hasValue: false },
{ value: "br", label: "br", hasValue: false },
{ value: "gzip, deflate", label: "gzip, deflate", hasValue: false },
{ value: "gzip, deflate, br", label: "gzip, deflate, br", hasValue: false },
...defaultOptions,
];
headerOptions["accept-language"] = [
{ value: "*", label: "*", hasValue: false },
{ value: "en-GB, en-US, en;q=0.9", label: "en-GB, en-US, en;q=0.9", hasValue: false },
{ value: "de-AT, de-DE;q=0.9, en;q=0.5", label: "de-AT, de-DE;q=0.9, en;q=0.5", hasValue: false },
{ value: "es-mx,es,en;q=0.5", label: "es-mx,es,en;q=0.5", hasValue: false },
{ value: "fr-CH, fr;q=0.9, en;q=0.8", label: "fr-CH, fr;q=0.9, en;q=0.8", hasValue: false },
{ value: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", label: "zh-CN, zh-TW; q = 0.9, zh-HK; q = 0.8, zh; q = 0.7, en; q = 0.6", hasValue: false },
{ value: "ja-JP, jp", label: "ja-JP, jp", hasValue: false },
...defaultOptions,
];
headerOptions["content-type"] = [
{ value: "text/css", label: "text/css", hasValue: false },
{ value: "text/plain", label: "text/plain", hasValue: false },
{ value: "text/html", label: "text/html", hasValue: false },
{ value: "application/json", label: "application/json", hasValue: false },
{ value: "application/octet-stream", label: "application/octet-stream", hasValue: false },
{ value: "application/pdf", label: "application/pdf", hasValue: false },
{ value: "application/xml", label: "application/xml", hasValue: false },
{ value: "application/zip", label: "application/zip", hasValue: false },
{ value: "multipart/form-data", label: "multipart/form-data", hasValue: false },
{ value: "audio/aac", label: "audio/aac", hasValue: false },
{ value: "audio/ac3", label: "audio/ac3", hasValue: false },
{ value: "audio/basic", label: "audio/basic", hasValue: false },
{ value: "audio/mp4", label: "audio/mp4", hasValue: false },
{ value: "audio/ogg", label: "audio/ogg", hasValue: false },
{ value: "image/bmp", label: "image/bmp", hasValue: false },
{ value: "image/gif", label: "image/gif", hasValue: false },
{ value: "image/jpeg", label: "image/jpeg", hasValue: false },
{ value: "image/png", label: "image/png", hasValue: false },
{ value: "image/tiff", label: "image/tiff", hasValue: false },
...defaultOptions,
];
headerOptions["cache-control"] = [
{ value: "max-age=0", label: "max-age=0", hasValue: false },
{ value: "max-age=86400", label: "max-age=86400", hasValue: false },
{ value: "no-cache", label: "no-cache", hasValue: false },
...defaultOptions,
];
headerOptions["user-agent"] = [
{ value: "Mozilla/5.0", label: "Mozilla/5.0", hasValue: false },
...defaultOptions,
];
function getHeaderOptions(headerName) {
const lc = (headerName || "").toLowerCase();
let opts = headerOptions[lc];
return opts || defaultOptions;
}
RED.nodes.registerType('http request',{
category: 'network',
color:"rgb(231, 231, 174)",
@@ -121,7 +214,8 @@
persist: {value:false},
proxy: {type:"http proxy",required: false},
authType: {value: ""},
senderr: {value: false}
senderr: {value: false},
headers: { value: [] }
},
credentials: {
user: {type:"text"},
@@ -144,6 +238,7 @@
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
const node = this;
$("#node-input-useAuth").on("change", function() {
if ($(this).is(":checked")) {
$(".node-input-useAuth-row").show();
@@ -157,9 +252,10 @@
$('#node-input-user').val('');
$('#node-input-password').val('');
}
RED.tray.resize();
});
$("#node-input-authType-select").on("change", function() {
var val = $(this).val();
const val = $(this).val();
$("#node-input-authType").val(val);
if (val === "basic" || val === "digest") {
$(".node-input-basic-row").show();
@@ -171,6 +267,7 @@
$('#node-span-token').show();
$('#node-input-user').val('');
}
RED.tray.resize();
});
$("#node-input-method").on("change", function() {
if ($(this).val() == "GET") {
@@ -178,17 +275,18 @@
} else {
$(".node-input-paytoqs-row").hide();
}
RED.tray.resize();
});
if (this.paytoqs === true || this.paytoqs == "query") {
if (node.paytoqs === true || node.paytoqs == "query") {
$("#node-input-paytoqs").val("query");
} else if (this.paytoqs === "body") {
} else if (node.paytoqs === "body") {
$("#node-input-paytoqs").val("body");
} else {
$("#node-input-paytoqs").val("ignore");
}
if (this.authType) {
if (node.authType) {
$('#node-input-useAuth').prop('checked', true);
$("#node-input-authType-select").val(this.authType);
$("#node-input-authType-select").val(node.authType);
$("#node-input-authType-select").change();
} else {
$('#node-input-useAuth').prop('checked', false);
@@ -201,8 +299,9 @@
} else {
$("#node-row-tls").hide();
}
RED.tray.resize();
}
if (this.tls) {
if (node.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
@@ -218,8 +317,9 @@
} else {
$("#node-input-useProxy-row").hide();
}
RED.tray.resize();
}
if (this.proxy) {
if (node.proxy) {
$("#node-input-useProxy").prop("checked", true);
} else {
$("#node-input-useProxy").prop("checked", false);
@@ -235,7 +335,70 @@
} else {
$("#tip-json").hide();
}
RED.tray.resize();
});
const hasMatch = function (arr, value) {
return arr.some(function (ht) {
return ht.value === value
});
}
const headerList = $("#node-input-headers-container").css('min-height', '150px').css('min-width', '450px').editableList({
addItem: function (container, i, header) {
const row = $('<div/>').css({
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'flex'
}).appendTo(container);
const propertNameCell = $('<div/>').css({ 'flex-grow': 1 }).appendTo(row);
const propertyName = $('<input/>', { class: "node-input-header-name", type: "text", style: "width: 100%" })
.appendTo(propertNameCell)
.typedInput({ types: headerTypes });
const propertyValueCell = $('<div/>').css({ 'flex-grow': 1, 'margin-left': '10px' }).appendTo(row);
const propertyValue = $('<input/>', { class: "node-input-header-value", type: "text", style: "width: 100%" })
.appendTo(propertyValueCell)
.typedInput({
types: getHeaderOptions(header.keyType)
});
const setup = function(_header) {
const headerTypeIsAPreset = function(h) {return hasMatch(headerTypes, h) };
const headerValueIsAPreset = function(h, v) {return hasMatch(getHeaderOptions(h), v) };
const {keyType, keyValue, valueType, valueValue} = header;
if(keyType == "msg" || keyType == "other") {
propertyName.typedInput('type', keyType);
propertyName.typedInput('value', keyValue);
} else if (headerTypeIsAPreset(keyType)) {
propertyName.typedInput('type', keyType);
} else {
propertyName.typedInput('type', "other");
propertyName.typedInput('value', keyValue);
}
if(valueType == "msg" || valueType == "other") {
propertyValue.typedInput('type', valueType);
propertyValue.typedInput('value', valueValue);
} else if (headerValueIsAPreset(propertyName.typedInput('type'), valueType)) {
propertyValue.typedInput('type', valueType);
} else {
propertyValue.typedInput('type', "other");
propertyValue.typedInput('value', valueValue);
}
}
setup(header);
propertyName.on('change', function (event) {
propertyValue.typedInput('types', getHeaderOptions(propertyName.typedInput('type')));
});
},
removable: true
});
if (node.headers) {
for (let index = 0; index < node.headers.length; index++) {
const element = node.headers[index];
headerList.editableList('addItem', node.headers[index]);
}
}
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
@@ -244,6 +407,36 @@
if (!$("#node-input-useProxy").is(":checked")) {
$("#node-input-proxy").val("_ADD_");
}
const headers = $("#node-input-headers-container").editableList('items');
const node = this;
node.headers = [];
headers.each(function(i) {
const header = $(this);
const keyType = header.find(".node-input-header-name").typedInput('type');
const keyValue = header.find(".node-input-header-name").typedInput('value');
const valueType = header.find(".node-input-header-value").typedInput('type');
const valueValue = header.find(".node-input-header-value").typedInput('value');
if (keyType !== '' || keyType === 'other' || keyType === 'msg') {
node.headers.push({
keyType, keyValue, valueType, valueValue
})
}
});
},
oneditresize: function(size) {
const dlg = $("#dialog-form");
const expandRow = dlg.find('.node-input-headers-container-row');
let height = dlg.height() - 5;
if(expandRow && expandRow.length){
const siblingRows = dlg.find('> .form-row:not(.node-input-headers-container-row)');
for (let i = 0; i < siblingRows.size(); i++) {
const cr = $(siblingRows[i]);
if(cr.is(":visible"))
height -= cr.outerHeight(true);
}
$("#node-input-headers-container").editableList('height',height);
}
}
});
})();
</script>

View File

@@ -73,7 +73,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
var paytobody = false;
var redirectList = [];
var sendErrorsToCatch = n.senderr;
node.headers = n.headers || [];
var nodeHTTPPersistent = n["persist"];
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
@@ -105,6 +105,37 @@ in your Node-RED user directory (${RED.settings.userDir}).
timingLog = RED.settings.httpRequestTimingLog;
}
/**
* Case insensitive header value update util function
* @param {object} headersObject The opt.headers object to update
* @param {string} name The header name
* @param {string} value The header value to set (if blank, header is removed)
*/
const updateHeader = function(headersObject, name, value ) {
const hn = name.toLowerCase();
const keys = Object.keys(headersObject);
const matchingKeys = keys.filter(e => e.toLowerCase() == hn)
const updateKey = (k,v) => {
delete headersObject[k]; //delete incase of case change
if(v) { headersObject[name] = v } //re-add with requested name & value
}
if(matchingKeys.length == 0) {
updateKey(name, value)
} else {
matchingKeys.forEach(k => {
updateKey(k, value);
});
}
}
/**
* @param {Object} headersObject
* @param {string} name
* @return {any} value
*/
const getHeaderValue = (headersObject, name) => {
const asLowercase = name.toLowercase();
return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
}
this.on("input",function(msg,nodeSend,nodeDone) {
checkNodeAgentPatch();
//reset redirectList on each request
@@ -183,7 +214,6 @@ in your Node-RED user directory (${RED.settings.userDir}).
// TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
opts.decompress = false;
opts.method = method;
opts.headers = {};
opts.retry = 0;
opts.responseType = 'buffer';
opts.maxRedirects = 21;
@@ -229,34 +259,85 @@ in your Node-RED user directory (${RED.settings.userDir}).
]
}
var ctSet = "Content-Type"; // set default camel case
var clSet = "Content-Length";
let ctSet = "Content-Type"; // set default camel case
let clSet = "Content-Length";
const normaliseKnownHeader = function (name) {
const _name = name.toLowerCase();
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
switch (_name) {
case "content-type":
ctSet = name;
name = _name;
break;
case "content-length":
clSet = name;
name = _name;
break;
}
return name;
}
opts.headers = {};
//add msg.headers
//NOTE: ui headers will take precidence over msg.headers
if (msg.headers) {
if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
var headerHash = msg.headers['x-node-red-request-node'];
const headerHash = msg.headers['x-node-red-request-node'];
delete msg.headers['x-node-red-request-node'];
var hash = hashSum(msg.headers);
const hash = hashSum(msg.headers);
if (hash === headerHash) {
delete msg.headers;
}
}
if (msg.headers) {
for (var v in msg.headers) {
if (msg.headers.hasOwnProperty(v)) {
var name = v.toLowerCase();
if (name !== "content-type" && name !== "content-length") {
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
name = v;
}
else if (name === 'content-type') { ctSet = v; }
else { clSet = v; }
opts.headers[name] = msg.headers[v];
}
for (let hn in msg.headers) {
const name = normaliseKnownHeader(hn);
updateHeader(opts.headers, name, msg.headers[hn]);
}
}
}
//add/remove/update headers from UI.
if (node.headers.length) {
for (let index = 0; index < node.headers.length; index++) {
const header = node.headers[index];
let headerName, headerValue;
if (header.keyType === "other") {
headerName = header.keyValue
} else if (header.keyType === "msg") {
RED.util.evaluateNodeProperty(header.keyValue, header.keyType, node, msg, (err, value) => {
if (err) {
//ignore header
} else {
headerName = value;
}
});
} else {
headerName = header.keyType
}
if (!headerName) {
continue; //skip if header name is empyy
}
if (header.valueType === "other") {
headerValue = header.valueValue
} else if (header.valueType === "msg") {
RED.util.evaluateNodeProperty(header.valueValue, header.valueType, node, msg, (err, value) => {
if (err) {
//ignore header
} else {
headerValue = value;
}
});
} else {
headerValue = header.valueType
}
const hn = normaliseKnownHeader(headerName);
updateHeader(opts.headers, hn, headerValue);
}
}
if (msg.hasOwnProperty('followRedirects')) {
opts.followRedirect = !!msg.followRedirects;
}

View File

@@ -21,7 +21,7 @@
<label style="width:100%;"><span data-i18n="json.label.o2j"></span></label>
</div>
<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>
<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"></label>
</div>
</script>

View File

@@ -47,7 +47,7 @@
<div class="form-row">
<input type="checkbox" id="node-input-allowEmptySequence" style="margin-left:20px; margin-right: 10px; vertical-align:top; width:auto;">
<label for="node-input-allowEmptySequence" style="width:auto;" data-i18n="batch.interval.empty"></label>
</div>
</div>
</div>
<div class="node-row-msg-concat">