Compare commits

..

41 Commits

Author SHA1 Message Date
Kazuki-Nakanishi
3b3d696e45 Add the node setting tlsConfigDisableLocalFiles for tls node. (#1190)
* Add the node setting tlsConfigDisableLocalFiles for tls node.

* Fix the bug that shows node setting when specified in settings.js and exportable is false.
2017-03-09 19:58:34 +00:00
Kazuki-Nakanishi
34089aec70 Allow a node to declare what settings should be made available to the editor. (#1185)
* Implement register/exportNodeSettings.

* Change normaliseRegisterTypeName to normaliseNodeTypeName. Force it to name in a camel case.
2017-03-08 14:38:33 +00:00
Nick O'Leary
fca77a868f Allow a node to declare settings that should be exported 2017-03-01 15:01:07 +00:00
Nick O'Leary
4794fe495c Add events to test helper 2017-02-15 23:15:24 +00:00
Nick O'Leary
869fdbcc6a Remove event passing for icons/examples from the api layer 2017-02-15 23:07:50 +00:00
Dave Conway-Jones
702e6d3b51 slight filed size adjust for mqtt broker port field - allow 5 digits 2017-02-14 20:59:52 +00:00
Nick O'Leary
5f1e37b7fa Leave a node to nls its own port labels 2017-02-10 22:10:53 +00:00
Nick O'Leary
ec0209b175 Allow a node to override default labels 2017-02-09 23:24:16 +00:00
Nick O'Leary
a17dcbde0f Remove console log from Switch node 2017-02-08 20:43:26 +00:00
Nick O'Leary
fbd159a23a Add placeholder text on label inputs and clear buttons 2017-02-08 10:48:26 +00:00
Nick O'Leary
599a6bf050 Add port labels to Subflow nodes 2017-02-08 10:48:25 +00:00
Nick O'Leary
185b16a858 Keep port label form in sync with output reordering 2017-02-08 10:48:25 +00:00
Nick O'Leary
e7e3ed4923 Basic node label editor 2017-02-08 10:48:25 +00:00
Nick O'Leary
47df5476ba Add RED.stack as a common ui component 2017-02-08 10:48:25 +00:00
Nick O'Leary
d7c516ab00 Port label editor starting point 2017-02-08 10:48:24 +00:00
Dave Conway-Jones
50838970ec let css node handle ip addresses without trying to parse
and only warn once if no template (and then send object anyway)
to close #1142
2017-02-07 21:14:16 +00:00
Dave Conway-Jones
1d15ee7034 let Hypriot on Pi detect gpio correctly
clean up duplicate labels
2017-02-07 21:14:16 +00:00
Dave Conway-Jones
7029541b4f Let watch node recurse into subdirectories
to close #1140
2017-02-07 21:14:16 +00:00
Dave Conway-Jones
ada8e447cc exec node can be killed on demand 2017-02-07 21:14:16 +00:00
Dave Conway-Jones
1841fc18fa let trigger node set repeated outputs 2017-02-07 21:14:16 +00:00
Nick O'Leary
f2235dacdc Shuffle promises for creating default package.json 2017-01-28 14:21:22 +00:00
Nick O'Leary
50017c28da Allow port labels be i18n identifiers 2017-01-27 22:36:00 +00:00
Nick O'Leary
85b2a03a42 Create a package.json file in userDir if one doesn't exist 2017-01-27 22:35:17 +00:00
Nick O'Leary
829087550d Add inputLabels and outputLabels to node defn + Update Change node 2017-01-27 18:11:25 +00:00
Nick O'Leary
dd6f71fe85 Resize port labels based on content 2017-01-27 16:33:11 +00:00
Nick O'Leary
92a928680c Initial port label behaviour 2017-01-26 15:38:25 +00:00
Nick O'Leary
d008b1970c Add option to parse Template result as JSON before sending 2017-01-25 17:12:53 +00:00
Nick O'Leary
4affbb8c6b Numeric validator that accepts blank should accept undefined 2017-01-25 16:11:56 +00:00
Nick O'Leary
ddb2ea4b5f autoInstallModules option must honour version/pending_version 2017-01-25 11:07:02 +00:00
Nick O'Leary
a69683183f Refuse to update a non-local node module 2017-01-24 22:50:40 +00:00
Nick O'Leary
8d34f87667 Add websocketVerifyClient option to enable custom websocket auth
Fixes #1127
2017-01-24 21:37:08 +00:00
Nick O'Leary
128c4fe222 Add visual cue as to whether the workspace is focused 2017-01-24 16:14:03 +00:00
Nick O'Leary
b10141d71f Allow statusCode/headers to be set directly within HTTP Response node 2017-01-24 14:56:48 +00:00
Nick O'Leary
68e0b35364 Allow RED.validators.number to allow blank values as valid 2017-01-24 14:28:15 +00:00
Nick O'Leary
e27f5d0460 Add node module update api and expose in palette editor 2017-01-21 23:46:44 +00:00
Nick O'Leary
0720128bd4 Support dropping json files into the editor 2017-01-19 15:34:14 +00:00
Nick O'Leary
b8888a5d46 Add RED.utils.getNodeLabel utility function 2017-01-18 15:52:09 +00:00
Nick O'Leary
0857f979ff Update ui_spec for icon module path 2017-01-18 13:14:12 +00:00
Nick O'Leary
11f4ae019c Include module name in requests for node icons 2017-01-18 13:06:22 +00:00
Nick O'Leary
64daaeb310 Add file upload support to HTTP In node 2017-01-16 22:39:30 +00:00
Nick O'Leary
0646b0060e Display buffer data properly for truncated buffers under Object property 2017-01-16 17:43:39 +00:00
77 changed files with 2149 additions and 704 deletions

View File

@@ -116,6 +116,7 @@ module.exports = function(grunt) {
"editor/js/ui/common/popover.js",
"editor/js/ui/common/searchBox.js",
"editor/js/ui/common/tabs.js",
"editor/js/ui/common/stack.js",
"editor/js/ui/common/typedInput.js",
"editor/js/ui/actions.js",
"editor/js/ui/deploy.js",

View File

@@ -191,7 +191,7 @@ RED.history = (function() {
} else if (ev.t == "edit") {
for (i in ev.changes) {
if (ev.changes.hasOwnProperty(i)) {
if (ev.node._def.defaults[i].type) {
if (ev.node._def.defaults[i] && ev.node._def.defaults[i].type) {
// This is a config node property
var currentConfigNode = RED.nodes.node(ev.node[i]);
if (currentConfigNode) {
@@ -239,7 +239,7 @@ RED.history = (function() {
if (ev.outputMap) {
outputMap = {};
for (var port in ev.outputMap) {
if (ev.outputMap.hasOwnProperty(port) && ev.outputMap[port] !== -1) {
if (ev.outputMap.hasOwnProperty(port) && ev.outputMap[port] !== "-1") {
outputMap[ev.outputMap[port]] = port;
}
}

View File

@@ -24,7 +24,6 @@
url: 'nodes',
success: function(data) {
RED.nodes.setNodeList(data);
var nsCount = 0;
for (var i=0;i<data.length;i++) {
var ns = data[i];
@@ -158,6 +157,9 @@
typeList = "<ul><li>"+msg.types.join("</li><li>")+"</li></ul>";
RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success");
}
} else if (topic == "node/upgraded") {
RED.notify(RED._("palette.event.nodeUpgraded", {module:msg.module,version:msg.version}),"success");
RED.nodes.registry.setModulePendingUpdated(msg.module,msg.version);
}
// Refresh flow library to ensure any examples are updated
RED.library.loadFlowLibrary();

View File

@@ -42,6 +42,10 @@ RED.nodes = (function() {
var nodeDefinitions = {};
var exports = {
setModulePendingUpdated: function(module,version) {
moduleList[module].pending_version = version;
RED.events.emit("registry:module-updated",{module:module,version:version});
},
getModule: function(module) {
return moduleList[module];
},
@@ -78,6 +82,9 @@ RED.nodes = (function() {
local:ns.local,
sets:{}
};
if (ns.pending_version) {
moduleList[ns.module].pending_version = ns.pending_version;
}
moduleList[ns.module].sets[ns.name] = ns;
RED.events.emit("registry:node-set-added",ns);
},
@@ -115,6 +122,7 @@ RED.nodes = (function() {
},
registerNodeType: function(nt,def) {
nodeDefinitions[nt] = def;
def.type = nt;
if (def.category != "subflows") {
def.set = nodeSets[typeToId[nt]];
nodeSets[typeToId[nt]].added = true;
@@ -128,10 +136,15 @@ RED.nodes = (function() {
}
def["_"] = function() {
var args = Array.prototype.slice.call(arguments, 0);
var original = args[0];
if (args[0].indexOf(":") === -1) {
args[0] = ns+":"+args[0];
}
return RED._.apply(null,args);
var result = RED._.apply(null,args);
if (result === args[0]) {
result = original;
}
return result;
}
// TODO: too tightly coupled into palette UI
@@ -160,6 +173,8 @@ RED.nodes = (function() {
function addNode(n) {
if (n.type.indexOf("subflow") !== 0) {
n["_"] = n._def._;
} else {
n["_"] = RED._;
}
if (n._def.category == "config") {
configNodes[n.id] = n;
@@ -316,14 +331,6 @@ RED.nodes = (function() {
});
sf.name = subflowName;
}
sf._def = {
defaults:{},
icon:"subflow.png",
category: "subflows",
color: "#da9",
inputs: sf.in.length,
outputs: sf.out.length
}
subflows[sf.id] = sf;
RED.nodes.registerType("subflow:"+sf.id, {
defaults:{name:{value:""}},
@@ -336,12 +343,13 @@ RED.nodes = (function() {
label: function() { return this.name||RED.nodes.subflow(sf.id).name },
labelStyle: function() { return this.name?"node_label_italic":""; },
paletteLabel: function() { return RED.nodes.subflow(sf.id).name },
inputLabels: function(i) { return sf.inputLabels?sf.inputLabels[i]:null },
outputLabels: function(i) { return sf.outputLabels?sf.outputLabels[i]:null },
set:{
module: "node-red"
}
});
sf._def = RED.nodes.getType("subflow:"+sf.id);
}
function getSubflow(id) {
return subflows[id];
@@ -418,6 +426,7 @@ RED.nodes = (function() {
node.id = n.id;
node.type = n.type;
node.z = n.z;
if (node.type == "unknown") {
for (var p in n._orig) {
if (n._orig.hasOwnProperty(p)) {
@@ -465,6 +474,13 @@ RED.nodes = (function() {
node.wires[w.sourcePort].push(w.target.id);
}
}
if (n.inputs > 0 && n.inputLabels && !/^\s*$/.test(n.inputLabels.join(""))) {
node.inputLabels = n.inputLabels.slice();
}
if (n.outputs > 0 && n.outputLabels && !/^\s*$/.test(n.outputLabels.join(""))) {
node.outputLabels = n.outputLabels.slice();
}
}
return node;
}
@@ -502,6 +518,13 @@ RED.nodes = (function() {
node.out.push(nOut);
});
if (node.in.length > 0 && n.inputLabels && !/^\s*$/.test(n.inputLabels.join(""))) {
node.inputLabels = n.inputLabels.slice();
}
if (node.out.length > 0 && n.outputLabels && !/^\s*$/.test(n.outputLabels.join(""))) {
node.outputLabels = n.outputLabels.slice();
}
return node;
}
@@ -880,7 +903,17 @@ RED.nodes = (function() {
if (n.type !== "workspace" && n.type !== "tab" && n.type !== "subflow") {
def = registry.getNodeType(n.type);
if (!def || def.category != "config") {
var node = {x:n.x,y:n.y,z:n.z,type:0,wires:n.wires,changed:false,_config:{}};
var node = {
x:n.x,
y:n.y,
z:n.z,
type:0,
wires:n.wires,
inputLabels: n.inputLabels,
outputLabels: n.outputLabels,
changed:false,
_config:{}
};
if (createNewIds) {
if (subflow_blacklist[n.z]) {
continue;

View File

@@ -298,14 +298,16 @@ RED.clipboard = (function() {
$('#chart').on("dragenter",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1 ||
$.inArray("Files",event.originalEvent.dataTransfer.types) != -1) {
$("#dropTarget").css({display:'table'});
RED.keyboard.add("*", "escape" ,hideDropTarget);
}
});
$('#dropTarget').on("dragover",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1 ||
$.inArray("Files",event.originalEvent.dataTransfer.types) != -1) {
event.preventDefault();
}
})
@@ -313,10 +315,24 @@ RED.clipboard = (function() {
hideDropTarget();
})
.on("drop",function(event) {
var data = event.originalEvent.dataTransfer.getData("text/plain");
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
var data = event.originalEvent.dataTransfer.getData("text/plain");
data = data.substring(data.indexOf('['),data.lastIndexOf(']')+1);
RED.view.importNodes(data);
} else if ($.inArray("Files",event.originalEvent.dataTransfer.types) != -1) {
var files = event.originalEvent.dataTransfer.files;
if (files.length === 1) {
var file = files[0];
var reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
RED.view.importNodes(e.target.result);
};
})(file);
reader.readAsText(file);
}
}
hideDropTarget();
data = data.substring(data.indexOf('['),data.lastIndexOf(']')+1);
RED.view.importNodes(data);
event.preventDefault();
});

View File

@@ -0,0 +1,95 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
RED.stack = (function() {
function createStack(options) {
var container = options.container;
var entries = [];
return {
add: function(entry) {
entries.push(entry);
var entryContainer = $('<div class="palette-category">').appendTo(container);
var header = $('<div class="palette-header"></div>').appendTo(entryContainer);
var icon = $('<i class="fa fa-angle-down"></i>').appendTo(header);
$('<span></span>').html(entry.title).appendTo(header);
entry.content = $('<div class="editor-tray-content"></div>').appendTo(entryContainer);
if (entry.expanded) {
icon.addClass("expanded");
} else {
entry.content.hide();
}
header.click(function() {
if (options.singleExpanded) {
if (!entry.isExpanded()) {
for (var i=0;i<entries.length;i++) {
if (entries[i].isExpanded()) {
entries[i].collapse();
}
}
entry.expand();
}
} else {
entry.toggle();
}
});
entry.toggle = function() {
if (entry.isExpanded()) {
entry.collapse();
return false;
} else {
entry.expand();
return true;
}
};
entry.expand = function() {
if (!entry.isExpanded()) {
if (entry.onexpand) {
entry.onexpand.call(entry);
}
icon.addClass("expanded");
entry.content.slideDown(200);
return true;
}
};
entry.collapse = function() {
if (entry.isExpanded()) {
icon.removeClass("expanded");
entry.content.slideUp(200);
return true;
}
};
entry.isExpanded = function() {
return icon.hasClass("expanded");
};
return entry;
},
}
}
return {
create: createStack
}
})();

View File

@@ -73,6 +73,9 @@ RED.tabs = (function() {
ul.children().addClass("red-ui-tab");
function onTabClick() {
if (options.onclick) {
options.onclick(tabs[$(this).attr('href').slice(1)]);
}
activateTab($(this));
return false;
}

View File

@@ -212,18 +212,7 @@ RED.deploy = (function() {
tabLabel = tab.label;
}
}
var label = "";
if (typeof node._def.label == "function") {
try {
label = node._def.label.call(node);
} catch(err) {
console.log("Definition error: "+node_def.type+".label",err);
label = node_def.type;
}
} else {
label = node._def.label;
}
label = label || node.id;
var label = RED.utils.getNodeLabel(node,node.id);
return {tab:tabLabel,type:node.type,label:label};
}
function sortNodeInfo(A,B) {

View File

@@ -342,21 +342,14 @@ RED.diff = (function() {
function createNodeIcon(node,def) {
var nodeDiv = $("<div>",{class:"node-diff-node-entry-node"});
var colour = def.color;
var icon_url = "arrow-in.png";
var icon_url = RED.utils.getNodeIcon(def,node);
if (node.type === 'tab') {
colour = "#C0DEED";
icon_url = "subflow.png";
} else if (def.category === 'config') {
icon_url = "cog.png";
} else if (node.type === 'unknown') {
icon_url = "alert.png";
} else {
icon_url = def.icon;
}
nodeDiv.css('backgroundColor',colour);
var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv);
$('<div/>',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer);
$('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
return nodeDiv;
}

View File

@@ -191,7 +191,7 @@ RED.editor = (function() {
if (outputMap) {
RED.nodes.eachLink(function(l) {
if (l.source === node && outputMap.hasOwnProperty(l.sourcePort)) {
if (outputMap[l.sourcePort] === -1) {
if (outputMap[l.sourcePort] === "-1") {
removedLinks.push(l);
} else {
l.sourcePort = outputMap[l.sourcePort];
@@ -265,17 +265,8 @@ RED.editor = (function() {
var configNode = RED.nodes.node(node[property]);
var node_def = RED.nodes.getType(type);
if (configNode && node_def.label) {
if (typeof node_def.label == "function") {
try {
label = node_def.label.call(configNode);
} catch(err) {
console.log("Definition error: "+node_def.type+".label",err);
label = node_def.type;
}
} else {
label = node_def.label;
}
if (configNode) {
label = RED.utils.getNodeLabel(configNode,configNode.id);
}
input.val(label);
}
@@ -527,9 +518,8 @@ RED.editor = (function() {
return title;
}
function buildEditForm(tray,formId,type,ns) {
var trayBody = tray.find('.editor-tray-body');
var dialogForm = $('<form id="'+formId+'" class="form-horizontal" autocomplete="off"></form>').appendTo(trayBody);
function buildEditForm(container,formId,type,ns) {
var dialogForm = $('<form id="'+formId+'" class="form-horizontal" autocomplete="off"></form>').appendTo(container);
dialogForm.html($("script[data-template-name='"+type+"']").html());
ns = ns||"node-red";
dialogForm.find('[data-i18n]').each(function() {
@@ -558,6 +548,118 @@ RED.editor = (function() {
return dialogForm;
}
function refreshLabelForm(container,node) {
var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
var inputsDiv = $("#node-label-form-inputs");
var outputsDiv = $("#node-label-form-outputs");
var inputCount = node.inputs || node._def.inputs || 0;
var children = inputsDiv.children();
if (children.length < inputCount) {
for (i = children.length;i<inputCount;i++) {
buildLabelRow("input",i,"",inputPlaceholder).appendTo(inputsDiv);
}
} else if (children.length > inputCount) {
for (i=inputCount;i<children.length;i++) {
$(children[i]).remove();
}
}
var outputCount;
var i;
var formOutputs = $("#node-input-outputs").val();
if (formOutputs === undefined) {
outputCount = node.outputs || node._def.outputs || 0;
} else if (isNaN(formOutputs)) {
var outputMap = JSON.parse(formOutputs);
var keys = Object.keys(outputMap);
outputCount = 0;
var rows = [];
keys.forEach(function(p) {
var row = $("#node-label-form-output-"+p).parent();
if (row.length === 0 && outputMap[p] !== -1) {
row = buildLabelRow("output",p,"",outputPlaceholder);
}
if (outputMap[p] !== -1) {
outputCount++;
rows.push({i:parseInt(outputMap[p]),r:row});
}
});
rows.sort(function(A,B) {
return A.i-B.i;
})
outputsDiv.children().detach();
rows.forEach(function(r,i) {
r.r.find("label").html((i+1)+".");
r.r.appendTo(outputsDiv);
})
} else {
outputCount = Math.max(0,parseInt(formOutputs));
}
children = outputsDiv.children();
if (children.length < outputCount) {
for (i = children.length;i<outputCount;i++) {
buildLabelRow("output",i,"").appendTo(outputsDiv);
}
} else if (children.length > outputCount) {
for (i=outputCount;i<children.length;i++) {
$(children[i]).remove();
}
}
}
function buildLabelRow(type, index, value, placeHolder) {
var result = $('<div>',{style:"margin: 5px 0px"});
var id = "node-label-form-"+type+"-"+index;
$('<label>',{for:id,style:"margin-right: 20px; text-align: right; width: 30px;"}).html((index+1)+".").appendTo(result);
var input = $('<input>',{type:"text",id:id, placeholder: placeHolder}).val(value).appendTo(result);
var clear = $('<button class="editor-button editor-button-small" style="margin-left: 10px"><i class="fa fa-times"></i></button>').appendTo(result);
clear.click(function(evt) {
evt.preventDefault();
input.val("");
})
return result;
}
function buildLabelForm(container,node) {
var dialogForm = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').appendTo(container);
var inputCount = node.inputs || node._def.inputs || 0;
var outputCount = node.outputs || node._def.outputs || 0;
if (node.type === 'subflow') {
inputCount = node.in.length;
outputCount = node.out.length;
}
var inputLabels = node.inputLabels || [];
var outputLabels = node.outputLabels || [];
var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel");
var i,row;
$('<div class="form-row"><i class="fa fa-tag"></i> <span data-i18n="editor.labelInputs"></span><div id="node-label-form-inputs"></div></div>').appendTo(dialogForm);
var inputsDiv = $("#node-label-form-inputs");
if (inputCount > 0) {
for (i=0;i<inputCount;i++) {
buildLabelRow("input",i,inputLabels[i],inputPlaceholder).appendTo(inputsDiv);
}
} else {
}
$('<div class="form-row"><i class="fa fa-tag"></i> <span data-i18n="editor.labelOutputs"></span><div id="node-label-form-outputs"></div></div>').appendTo(dialogForm);
var outputsDiv = $("#node-label-form-outputs");
if (outputCount > 0) {
for (i=0;i<outputCount;i++) {
buildLabelRow("output",i,outputLabels[i],outputPlaceholder).appendTo(outputsDiv);
}
} else {
}
}
function showEditDialog(node) {
var editing_node = node;
editStack.push(node);
@@ -680,11 +782,11 @@ RED.editor = (function() {
}
}
var newValue;
if (editing_node._def.defaults) {
for (d in editing_node._def.defaults) {
if (editing_node._def.defaults.hasOwnProperty(d)) {
var input = $("#node-input-"+d);
var newValue;
if (input.attr('type') === "checkbox") {
newValue = input.prop('checked');
} else if ("format" in editing_node._def.defaults[d] && editing_node._def.defaults[d].format !== "" && input[0].nodeName === "DIV") {
@@ -693,8 +795,42 @@ RED.editor = (function() {
newValue = input.val();
}
if (newValue != null) {
if (d === "outputs" && (newValue.trim() === "" || isNaN(newValue))) {
continue;
if (d === "outputs") {
if (newValue.trim() === "") {
continue;
}
if (isNaN(newValue)) {
outputMap = JSON.parse(newValue);
var outputCount = 0;
var outputsChanged = false;
var keys = Object.keys(outputMap);
keys.forEach(function(p) {
if (isNaN(p)) {
// New output;
outputCount ++;
delete outputMap[p];
} else {
outputMap[p] = outputMap[p]+"";
if (outputMap[p] !== "-1") {
outputCount++;
if (outputMap[p] !== p) {
// Output moved
outputsChanged = true;
} else {
delete outputMap[p];
}
} else {
// Output removed
outputsChanged = true;
}
}
});
newValue = outputCount;
if (outputsChanged) {
changed = true;
}
}
}
if (editing_node[d] != newValue) {
if (editing_node._def.defaults[d].type) {
@@ -726,14 +862,31 @@ RED.editor = (function() {
var credsChanged = updateNodeCredentials(editing_node,credDefinition,prefix);
changed = changed || credsChanged;
}
if (editing_node.hasOwnProperty("_outputs")) {
outputMap = editing_node._outputs;
delete editing_node._outputs;
if (Object.keys(outputMap).length > 0) {
changed = true;
}
}
// if (editing_node.hasOwnProperty("_outputs")) {
// outputMap = editing_node._outputs;
// delete editing_node._outputs;
// if (Object.keys(outputMap).length > 0) {
// changed = true;
// }
// }
var removedLinks = updateNodeProperties(editing_node,outputMap);
var inputLabels = $("#node-label-form-inputs").children().find("input");
var outputLabels = $("#node-label-form-outputs").children().find("input");
newValue = inputLabels.map(function() { return $(this).val();}).toArray().slice(0,editing_node.inputs);
if (JSON.stringify(newValue) !== JSON.stringify(editing_node.inputLabels)) {
changes.inputLabels = editing_node.inputLabels;
editing_node.inputLabels = newValue;
changed = true;
}
newValue = outputLabels.map(function() { return $(this).val();}).toArray().slice(0,editing_node.outputs);
if (JSON.stringify(newValue) !== JSON.stringify(editing_node.outputLabels)) {
changes.outputLabels = editing_node.outputLabels;
editing_node.outputLabels = newValue;
changed = true;
}
if (changed) {
var wasChanged = editing_node.changed;
editing_node.changed = true;
@@ -782,8 +935,9 @@ RED.editor = (function() {
],
resize: function(dimensions) {
editTrayWidthCache[type] = dimensions.width;
$(".editor-tray-content").height(dimensions.height - 78);
var form = $(".editor-tray-content form").height(dimensions.height - 78 - 40);
if (editing_node && editing_node._def.oneditresize) {
var form = $("#dialog-form");
try {
editing_node._def.oneditresize.call(editing_node,{width:form.width(),height:form.height()});
} catch(err) {
@@ -792,6 +946,25 @@ RED.editor = (function() {
}
},
open: function(tray) {
var trayFooter = tray.find(".editor-tray-footer");
var trayBody = tray.find('.editor-tray-body');
trayBody.parent().css('overflow','hidden');
var stack = RED.stack.create({
container: trayBody,
singleExpanded: true
});
var nodeProperties = stack.add({
title: RED._("editor.nodeProperties"),
expanded: true
});
var portLabels = stack.add({
title: RED._("editor.portLabels"),
onexpand: function() {
refreshLabelForm(this.content,node);
}
});
if (editing_node) {
RED.sidebar.info.refresh(editing_node);
}
@@ -801,9 +974,11 @@ RED.editor = (function() {
} else {
ns = node._def.set.id;
}
var dialogForm = buildEditForm(tray,"dialog-form",type,ns);
buildEditForm(nodeProperties.content,"dialog-form",type,ns);
buildLabelForm(portLabels.content,node);
prepareEditDialog(node,node._def,"node-input");
dialogForm.i18n();
trayBody.i18n();
},
close: function() {
if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
@@ -901,7 +1076,7 @@ RED.editor = (function() {
}
trayFooter.append('<span id="node-config-dialog-scope-container"><span id="node-config-dialog-scope-warning" data-i18n="[title]editor.errors.scopeChange"><i class="fa fa-warning"></i></span><select id="node-config-dialog-scope"></select></span>');
var dialogForm = buildEditForm(tray,"node-config-dialog-edit-form",type,ns);
var dialogForm = buildEditForm(tray.find('.editor-tray-body'),"node-config-dialog-edit-form",type,ns);
prepareEditDialog(editing_config_node,node_def,"node-config-input");
if (editing_config_node._def.exclusive) {
@@ -1207,17 +1382,7 @@ RED.editor = (function() {
RED.nodes.eachConfig(function(config) {
if (config.type == type && (!config.z || config.z === activeWorkspace.id)) {
var label = "";
if (typeof node_def.label == "function") {
try {
label = node_def.label.call(config);
} catch(err) {
console.log("Definition error: "+node_def.type+".label",err);
label = node_def.type;
}
} else {
label = node_def.label;
}
var label = RED.utils.getNodeLabel(config,config.id);
config.__label__ = label;
configNodes.push(config);
}
@@ -1284,6 +1449,21 @@ RED.editor = (function() {
editing_node.info = newDescription;
changed = true;
}
var inputLabels = $("#node-label-form-inputs").children().find("input");
var outputLabels = $("#node-label-form-outputs").children().find("input");
var newValue = inputLabels.map(function() { return $(this).val();}).toArray().slice(0,editing_node.inputs);
if (JSON.stringify(newValue) !== JSON.stringify(editing_node.inputLabels)) {
changes.inputLabels = editing_node.inputLabels;
editing_node.inputLabels = newValue;
changed = true;
}
newValue = outputLabels.map(function() { return $(this).val();}).toArray().slice(0,editing_node.outputs);
if (JSON.stringify(newValue) !== JSON.stringify(editing_node.outputLabels)) {
changes.outputLabels = editing_node.outputLabels;
editing_node.outputLabels = newValue;
changed = true;
}
RED.palette.refresh();
@@ -1321,7 +1501,10 @@ RED.editor = (function() {
}
}
],
resize: function() {
resize: function(dimensions) {
$(".editor-tray-content").height(dimensions.height - 78);
var form = $(".editor-tray-content form").height(dimensions.height - 78 - 40);
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var editorRow = $("#dialog-form>div.node-text-editor-row");
var height = $("#dialog-form").height();
@@ -1333,10 +1516,28 @@ RED.editor = (function() {
subflowEditor.resize();
},
open: function(tray) {
var trayFooter = tray.find(".editor-tray-footer");
var trayBody = tray.find('.editor-tray-body');
trayBody.parent().css('overflow','hidden');
var stack = RED.stack.create({
container: trayBody,
singleExpanded: true
});
var nodeProperties = stack.add({
title: RED._("editor.nodeProperties"),
expanded: true
});
var portLabels = stack.add({
title: RED._("editor.portLabels")
});
if (editing_node) {
RED.sidebar.info.refresh(editing_node);
}
var dialogForm = buildEditForm(tray,"dialog-form","subflow-template");
var dialogForm = buildEditForm(nodeProperties.content,"dialog-form","subflow-template");
subflowEditor = RED.editor.createEditor({
id: 'subflow-input-info-editor',
mode: 'ace/mode/markdown',
@@ -1355,7 +1556,9 @@ RED.editor = (function() {
}
});
$("#subflow-dialog-user-count").html(RED._("subflow.subflowInstances", {count:userCount})).show();
dialogForm.i18n();
buildLabelForm(portLabels.content,subflow);
trayBody.i18n();
},
close: function() {
if (RED.view.state() != RED.state.IMPORT_DRAGGING) {
@@ -1417,7 +1620,7 @@ RED.editor = (function() {
},
open: function(tray) {
var trayBody = tray.find('.editor-tray-body');
var dialogForm = buildEditForm(tray,'dialog-form','_expression','editor');
var dialogForm = buildEditForm(tray.find('.editor-tray-body'),'dialog-form','_expression','editor');
var funcSelect = $("#node-input-expression-func");
Object.keys(jsonata.functions).forEach(function(f) {
funcSelect.append($("<option></option>").val(f).text(f));

View File

@@ -31,6 +31,17 @@ RED.palette.editor = (function() {
var eventTimers = {};
var activeFilter = "";
function semVerCompare(A,B) {
var aParts = A.split(".").map(function(m) { return parseInt(m);});
var bParts = B.split(".").map(function(m) { return parseInt(m);});
for (var i=0;i<3;i++) {
var j = aParts[i]-bParts[i];
if (j<0) { return -1 }
if (j>0) { return 1 }
}
return 0;
}
function delayCallback(start,callback) {
var delta = Date.now() - start;
if (delta < 300) {
@@ -64,14 +75,21 @@ RED.palette.editor = (function() {
});
})
}
function installNodeModule(id,shade,callback) {
function installNodeModule(id,version,shade,callback) {
var requestBody = {
module: id
};
if (callback === undefined) {
callback = shade;
shade = version;
} else {
requestBody.version = version;
}
shade.show();
$.ajax({
url:"nodes",
type: "POST",
data: JSON.stringify({
module: id
}),
data: JSON.stringify(requestBody),
contentType: "application/json; charset=utf-8"
}).done(function(data,textStatus,xhr) {
shade.hide();
@@ -266,19 +284,19 @@ RED.palette.editor = (function() {
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
}
}
nodeEntry.updateButton.hide();
// if (loadedIndex.hasOwnProperty(module)) {
// if (moduleInfo.version !== loadedIndex[module].version) {
// nodeEntry.updateButton.show();
// nodeEntry.updateButton.html(RED._('palette.editor.update',{version:loadedIndex[module].version}));
// } else {
// nodeEntry.updateButton.hide();
// }
//
// } else {
// nodeEntry.updateButton.hide();
// }
if (moduleInfo.pending_version) {
nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
nodeEntry.updateButton.html(RED._('palette.editor.updated')).addClass('disabled').show();
} else if (loadedIndex.hasOwnProperty(module)) {
if (semVerCompare(loadedIndex[module].version,moduleInfo.version) === 1) {
nodeEntry.updateButton.show();
nodeEntry.updateButton.html(RED._('palette.editor.update',{version:loadedIndex[module].version}));
} else {
nodeEntry.updateButton.hide();
}
} else {
nodeEntry.updateButton.hide();
}
}
}
@@ -515,7 +533,7 @@ RED.palette.editor = (function() {
var titleRow = $('<div class="palette-module-meta palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
$('<span>').html(entry.name).appendTo(titleRow);
var metaRow = $('<div class="palette-module-meta palette-module-version"><i class="fa fa-tag"></i></div>').appendTo(headerRow);
$('<span>').html(entry.version).appendTo(metaRow);
var versionSpan = $('<span>').html(entry.version).appendTo(metaRow);
var buttonRow = $('<div>',{class:"palette-module-meta"}).appendTo(headerRow);
var setButton = $('<a href="#" class="editor-button editor-button-small palette-module-set-button"><i class="fa fa-angle-right palette-module-node-chevron"></i> </a>').appendTo(buttonRow);
var setCount = $('<span>').appendTo(setButton);
@@ -524,6 +542,27 @@ RED.palette.editor = (function() {
var updateButton = $('<a href="#" class="editor-button editor-button-small"></a>').html(RED._('palette.editor.update')).appendTo(buttonGroup);
updateButton.click(function(evt) {
evt.preventDefault();
if ($(this).hasClass('disabled')) {
return;
}
$("#palette-module-install-confirm").data('module',entry.name);
$("#palette-module-install-confirm").data('version',loadedIndex[entry.name].version);
$("#palette-module-install-confirm").data('shade',shade);
$("#palette-module-install-confirm-body").html(entry.local?
RED._("palette.editor.confirm.update.body"):
RED._("palette.editor.confirm.cannotUpdate.body")
);
$(".palette-module-install-confirm-button-install").hide();
$(".palette-module-install-confirm-button-remove").hide();
if (entry.local) {
$(".palette-module-install-confirm-button-update").show();
} else {
$(".palette-module-install-confirm-button-update").hide();
}
$("#palette-module-install-confirm")
.dialog('option', 'title',RED._("palette.editor.confirm.update.title"))
.dialog('open');
})
@@ -536,6 +575,7 @@ RED.palette.editor = (function() {
$("#palette-module-install-confirm-body").html(RED._("palette.editor.confirm.remove.body"));
$(".palette-module-install-confirm-button-install").hide();
$(".palette-module-install-confirm-button-remove").show();
$(".palette-module-install-confirm-button-update").hide();
$("#palette-module-install-confirm")
.dialog('option', 'title', RED._("palette.editor.confirm.remove.title"))
.dialog('open');
@@ -555,6 +595,7 @@ RED.palette.editor = (function() {
setCount: setCount,
container: container,
shade: shade,
versionSpan: versionSpan,
sets: {}
}
setButton.click(function(evt) {
@@ -727,11 +768,13 @@ RED.palette.editor = (function() {
e.preventDefault();
if (!$(this).hasClass('disabled')) {
$("#palette-module-install-confirm").data('module',entry.id);
$("#palette-module-install-confirm").data('version',entry.version);
$("#palette-module-install-confirm").data('url',entry.url);
$("#palette-module-install-confirm").data('shade',shade);
$("#palette-module-install-confirm-body").html(RED._("palette.editor.confirm.install.body"));
$(".palette-module-install-confirm-button-install").show();
$(".palette-module-install-confirm-button-remove").hide();
$(".palette-module-install-confirm-button-update").hide();
$("#palette-module-install-confirm")
.dialog('option', 'title', RED._("palette.editor.confirm.install.title"))
.dialog('open');
@@ -780,8 +823,9 @@ RED.palette.editor = (function() {
class: "primary palette-module-install-confirm-button-install",
click: function() {
var id = $(this).data('module');
var version = $(this).data('version');
var shade = $(this).data('shade');
installNodeModule(id,shade,function(xhr) {
installNodeModule(id,version,shade,function(xhr) {
if (xhr) {
if (xhr.responseJSON) {
RED.notify(RED._('palette.editor.errors.installFailed',{module: id,message:xhr.responseJSON.message}));
@@ -807,12 +851,34 @@ RED.palette.editor = (function() {
}
})
$( this ).dialog( "close" );
}
},
{
text: RED._("palette.editor.confirm.button.update"),
class: "primary palette-module-install-confirm-button-update",
click: function() {
var id = $(this).data('module');
var version = $(this).data('version');
var shade = $(this).data('shade');
shade.show();
installNodeModule(id,version,shade,function(xhr) {
if (xhr) {
if (xhr.responseJSON) {
RED.notify(RED._('palette.editor.errors.updateFailed',{module: id,message:xhr.responseJSON.message}));
}
}
});
$( this ).dialog( "close" );
}
}
]
})
RED.events.on('registry:module-updated', function(ns) {
refreshNodeModule(ns.module);
});
RED.events.on('registry:node-set-enabled', function(ns) {
refreshNodeModule(ns.module);
});

View File

@@ -149,14 +149,9 @@ RED.palette = (function() {
if (def.icon) {
var icon_url = "arrow-in.png";
try {
icon_url = (typeof def.icon === "function" ? def.icon.call({}) : def.icon);
} catch(err) {
console.log("Definition error: "+nt+".icon",err);
}
var icon_url = RED.utils.getNodeIcon(def);
var iconContainer = $('<div/>',{class:"palette_icon_container"+(def.align=="right"?" palette_icon_container_right":"")}).appendTo(d);
$('<div/>',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer);
$('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
}
d.style.backgroundColor = def.color;
@@ -236,7 +231,7 @@ RED.palette = (function() {
// it here makes me sad
//console.log(ui.helper.position());
ui.position.left += 17.5;
if (def.inputs > 0 && def.outputs > 0) {
mouseX = ui.position.left+(ui.helper.width()/2) - chartOffset.left + chart.scrollLeft();
mouseY = ui.position.top+(ui.helper.height()/2) - chartOffset.top + chart.scrollTop();

View File

@@ -27,19 +27,11 @@ RED.search = (function() {
var results = [];
function indexNode(n) {
var l = "";
if (n._def && n._def.label) {
l = n._def.label;
try {
l = (typeof l === "function" ? l.call(n) : l);
if (l) {
l = (""+l).toLowerCase();
index[l] = index[l] || {};
index[l][n.id] = {node:n,label:l}
}
} catch(err) {
console.log("Definition error: "+n.type+".label",err);
}
var l = RED.utils.getNodeLabel(n);
if (l) {
l = (""+l).toLowerCase();
index[l] = index[l] || {};
index[l][n.id] = {node:n,label:l}
}
l = l||n.label||n.name||n.id||"";
@@ -189,25 +181,14 @@ RED.search = (function() {
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
var colour = def.color;
var icon_url = "arrow-in.png";
var icon_url = RED.utils.getNodeIcon(def,node);
if (node.type === 'tab') {
colour = "#C0DEED";
icon_url = "subflow.png";
} else if (def.category === 'config') {
icon_url = "cog.png";
} else if (node.type === 'unknown') {
icon_url = "alert.png";
} else {
try {
icon_url = (typeof def.icon === "function" ? def.icon.call({}) : def.icon);
} catch(err) {
console.log("Definition error: "+nt+".icon",err);
}
}
nodeDiv.css('backgroundColor',colour);
var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv);
$('<div/>',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer);
$('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
if (node.z) {

View File

@@ -131,19 +131,7 @@ RED.sidebar.config = (function() {
} else {
var currentType = "";
nodes.forEach(function(node) {
var label = "";
if (typeof node._def.label == "function") {
try {
label = node._def.label.call(node);
} catch(err) {
console.log("Definition error: "+node._def.type+".label",err);
label = node._def.type;
}
} else {
label = node._def.label;
}
label = label || node.id;
var label = RED.utils.getNodeLabel(node,node.id);
if (node.type != currentType) {
$('<li class="config_node_type">'+node.type+'</li>').appendTo(list);
currentType = node.type;

View File

@@ -189,7 +189,7 @@ RED.tray = (function() {
if (stack.length > 0) {
var tray = stack[stack.length-1];
var trayHeight = tray.tray.height()-tray.header.outerHeight()-tray.footer.outerHeight();
tray.body.height(trayHeight-40);
tray.body.height(trayHeight);
if (tray.width > $("#editor-stack").position().left-8) {
tray.width = $("#editor-stack").position().left-8;
tray.tray.width(tray.width);
@@ -200,7 +200,7 @@ RED.tray = (function() {
// tray.body.parent().width(tray.width);
}
if (tray.options.resize) {
tray.options.resize({width:tray.width});
tray.options.resize({width:tray.width, height:trayHeight});
}
}
}

View File

@@ -110,20 +110,11 @@ RED.typeSearch = (function() {
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
var colour = def.color;
var icon_url = "arrow-in.png";
if (def.category === 'config') {
icon_url = "cog.png";
} else {
try {
icon_url = (typeof def.icon === "function" ? def.icon.call({}) : def.icon);
} catch(err) {
console.log("Definition error: "+object.type+".icon",err);
}
}
var icon_url = RED.utils.getNodeIcon(def);
nodeDiv.css('backgroundColor',colour);
var iconContainer = $('<div/>',{class:"palette_icon_container"}).appendTo(nodeDiv);
$('<div/>',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer);
$('<div/>',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer);
if (def.inputs > 0) {
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);

View File

@@ -146,6 +146,9 @@ RED.utils = (function() {
if (originalLength === undefined) {
originalLength = data.length;
}
if (data.__encoded__) {
data = data.data;
}
type = obj.type.toLowerCase();
} else if (/buffer/.test(typeHint)) {
type = 'buffer';
@@ -364,8 +367,44 @@ RED.utils = (function() {
return true;
}
function getNodeIcon(def,node) {
if (def.category === 'config') {
return "icons/node-red/cog.png"
} else if (node && node.type === 'tab') {
return "icons/node-red/subflow.png"
} else if (node && node.type === 'unknown') {
return "icons/node-red/alert.png"
}
var icon_url;
if (typeof def.icon === "function") {
try {
icon_url = def.icon.call(node);
} catch(err) {
console.log("Definition error: "+def.type+".icon",err);
icon_url = "arrow-in.png";
}
} else {
icon_url = def.icon;
}
return "icons/"+def.set.module+"/"+icon_url;
}
function getNodeLabel(node,defaultLabel) {
defaultLabel = defaultLabel||"";
var l = node._def.label;
try {
l = (typeof l === "function" ? l.call(node) : l)||defaultLabel;
} catch(err) {
console.log("Definition error: "+node.type+".label",err);
l = defaultLabel;
}
return RED.text.bidi.enforceTextDirectionWithUCC(l);
}
return {
createObjectElement: buildMessageElement,
validatePropertyExpression: validatePropertyExpression
validatePropertyExpression: validatePropertyExpression,
getNodeIcon: getNodeIcon,
getNodeLabel: getNodeLabel
}
})();

View File

@@ -70,6 +70,9 @@ RED.view = (function() {
"grey": "#d3d3d3"
}
var PORT_TYPE_INPUT = 1;
var PORT_TYPE_OUTPUT = 0;
var outer = d3.select("#chart")
.append("svg:svg")
.attr("width", space_width)
@@ -84,6 +87,7 @@ RED.view = (function() {
.append("svg:g")
.on("dblclick.zoom", null)
.append("svg:g")
.attr('class','innerCanvas')
.on("mousemove", canvasMouseMove)
.on("mousedown", canvasMouseDown)
.on("mouseup", canvasMouseUp)
@@ -384,6 +388,12 @@ RED.view = (function() {
}
}
});
$("#chart").focus(function() {
$("#workspace-tabs").addClass("workspace-focussed")
});
$("#chart").blur(function() {
$("#workspace-tabs").removeClass("workspace-focussed")
});
RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection);
RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();});
@@ -542,11 +552,11 @@ RED.view = (function() {
var drag_line = drag_lines[0];
var src = null,dst,src_port;
if (drag_line.portType === 0 && nn.inputs > 0) {
if (drag_line.portType === PORT_TYPE_OUTPUT && nn.inputs > 0) {
src = drag_line.node;
src_port = drag_line.port;
dst = nn;
} else if (drag_line.portType === 1 && nn.outputs > 0) {
} else if (drag_line.portType === PORT_TYPE_INPUT && nn.outputs > 0) {
src = nn;
dst = drag_line.node;
src_port = 0;
@@ -556,10 +566,10 @@ RED.view = (function() {
RED.nodes.addLink(link);
historyEvent.links = [link];
hideDragLines();
if (drag_line.portType === 0 && nn.outputs > 0) {
showDragLines([{node:nn,port:0,portType:0}]);
} else if (drag_line.portType === 1 && nn.inputs > 0) {
showDragLines([{node:nn,port:0,portType:1}]);
if (drag_line.portType === PORT_TYPE_OUTPUT && nn.outputs > 0) {
showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]);
} else if (drag_line.portType === PORT_TYPE_INPUT && nn.inputs > 0) {
showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]);
} else {
resetMouseVars();
}
@@ -569,9 +579,9 @@ RED.view = (function() {
}
} else {
if (nn.outputs > 0) {
showDragLines([{node:nn,port:0,portType:0}]);
showDragLines([{node:nn,port:0,portType:PORT_TYPE_OUTPUT}]);
} else if (nn.inputs > 0) {
showDragLines([{node:nn,port:0,portType:1}]);
showDragLines([{node:nn,port:0,portType:PORT_TYPE_INPUT}]);
} else {
resetMouseVars();
}
@@ -671,18 +681,18 @@ RED.view = (function() {
var links = [];
var existingLinks = [];
if (selected_link &&
((mousedown_port_type === 0 &&
((mousedown_port_type === PORT_TYPE_OUTPUT &&
selected_link.source === mousedown_node &&
selected_link.sourcePort === mousedown_port_index
) ||
(mousedown_port_type === 1 &&
(mousedown_port_type === PORT_TYPE_INPUT &&
selected_link.target === mousedown_node
))
) {
existingLinks = [selected_link];
} else {
var filter;
if (mousedown_port_type === 0) {
if (mousedown_port_type === PORT_TYPE_OUTPUT) {
filter = {
source:mousedown_node,
sourcePort: mousedown_port_index
@@ -699,9 +709,9 @@ RED.view = (function() {
RED.nodes.removeLink(link);
links.push({
link:link,
node: (mousedown_port_type===0)?link.target:link.source,
port: (mousedown_port_type===0)?0:link.sourcePort,
portType: (mousedown_port_type===0)?1:0
node: (mousedown_port_type===PORT_TYPE_OUTPUT)?link.target:link.source,
port: (mousedown_port_type===PORT_TYPE_OUTPUT)?0:link.sourcePort,
portType: (mousedown_port_type===PORT_TYPE_OUTPUT)?PORT_TYPE_INPUT:PORT_TYPE_OUTPUT
})
}
if (links.length === 0) {
@@ -722,11 +732,11 @@ RED.view = (function() {
mousePos = mouse_position;
for (i=0;i<drag_lines.length;i++) {
var drag_line = drag_lines[i];
var numOutputs = (drag_line.portType === 0)?(drag_line.node.outputs || 1):1;
var numOutputs = (drag_line.portType === PORT_TYPE_OUTPUT)?(drag_line.node.outputs || 1):1;
var sourcePort = drag_line.port;
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
var sc = (drag_line.portType === 0)?1:-1;
var sc = (drag_line.portType === PORT_TYPE_OUTPUT)?1:-1;
var dy = mousePos[1]-(drag_line.node.y+portY);
var dx = mousePos[0]-(drag_line.node.x+sc*drag_line.node.w/2);
@@ -1293,6 +1303,10 @@ RED.view = (function() {
function calculateTextWidth(str, className, offset) {
return calculateTextDimensions(str,className,offset,0)[0];
}
function calculateTextDimensions(str,className,offsetW,offsetH) {
var sp = document.createElement("span");
sp.className = className;
sp.style.position = "absolute";
@@ -1300,8 +1314,9 @@ RED.view = (function() {
sp.innerHTML = (str||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
document.body.appendChild(sp);
var w = sp.offsetWidth;
var h = sp.offsetHeight;
document.body.removeChild(sp);
return offset+w;
return [offsetW+w,offsetH+h];
}
function resetMouseVars() {
@@ -1309,7 +1324,7 @@ RED.view = (function() {
mouseup_node = null;
mousedown_link = null;
mouse_mode = 0;
mousedown_port_type = 0;
mousedown_port_type = PORT_TYPE_OUTPUT;
activeSpliceLink = null;
spliceActive = false;
d3.select(".link_splice").classed("link_splice",false);
@@ -1366,7 +1381,7 @@ RED.view = (function() {
if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
mouseup_node = n;
portType = mouseup_node.inputs>0?1:0;
portType = mouseup_node.inputs>0?PORT_TYPE_INPUT:PORT_TYPE_OUTPUT;
portIndex = 0;
}
}
@@ -1386,11 +1401,11 @@ RED.view = (function() {
if (portType != drag_lines[i].portType && mouseup_node !== drag_lines[i].node) {
var drag_line = drag_lines[i];
var src,dst,src_port;
if (drag_line.portType === 0) {
if (drag_line.portType === PORT_TYPE_OUTPUT) {
src = drag_line.node;
src_port = drag_line.port;
dst = mouseup_node;
} else if (drag_line.portType == 1) {
} else if (drag_line.portType === PORT_TYPE_INPUT) {
src = mouseup_node;
dst = drag_line.node;
src_port = portIndex;
@@ -1427,10 +1442,10 @@ RED.view = (function() {
if (mouse_mode === RED.state.QUICK_JOINING) {
if (addedLinks.length > 0) {
hideDragLines();
if (portType === 1 && d.outputs > 0) {
showDragLines([{node:d,port:0,portType:0}]);
} else if (portType === 0 && d.inputs > 0) {
showDragLines([{node:d,port:0,portType:1}]);
if (portType === PORT_TYPE_INPUT && d.outputs > 0) {
showDragLines([{node:d,port:0,portType:PORT_TYPE_OUTPUT}]);
} else if (portType === PORT_TYPE_OUTPUT && d.inputs > 0) {
showDragLines([{node:d,port:0,portType:PORT_TYPE_INPUT}]);
} else {
resetMouseVars();
}
@@ -1446,6 +1461,106 @@ RED.view = (function() {
}
}
var portLabelHoverTimeout = null;
var portLabelHover = null;
function getElementPosition(node) {
var d3Node = d3.select(node);
if (d3Node.attr('class') === 'innerCanvas') {
return [0,0];
}
var result = [];
var localPos = [0,0];
if (node.nodeName.toLowerCase() === 'g') {
var transform = d3Node.attr("transform");
if (transform) {
localPos = d3.transform(transform).translate;
}
} else {
localPos = [d3Node.attr("x")||0,d3Node.attr("y")||0];
}
var parentPos = getElementPosition(node.parentNode);
return [localPos[0]+parentPos[0],localPos[1]+parentPos[1]]
}
function getPortLabel(node,portType,portIndex) {
var result;
var nodePortLabels = (portType === PORT_TYPE_INPUT)?node.inputLabels:node.outputLabels;
if (nodePortLabels && nodePortLabels[portIndex]) {
return nodePortLabels[portIndex];
}
var portLabels = (portType === PORT_TYPE_INPUT)?node._def.inputLabels:node._def.outputLabels;
if (typeof portLabels === 'string') {
result = portLabels;
} else if (typeof portLabels === 'function') {
try {
result = portLabels.call(node,portIndex);
} catch(err) {
console.log("Definition error: "+node.type+"."+((portType === PORT_TYPE_INPUT)?"inputLabels":"outputLabels"),err);
result = null;
}
} else if ($.isArray(portLabels)) {
result = portLabels[portIndex];
}
return result;
}
function portMouseOver(port,d,portType,portIndex) {
clearTimeout(portLabelHoverTimeout);
var active = (mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== portType));
if (active && ((portType === PORT_TYPE_INPUT && (d._def.inputLabels||d.inputLabels)) || (portType === PORT_TYPE_OUTPUT && (d._def.outputLabels||d.outputLabels)))) {
portLabelHoverTimeout = setTimeout(function() {
var tooltip = getPortLabel(d,portType,portIndex);
if (!tooltip) {
return;
}
var pos = getElementPosition(port.node());
portLabelHoverTimeout = null;
portLabelHover = vis.append("g")
.attr("transform","translate("+(pos[0]+(portType===PORT_TYPE_INPUT?-2:12))+","+(pos[1]+5)+")")
.attr("class","port_tooltip");
var lines = tooltip.split("\n");
var labelWidth = 0;
var labelHeight = 4;
var labelHeights = [];
lines.forEach(function(l) {
var labelDimensions = calculateTextDimensions(l, "port_tooltip_label", 8,0);
labelWidth = Math.max(labelWidth,labelDimensions[0]);
labelHeights.push(0.8*labelDimensions[1]);
labelHeight += 0.8*labelDimensions[1];
});
var labelHeight1 = (labelHeight/2)-5-2;
var labelHeight2 = labelHeight - 4;
portLabelHover.append("path").attr("d",
portType===PORT_TYPE_INPUT?
"M0 0 l -5 -5 v -"+(labelHeight1)+" q 0 -2 -2 -2 h -"+labelWidth+" q -2 0 -2 2 v "+(labelHeight2)+" q 0 2 2 2 h "+labelWidth+" q 2 0 2 -2 v -"+(labelHeight1)+" l 5 -5"
:
"M0 0 l 5 -5 v -"+(labelHeight1)+" q 0 -2 2 -2 h "+labelWidth+" q 2 0 2 2 v "+(labelHeight2)+" q 0 2 -2 2 h -"+labelWidth+" q -2 0 -2 -2 v -"+(labelHeight1)+" l -5 -5"
);
var y = -labelHeight/2-2;
lines.forEach(function(l,i) {
y += labelHeights[i];
portLabelHover.append("svg:text").attr("class","port_tooltip_label")
.attr("x", portType===PORT_TYPE_INPUT?-10:10)
.attr("y", y)
.attr("text-anchor",portType===PORT_TYPE_INPUT?"end":"start")
.text(l)
});
},500);
}
port.classed("port_hovered",active);
}
function portMouseOut(port,d,portType,portIndex) {
clearTimeout(portLabelHoverTimeout);
if (portLabelHover) {
portLabelHover.remove();
portLabelHover = null;
}
port.classed("port_hovered",false);
}
function nodeMouseUp(d) {
if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
mouse_mode = RED.state.DEFAULT;
@@ -1640,13 +1755,13 @@ RED.view = (function() {
nodeMouseUp.call(this,d);
});
outGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10).attr("x",-5).attr("y",15)
.on("mousedown", function(d,i){portMouseDown(d,1,0);} )
.on("touchstart", function(d,i){portMouseDown(d,1,0);} )
.on("mouseup", function(d,i){portMouseUp(d,1,0);})
.on("touchend",function(d,i){portMouseUp(d,1,0);} )
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 1)));})
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
outGroup.append("g").attr('transform','translate(-5,15)').append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} )
.on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_INPUT,0);} )
.on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);})
.on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_INPUT,0);} )
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
.on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
outGroup.append("svg:text").attr("class","port_label").attr("x",20).attr("y",8).style("font-size","10px").text("output");
outGroup.append("svg:text").attr("class","port_label port_index").attr("x",20).attr("y",24).text(function(d,i){ return i+1});
@@ -1683,13 +1798,15 @@ RED.view = (function() {
nodeMouseUp.call(this,d);
});
inGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10).attr("x",35).attr("y",15)
.on("mousedown", function(d,i){portMouseDown(d,0,i);} )
.on("touchstart", function(d,i){portMouseDown(d,0,i);} )
.on("mouseup", function(d,i){portMouseUp(d,0,i);})
.on("touchend",function(d,i){portMouseUp(d,0,i);} )
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 0) ));})
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
inGroup.append("g").attr('transform','translate(35,15)').append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);} )
.on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE_OUTPUT,i);} )
.on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);})
.on("touchend",function(d,i){portMouseUp(d,PORT_TYPE_OUTPUT,i);} )
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_OUTPUT,0);})
.on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_OUTPUT,0);});
inGroup.append("svg:text").attr("class","port_label").attr("x",18).attr("y",20).style("font-size","10px").text("input");
@@ -1730,14 +1847,7 @@ RED.view = (function() {
var node = d3.select(this);
var isLink = d.type === "link in" || d.type === "link out";
node.attr("id",d.id);
var l = d._def.label;
try {
l = (typeof l === "function" ? l.call(d) : l)||"";
} catch(err) {
console.log("Definition error: "+d.type+".label",err);
l = d.type;
}
var l = RED.utils.getNodeLabel(d);
if (isLink) {
d.w = node_height;
} else {
@@ -1832,7 +1942,7 @@ RED.view = (function() {
//node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none");
if (d._def.icon) {
var icon_url = RED.utils.getNodeIcon(d._def,d);
var icon_group = node.append("g")
.attr("class","node_icon_group")
.attr("x",0).attr("y",0);
@@ -1847,7 +1957,7 @@ RED.view = (function() {
.attr("height",function(d){return Math.min(50,d.h-4);});
var icon = icon_group.append("image")
.attr("xlink:href","icons/"+d._def.icon)
.attr("xlink:href",icon_url)
.attr("class","node_icon")
.attr("x",0)
.attr("width","30")
@@ -1878,7 +1988,7 @@ RED.view = (function() {
//}
var img = new Image();
img.src = "icons/"+d._def.icon;
img.src = icon_url;
img.onload = function() {
icon.attr("width",Math.min(img.width,30));
icon.attr("height",Math.min(img.height,30));
@@ -1915,8 +2025,10 @@ RED.view = (function() {
//node.append("circle").attr({"class":"centerDot","cx":0,"cy":0,"r":5});
//node.append("path").attr("class","node_error").attr("d","M 3,-3 l 10,0 l -5,-8 z");
node.append("image").attr("class","node_error hidden").attr("xlink:href","icons/node-error.png").attr("x",0).attr("y",-6).attr("width",10).attr("height",9);
node.append("image").attr("class","node_changed hidden").attr("xlink:href","icons/node-changed.png").attr("x",12).attr("y",-6).attr("width",10).attr("height",10);
//TODO: these ought to be SVG
node.append("image").attr("class","node_error hidden").attr("xlink:href","icons/node-red/node-error.png").attr("x",0).attr("y",-6).attr("width",10).attr("height",9);
node.append("image").attr("class","node_changed hidden").attr("xlink:href","icons/node-red/node-changed.png").attr("x",12).attr("y",-6).attr("width",10).attr("height",10);
});
node.each(function(d,i) {
@@ -1925,13 +2037,7 @@ RED.view = (function() {
dirtyNodes[d.id] = d;
//if (d.x < -50) deleteSelection(); // Delete nodes if dragged back to palette
if (!isLink && d.resize) {
var l = d._def.label;
try {
l = (typeof l === "function" ? l.call(d) : l)||"";
} catch(err) {
console.log("Definition error: "+d.type+".label",err);
l = d.type;
}
var l = RED.utils.getNodeLabel(d);
var ow = d.w;
d.w = Math.max(node_width,gridSize*(Math.ceil((calculateTextWidth(l, "node_label", 50)+(d._def.inputs>0?7:0))/gridSize)) );
d.h = Math.max(node_height,(d.outputs||0) * 15);
@@ -1965,12 +2071,12 @@ RED.view = (function() {
} else if (d.inputs === 1 && inputPorts.empty()) {
var inputGroup = thisNode.append("g").attr("class","port_input");
inputGroup.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown",function(d){portMouseDown(d,1,0);})
.on("touchstart",function(d){portMouseDown(d,1,0);})
.on("mouseup",function(d){portMouseUp(d,1,0);} )
.on("touchend",function(d){portMouseUp(d,1,0);} )
.on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 1) ));})
.on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
.on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
.on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
.on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
.on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
}
var numOutputs = d.outputs;
@@ -1980,12 +2086,12 @@ RED.view = (function() {
var output_group = d._ports.enter().append("g").attr("class","port_output");
output_group.append("rect").attr("class","port").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
.on("mousedown",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
.on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
.on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
.on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 0) ));})
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
.on("mousedown",(function(){var node = d; return function(d,i){portMouseDown(node,PORT_TYPE_OUTPUT,i);}})() )
.on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,PORT_TYPE_OUTPUT,i);}})() )
.on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,PORT_TYPE_OUTPUT,i);}})() )
.on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,PORT_TYPE_OUTPUT,i);}})() )
.on("mouseover",(function(){var node = d; return function(d,i){portMouseOver(d3.select(this),node,PORT_TYPE_OUTPUT,i);}})())
.on("mouseout",(function(){var node = d; return function(d,i) {portMouseOut(d3.select(this),node,PORT_TYPE_OUTPUT,i);}})());
d._ports.exit().remove();
if (d._ports) {
@@ -2032,21 +2138,11 @@ RED.view = (function() {
if (d._def.icon) {
icon = thisNode.select(".node_icon");
var current_url = icon.attr("xlink:href");
var icon_url;
if (typeof d._def.icon == "function") {
try {
icon_url = d._def.icon.call(d);
} catch(err) {
console.log("icon",err);
icon_url = "arrow-in.png";
}
} else {
icon_url = d._def.icon;
}
if ("icons/"+icon_url != current_url) {
icon.attr("xlink:href","icons/"+icon_url);
var new_url = RED.utils.getNodeIcon(d._def,d);
if (new_url !== current_url) {
icon.attr("xlink:href",new_url);
var img = new Image();
img.src = "icons/"+d._def.icon;
img.src = new_url;
img.onload = function() {
icon.attr("width",Math.min(img.width,30));
icon.attr("height",Math.min(img.height,30));

View File

@@ -39,6 +39,7 @@ RED.workspaces = (function() {
RED.nodes.dirty(true);
}
}
RED.view.focus();
return ws;
}
function deleteWorkspace(ws) {
@@ -139,6 +140,10 @@ RED.workspaces = (function() {
RED.events.emit("workspace:change",event);
window.location.hash = 'flow/'+tab.id;
RED.sidebar.config.refresh();
RED.view.focus();
},
onclick: function(tab) {
RED.view.focus();
},
ondblclick: function(tab) {
if (tab.type != "subflow") {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
**/
RED.validators = {
number: function(){return function(v) { return v!=='' && !isNaN(v);}},
number: function(blankAllowed){return function(v) { return (blankAllowed&&(v===''||v===undefined)) || (v!=='' && !isNaN(v));}},
regex: function(re){return function(v) { return re.test(v);}},
typedInput: function(ptypeName,isConfig) { return function(v) {
var ptype = $("#node-"+(isConfig?"config-":"")+"input-"+ptypeName).val() || this[ptypeName];

View File

@@ -23,6 +23,7 @@
width: 100%;
height: 100%;
display: none;
z-index:100;
}
#dropTarget div {
display: table-cell;
@@ -34,4 +35,3 @@
#dropTarget div i {
font-size: 80px;
}

View File

@@ -46,7 +46,12 @@
overflow: auto;
}
.editor-tray-body {
margin: 20px;
form {
margin: 20px;
}
}
.editor-tray-content {
overflow: auto;
}
.editor-tray-header {
@include disable-selection;
@@ -65,6 +70,13 @@
.editor-tray-footer {
@include component-footer;
height: 35px;
button {
@include editor-button;
padding: 3px 7px;
font-size: 11px;
}
}
.editor-tray-toolbar {
@@ -72,48 +84,9 @@
padding: 6px;
button {
@include workspace-button;
font-size: 14px;
padding: 6px 14px;
margin-right: 8px;
color: $editor-button-color !important;
background: $editor-button-background;
&.primary {
border-color: $editor-button-background-primary;
color: $editor-button-color-primary !important;
background: $editor-button-background-primary;
&.disabled, &.ui-state-disabled {
background: none;
color: $editor-button-color !important;
border-color: $form-input-border-color;
}
&:not(.disabled):not(.ui-button-disabled):hover {
border-color: $editor-button-background-primary-hover;
background: $editor-button-background-primary-hover;
color: $editor-button-color-primary !important;
}
}
&:not(.disabled):hover {
//color: $editor-button-color;
}
&.disabled {
background: none;
}
&.disabled:focus {
outline: none;
}
&.leftButton {
float: left;
margin-top: 1px;
}
&:not(.leftButton):not(:last-child) {
margin-right: 16px;
}
&.ui-state-disabled {
opacity: 1;
@include editor-button;
&.toggle {
@include workspace-button-toggle;
}
}
}
@@ -179,7 +152,6 @@
.dialog-form,#dialog-form, #dialog-config-form {
margin: 0;
height: 100%;
}
@@ -244,17 +216,6 @@
padding: 0 5px;
}
.dialog-form {
.button-group {
.editor-button {
&:first-child {
}
}
}
}
#node-config-dialog-scope-container {
cursor: auto;
float: right;

View File

@@ -272,3 +272,25 @@ g.link_unknown path.link_line {
stroke-width: 2;
stroke-dasharray: 10, 4;
}
@keyframes port_tooltip_fadeIn { from { opacity:0; } to { opacity:1; } }
.port_tooltip {
opacity:0;
animation: 0.1s ease-in 0s 1 normal forwards port_tooltip_fadeIn;
pointer-events: none;
path {
fill: white;
stroke: #999;
stroke-width: 1;
}
}
.port_tooltip_label {
stroke-width: 0;
fill: #666;
font-size: 12px;
pointer-events: none;
-webkit-touch-callout: none;
@include disable-selection;
}

View File

@@ -108,7 +108,51 @@
color: $workspace-button-toggle-color-disabled !important;
}
}
@mixin editor-button {
@include workspace-button;
font-size: 14px;
padding: 6px 14px;
margin-right: 8px;
color: $editor-button-color !important;
background: $editor-button-background;
&.primary {
border-color: $editor-button-background-primary;
color: $editor-button-color-primary !important;
background: $editor-button-background-primary;
&.disabled, &.ui-state-disabled {
background: none;
color: $editor-button-color !important;
border-color: $form-input-border-color;
}
&:not(.disabled):not(.ui-button-disabled):hover {
border-color: $editor-button-background-primary-hover;
background: $editor-button-background-primary-hover;
color: $editor-button-color-primary !important;
}
}
&:not(.disabled):hover {
//color: $editor-button-color;
}
&.disabled {
background: none;
}
&.disabled:focus {
outline: none;
}
&.leftButton {
float: left;
margin-top: 1px;
}
&:not(.leftButton):not(:last-child) {
margin-right: 16px;
}
&.ui-state-disabled {
opacity: 1;
}
}
@mixin component-footer {
border-top: 1px solid $primary-border-color;

View File

@@ -51,3 +51,10 @@
#workspace-footer {
@include component-footer;
}
#workspace-tabs:not(.workspace-focussed) {
opacity:0.8;
li.red-ui-tab.active a {
color:#666;
}
}

View File

@@ -55,8 +55,10 @@
things like pipe the result to another command.</p>
<p>Commands or parameters with spaces should be enclosed in quotes - <i>"This is a single parameter"</i></p>
<p>If stdout is binary a <i>buffer</i> is returned - otherwise returns a <i>string</i>.</p>
<p>The blue status icon will be visible while the node is active.</p>
<p>If running a Python app you may need to use the <code>-u</code> parameter to stop the output being buffered.</p>
<p>The blue status icon and PID will be visible while the node is active.</p>
<p>Sending <code>msg.kill</code> will kill a single active process. If there is more than one process running then
<code>msg.kill</code> must be set with the value of the PID to be killed.</p>
<p>Tip: If running a Python app you may need to use the <code>-u</code> parameter to stop the output being buffered.</p>
</script>
<script type="text/javascript">
@@ -73,6 +75,7 @@
},
inputs:1,
outputs:3,
outputLabels: ["stdout","stderr","rc"],
icon: "arrow-in.png",
align: "right",
label: function() {

View File

@@ -38,88 +38,103 @@ module.exports = function(RED) {
}
this.on("input", function(msg) {
var child;
node.status({fill:"blue",shape:"dot",text:" "});
if (this.useSpawn === true) {
// make the extra args into an array
// then prepend with the msg.payload
var arg = node.cmd;
if ((node.addpay === true) && msg.hasOwnProperty("payload")) { arg += " "+msg.payload; }
if (node.append.trim() !== "") { arg += " "+node.append; }
// slice whole line by spaces (trying to honour quotes);
arg = arg.match(/(?:[^\s"]+|"[^"]*")+/g);
var cmd = arg.shift();
if (/^".*"$/.test(cmd)) { cmd = cmd.slice(1,-1); }
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(cmd+" ["+arg+"]"); }
child = spawn(cmd,arg);
var unknownCommand = (child.pid === undefined);
if (node.timer !== 0) {
child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer);
if (msg.hasOwnProperty("kill")) {
if (node.activeProcesses.hasOwnProperty(msg.kill) ) {
node.activeProcesses[msg.kill].kill();
node.status({fill:"red",shape:"dot",text:"killed"});
}
node.activeProcesses[child.pid] = child;
child.stdout.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
// console.log('[exec] stdout: ' + data,child.pid);
if (isUtf8(data)) { msg.payload = data.toString(); }
else { msg.payload = data; }
node.send([RED.util.cloneMessage(msg),null,null]);
else {
if (Object.keys(node.activeProcesses).length === 1) {
node.activeProcesses[Object.keys(node.activeProcesses)[0]].kill();
node.status({fill:"red",shape:"dot",text:"killed"});
}
});
child.stderr.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
if (isUtf8(data)) { msg.payload = data.toString(); }
else { msg.payload = new Buffer(data); }
node.send([null,RED.util.cloneMessage(msg),null]);
}
});
child.on('close', function (code) {
if (unknownCommand || (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null)) {
delete node.activeProcesses[child.pid];
if (child.tout) { clearTimeout(child.tout); }
msg.payload = code;
if (code === 0) { node.status({}); }
if (code === null) { node.status({fill:"red",shape:"dot",text:"timeout"}); }
else if (code < 0) { node.status({fill:"red",shape:"dot",text:"rc: "+code}); }
else { node.status({fill:"yellow",shape:"dot",text:"rc: "+code}); }
node.send([null,null,RED.util.cloneMessage(msg)]);
}
});
child.on('error', function (code) {
if (child.tout) { clearTimeout(child.tout); }
delete node.activeProcesses[child.pid];
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
node.error(code,RED.util.cloneMessage(msg));
}
});
}
}
else {
var cl = node.cmd;
if ((node.addpay === true) && msg.hasOwnProperty("payload")) { cl += " "+msg.payload; }
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) {
msg.payload = new Buffer(stdout,"binary");
if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); }
var msg2 = {payload:stderr};
var msg3 = null;
//console.log('[exec] stdout: ' + stdout);
//console.log('[exec] stderr: ' + stderr);
if (error !== null) {
msg3 = {payload:error};
//console.log('[exec] error: ' + error);
var child;
if (this.useSpawn === true) {
// make the extra args into an array
// then prepend with the msg.payload
var arg = node.cmd;
if ((node.addpay === true) && msg.hasOwnProperty("payload")) { arg += " "+msg.payload; }
if (node.append.trim() !== "") { arg += " "+node.append; }
// slice whole line by spaces (trying to honour quotes);
arg = arg.match(/(?:[^\s"]+|"[^"]*")+/g);
var cmd = arg.shift();
if (/^".*"$/.test(cmd)) { cmd = cmd.slice(1,-1); }
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(cmd+" ["+arg+"]"); }
child = spawn(cmd,arg);
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
var unknownCommand = (child.pid === undefined);
if (node.timer !== 0) {
child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer);
}
node.status({});
node.send([msg,msg2,msg3]);
if (child.tout) { clearTimeout(child.tout); }
delete node.activeProcesses[child.pid];
});
child.on('error',function() {});
if (node.timer !== 0) {
child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer);
node.activeProcesses[child.pid] = child;
child.stdout.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
// console.log('[exec] stdout: ' + data,child.pid);
if (isUtf8(data)) { msg.payload = data.toString(); }
else { msg.payload = data; }
node.send([RED.util.cloneMessage(msg),null,null]);
}
});
child.stderr.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
if (isUtf8(data)) { msg.payload = data.toString(); }
else { msg.payload = new Buffer(data); }
node.send([null,RED.util.cloneMessage(msg),null]);
}
});
child.on('close', function (code) {
if (unknownCommand || (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null)) {
delete node.activeProcesses[child.pid];
if (child.tout) { clearTimeout(child.tout); }
msg.payload = code;
if (code === 0) { node.status({}); }
if (code === null) { node.status({fill:"red",shape:"dot",text:"timeout"}); }
else if (code < 0) { node.status({fill:"red",shape:"dot",text:"rc:"+code}); }
else { node.status({fill:"yellow",shape:"dot",text:"rc:"+code}); }
node.send([null,null,RED.util.cloneMessage(msg)]);
}
});
child.on('error', function (code) {
if (child.tout) { clearTimeout(child.tout); }
delete node.activeProcesses[child.pid];
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
node.error(code,RED.util.cloneMessage(msg));
}
});
}
else {
var cl = node.cmd;
if ((node.addpay === true) && msg.hasOwnProperty("payload")) { cl += " "+msg.payload; }
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) {
msg.payload = new Buffer(stdout,"binary");
if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); }
var msg2 = {payload:stderr};
var msg3 = null;
//console.log('[exec] stdout: ' + stdout);
//console.log('[exec] stderr: ' + stderr);
if (error !== null) {
msg3 = {payload:error};
//console.log('[exec] error: ' + error);
}
if (!msg3) { node.status({}); }
node.send([msg,msg2,msg3]);
if (child.tout) { clearTimeout(child.tout); }
delete node.activeProcesses[child.pid];
});
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
child.on('error',function() {});
if (node.timer !== 0) {
child.tout = setTimeout(function() { cleanup(child.pid); }, node.timer);
}
node.activeProcesses[child.pid] = child;
}
node.activeProcesses[child.pid] = child;
}
});
this.on('close',function() {

View File

@@ -24,6 +24,14 @@
<input type="text" id="node-input-field" placeholder="payload" style="width:250px;">
<input type="hidden" id="node-input-fieldType">
</div>
<div class="form-row">
<label for="node-input-syntax"><i class="fa fa-code"></i> <span data-i18n="template.label.syntax"></span></label>
<select id="node-input-syntax" style="width:180px;">
<option value="mustache" data-i18n="template.label.mustache"></option>
<option value="plain" data-i18n="template.label.plain"></option>
</select>
</div>
<div class="form-row" style="position: relative; margin-bottom: 0px;">
<label for="node-input-template"><i class="fa fa-file-code-o"></i> <span data-i18n="template.label.template"></span></label>
<input type="hidden" id="node-input-template" autofocus="autofocus">
@@ -45,12 +53,13 @@
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-template-editor" ></div>
</div>
<div class="form-row">
<label for="node-input-syntax"><i class="fa fa-code"></i> <span data-i18n="template.label.syntax"></span></label>
<select id="node-input-syntax" style="width:180px;">
<option value="mustache" data-i18n="template.label.mustache"></option>
<option value="plain" data-i18n="template.label.plain"></option>
<label for="node-input-output"><i class="fa fa-long-arrow-right"></i> <span data-i18n="template.label.output"></span></label>
<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>
</select>
</div>
</script>
<script type="text/x-red" data-help-name="template">
@@ -83,6 +92,7 @@
format: {value:"handlebars"},
syntax: {value:"mustache"},
template: {value:"This is the payload: {{payload}} !"},
output: {value:"str"}
},
inputs:1,
outputs:1,

View File

@@ -19,8 +19,8 @@ module.exports = function(RED) {
var mustache = require("mustache");
/**
* Custom Mustache Context capable to resolve message property and node
* flow and global context
* Custom Mustache Context capable to resolve message property and node
* flow and global context
*/
function NodeContext(msg, nodeContext) {
this.msgContext = new mustache.Context(msg);
@@ -58,6 +58,7 @@ module.exports = function(RED) {
this.template = n.template;
this.syntax = n.syntax || "mustache";
this.fieldType = n.fieldType || "msg";
this.outputFormat = n.output || "str";
var node = this;
node.on("input", function(msg) {
@@ -68,6 +69,10 @@ module.exports = function(RED) {
} else {
value = node.template;
}
if (node.outputFormat === "json") {
value = JSON.parse(value);
}
if (node.fieldType === 'msg') {
RED.util.setMessageProperty(msg,node.field,value);
} else if (node.fieldType === 'flow') {

View File

@@ -25,9 +25,10 @@
<select id="node-then-type" style="width:70%;">
<option value="block" data-i18n="trigger.wait-reset"></option>
<option value="wait" data-i18n="trigger.wait-for"></option>
<option value="loop" data-i18n="trigger.wait-loop"></option>
</select>
</div>
<div class="form-row node-type-wait">
<div class="form-row node-type-duration">
<label></label>
<input type="text" id="node-input-duration" style="text-align:end; width:70px !important">
<select id="node-input-units" style="width:140px !important">
@@ -67,16 +68,17 @@
<p>The two output states can be specified as can the duration of the timer.
Either output can be set to a value, or templated from the inbound
<code>msg</code> using mustache syntax. <pre>The payload is {{payload}}</pre></p>
<p>If the <code>msg.payload</code> is an object then setting the output to
<i>existing payload</i> will pass the complete payload object through.</p>
<p>Or you can pass through either the <i>original msg</i> or the <i>latest msg</i> to arrive.</p>
<p>Optionally the timer can be extended by being retriggered... or not.</p>
<p>By setting the first output to <i>nothing</i>, and selecting extend timer - a watchdog timer can be created.
No output will happen as long as repeated inputs occur within the timeout period.</p>
<p>Setting the timer to 0 creates an "infinite" timeout - the first output will happen but the second
never will, and neither can the first be retriggered - so a true one shot.</p>
<p>If a <code>msg.reset</code> property is present, or the <code>msg.payload</code>
matches the optional reset value, any timeout currently in progress
matches the optional reset value, any timeout or repeat currently in progress
will be cleared and the second output will not happen.</p>
<p>The node can be set to repeat the input <code>msg</code> at regular intervals until the input changes,
or the node is reset.</p>
<p>The blue status icon will be visible while the node is active.</p>
</script>
@@ -102,6 +104,9 @@
if (this.duration > 0) {
return this.name|| this._("trigger.label.trigger")+" "+this.duration+this.units;
}
if (this.duration < 0) {
return this.name|| this._("trigger.label.trigger-loop")+" "+(this.duration * -1)+this.units;
}
else {
return this.name|| this._("trigger.label.trigger-block");
}
@@ -113,8 +118,14 @@
$("#node-then-type").change(function() {
if ($(this).val() == "block") {
$(".node-type-wait").hide();
$(".node-type-duration").hide();
}
else if ($(this).val() == "loop") {
$(".node-type-wait").hide();
$(".node-type-duration").show();
} else {
$(".node-type-wait").show();
$(".node-type-duration").show();
}
});
@@ -127,7 +138,6 @@
var optionNothing = {value:"nul",label:this._("trigger.output.nothing"),hasValue:false};
var optionPayload = {value:"pay",label:this._("trigger.output.existing"),hasValue:false}
var optionOriginalPayload = {value:"pay",label:this._("trigger.output.original"),hasValue:false}
var optionLatestPayload = {value:"payl",label:this._("trigger.output.latest"),hasValue:false}
@@ -151,6 +161,11 @@
if (this.duration == "0") {
$("#node-then-type").val("block");
}
else if ((this.duration * 1) < 0) {
$("#node-then-type").val("loop");
this.duration = this.duration * -1;
$("#node-input-duration").val(this.duration);
} else {
$("#node-then-type").val("wait");
}
@@ -167,6 +182,9 @@
if ($("#node-then-type").val() == "block") {
$("#node-input-duration").val("0");
}
if ($("#node-then-type").val() == "loop") {
$("#node-input-duration").val($("#node-input-duration").val() * -1);
}
}
});

View File

@@ -48,12 +48,16 @@ module.exports = function(RED) {
this.units = n.units || "ms";
this.reset = n.reset || '';
this.duration = n.duration || 250;
if (this.duration <= 0) { this.duration = 0; }
else {
if (this.units == "s") { this.duration = this.duration * 1000; }
if (this.units == "min") { this.duration = this.duration * 1000 * 60; }
if (this.units == "hr") { this.duration = this.duration * 1000 *60 * 60; }
if (this.duration < 0) {
this.loop = true;
this.duration = this.duration * -1;
this.extend = false;
}
if (this.units == "s") { this.duration = this.duration * 1000; }
if (this.units == "min") { this.duration = this.duration * 1000 * 60; }
if (this.units == "hr") { this.duration = this.duration * 1000 *60 * 60; }
this.op1Templated = (this.op1type === 'str' && this.op1.indexOf("{{") != -1);
this.op2Templated = (this.op2type === 'str' && this.op2.indexOf("{{") != -1);
if ((this.op1type === "num") && (!isNaN(this.op1))) { this.op1 = Number(this.op1); }
@@ -69,13 +73,14 @@ module.exports = function(RED) {
var tout = null;
var m2;
this.on("input", function(msg) {
if (msg.hasOwnProperty("reset") || ((node.reset !== '')&&(msg.payload == node.reset)) ) {
clearTimeout(tout);
if (msg.hasOwnProperty("reset") || ((node.reset !== '') && (msg.payload == node.reset)) ) {
if (node.loop === true) { clearInterval(tout); }
else { clearTimeout(tout); }
tout = null;
node.status({});
}
else {
if ((!tout) && (tout !== 0)) {
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); }
else if (node.op2type !== "nul") {
@@ -91,6 +96,13 @@ module.exports = function(RED) {
if (node.op1type !== "nul") { node.send(msg); }
if (node.duration === 0) { tout = 0; }
else if (node.loop === true) {
if (tout) { clearInterval(tout); }
if (node.op1type !== "nul") {
var msg2 = RED.util.cloneMessage(msg);
tout = setInterval(function() { node.send(msg2); },node.duration);
}
}
else {
tout = setTimeout(function() {
if (node.op2type !== "nul") {
@@ -108,7 +120,7 @@ module.exports = function(RED) {
node.status({fill:"blue",shape:"dot",text:" "});
}
else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) {
clearTimeout(tout);
if (tout) { clearTimeout(tout); }
if (node.op2type === "payl") { m2 = msg.payload; }
tout = setTimeout(function() {
if (node.op2type !== "nul") {
@@ -122,14 +134,17 @@ module.exports = function(RED) {
tout = null;
node.status({});
},node.duration);
} else {
}
else {
if (node.op2type === "payl") { m2 = msg.payload; }
}
}
});
this.on("close", function() {
if (tout) {
clearTimeout(tout);
if (node.loop === true) { clearInterval(tout); }
else { clearTimeout(tout); }
tout = null;
}
node.status({});
});

View File

@@ -218,7 +218,6 @@
var pinsInUse = {};
RED.nodes.registerType('rpi-gpio out',{
category: 'Raspberry Pi',
label: 'Raspberry Pi',
color:"#c6dbef",
defaults: {
name: { value:"" },
@@ -351,7 +350,6 @@
<script type="text/javascript">
RED.nodes.registerType('rpi-mouse',{
category: 'Raspberry Pi',
label: 'Raspberry Pi',
color:"#c6dbef",
defaults: {
name: { value:"" },
@@ -390,7 +388,6 @@
<script type="text/javascript">
RED.nodes.registerType('rpi-keyboard',{
category: 'Raspberry Pi',
label: 'Raspberry Pi',
color:"#c6dbef",
defaults: {
name: { value:"" }

View File

@@ -37,8 +37,13 @@ module.exports = function(RED) {
fs.statSync("/usr/lib/python2.7/site-packages/RPi/GPIO"); // test on Arch
}
catch(err) {
RED.log.warn(RED._("rpi-gpio.errors.libnotfound"));
throw "Warning : "+RED._("rpi-gpio.errors.libnotfound");
try {
fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot
}
catch(err) {
RED.log.warn(RED._("rpi-gpio.errors.libnotfound"));
throw "Warning : "+RED._("rpi-gpio.errors.libnotfound");
}
}
}

View File

@@ -48,7 +48,14 @@ module.exports = function(RED) {
return;
}
}
RED.nodes.registerType("tls-config",TLSConfig);
RED.nodes.registerType("tls-config",TLSConfig,{
settings: {
tlsConfigDisableLocalFiles: {
value: true,
exportable: false
}
}
});
TLSConfig.prototype.addTLSOptions = function(opts) {
if (this.valid) {

View File

@@ -147,9 +147,9 @@
<div id="mqtt-broker-tab-connection" style="display:none">
<div class="form-row node-input-broker">
<label for="node-config-input-broker"><i class="fa fa-globe"></i> <span data-i18n="mqtt.label.broker"></span></label>
<input class="input-append-left" type="text" id="node-config-input-broker" placeholder="e.g. localhost" style="width: 40%;" >
<label for="node-config-input-port" style="margin-left: 10px; width: 35px; "> <span data-i18n="mqtt.label.port"></span></label>
<input type="text" id="node-config-input-port" data-i18n="[placeholder]mqtt.label.port" style="width:45px">
<input class="input-append-left" type="text" id="node-config-input-broker" placeholder="e.g. localhost" style="width:40%;" >
<label for="node-config-input-port" style="margin-left:20px; width:35px; "> <span data-i18n="mqtt.label.port"></span></label>
<input type="text" id="node-config-input-port" data-i18n="[placeholder]mqtt.label.port" style="width:55px">
</div>
<div class="form-row">
<input type="checkbox" id="node-config-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">

View File

@@ -25,6 +25,11 @@
<option value="patch">PATCH</option>
</select>
</div>
<div class="form-row form-row-http-in-upload hide">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-upload" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-upload" style="width: 70%;" data-i18n="httpin.label.upload"></label>
</div>
<div class="form-row">
<label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label>
<input id="node-input-url" type="text" placeholder="/url">
@@ -59,6 +64,9 @@
To send JSON encoded data to the node, the content-type header of the request must be set to
<code>application/json</code>.
</p>
<p>
If file uploads are enabled for POST requests, the files will be available under
<code>msg.req.files</code>.
<p>
<b>Note: </b>This node does not send any response to the http request.
This should be done with a subsequent HTTP Response node.
@@ -71,6 +79,16 @@
<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-row">
<label for="node-input-statusCode"><i class="fa fa-long-arrow-left"></i> <span data-i18n="httpin.label.status"></span></label>
<input type="text" id="node-input-statusCode" placeholder="msg.statusCode">
</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-tips"><span data-i18n="[html]httpin.tip.res"></span></div>
</script>
@@ -84,6 +102,9 @@
pairs to be added as response headers.</li>
<li><code>cookies</code>, if set, can be used to set or delete cookies.
</ul>
<p>The <code>statusCode</code> and <code>headers</code> can also be set within
the node itself. If a property is set within the node, it cannot be overridden
by the corresponding message property.</p>
<h3>Cookie handling</h3>
<p>The <code>cookies</code> property must be an object of name/value pairs.
The value can be either a string to set the value of the cookie with default
@@ -112,6 +133,7 @@ msg.cookies = {
</script>
<script type="text/javascript">
(function() {
RED.nodes.registerType('http in',{
category: 'input',
color:"rgb(231, 231, 174)",
@@ -119,6 +141,7 @@ msg.cookies = {
name: {value:""},
url: {value:"",required:true},
method: {value:"get",required:true},
upload: {value:false},
swaggerDoc: {type:"swagger-doc", required:false}
},
inputs:0,
@@ -159,25 +182,165 @@ msg.cookies = {
if(!RED.nodes.getType("swagger-doc")){
$('.row-swagger-doc').hide();
}
$("#node-input-method").change(function() {
if ($(this).val() === "post") {
$(".form-row-http-in-upload").show();
} else {
$(".form-row-http-in-upload").hide();
}
}).change();
}
});
var headerTypes = [
{value:"content-type",label:"Content-Type",hasValue: false},
{value:"location",label:"Location",hasValue: false},
{value:"other",label:"other",icon:"red/images/typedInput/az.png"}
]
var contentTypes = [
{value:"application/json",label:"application/json",hasValue: false},
{value:"application/xml",label:"application/xml",hasValue: false},
{value:"text/css",label:"text/css",hasValue: false},
{value:"text/html",label:"text/html",hasValue: false},
{value:"text/plain",label:"text/plain",hasValue: false},
{value:"image/gif",label:"image/gif",hasValue: false},
{value:"image/png",label:"image/png",hasValue: false},
{value:"other",label:"other",icon:"red/images/typedInput/az.png"}
];
RED.nodes.registerType('http response',{
category: 'output',
color:"rgb(231, 231, 174)",
defaults: {
name: {value:""}
name: {value:""},
statusCode: {value:"",validate: RED.validators.number(true)},
headers: {value:{}}
},
inputs:1,
outputs:0,
align: "right",
icon: "white-globe.png",
label: function() {
return this.name||"http";
return this.name||("http"+(this.statusCode?" ("+this.statusCode+")":""));
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
function resizeRule(rule) {
var newWidth = rule.width();
rule.find('.red-ui-typedInput').typedInput("width",(newWidth-15)/2);
}
var headerList = $("#node-input-headers-container").css('min-height','150px').css('min-width','450px').editableList({
addItem: function(container,i,header) {
var row = $('<div/>').appendTo(container);
var propertyName = $('<input/>',{class:"node-input-header-name",type:"text"})
.appendTo(row)
.typedInput({types:headerTypes});
var propertyValue = $('<input/>',{class:"node-input-header-value",type:"text",style:"margin-left: 10px"})
.appendTo(row)
.typedInput({types:
header.h === 'content-type'?contentTypes:[{value:"other",label:"other",icon:"red/images/typedInput/az.png"}]
});
var matchedType = headerTypes.filter(function(ht) {
return ht.value === header.h
});
if (matchedType.length === 0) {
propertyName.typedInput('type','other');
propertyName.typedInput('value',header.h);
propertyValue.typedInput('value',header.v);
} else {
propertyName.typedInput('type',header.h);
if (header.h === "content-type") {
matchedType = contentTypes.filter(function(ct) {
return ct.value === header.v;
});
if (matchedType.length === 0) {
propertyValue.typedInput('type','other');
propertyValue.typedInput('value',header.v);
} else {
propertyValue.typedInput('type',header.v);
}
} else {
propertyValue.typedInput('value',header.v);
}
}
matchedType = headerTypes.filter(function(ht) {
return ht.value === header.h
});
if (matchedType.length === 0) {
propertyName.typedInput('type','other');
propertyName.typedInput('value',header.h);
} else {
propertyName.typedInput('type',header.h);
}
propertyName.on('change',function(event) {
var type = propertyName.typedInput('type');
if (type === 'content-type') {
propertyValue.typedInput('types',contentTypes);
} else {
propertyValue.typedInput('types',[{value:"other",label:"other",icon:"red/images/typedInput/az.png"}]);
}
});
resizeRule(container);
},
resizeItem: resizeRule,
removable: true
});
if (this.headers) {
for (var key in this.headers) {
if (this.headers.hasOwnProperty(key)) {
headerList.editableList('addItem',{h:key,v:this.headers[key]});
}
}
}
},
oneditsave: function() {
var headers = $("#node-input-headers-container").editableList('items');
var node = this;
node.headers = {};
headers.each(function(i) {
var header = $(this);
var keyType = header.find(".node-input-header-name").typedInput('type');
var keyValue = header.find(".node-input-header-name").typedInput('value');
var valueType = header.find(".node-input-header-value").typedInput('type');
var valueValue = header.find(".node-input-header-value").typedInput('value');
var key = keyType;
var value = valueType;
if (keyType === 'other') {
key = keyValue;
}
if (valueType === 'other') {
value = valueValue;
}
if (key !== '') {
node.headers[key] = value;
}
});
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-headers-container-row)");
var height = size.height;
for (var i=0; i<rows.size(); i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-headers-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-headers-container").editableList('height',height);
}
});
})();
</script>

View File

@@ -17,6 +17,7 @@
module.exports = function(RED) {
"use strict";
var bodyParser = require("body-parser");
var multer = require("multer");
var cookieParser = require("cookie-parser");
var getBody = require('raw-body');
var cors = require('cors');
@@ -177,6 +178,7 @@ module.exports = function(RED) {
}
this.url = n.url;
this.method = n.method;
this.upload = n.upload;
this.swaggerDoc = n.swaggerDoc;
var node = this;
@@ -225,10 +227,21 @@ module.exports = function(RED) {
};
}
var multipartParser = function(req,res,next) { next(); }
if (this.upload) {
var mp = multer({ storage: multer.memoryStorage() }).any();
multipartParser = function(req,res,next) {
mp(req,res,function(err) {
req._body = true;
next(err);
})
};
}
if (this.method == "get") {
RED.httpNode.get(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,this.callback,this.errorHandler);
} else if (this.method == "post") {
RED.httpNode.post(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler);
RED.httpNode.post(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,multipartParser,rawBodyParser,this.callback,this.errorHandler);
} else if (this.method == "put") {
RED.httpNode.put(this.url,cookieParser(),httpMiddleware,corsHandler,metricsHandler,jsonParser,urlencParser,rawBodyParser,this.callback,this.errorHandler);
} else if (this.method == "patch") {
@@ -255,10 +268,20 @@ module.exports = function(RED) {
function HTTPOut(n) {
RED.nodes.createNode(this,n);
var node = this;
this.headers = n.headers||{};
this.statusCode = n.statusCode;
this.on("input",function(msg) {
if (msg.res) {
var headers = RED.util.cloneMessage(node.headers);
if (msg.headers) {
msg.res._res.set(msg.headers);
for (var h in msg.headers) {
if (msg.headers.hasOwnProperty(h) && !headers.hasOwnProperty(h)) {
headers[h] = msg.headers[h];
}
}
}
if (Object.keys(headers).length > 0) {
msg.res._res.set(headers);
}
if (msg.cookies) {
for (var name in msg.cookies) {
@@ -277,7 +300,7 @@ module.exports = function(RED) {
}
}
}
var statusCode = msg.statusCode || 200;
var statusCode = node.statusCode || msg.statusCode || 200;
if (typeof msg.payload == "object" && !Buffer.isBuffer(msg.payload)) {
msg.res._res.status(statusCode).jsonp(msg.payload);
} else {

View File

@@ -82,16 +82,15 @@ module.exports = function(RED) {
RED.server.addListener('newListener',storeListener);
// Create a WebSocket Server
node.server = new ws.Server({
var serverOptions = {
server:RED.server,
path:path,
// Disable the deflate option due to this issue
// https://github.com/websockets/ws/pull/632
// that is fixed in the 1.x release of the ws module
// that we cannot currently pickup as it drops node 0.10 support
perMessageDeflate: false
});
path:path
}
if (RED.settings.webSocketNodeVerifyClient) {
serverOptions.verifyClient = RED.settings.webSocketNodeVerifyClient;
}
// Create a WebSocket Server
node.server = new ws.Server(serverOptions);
// Workaround https://github.com/einaros/ws/pull/253
// Stop listening for new listener events

View File

@@ -19,6 +19,11 @@
<label for="node-input-files"><i class="fa fa-file"></i> <span data-i18n="watch.label.files"></span></label>
<input id="node-input-files" type="text" tabindex="1" data-i18n="[placeholder]watch.placeholder.files">
</div>
<div class="form-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-recursive" style="display:inline-block; width:auto; vertical-align:top;">
<label for="node-input-recursive" style="width:70%;"> <span data-i18n="watch.label.recursive"></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>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@@ -46,7 +51,8 @@
category: 'advanced-input',
defaults: {
name: {value:""},
files: {value:"",required:true}
files: {value:"",required:true},
recursive: {value:""}
},
color:"BurlyWood",
inputs:0,

View File

@@ -19,10 +19,23 @@ module.exports = function(RED) {
var Notify = require("fs.notify");
var fs = require("fs");
var sep = require("path").sep;
var path = require("path");
var getAllDirs = function (dir, filelist) {
filelist = filelist || [];
fs.readdirSync(dir).forEach(file => {
if (fs.statSync(path.join(dir, file)).isDirectory() ) {
filelist.push(path.join(dir, file));
getAllDirs(path.join(dir, file), filelist);
}
});
return filelist;
}
function WatchNode(n) {
RED.nodes.createNode(this,n);
this.recursive = n.recursive || false;
this.files = (n.files || "").split(",");
for (var f=0; f < this.files.length; f++) {
this.files[f] = this.files[f].trim();
@@ -30,6 +43,14 @@ module.exports = function(RED) {
this.p = (this.files.length === 1) ? this.files[0] : JSON.stringify(this.files);
var node = this;
if (node.recursive) {
for (var fi in node.files) {
if (node.files.hasOwnProperty(fi)) {
node.files = node.files.concat(getAllDirs( node.files[fi]));
}
}
}
var notifications = new Notify(node.files);
notifications.on('change', function (file, event, path) {
var stat;
@@ -41,11 +62,17 @@ module.exports = function(RED) {
var msg = { payload:path, topic:node.p, file:file };
if (stat) {
if (stat.isFile()) { type = "file"; msg.size = stat.size; }
else if (stat.isDirectory()) { type = "directory"; }
else if (stat.isBlockDevice()) { type = "blockdevice"; }
else if (stat.isCharacterDevice()) { type = "characterdevice"; }
else if (stat.isSocket()) { type = "socket"; }
else if (stat.isFIFO()) { type = "fifo"; }
else if (stat.isDirectory()) {
type = "directory";
if (node.recursive) {
notifications.add([path]);
notifications.add(getAllDirs(path));
}
}
else { type = "n/a"; }
}
msg.type = type;

View File

@@ -168,8 +168,10 @@
"property": "Set property",
"format": "Syntax Highlight",
"syntax": "Format",
"output": "Output as",
"mustache": "Mustache template",
"plain": "Plain text"
"plain": "Plain text",
"json": "Parsed JSON"
},
"templatevalue": "This is the payload: {{payload}} !"
},
@@ -231,13 +233,14 @@
"output": {
"string": "the string",
"number": "the number",
"existing": "the existing msg.payload",
"original": "the original msg.payload",
"latest": "the latest msg.payload",
"existing": "the existing msg object",
"original": "the original msg object",
"latest": "the latest msg object",
"nothing": "nothing"
},
"wait-reset": "wait to be reset",
"wait-for": "wait for",
"wait-loop": "resend it every",
"duration": {
"ms": "Milliseconds",
"s": "Seconds",
@@ -248,6 +251,7 @@
"label": {
"trigger": "trigger",
"trigger-block": "trigger & block",
"trigger-loop": "resend every",
"reset": "Reset the trigger if:",
"resetMessage":"msg.reset is set",
"resetPayload":"msg.payload equals",
@@ -313,7 +317,10 @@
"method": "Method",
"url": "URL",
"doc": "Docs",
"return": "Return"
"return": "Return",
"upload": "Accept file uploads?",
"status": "Status code",
"headers": "Headers"
},
"setby": "- set by msg.method -",
"basicauth": "Use basic authentication",
@@ -364,7 +371,8 @@
},
"watch": {
"label": {
"files": "File(s)"
"files": "File(s)",
"recursive": "Watch sub-directories recursively"
},
"placeholder": {
"files": "Comma-separated list of files and/or directories"

View File

@@ -22,6 +22,7 @@
<div class="form-row">
<label data-i18n="switch.label.property"></label>
<input type="text" id="node-input-property" style="width: 70%"/>
<input type="hidden" id="node-input-outputs"/>
</div>
<div class="form-row node-input-rule-container-row">
<ol id="node-input-rule-container"></ol>
@@ -43,6 +44,38 @@
</script>
<script type="text/javascript">
(function() {
var operators = [
{v:"eq",t:"=="},
{v:"neq",t:"!="},
{v:"lt",t:"<"},
{v:"lte",t:"<="},
{v:"gt",t:">"},
{v:"gte",t:">="},
{v:"btwn",t:"switch.rules.btwn"},
{v:"cont",t:"switch.rules.cont"},
{v:"regex",t:"switch.rules.regex"},
{v:"true",t:"switch.rules.true"},
{v:"false",t:"switch.rules.false"},
{v:"null",t:"switch.rules.null"},
{v:"nnull",t:"switch.rules.nnull"},
{v:"else",t:"switch.rules.else"}
];
function clipValueLength(v) {
if (v.length > 15) {
return v.substring(0,15)+"...";
}
return v;
}
function getValueLabel(t,v) {
if (t === 'str') {
return '"'+clipValueLength(v)+'"';
} else if (t === 'msg' || t==='flow' || t==='global') {
return t+"."+clipValueLength(v);
}
return clipValueLength(v);
}
RED.nodes.registerType('switch', {
color: "#E2D96E",
category: 'function',
@@ -56,6 +89,24 @@
},
inputs: 1,
outputs: 1,
outputLabels: function(index) {
var rule = this.rules[index];
var label = "";
if (rule) {
for (var i=0;i<operators.length;i++) {
if (operators[i].v === rule.t) {
label = /^switch/.test(operators[i].t)?this._(operators[i].t):operators[i].t
break;
}
}
if (rule.t === 'btwn') {
label += " "+getValueLabel(rule.vt,rule.v)+" & "+getValueLabel(rule.v2t,rule.v2);
} else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'else' ) {
label += " "+getValueLabel(rule.vt,rule.v);
}
return label;
}
},
icon: "switch.png",
label: function() {
return this.name||"switch";
@@ -65,22 +116,7 @@
var previousValueType = {value:"prev",label:this._("inject.previous"),hasValue:false};
$("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global','jsonata']});
var operators = [
{v:"eq",t:"=="},
{v:"neq",t:"!="},
{v:"lt",t:"<"},
{v:"lte",t:"<="},
{v:"gt",t:">"},
{v:"gte",t:">="},
{v:"btwn",t:this._("switch.rules.btwn")},
{v:"cont",t:this._("switch.rules.cont")},
{v:"regex",t:this._("switch.rules.regex")},
{v:"true",t:this._("switch.rules.true")},
{v:"false",t:this._("switch.rules.false")},
{v:"null",t:this._("switch.rules.null")},
{v:"nnull",t:this._("switch.rules.nnull")},
{v:"else",t:this._("switch.rules.else")}
];
var outputCount = $("#node-input-outputs").val("{}");
var andLabel = this._("switch.and");
var caseLabel = this._("switch.ignorecase");
@@ -122,12 +158,15 @@
if (!rule.hasOwnProperty('t')) {
rule.t = 'eq';
}
if (!opt.hasOwnProperty('i')) {
opt._i = Math.floor((0x99999-0x10000)*Math.random()).toString(16)
}
var row = $('<div/>').appendTo(container);
var row2 = $('<div/>',{style:"padding-top: 5px; padding-left: 175px;"}).appendTo(container);
var row3 = $('<div/>',{style:"padding-top: 5px; padding-left: 102px;"}).appendTo(container);
var selectField = $('<select/>',{style:"width:120px; margin-left: 5px; text-align: center;"}).appendTo(row);
for (var d in operators) {
selectField.append($("<option></option>").val(operators[d].v).text(operators[d].t));
selectField.append($("<option></option>").val(operators[d].v).text(/^switch/.test(operators[d].t)?node._(operators[d].t):operators[d].t));
}
var valueField = $('<input/>',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata',previousValueType]});
var btwnValueField = $('<input/>',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]});
@@ -178,21 +217,36 @@
caseSensitive.prop('checked',false);
}
selectField.change();
var currentOutputs = JSON.parse(outputCount.val()||"{}");
currentOutputs[opt.hasOwnProperty('i')?opt.i:opt._i] = i;
outputCount.val(JSON.stringify(currentOutputs));
},
removeItem: function(opt) {
var currentOutputs = JSON.parse(outputCount.val()||"{}");
if (opt.hasOwnProperty('i')) {
var removedList = $("#node-input-rule-container").data('removedList')||[];
removedList.push(opt.i);
$("#node-input-rule-container").data('removedList',removedList);
currentOutputs[opt.i] = -1;
} else {
delete currentOutputs[opt._i];
}
var rules = $("#node-input-rule-container").editableList('items');
rules.each(function(i) { $(this).find(".node-input-rule-index").html(i+1); });
rules.each(function(i) {
$(this).find(".node-input-rule-index").html(i+1);
var data = $(this).data('data');
currentOutputs[data.hasOwnProperty('i')?data.i:data._i] = i;
});
outputCount.val(JSON.stringify(currentOutputs));
},
resizeItem: resizeRule,
sortItems: function(rules) {
var currentOutputs = JSON.parse(outputCount.val()||"{}");
var rules = $("#node-input-rule-container").editableList('items');
rules.each(function(i) { $(this).find(".node-input-rule-index").html(i+1); });
rules.each(function(i) {
$(this).find(".node-input-rule-index").html(i+1);
var data = $(this).data('data');
currentOutputs[data.hasOwnProperty('i')?data.i:data._i] = i;
});
outputCount.val(JSON.stringify(currentOutputs));
},
sortable: true,
removable: true
@@ -208,11 +262,6 @@
var ruleset;
var node = this;
node.rules = [];
var changedOutputs = {};
var removedList = $("#node-input-rule-container").data('removedList')||[];
removedList.forEach(function(i) {
changedOutputs[i] = -1;
});
rules.each(function(i) {
var ruleData = $(this).data('data');
var rule = $(this);
@@ -232,15 +281,8 @@
r.case = rule.find(".node-input-rule-case").prop("checked");
}
}
if (ruleData.hasOwnProperty('i')) {
if (ruleData.i !== i) {
changedOutputs[ruleData.i] = i;
}
}
node.rules.push(r);
});
this._outputs = changedOutputs;
this.outputs = node.rules.length;
this.propertyType = $("#node-input-property").typedInput('type');
},
oneditresize: function(size) {
@@ -254,4 +296,5 @@
$("#node-input-rule-container").editableList('height',height);
}
});
})();
</script>

View File

@@ -28,6 +28,7 @@ module.exports = function(RED) {
this.hdrin = n.hdrin || false;
this.hdrout = n.hdrout || false;
this.goodtmpl = true;
var tmpwarn = true;
var node = this;
// pass in an array of column names to be trimed, de-quoted and retrimed
@@ -71,7 +72,19 @@ module.exports = function(RED) {
}
else {
if ((node.template.length === 1) && (node.template[0] === '')) {
node.warn(RED._("csv.errors.obj_csv"));
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]) {
if (msg.payload[0].hasOwnProperty(p)) {
if (typeof msg.payload[0][p] !== "object") {
ou += msg.payload[0][p] + ",";
}
}
}
ou = ou.slice(0,-1) + node.ret;
}
else {
for (var t=0; t < node.template.length; t++) {
@@ -112,7 +125,7 @@ module.exports = function(RED) {
var first = true; // is this the first line
var line = msg.payload;
var tmp = "";
var reg = new RegExp("^[-]?[0-9.]*[\.]?[0-9]*$");
var reg = /^[-]?[0-9]*\.?[0-9]+$/;
// 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...

View File

@@ -45,6 +45,7 @@
"jsonata":"1.0.10",
"media-typer": "0.3.0",
"mqtt": "2.2.1",
"multer": "1.2.1",
"mustache": "2.3.0",
"nopt": "3.0.6",
"oauth2orize":"1.7.0",

View File

@@ -94,7 +94,7 @@ function init(_server,_runtime) {
});
}
editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor);
editorApp.get("/icons/:icon",ui.icon);
editorApp.get("/icons/:module/:icon",ui.icon);
theme.init(runtime);
if (settings.editorTheme) {
editorApp.use("/theme",theme.app());

View File

@@ -48,6 +48,8 @@ module.exports = {
safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {};
safeSettings.editorTheme.palette.editable = false;
}
settings.exportNodeSettings(safeSettings);
res.json(safeSettings);
}

View File

@@ -20,6 +20,7 @@ var when = require('when');
var redApp = null;
var storage;
var log;
var redNodes;
var needsPermission = require("./auth").needsPermission;
function createLibrary(type) {
@@ -72,80 +73,22 @@ function createLibrary(type) {
}
}
var exampleRoots = {};
var exampleFlows = {d:{}};
var exampleCount = 0;
function getFlowsFromPath(path) {
return when.promise(function(resolve,reject) {
var result = {};
fs.readdir(path,function(err,files) {
var promises = [];
var validFiles = [];
files.forEach(function(file) {
var fullPath = fspath.join(path,file);
var stats = fs.lstatSync(fullPath);
if (stats.isDirectory()) {
validFiles.push(file);
promises.push(getFlowsFromPath(fullPath));
} else if (/\.json$/.test(file)){
validFiles.push(file);
exampleCount++;
promises.push(when.resolve(file.split(".")[0]))
}
})
var i=0;
when.all(promises).then(function(results) {
results.forEach(function(r) {
if (typeof r === 'string') {
result.f = result.f||[];
result.f.push(r);
} else {
result.d = result.d||{};
result.d[validFiles[i]] = r;
}
i++;
})
resolve(result);
})
});
})
}
function addNodeExamplesDir(module) {
exampleRoots[module.name] = module.path;
getFlowsFromPath(module.path).then(function(result) {
exampleFlows.d[module.name] = result;
});
}
function removeNodeExamplesDir(module) {
delete exampleRoots[module];
delete exampleFlows.d[module];
}
module.exports = {
init: function(app,runtime) {
redApp = app;
log = runtime.log;
storage = runtime.storage;
// TODO: this allows init to be called multiple times without
// registering multiple instances of the listener.
// It isn't.... ideal.
runtime.events.removeListener("node-examples-dir",addNodeExamplesDir);
runtime.events.on("node-examples-dir",addNodeExamplesDir);
runtime.events.removeListener("node-module-uninstalled",removeNodeExamplesDir);
runtime.events.on("node-module-uninstalled",removeNodeExamplesDir);
redNodes = runtime.nodes;
},
register: createLibrary,
getAll: function(req,res) {
storage.getAllFlows().then(function(flows) {
log.audit({event: "library.get.all",type:"flow"},req);
if (exampleCount > 0) {
var examples = redNodes.getNodeExampleFlows();
if (examples) {
flows.d = flows.d||{};
flows.d._examples_ = exampleFlows;
flows.d._examples_ = redNodes.getNodeExampleFlows();
}
res.json(flows);
});
@@ -155,9 +98,9 @@ module.exports = {
var m = /^_examples_\/([^\/]+)\/(.*)$/.exec(req.params[0]);
if (m) {
var module = m[1];
var path = m[2]+".json";
if (exampleRoots[module]) {
var fullPath = fspath.join(exampleRoots[module],path);
var path = m[2];
var fullPath = redNodes.getNodeExampleFlowPath(module,path);
if (fullPath) {
try {
fs.statSync(fullPath);
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);

View File

@@ -68,7 +68,8 @@
"warnings": {
"undeployedChanges": "node has undeployed changes",
"nodeActionDisabled": "node actions disabled within subflow",
"missing-types": "Flows stopped due to missing node types. Check logs for details."
"missing-types": "Flows stopped due to missing node types. Check logs for details.",
"restartRequired": "Node-RED must be restarted to enable upgraded modules"
},
"error": "<strong>Error</strong>: __message__",
@@ -184,6 +185,12 @@
"editNode": "Edit __type__ node",
"editConfig": "Edit __type__ config node",
"addNewType": "Add new __type__...",
"nodeProperties": "node properties",
"portLabels": "port labels",
"labelInputs": "Inputs",
"labelOutputs": "Outputs",
"noDefaultLabel": "none",
"defaultLabel": "use default label",
"errors": {
"scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it"
}
@@ -250,7 +257,8 @@
"nodeEnabled": "Node enabled:",
"nodeEnabled_plural": "Nodes enabled:",
"nodeDisabled": "Node disabled:",
"nodeDisabled_plural": "Nodes disabled:"
"nodeDisabled_plural": "Nodes disabled:",
"nodeUpgraded": "Node module __module__ upgraded to version __version__"
},
"editor": {
"title": "Manage palette",
@@ -283,6 +291,7 @@
"disable": "disable",
"remove": "remove",
"update": "update to __version__",
"updated": "updated",
"install": "install",
"installed": "installed",
"loading": "Loading catalogues...",
@@ -295,7 +304,8 @@
"errors": {
"catalogLoadFailed": "Failed to load node catalogue.<br>Check the browser console for more information",
"installFailed": "Failed to install: __module__<br>__message__<br>Check the log for more information",
"removeFailed": "Failed to remove: __module__<br>__message__<br>Check the log for more information"
"removeFailed": "Failed to remove: __module__<br>__message__<br>Check the log for more information",
"updateFailed": "Failed to update: __module__<br>__message__<br>Check the log for more information"
},
"confirm": {
"install": {
@@ -306,10 +316,18 @@
"body":"Removing the node will uninstall it from Node-RED. The node may continue to use resources until Node-RED is restarted.",
"title": "Remove nodes"
},
"update": {
"body":"Updating the node will require a restart of Node-RED to complete the update. This must be done manually.",
"title": "Update nodes"
},
"cannotUpdate": {
"body":"An update for this node is available, but it is not installed in a location that the palette manager can update.<br/><br/>Please refer to the documentation for how to update this node."
},
"button": {
"review": "Open node information",
"install": "Install",
"remove": "Remove"
"remove": "Remove",
"update": "Update"
}
}

View File

@@ -48,34 +48,47 @@ module.exports = {
}
var node = req.body;
var promise;
var isUpgrade = false;
if (node.module) {
var module = redNodes.getModuleInfo(node.module);
if (module) {
log.audit({event: "nodes.install",module:node.module,error:"module_already_loaded"},req);
res.status(400).json({error:"module_already_loaded", message:"Module already loaded"});
return;
if (!node.version || module.version === node.version) {
log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_already_loaded"},req);
res.status(400).json({error:"module_already_loaded", message:"Module already loaded"});
return;
}
if (!module.local) {
log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_not_local"},req);
res.status(400).json({error:"module_not_local", message:"Module not locally installed"});
return;
}
isUpgrade = true;
}
promise = redNodes.installModule(node.module);
promise = redNodes.installModule(node.module,node.version);
} else {
log.audit({event: "nodes.install",module:node.module,error:"invalid_request"},req);
res.status(400).json({error:"invalid_request", message:"Invalid request"});
return;
}
promise.then(function(info) {
comms.publish("node/added",info.nodes,false);
if (isUpgrade) {
comms.publish("node/upgraded",{module:node.module,version:node.version},false);
} else {
comms.publish("node/added",info.nodes,false);
}
if (node.module) {
log.audit({event: "nodes.install",module:node.module},req);
log.audit({event: "nodes.install",module:node.module,version:node.version},req);
res.json(info);
}
}).otherwise(function(err) {
if (err.code === 404) {
log.audit({event: "nodes.install",module:node.module,error:"not_found"},req);
log.audit({event: "nodes.install",module:node.module,version:node.version,error:"not_found"},req);
res.status(404).end();
} else if (err.code) {
log.audit({event: "nodes.install",module:node.module,error:err.code},req);
log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code},req);
res.status(400).json({error:err.code, message:err.message});
} else {
log.audit({event: "nodes.install",module:node.module,error:err.code||"unexpected_error",message:err.toString()},req);
log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
}
});

View File

@@ -16,31 +16,20 @@
var express = require('express');
var fs = require("fs");
var path = require("path");
var Mustache = require("mustache");
var theme = require("./theme");
var Mustache = require("mustache");
var redNodes;
var icon_paths = [path.resolve(__dirname + '/../../public/icons')];
var iconCache = {};
//TODO: create a default icon
var defaultIcon = path.resolve(__dirname + '/../../public/icons/arrow-in.png');
var templateDir = path.resolve(__dirname+"/../../editor/templates");
var editorTemplate;
function nodeIconDir(dir) {
icon_paths.push(path.resolve(dir));
}
module.exports = {
init: function(runtime) {
redNodes = runtime.nodes;
editorTemplate = fs.readFileSync(path.join(templateDir,"index.mst"),"utf8");
Mustache.parse(editorTemplate);
// TODO: this allows init to be called multiple times without
// registering multiple instances of the listener.
// It isn't.... ideal.
runtime.events.removeListener("node-icon-dir",nodeIconDir);
runtime.events.on("node-icon-dir",nodeIconDir);
},
ensureSlash: function(req,res,next) {
@@ -54,22 +43,10 @@ module.exports = {
}
},
icon: function(req,res) {
if (iconCache[req.params.icon]) {
res.sendFile(iconCache[req.params.icon]); // if not found, express prints this to the console and serves 404
} else {
for (var p=0;p<icon_paths.length;p++) {
var iconPath = path.join(icon_paths[p],req.params.icon);
try {
fs.statSync(iconPath);
res.sendFile(iconPath);
iconCache[req.params.icon] = iconPath;
return;
} catch(err) {
// iconPath doesn't exist
}
}
res.sendFile(defaultIcon);
}
var icon = req.params.icon;
var module = req.params.module;
var iconPath = redNodes.getNodeIconPath(module,icon);
res.sendFile(iconPath);
},
editor: function(req,res) {
res.send(Mustache.render(editorTemplate,theme.context()));

View File

@@ -126,14 +126,19 @@ function start() {
var missingModules = {};
for (i=0;i<nodeMissing.length;i++) {
var missing = nodeMissing[i];
missingModules[missing.module] = (missingModules[missing.module]||[]).concat(missing.types);
missingModules[missing.module] = missingModules[missing.module]||{
module:missing.module,
version:missing.pending_version||missing.version,
types:[]
}
missingModules[missing.module].types = missingModules[missing.module].types.concat(missing.types);
}
var promises = [];
for (i in missingModules) {
if (missingModules.hasOwnProperty(i)) {
log.warn(" - "+i+": "+missingModules[i].join(", "));
log.warn(" - "+i+" ("+missingModules[i].version+"): "+missingModules[i].types.join(", "));
if (settings.autoInstallModules && i != "node-red") {
redNodes.installModule(i).otherwise(function(err) {
redNodes.installModule(i,missingModules[i].version).otherwise(function(err) {
// Error already reported. Need the otherwise handler
// to stop the error propagating any further
});

View File

@@ -24,12 +24,14 @@
"removed-types": "Removed node types:",
"install": {
"invalid": "Invalid module name",
"installing": "Installing module: __name__",
"installing": "Installing module: __name__, version: __version__",
"installed": "Installed module: __name__",
"install-failed": "Install failed",
"install-failed-long": "Installation of module __name__ failed:",
"install-failed-not-found": "$t(install-failed-long) module not found",
"upgrading": "Upgrading module: __name__ to version: __version__",
"upgraded": "Upgraded module: __name__. Restart Node-RED to use the new version",
"upgrade-failed-not-found": "$t(server.install.install-failed-long) version not found",
"uninstalling": "Uninstalling module: __name__",
"uninstall-failed": "Uninstall failed",
"uninstall-failed-long": "Uninstall of module __name__ failed:",

View File

@@ -25,6 +25,7 @@ var flowUtil = require("./flows/util")
var context = require("./context");
var Node = require("./Node");
var log = require("../log");
var library = require("./library");
var events = require("../events");
@@ -49,8 +50,13 @@ function registerType(nodeSet,type,constructor,opts) {
type = nodeSet;
nodeSet = "";
}
if (opts && opts.credentials) {
credentials.register(type,opts.credentials);
if (opts) {
if (opts.credentials) {
credentials.register(type,opts.credentials);
}
if (opts.settings) {
settings.registerNodeSettings(type,opts.settings);
}
}
registry.registerType(nodeSet,type,constructor);
}
@@ -88,6 +94,7 @@ function init(runtime) {
flows.init(runtime);
registry.init(runtime);
context.init(runtime.settings);
library.init(runtime);
}
function disableNode(id) {
@@ -135,6 +142,9 @@ module.exports = {
getNodeConfigs: registry.getNodeConfigs,
getNodeConfig: registry.getNodeConfig,
getNodeIconPath: registry.getNodeIconPath,
getNodeExampleFlows: library.getExampleFlows,
getNodeExampleFlowPath: library.getExampleFlowPath,
clearRegistry: registry.clear,
cleanModuleList: registry.cleanModuleList,

View File

@@ -0,0 +1,108 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var fs = require('fs');
var fspath = require('path');
var when = require('when');
var runtime;
var exampleRoots = {};
var exampleFlows = null;
function getFlowsFromPath(path) {
return when.promise(function(resolve,reject) {
var result = {};
fs.readdir(path,function(err,files) {
var promises = [];
var validFiles = [];
files.forEach(function(file) {
var fullPath = fspath.join(path,file);
var stats = fs.lstatSync(fullPath);
if (stats.isDirectory()) {
validFiles.push(file);
promises.push(getFlowsFromPath(fullPath));
} else if (/\.json$/.test(file)){
validFiles.push(file);
promises.push(when.resolve(file.split(".")[0]))
}
})
var i=0;
when.all(promises).then(function(results) {
results.forEach(function(r) {
if (typeof r === 'string') {
result.f = result.f||[];
result.f.push(r);
} else {
result.d = result.d||{};
result.d[validFiles[i]] = r;
}
i++;
})
resolve(result);
})
});
})
}
function addNodeExamplesDir(module) {
exampleRoots[module.name] = module.path;
getFlowsFromPath(module.path).then(function(result) {
exampleFlows = exampleFlows||{d:{}};
exampleFlows.d[module.name] = result;
});
}
function removeNodeExamplesDir(module) {
delete exampleRoots[module];
if (exampleFlows && exampleFlows.d) {
delete exampleFlows.d[module];
}
if (exampleFlows && Object.keys(exampleFlows.d).length === 0) {
exampleFlows = null;
}
}
function init(_runtime) {
runtime = _runtime;
exampleRoots = {};
exampleFlows = null;
runtime.events.removeListener("node-examples-dir",addNodeExamplesDir);
runtime.events.on("node-examples-dir",addNodeExamplesDir);
runtime.events.removeListener("node-module-uninstalled",removeNodeExamplesDir);
runtime.events.on("node-module-uninstalled",removeNodeExamplesDir);
}
function getExampleFlows() {
return exampleFlows;
}
function getExampleFlowPath(module,path) {
if (exampleRoots[module]) {
return fspath.join(exampleRoots[module],path)+".json";
}
return null;
}
module.exports = {
init: init,
getExampleFlows: getExampleFlows,
getExampleFlowPath: getExampleFlowPath
}

View File

@@ -70,6 +70,7 @@ module.exports = {
getNodeConfigs: registry.getAllNodeConfigs,
getNodeConfig: registry.getNodeConfig,
getNodeIconPath: registry.getNodeIconPath,
enableNode: enableNodeSet,
disableNode: registry.disableNodeSet,

View File

@@ -59,45 +59,63 @@ function checkModulePath(folder) {
return moduleName;
}
function checkExistingModule(module) {
if (registry.getModuleInfo(module)) {
// TODO: nls
var err = new Error("Module already loaded");
err.code = "module_already_loaded";
throw err;
function checkExistingModule(module,version) {
var info = registry.getModuleInfo(module);
if (info) {
if (!version || info.version === version) {
var err = new Error("Module already loaded");
err.code = "module_already_loaded";
throw err;
}
return true;
}
return false;
}
function installModule(module) {
function installModule(module,version) {
//TODO: ensure module is 'safe'
return when.promise(function(resolve,reject) {
var installName = module;
var isUpgrade = false;
try {
if (moduleRe.test(module)) {
// Simple module name - assume it can be npm installed
if (version) {
installName += "@"+version;
}
} else if (slashRe.test(module)) {
// A path - check if there's a valid package.json
installName = module;
module = checkModulePath(module);
}
checkExistingModule(module);
isUpgrade = checkExistingModule(module,version);
} catch(err) {
return reject(err);
}
log.info(log._("server.install.installing",{name: module}));
if (!isUpgrade) {
log.info(log._("server.install.installing",{name: module,version: version||"latest"}));
} else {
log.info(log._("server.install.upgrading",{name: module,version: version||"latest"}));
}
var installDir = settings.userDir || process.env.NODE_RED_HOME || ".";
var child = child_process.execFile(npmCommand,['install','--production',installName],
var child = child_process.execFile(npmCommand,['install','--save','--save-exact','--production',installName],
{
cwd: installDir
},
function(err, stdin, stdout) {
if (err) {
var lookFor404 = new RegExp(" 404 .*"+installName+"$","m");
var e;
var lookFor404 = new RegExp(" 404 .*"+module+"$","m");
var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m");
if (lookFor404.test(stdout)) {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
var e = new Error("Module not found");
e = new Error("Module not found");
e.code = 404;
reject(e);
} else if (isUpgrade && lookForVersionNotFound.test(stdout)) {
log.warn(log._("server.install.upgrade-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
reject(e);
} else {
@@ -108,8 +126,14 @@ function installModule(module) {
reject(new Error(log._("server.install.install-failed")));
}
} else {
log.info(log._("server.install.installed",{name:module}));
resolve(require("./index").addModule(module).then(reportAddedModules));
if (!isUpgrade) {
log.info(log._("server.install.installed",{name:module}));
resolve(require("./index").addModule(module).then(reportAddedModules));
} else {
log.info(log._("server.install.upgraded",{name:module, version:version}));
events.emit("runtime-event",{id:"restart-required",type:"warning",text:"notification.warnings.restartRequired"});
resolve(require("./registry").setModulePendingUpdated(module,version));
}
}
}
);
@@ -162,7 +186,7 @@ function uninstallModule(module) {
var list = registry.removeModule(module);
log.info(log._("server.install.uninstalling",{name:module}));
var child = child_process.execFile(npmCommand,['remove',module],
var child = child_process.execFile(npmCommand,['remove','--save',module],
{
cwd: installDir
},

View File

@@ -89,7 +89,7 @@ function getLocalNodeFiles(dir) {
if (!/^(\..*|lib|icons|node_modules|test|locales)$/.test(fn)) {
result = result.concat(getLocalNodeFiles(path.join(dir,fn)));
} else if (fn === "icons") {
events.emit("node-icon-dir",path.join(dir,fn));
events.emit("node-icon-dir",{name:'node-red',path:path.join(dir,fn)});
}
}
});
@@ -182,7 +182,7 @@ function getModuleNodeFiles(module) {
if (iconDirs.indexOf(iconDir) == -1) {
try {
fs.statSync(iconDir);
events.emit("node-icon-dir",iconDir);
events.emit("node-icon-dir",{name:pkg.name,path:iconDir});
iconDirs.push(iconDir);
} catch(err) {
}

View File

@@ -17,6 +17,9 @@
//var UglifyJS = require("uglify-js");
var util = require("util");
var when = require("when");
var path = require("path");
var fs = require("fs");
var events = require("../../events");
var settings;
@@ -41,6 +44,9 @@ function init(_settings,_loader) {
nodeList = [];
nodeConfigCache = null;
Node = require("../Node");
events.removeListener("node-icon-dir",nodeIconDir);
events.on("node-icon-dir",nodeIconDir);
}
function load() {
@@ -82,7 +88,8 @@ function getNode(id) {
function saveNodeList() {
var moduleList = {};
var hadPending = false;
var hasPending = false;
for (var module in moduleConfigs) {
/* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(module)) {
@@ -94,6 +101,15 @@ function saveNodeList() {
local: moduleConfigs[module].local||false,
nodes: {}
};
if (moduleConfigs[module].hasOwnProperty('pending_version')) {
hadPending = true;
if (moduleConfigs[module].pending_version !== moduleConfigs[module].version) {
moduleList[module].pending_version = moduleConfigs[module].pending_version;
hasPending = true;
} else {
delete moduleConfigs[module].pending_version;
}
}
}
var nodes = moduleConfigs[module].nodes;
for(var node in nodes) {
@@ -111,6 +127,9 @@ function saveNodeList() {
}
}
}
if (hadPending && !hasPending) {
events.emit("runtime-event",{id:"restart-required"});
}
if (settings.available()) {
return settings.set("nodes",moduleList);
} else {
@@ -280,6 +299,9 @@ function getNodeList(filter) {
if (nodes.hasOwnProperty(node)) {
var nodeInfo = filterNodeInfo(nodes[node]);
nodeInfo.version = moduleConfigs[module].version;
if (moduleConfigs[module].pending_version) {
nodeInfo.pending_version = moduleConfigs[module].pending_version;
}
if (!filter || filter(nodes[node])) {
list.push(nodeInfo);
}
@@ -471,6 +493,7 @@ function enableNodeSet(typeOrId) {
delete config.err;
config.enabled = true;
nodeConfigCache = null;
settings.enableNodeSettings(config.types);
return saveNodeList().then(function() {
return filterNodeInfo(config);
});
@@ -493,6 +516,7 @@ function disableNodeSet(typeOrId) {
// TODO: persist setting
config.enabled = false;
nodeConfigCache = null;
settings.disableNodeSettings(config.types);
return saveNodeList().then(function() {
return filterNodeInfo(config);
});
@@ -539,6 +563,46 @@ function cleanModuleList() {
saveNodeList();
}
}
function setModulePendingUpdated(module,version) {
moduleConfigs[module].pending_version = version;
return saveNodeList().then(function() {
return getModuleInfo(module);
});
}
var icon_paths = {
"node-red":[path.resolve(__dirname + '/../../../../public/icons')]
};
var iconCache = {};
var defaultIcon = path.resolve(__dirname + '/../../../../public/icons/arrow-in.png');
function nodeIconDir(dir) {
icon_paths[dir.name] = icon_paths[dir.name] || [];
icon_paths[dir.name].push(path.resolve(dir.path));
}
function getNodeIconPath(module,icon) {
var iconName = module+"/"+icon;
if (iconCache[iconName]) {
return iconCache[iconName];
} else {
var paths = icon_paths[module];
if (paths) {
for (var p=0;p<paths.length;p++) {
var iconPath = path.join(paths[p],icon);
try {
fs.statSync(iconPath);
iconCache[iconName] = iconPath;
return iconPath;
} catch(err) {
// iconPath doesn't exist
}
}
}
return defaultIcon;
}
}
var registry = module.exports = {
init: init,
@@ -552,6 +616,7 @@ var registry = module.exports = {
enableNodeSet: enableNodeSet,
disableNodeSet: disableNodeSet,
setModulePendingUpdated: setModulePendingUpdated,
removeModule: removeModule,
getNodeInfo: getNodeInfo,
@@ -560,6 +625,7 @@ var registry = module.exports = {
getModuleList: getModuleList,
getModuleInfo: getModuleInfo,
getNodeIconPath: getNodeIconPath,
/**
* Gets all of the node template configs
* @return all of the node templates in a single string

View File

@@ -18,9 +18,12 @@ var when = require("when");
var clone = require("clone");
var assert = require("assert");
var log = require("./log");
var util = require("./util");
var userSettings = null;
var globalSettings = null;
var nodeSettings = null;
var disableNodeSettings = null;
var storage = null;
var persistentSettings = {
@@ -38,6 +41,8 @@ var persistentSettings = {
}
}
globalSettings = null;
nodeSettings = {};
disableNodeSettings = {};
},
load: function(_storage) {
storage = _storage;
@@ -99,6 +104,53 @@ var persistentSettings = {
userSettings = null;
globalSettings = null;
storage = null;
},
registerNodeSettings: function(type, opts) {
try {
for (var property in opts) {
if (opts.hasOwnProperty(property)) {
var normalisedType = util.normaliseNodeTypeName(type);
if (!property.startsWith(normalisedType)) {
throw new Error("The name of node setting property " + property + " must start with \"" + normalisedType + "\" (case sensitive).");
}
}
}
nodeSettings[type] = opts;
} catch (err) {
console.log(err.toString());
}
},
exportNodeSettings: function(safeSettings) {
safeSettings["nodeSettings"] = {};
for (var type in nodeSettings) {
if (nodeSettings.hasOwnProperty(type) && !disableNodeSettings[type]) {
var nodeTypeSettings = nodeSettings[type];
for (var property in nodeTypeSettings) {
if (nodeTypeSettings.hasOwnProperty(property)) {
var setting = nodeTypeSettings[property];
if (setting.exportable) {
if (userSettings.hasOwnProperty(property)) {
safeSettings["nodeSettings"][property] = userSettings[property];
} else {
safeSettings["nodeSettings"][property] = setting.value;
}
}
}
}
}
}
return safeSettings;
},
enableNodeSettings: function(types) {
types.forEach(function(type) {
disableNodeSettings[type] = false;
});
},
disableNodeSettings: function(types) {
types.forEach(function(type) {
disableNodeSettings[type] = true;
});
}
}

View File

@@ -230,11 +230,25 @@ var localfilesystem = {
globalSettingsFile = fspath.join(settings.userDir,".config.json");
var packageFile = fspath.join(settings.userDir,"package.json");
var packagePromise = when.resolve();
if (!settings.readOnly) {
promises.push(promiseDir(libFlowsDir));
packagePromise = function() {
try {
fs.statSync(packageFile);
} catch(err) {
var defaultPackage = {
"name": "node-red-project",
"description": "A Node-RED Project",
"version": "0.0.1"
};
return writeFile(packageFile,JSON.stringify(defaultPackage,"",4));
}
return true;
}
}
return when.all(promises);
return when.all(promises).then(packagePromise);
},
getFlows: function() {

View File

@@ -328,6 +328,18 @@ function evaluateNodeProperty(value, type, node, msg) {
return value;
}
function normaliseNodeTypeName(name) {
var result = name.replace(/[^a-zA-Z0-9]/g, " ");
result = result.trim();
result = result.replace(/ +/g, " ");
result = result.replace(/ ./g,
function(s) {
return s.charAt(1).toUpperCase();
}
);
result = result.charAt(0).toLowerCase() + result.slice(1);
return result;
}
module.exports = {
ensureString: ensureString,
@@ -338,5 +350,6 @@ module.exports = {
getMessageProperty: getMessageProperty,
setMessageProperty: setMessageProperty,
evaluateNodeProperty: evaluateNodeProperty,
normalisePropertyExpression: normalisePropertyExpression
normalisePropertyExpression: normalisePropertyExpression,
normaliseNodeTypeName: normaliseNodeTypeName
};

View File

@@ -167,6 +167,25 @@ module.exports = {
// next();
//},
// The following property can be used to verify websocket connection attempts.
// This allows, for example, the HTTP request headers to be checked to ensure
// they include valid authentication information.
//webSocketVerifyClient: function(info) {
// // 'info' has three properties:
// // - origin : the value in the Origin header
// // - req : the HTTP request
// // - secure : true if req.connection.authorized or req.connection.encrypted is set
// //
// // The function should return true if the connection should be accepted, false otherwise.
// //
// // Alternatively, if this function is defined to accept a second argument, callback,
// // it can be used to verify the client asynchronously.
// // The callback takes three arguments:
// // - result : boolean, whether to accept the connection or not
// // - code : if result is false, the HTTP error status to return
// // - reason: if result is false, the HTTP reason string to return
//},
// Anything in this hash is globally available to all functions.
// It is accessed as context.global.
// eg:

View File

@@ -58,42 +58,42 @@ describe('exec node', function() {
arg3(null,arg1,arg1.toUpperCase());
});
helper.load(execNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
var n4 = helper.getNode("n4");
var received = 0;
var messages = [null,null];
var completeTest = function() {
received++;
if (received < 2) {
return;
}
try{
var msg = messages[0];
msg.should.have.property("payload");
msg.payload.should.be.a.String();
msg.payload.should.equal("echo");
helper.load(execNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
var n4 = helper.getNode("n4");
var received = 0;
var messages = [null,null];
var completeTest = function() {
received++;
if (received < 2) {
return;
}
try {
var msg = messages[0];
msg.should.have.property("payload");
msg.payload.should.be.a.String();
msg.payload.should.equal("echo");
msg = messages[1];
msg.should.have.property("payload");
msg.payload.should.be.a.String,
msg.payload.should.equal("ECHO");
child_process.exec.restore();
done();
} catch(err) {
child_process.exec.restore();
done(err);
}
};
n2.on("input", function(msg) {
messages[0] = msg;
completeTest();
});
n3.on("input", function(msg) {
messages[1] = msg;
completeTest();
msg = messages[1];
msg.should.have.property("payload");
msg.payload.should.be.a.String;
msg.payload.should.equal("ECHO");
child_process.exec.restore();
done();
} catch(err) {
child_process.exec.restore();
done(err);
}
};
n2.on("input", function(msg) {
messages[0] = msg;
completeTest();
});
n3.on("input", function(msg) {
messages[1] = msg;
completeTest();
});
n1.receive({payload:"and"});
});
@@ -121,7 +121,7 @@ describe('exec node', function() {
if (received < 2) {
return;
}
try{
try {
var msg = messages[0];
msg.should.have.property("payload");
msg.payload.should.be.a.String();
@@ -210,6 +210,27 @@ describe('exec node', function() {
n1.receive({});
});
});
it('should be able to kill a long running command', function(done) {
var flow = [{id:"n1",type:"exec",wires:[["n2"],["n3"],["n4"]],command:"sleep", addpay:false, append:"1", timer:"2"},
{id:"n2", type:"helper"},{id:"n3", type:"helper"},{id:"n4", type:"helper"}];
helper.load(execNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
var n4 = helper.getNode("n4");
n4.on("input", function(msg) {
msg.should.have.property("payload");
msg.payload.should.have.property("killed",true);
msg.payload.should.have.property("signal","SIGTERM");
done();
});
setTimeout(function() {
n1.receive({kill:true});
},150);
n1.receive({});
});
});
});
describe('calling spawn', function() {
@@ -297,7 +318,7 @@ describe('exec node', function() {
if (received < 2) {
return;
}
try{
try {
var msg = messages[0];
msg.should.have.property("payload");
msg.payload.should.be.a.String();
@@ -401,5 +422,26 @@ describe('exec node', function() {
});
});
it('should be able to kill a long running command', function(done) {
var flow = [{id:"n1",type:"exec",wires:[["n2"],["n3"],["n4"]],command:"sleep", addpay:false, append:"1", timer:"2"},
{id:"n2", type:"helper"},{id:"n3", type:"helper"},{id:"n4", type:"helper"}];
helper.load(execNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var n3 = helper.getNode("n3");
var n4 = helper.getNode("n4");
n4.on("input", function(msg) {
msg.should.have.property("payload");
msg.payload.should.have.property("killed",true);
msg.payload.should.have.property("signal","SIGTERM");
done();
});
setTimeout(function() {
n1.receive({kill:true});
},150);
n1.receive({});
});
});
});
});

View File

@@ -356,7 +356,7 @@ describe('trigger node', function() {
});
it('should be able to set infinite timeout, and clear timeout', function(done) {
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:-5, wires:[["n2"]] },
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:0, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(triggerNode, flow, function() {
var n1 = helper.getNode("n1");
@@ -378,7 +378,7 @@ describe('trigger node', function() {
});
it('should be able to set infinite timeout, and clear timeout by message', function(done) {
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", duration:-5, wires:[["n2"]] },
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", duration:0, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(triggerNode, flow, function() {
var n1 = helper.getNode("n1");
@@ -399,4 +399,26 @@ describe('trigger node', function() {
});
});
it('should be able to set a repeat, and clear loop by reset', function(done) {
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", duration:-25, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(triggerNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c += 1;
msg.should.have.a.property("payload", "foo");
});
n1.emit("input", {payload:"foo"}); // trigger
setTimeout( function() {
n1.emit("input", {reset:true}); // reset
},90);
setTimeout( function() {
c.should.equal(4); // should send foo 4 times.
done();
},150);
});
});
});

View File

@@ -179,6 +179,21 @@ describe('CSV node', function() {
n1.emit("input", {payload:testString});
});
});
it('should handle numbers in strings but not IP addresses', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: "a", b: "127.0.0.1", c: 56.7, d: -32.8, e: "+76.22C" });
done();
});
var testString = "a,127.0.0.1,56.7,-32.8,+76.22C";
n1.emit("input", {payload:testString});
});
});
});
describe('json object to csv', function() {

View File

@@ -36,6 +36,7 @@ var credentials = require("../../red/runtime/nodes/credentials");
var comms = require("../../red/api/comms.js");
var log = require("../../red/runtime/log.js");
var context = require("../../red/runtime/nodes/context.js");
var events = require("../../red/runtime/events.js");
var http = require('http');
var express = require('express');
@@ -92,7 +93,7 @@ module.exports = {
return messageId;
};
redNodes.init({settings:settings, storage:storage,log:log});
redNodes.init({events:events,settings:settings, storage:storage,log:log,});
RED.nodes.registerType("helper", helperNode);
if (Array.isArray(testNode)) {
for (i = 0; i < testNode.length; i++) {

View File

@@ -29,7 +29,7 @@ describe("api index", function() {
describe("disables editor", function() {
before(function() {
api.init({},{
settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:true},
settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:true, exportNodeSettings: function() {}},
events: {on:function(){},removeListener: function(){}},
log: {info:function(){},_:function(){}},
nodes: {paletteEditorEnabled: function(){return true}}

View File

@@ -42,7 +42,10 @@ describe("info api", function() {
foo: 123,
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"]
paletteCategories :["red","blue","green"],
exportNodeSettings: function(obj) {
obj.testNodeSetting = "helloWorld";
}
},
nodes: {
paletteEditorEnabled: function() { return true; }
@@ -59,7 +62,9 @@ describe("info api", function() {
res.body.should.have.property("version","testVersion");
res.body.should.have.property("paletteCategories",["red","blue","green"]);
res.body.should.have.property("editorTheme",{test:456});
res.body.should.have.property("testNodeSetting","helloWorld");
res.body.should.not.have.property("foo",123);
done();
});
});
@@ -68,8 +73,8 @@ describe("info api", function() {
settings: {
httpNodeRoot: "testHttpNodeRoot",
version: "testVersion",
paletteCategories :["red","blue","green"]
paletteCategories :["red","blue","green"],
exportNodeSettings: function() {}
},
nodes: {
paletteEditorEnabled: function() { return false; }

View File

@@ -27,7 +27,7 @@ var auth = require("../../../red/api/auth");
describe("library api", function() {
function initLibrary(_flows,_libraryEntries) {
function initLibrary(_flows,_libraryEntries,_examples) {
var flows = _flows;
var libraryEntries = _libraryEntries;
library.init(app,{
@@ -84,6 +84,11 @@ describe("library api", function() {
events: {
on: function(){},
removeListener: function(){}
},
nodes: {
getNodeExampleFlows: function() {
return _examples;
}
}
});
}
@@ -179,6 +184,22 @@ describe("library api", function() {
.expect(403)
.end(done);
});
it('includes examples flows if set', function(done) {
var examples = {"d":{"node-module":{"f":["example-one"]}}};
initLibrary({},{},examples);
request(app)
.get('/library/flows')
.expect(200)
.end(function(err,res) {
if (err) {
throw err;
}
res.body.should.have.property('d');
res.body.d.should.have.property('_examples_');
should.deepEqual(res.body.d._examples_,examples);
done();
});
});
});
describe("type", function() {

View File

@@ -29,7 +29,14 @@ describe("ui api", function() {
var app;
before(function() {
ui.init({events:events});
ui.init({
events:events,
nodes: {
getNodeIconPath: function(module,icon) {
return path.resolve(__dirname+'/../../../public/icons/arrow-in.png');
}
}
});
});
describe("slash handler", function() {
before(function() {
@@ -64,17 +71,17 @@ describe("ui api", function() {
describe("icon handler", function() {
before(function() {
app = express();
app.get("/icons/:icon",ui.icon);
app.get("/icons/:module/:icon",ui.icon);
});
function binaryParser(res, callback) {
res.setEncoding('binary');
res.data = '';
res.on('data', function (chunk) {
res.data += chunk;
res.data += chunk;
});
res.on('end', function () {
callback(null, new Buffer(res.data, 'binary'));
callback(null, new Buffer(res.data, 'binary'));
});
}
function compareBuffers(b1,b2) {
@@ -83,11 +90,10 @@ describe("ui api", function() {
b1[i].should.equal(b2[i]);
}
}
it('returns the default icon when getting an unknown icon', function(done) {
it('returns the requested icon', function(done) {
var defaultIcon = fs.readFileSync(path.resolve(__dirname+'/../../../public/icons/arrow-in.png'));
request(app)
.get("/icons/youwonthaveme.png")
.get("/icons/module/icon.png")
.expect("Content-Type", /image\/png/)
.expect(200)
.parse(binaryParser)
@@ -101,40 +107,6 @@ describe("ui api", function() {
});
});
it('returns a known icon', function(done) {
var injectIcon = fs.readFileSync(path.resolve(__dirname+'/../../../public/icons/inject.png'));
request(app)
.get("/icons/inject.png")
.expect("Content-Type", /image\/png/)
.expect(200)
.parse(binaryParser)
.end(function(err, res){
if (err){
return done(err);
}
Buffer.isBuffer(res.body).should.be.true();
compareBuffers(res.body,injectIcon);
done();
});
});
it('returns a registered icon' , function(done) {
var testIcon = fs.readFileSync(path.resolve(__dirname+'/../../resources/icons/test_icon.png'));
events.emit("node-icon-dir", path.resolve(__dirname+'/../../resources/icons'));
request(app)
.get("/icons/test_icon.png")
.expect("Content-Type", /image\/png/)
.expect(200)
.parse(binaryParser)
.end(function(err, res){
if (err){
return done(err);
}
Buffer.isBuffer(res.body).should.be.true();
compareBuffers(res.body,testIcon);
done();
});
});
});
describe("editor ui handler", function() {
@@ -174,8 +146,4 @@ describe("ui api", function() {
});
});
});
});

View File

@@ -56,10 +56,12 @@ describe("red/nodes/index", function() {
get: function() { return false }
};
var EventEmitter = require('events').EventEmitter;
var runtime = {
settings: settings,
storage: storage,
log: {debug:function() {}, warn:function() {}}
log: {debug:function() {}, warn:function() {}},
events: new EventEmitter()
};
function TestNode(n) {

View File

@@ -0,0 +1,69 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var EventEmitter = require('events').EventEmitter;
var events = new EventEmitter();
var should = require("should");
var fs = require("fs");
var path = require("path");
var library = require("../../../../red/runtime/nodes/library")
describe("library api", function() {
it('returns null list when no modules have been registered', function() {
library.init({events:events});
should.not.exist(library.getExampleFlows());
});
it('returns null path when module is not known', function() {
library.init({events:events});
should.not.exist(library.getExampleFlowPath('foo','bar'));
});
it('returns a valid example path', function(done) {
library.init({events:events});
events.emit('node-examples-dir',{
name: "test-module",
path: path.resolve(__dirname+'/../../../resources/examples')
});
setTimeout(function() {
try {
var flows = library.getExampleFlows();
flows.should.deepEqual({"d":{"test-module":{"f":["one"]}}});
var examplePath = library.getExampleFlowPath('test-module','one');
examplePath.should.eql(path.resolve(__dirname+'/../../../resources/examples/one.json'))
events.emit('node-module-uninstalled', 'test-module');
setTimeout(function() {
try {
should.not.exist(library.getExampleFlows());
should.not.exist(library.getExampleFlowPath('test-module','one'));
done();
} catch(err) {
done(err);
}
},20);
}catch(err) {
done(err);
}
},20);
})
});

View File

@@ -46,6 +46,9 @@ describe('nodes/registry/installer', function() {
if (registry.getModuleInfo.restore) {
registry.getModuleInfo.restore();
}
if (typeRegistry.getModuleInfo.restore) {
typeRegistry.getModuleInfo.restore();
}
if (require('fs').statSync.restore) {
require('fs').statSync.restore();
@@ -64,6 +67,32 @@ describe('nodes/registry/installer', function() {
done();
});
});
it("rejects when npm does not find specified version", function(done) {
sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) {
cb(new Error(),""," version not found: this_wont_exist@0.1.2");
});
sinon.stub(typeRegistry,"getModuleInfo", function() {
return {
version: "0.1.1"
}
});
installer.installModule("this_wont_exist","0.1.2").otherwise(function(err) {
err.code.should.be.eql(404);
done();
});
});
it("rejects when update requested to existing version", function(done) {
sinon.stub(typeRegistry,"getModuleInfo", function() {
return {
version: "0.1.1"
}
});
installer.installModule("this_wont_exist","0.1.1").otherwise(function(err) {
err.code.should.be.eql('module_already_loaded');
done();
});
});
it("rejects with generic error", function(done) {
sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) {
cb(new Error("test_error"),"","");

View File

@@ -17,6 +17,7 @@
var should = require("should");
var when = require("when");
var sinon = require("sinon");
var path = require("path");
var typeRegistry = require("../../../../../red/runtime/nodes/registry/registry");
@@ -483,4 +484,19 @@ describe("red/nodes/registry/registry",function() {
});
});
describe('#getNodeIconPath', function() {
it('returns the default icon when getting an unknown icon', function() {
var defaultIcon = path.resolve(__dirname+'/../../../../../public/icons/arrow-in.png');
var iconPath = typeRegistry.getNodeIconPath('random-module','youwonthaveme.png');
iconPath.should.eql(defaultIcon);
});
it('returns a registered icon' , function() {
var testIcon = path.resolve(__dirname+'/../../../../resources/icons/test_icon.png');
events.emit("node-icon-dir",{name:"test-module", path: path.resolve(__dirname+'/../../../../resources/icons')});
var iconPath = typeRegistry.getNodeIconPath('test-module','test_icon.png');
iconPath.should.eql(testIcon);
});
});
});

View File

@@ -142,4 +142,87 @@ describe("red/settings", function() {
settings.should.not.have.property("c");
});
it('registers node settings and exports them', function() {
var userSettings = {};
settings.init(userSettings);
settings.registerNodeSettings("inject", {injectColor:{value:"red", exportable:true}, injectSize:{value:"100", exportable:true}} );
settings.registerNodeSettings("mqtt", {mqttColor:{value:"purple", exportable:false}, mqttSize:{value:"50", exportable:true}} );
settings.registerNodeSettings("http request", {httpRequest1:{value:"a1", exportable:true}} );
settings.registerNodeSettings(" http--request<> ", {httpRequest2:{value:"a2", exportable:true}} );
settings.registerNodeSettings("_http_request_", {httpRequest3:{value:"a3", exportable:true}} );
settings.registerNodeSettings("mQtT", {mQtTColor:{value:"purple", exportable:true}} );
settings.registerNodeSettings("abc123", {abc123:{value:"def456", exportable:true}} );
var safeSettings = {};
settings.exportNodeSettings(safeSettings);
safeSettings["nodeSettings"].should.have.property("injectColor", "red");
safeSettings["nodeSettings"].should.have.property("injectSize", "100");
safeSettings["nodeSettings"].should.not.have.property("mqttColor");
safeSettings["nodeSettings"].should.have.property("mqttSize", "50");
safeSettings["nodeSettings"].should.have.property("httpRequest1", "a1");
safeSettings["nodeSettings"].should.have.property("httpRequest2", "a2");
safeSettings["nodeSettings"].should.have.property("httpRequest3", "a3");
safeSettings["nodeSettings"].should.have.property("mQtTColor", "purple");
safeSettings["nodeSettings"].should.have.property("abc123", "def456");
});
it('prohibits registering the property whose name do not start with type name', function() {
var userSettings = {};
settings.init(userSettings);
settings.registerNodeSettings("inject", {color:{value:"red", exportable:true}} );
settings.registerNodeSettings("_a_b_1_", {ab1Color:{value:"red", exportable:true}} );
settings.registerNodeSettings("AB2", {AB2Color:{value:"red", exportable:true}} );
settings.registerNodeSettings("abcDef", {abcColor:{value:"red", exportable:true}} );
var safeSettings = {};
settings.exportNodeSettings(safeSettings);
safeSettings["nodeSettings"].should.not.have.property("color");
safeSettings["nodeSettings"].should.not.have.property("ab1Color", "blue");
safeSettings["nodeSettings"].should.not.have.property("AB2Color");
safeSettings["nodeSettings"].should.not.have.property("abcColor");
});
it('overwrites node settings with user settings', function() {
var userSettings = {
injectColor: "green",
mqttColor: "yellow",
abColor: [1,2,3]
}
settings.init(userSettings);
settings.registerNodeSettings("inject", {injectColor:{value:"red", exportable:true}} );
settings.registerNodeSettings("ab", {abColor:{value:"red", exportable:false}} );
var safeSettings = {};
settings.exportNodeSettings(safeSettings);
safeSettings["nodeSettings"].should.have.property("injectColor", "green");
safeSettings["nodeSettings"].should.not.have.property("mqttColor");
safeSettings["nodeSettings"].should.not.have.property("abColor");
});
it('disables/enables node settings', function() {
var userSettings = {};
settings.init(userSettings);
var safeSettings = {};
settings.registerNodeSettings("inject", {injectColor:{value:"red", exportable:true}} );
settings.registerNodeSettings("mqtt", {mqttColor:{value:"purple", exportable:true}} );
settings.registerNodeSettings("http request", {httpRequestColor:{value:"yellow", exportable:true}} );
settings.exportNodeSettings(safeSettings);
safeSettings["nodeSettings"].should.have.property("injectColor", "red");
safeSettings["nodeSettings"].should.have.property("mqttColor", "purple");
safeSettings["nodeSettings"].should.have.property("httpRequestColor", "yellow");
var types = ["inject", "mqtt"];
settings.disableNodeSettings(types);
settings.exportNodeSettings(safeSettings);
safeSettings["nodeSettings"].should.not.have.property("injectColor");
safeSettings["nodeSettings"].should.not.have.property("mqttColor");
safeSettings["nodeSettings"].should.have.property("httpRequestColor", "yellow");
types = ["inject"];
settings.enableNodeSettings(types);
settings.exportNodeSettings(safeSettings);
safeSettings["nodeSettings"].should.have.property("injectColor", "red");
safeSettings["nodeSettings"].should.not.have.property("mqttColor");
safeSettings["nodeSettings"].should.have.property("httpRequestColor", "yellow");
});
});

View File

@@ -364,4 +364,21 @@ describe("red/util", function() {
it("fail <blank>",function() { testInvalid("");})
});
describe('normaliseNodeTypeName', function() {
function normalise(input, expected) {
var result = util.normaliseNodeTypeName(input);
result.should.eql(expected);
}
it('pass blank',function() { normalise("", "") });
it('pass ab1',function() { normalise("ab1", "ab1") });
it('pass AB1',function() { normalise("AB1", "aB1") });
it('pass a b 1',function() { normalise("a b 1", "aB1") });
it('pass a-b-1',function() { normalise("a-b-1", "aB1") });
it('pass ab1 ',function() { normalise(" ab1 ", "ab1") });
it('pass _a_b_1_',function() { normalise("_a_b_1_", "aB1") });
it('pass http request',function() { normalise("http request", "httpRequest") });
it('pass HttpRequest',function() { normalise("HttpRequest", "httpRequest") });
});
});

View File