Compare commits

...

27 Commits

Author SHA1 Message Date
Nick O'Leary
129ca0e39f bump version 2016-03-21 11:02:09 +00:00
Dave Conway-Jones
906703db5f Add timed release mode to delay node 2016-03-20 17:46:12 +00:00
Nick O'Leary
0cd4a2b4ec Add api/flow_spec tests
Part of #840
2016-03-18 21:01:21 +00:00
Nick O'Leary
aef8aaa0bd Enable link splicing for when import_dragging nodes
Closes #811
2016-03-17 11:12:45 +00:00
Nick O'Leary
428fbb8622 Fix uncaught exception on deploy whilst node sending messages 2016-03-16 15:37:44 +00:00
Nick O'Leary
b9f03e7d80 Deprecate old mqtt client and connection pool modules 2016-03-16 11:15:30 +00:00
Nick O'Leary
db686388b9 Fix registry test for Node 5 2016-03-16 11:05:10 +00:00
Nick O'Leary
626cba4002 Change node: add bool/num types to change mode
Closes #835 #835 #835
2016-03-13 23:10:10 +00:00
Nick O'Leary
37d4a6b9e2 Validate fields that are $(env-vars)
Closes #825
2016-03-13 14:29:36 +00:00
Nick O'Leary
12c4561aba Handle missing config nodes when validating node properties 2016-03-13 13:25:59 +00:00
Dave Conway-Jones
fed49e3718 pi node - don't try to send data if closing 2016-03-13 10:58:22 +00:00
Nick O'Leary
3af37d3984 Load node message catalog when added dynamically 2016-03-12 22:53:07 +00:00
Nick O'Leary
0f49a11228 Split palette labels on spaces and hypens when laying out 2016-03-12 22:41:23 +00:00
Nick O'Leary
27d3e165b0 Message catalog updates for zero-length flow file handling 2016-03-12 00:04:27 +00:00
Nick O'Leary
e941c22f6c Warn if editor routes are accessed but runtime not started
Closes #816
2016-03-12 00:03:50 +00:00
Nick O'Leary
7281e4deb6 Add zero-length flow file tests 2016-03-11 22:58:11 +00:00
Nick O'Leary
f2191e94b3 Better handling of zero-length flow files
Closes #819

If a flow file is found to be zero-bytes:
  If there is a non-empty backup, restore the backup and resolve
  If there is no backup or it is also empty, resolve empty flow
If a flow file is found to be invalid json:
  Log and resolve empty flow
2016-03-11 22:42:04 +00:00
Dave Conway-Jones
349ebfe4db remove extra brace added to library.js in error 2016-03-06 20:54:46 +00:00
Nick O'Leary
708365c4ac Allow runtime calls to RED._ to specify other namespace 2016-03-06 20:43:19 +00:00
Dave Conway-Jones
0e9ea0aff1 Replace - & _ from example node name labels with space
and align lines (ocd)
2016-03-05 17:07:39 +00:00
Dave Conway-Jones
63ba05a193 Better right alignment of numerics in delay and trigger nodes 2016-03-04 10:12:07 +00:00
Nick O'Leary
4b702815cf Strip node-red-contrib/node- prefix from Example menu labels 2016-03-03 11:02:37 +00:00
Nick O'Leary
55e66ebcac Allow node modules to include example flows 2016-03-02 23:34:24 +00:00
Nick O'Leary
dcd8b3699c Create node_modules in userDir
This ensures npm install puts modules under .node-red even if there's
already a node_modules dir in the parent directory.
2016-03-01 22:08:37 +00:00
Nick O'Leary
0e2d13172a Ensure errors in node def funcs don't break view rendering
Fixes #815

 - also fixes errors in the Catch/Status node label funcs #815
2016-03-01 21:58:57 +00:00
Nick O'Leary
2e2556fdad Merge pull request #805 from aryehof/InjectInfoUpdate
Updated Inject node info with instructions for flow and global options
2016-02-26 14:07:21 +00:00
Aryeh Hoffman
8bfab8f73d Updated Inject node info with instructions for flow and global options 2016-02-21 18:17:05 +02:00
37 changed files with 986 additions and 142 deletions

View File

@@ -23,6 +23,9 @@ RED.history = (function() {
undo_history[i].dirty = true;
}
},
list: function() {
return undo_history
},
depth: function() {
return undo_history.length;
},
@@ -284,6 +287,9 @@ RED.history = (function() {
RED.workspaces.refresh();
RED.sidebar.config.refresh();
}
},
peek: function() {
return undo_history[undo_history.length-1];
}
}

View File

@@ -103,8 +103,10 @@ var RED = (function() {
var id = m.id;
RED.nodes.addNodeSet(m);
addedTypes = addedTypes.concat(m.types);
$.get('nodes/'+id, function(data) {
$("body").append(data);
RED.i18n.loadCatalog(id, function() {
$.get('nodes/'+id, function(data) {
$("body").append(data);
});
});
}
if (addedTypes.length) {
@@ -142,6 +144,8 @@ var RED = (function() {
RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success");
}
}
// Refresh flow library to ensure any examples are updated
RED.library.loadFlowLibrary();
});
}
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -116,6 +116,9 @@ RED.editor = (function() {
*/
function validateNodeProperty(node,definition,property,value) {
var valid = true;
if (/^\$\([a-zA-Z_][a-zA-Z0-9_]*\)$/.test(value)) {
return true;
}
if ("required" in definition[property] && definition[property].required) {
valid = value !== "";
}
@@ -126,8 +129,8 @@ RED.editor = (function() {
if (!value || value == "_ADD_") {
valid = definition[property].hasOwnProperty("required") && !definition[property].required;
} else {
var v = RED.nodes.node(value).valid;
valid = (v==null || v);
var configNode = RED.nodes.node(value);
valid = (configNode !== null && (configNode.valid == null || configNode.valid));
}
}
return valid;

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,7 +27,9 @@ RED.library = (function() {
var li;
var a;
var ul = document.createElement("ul");
ul.id = "menu-item-import-library-submenu";
if (root === "") {
ul.id = "menu-item-import-library-submenu";
}
ul.className = "dropdown-menu";
if (data.d) {
for (i in data.d) {
@@ -36,7 +38,8 @@ RED.library = (function() {
li.className = "dropdown-submenu pull-left";
a = document.createElement("a");
a.href="#";
a.innerHTML = i;
var label = i.replace(/^node-red-contrib-/,"").replace(/^node-red-node-/,"").replace(/-/," ").replace(/_/," ");
a.innerHTML = label;
li.appendChild(a);
li.appendChild(buildMenu(data.d[i],root+(root!==""?"/":"")+i));
ul.appendChild(li);
@@ -53,7 +56,7 @@ RED.library = (function() {
a.flowName = root+(root!==""?"/":"")+data.f[i];
a.onclick = function() {
$.get('library/flows/'+this.flowName, function(data) {
RED.view.importNodes(data);
RED.view.importNodes(data);
});
};
li.appendChild(a);
@@ -63,7 +66,17 @@ RED.library = (function() {
}
return ul;
};
var examples;
if (data.d && data.d._examples_) {
examples = data.d._examples_;
delete data.d._examples_;
}
var menu = buildMenu(data,"");
$("#menu-item-import-examples").remove();
if (examples) {
RED.menu.addItem("menu-item-import",{id:"menu-item-import-examples",label:RED._("menu.label.examples"),options:[]})
$("#menu-item-import-examples-submenu").replaceWith(buildMenu(examples,"_examples_"));
}
//TODO: need an api in RED.menu for this
$("#menu-item-import-library-submenu").replaceWith(menu);
});
@@ -117,7 +130,7 @@ RED.library = (function() {
$(".active",bc).removeClass("active");
bc.append(bcli);
$.getJSON("library/"+options.url+root+dirName,function(data) {
$("#node-select-library").children().first().replaceWith(buildFileList(root+dirName+"/",data));
$("#node-select-library").children().first().replaceWith(buildFileList(root+dirName+"/",data));
});
}
})();
@@ -125,20 +138,20 @@ RED.library = (function() {
ul.appendChild(li);
} else {
// file
li = buildFileListItem(v);
li.innerHTML = v.name;
li.onclick = (function() {
var item = v;
return function(e) {
$(".list-selected",ul).removeClass("list-selected");
$(this).addClass("list-selected");
$.get("library/"+options.url+root+item.fn, function(data) {
selectedLibraryItem = item;
libraryEditor.setValue(data,-1);
});
}
})();
ul.appendChild(li);
li = buildFileListItem(v);
li.innerHTML = v.name;
li.onclick = (function() {
var item = v;
return function(e) {
$(".list-selected",ul).removeClass("list-selected");
$(this).addClass("list-selected");
$.get("library/"+options.url+root+item.fn, function(data) {
selectedLibraryItem = item;
libraryEditor.setValue(data,-1);
});
}
})();
ul.appendChild(li);
}
}
return ul;
@@ -428,8 +441,8 @@ RED.library = (function() {
data: $("#node-input-library-filename").attr('nodes'),
contentType: "application/json; charset=utf-8"
}).done(function() {
RED.library.loadFlowLibrary();
RED.notify(RED._("library.savedNodes"),"success");
RED.library.loadFlowLibrary();
RED.notify(RED._("library.savedNodes"),"success");
}).fail(function(xhr,textStatus,err) {
RED.notify(RED._("library.saveFailed",{message:xhr.responseText}),"error");
});
@@ -452,7 +465,7 @@ RED.library = (function() {
close: function(e) {
RED.keyboard.enable();
}
});
});
exportToLibraryDialog.children(".dialog-form").append($(
'<div class="form-row">'+
'<label for="node-input-library-filename" data-i18n="[append]editor:library.filename"><i class="fa fa-file"></i> </label>'+

View File

@@ -66,7 +66,7 @@ RED.palette = (function() {
var lineHeight = 20;
var portHeight = 10;
var words = label.split(" ");
var words = label.split(/[ -]/);
var displayLines = [];
@@ -134,22 +134,27 @@ RED.palette = (function() {
d.id = "palette_node_"+nodeTypeId;
d.type = nt;
var label;
if (typeof def.paletteLabel === "undefined") {
label = /^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];
} else {
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
var label = /^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];
if (typeof def.paletteLabel !== "undefined") {
try {
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
} catch(err) {
console.log("Definition error: "+nt+".paletteLabel",err);
}
}
$('<div/>',{class:"palette_label"+(def.align=="right"?" palette_label_right":"")}).appendTo(d);
d.className="palette_node";
if (def.icon) {
var icon_url = (typeof def.icon === "function" ? def.icon.call({}) : 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 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);
}

View File

@@ -350,7 +350,11 @@ RED.view = (function() {
}
if (nn._def.onadd) {
nn._def.onadd.call(nn);
try {
nn._def.onadd.call(nn);
} catch(err) {
console.log("onadd:",err);
}
}
} else {
var subflow = RED.nodes.subflow(m[1]);
@@ -396,7 +400,7 @@ RED.view = (function() {
var spliceLink = $(ui.helper).data('splice');
if (spliceLink) {
// TODO: DRY - canvasMouseUp
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
RED.nodes.removeLink(spliceLink);
var link1 = {
source:spliceLink.source,
@@ -643,7 +647,7 @@ RED.view = (function() {
}
}
}
if (mouse_mode == RED.state.MOVING_ACTIVE && moving_set.length === 1) {
if ((mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) && moving_set.length === 1) {
node = moving_set[0];
if (spliceActive) {
if (!spliceTimer) {
@@ -766,7 +770,7 @@ RED.view = (function() {
}
historyEvent = {t:'move',nodes:ns,dirty:RED.nodes.dirty()};
if (activeSpliceLink) {
// TODO: DRY - droppable
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
var spliceLink = d3.select(activeSpliceLink).data()[0];
RED.nodes.removeLink(spliceLink);
var link1 = {
@@ -1181,6 +1185,29 @@ RED.view = (function() {
//RED.touch.radialMenu.show(d3.select(this),pos);
if (mouse_mode == RED.state.IMPORT_DRAGGING) {
RED.keyboard.remove(/* ESCAPE */ 27);
if (activeSpliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
var spliceLink = d3.select(activeSpliceLink).data()[0];
RED.nodes.removeLink(spliceLink);
var link1 = {
source:spliceLink.source,
sourcePort:spliceLink.sourcePort,
target: moving_set[0].n
};
var link2 = {
source:moving_set[0].n,
sourcePort:0,
target: spliceLink.target
};
RED.nodes.addLink(link1);
RED.nodes.addLink(link2);
var historyEvent = RED.history.peek();
historyEvent.links = [link1,link2];
historyEvent.removedLinks = [spliceLink];
updateActiveNodes();
}
updateSelection();
RED.nodes.dirty(true);
redraw();
@@ -1253,7 +1280,11 @@ RED.view = (function() {
d.dirty = true;
}
if (d._def.button.onclick) {
d._def.button.onclick.call(d);
try {
d._def.button.onclick.call(d);
} catch(err) {
console.log("Definition error: "+d.type+".onclick",err);
}
}
if (d.dirty) {
redraw();
@@ -1408,7 +1439,12 @@ RED.view = (function() {
var node = d3.select(this);
node.attr("id",d.id);
var l = d._def.label;
l = (typeof l === "function" ? l.call(d) : l)||"";
try {
l = (typeof l === "function" ? l.call(d) : l)||"";
} catch(err) {
console.log("Definition error: "+d.type+".label",err);
l = d.type;
}
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);
@@ -1591,7 +1627,12 @@ RED.view = (function() {
//if (d.x < -50) deleteSelection(); // Delete nodes if dragged back to palette
if (d.resize) {
var l = d._def.label;
l = (typeof l === "function" ? l.call(d) : l)||"";
try {
l = (typeof l === "function" ? l.call(d) : l)||"";
} catch(err) {
console.log("Definition error: "+d.type+".label",err);
l = d.type;
}
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);
@@ -1659,20 +1700,33 @@ RED.view = (function() {
});
}
thisNode.selectAll('text.node_label').text(function(d,i){
var l = "";
if (d._def.label) {
if (typeof d._def.label == "function") {
return d._def.label.call(d);
} else {
return d._def.label;
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;
}
}
return "";
return l;
})
.attr('y', function(d){return (d.h/2)-1;})
.attr('class',function(d){
var s = "";
if (d._def.labelStyle) {
s = d._def.labelStyle;
try {
s = (typeof s === "function" ? s.call(d) : s)||"";
} catch(err) {
console.log("Definition error: "+d.type+".labelStyle",err);
s = "";
}
s = " "+s;
}
return 'node_label'+
(d._def.align?' node_label_'+d._def.align:'')+
(d._def.labelStyle?' '+(typeof d._def.labelStyle == "function" ? d._def.labelStyle.call(d):d._def.labelStyle):'') ;
(d._def.align?' node_label_'+d._def.align:'')+s;
});
if (d._def.icon) {
@@ -1680,7 +1734,12 @@ RED.view = (function() {
var current_url = icon.attr("xlink:href");
var icon_url;
if (typeof d._def.icon == "function") {
icon_url = d._def.icon.call(d);
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;
}
@@ -1744,7 +1803,12 @@ RED.view = (function() {
thisNode.selectAll('text.node_badge_label').text(function(d,i) {
if (d._def.badge) {
if (typeof d._def.badge == "function") {
return d._def.badge.call(d);
try {
return d._def.badge.call(d);
} catch(err) {
console.log("Definition error: "+d.type+".badge",err);
return "";
}
} else {
return d._def.badge;
}
@@ -1952,6 +2016,12 @@ RED.view = (function() {
}
if (!touchImport) {
mouse_mode = RED.state.IMPORT_DRAGGING;
spliceActive = false;
if (new_ms.length === 1) {
node = new_ms[0];
spliceActive = node.n._def.inputs > 0 &&
node.n._def.outputs > 0;
}
}
RED.keyboard.add(/* ESCAPE */ 27,function(){

View File

@@ -157,6 +157,9 @@
<p>The repeat function allows the payload to be sent on the required schedule.</p>
<p>The <i>Inject once at start</i> option actually waits a short interval before firing
to give other nodes a chance to instantiate properly.</p>
<p>The <i>Flow</i> and <i>Global</i> options allow one to inject a flow or global
context value.
</p>
<p><b>Note: </b>"Interval between times" and "at a specific time" uses cron.
This means that 20 minutes will be at the next hour, 20 minutes past and
40 minutes past - not in 20 minutes time. If you want every 20 minutes

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2015 IBM Corp.
Copyright 2015, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -106,7 +106,7 @@
outputs:1,
icon: "alert.png",
label: function() {
return this.name||this.scope?this._("catch.catchNodes",{number:this.scope.length}):this._("catch.catch");
return this.name||(this.scope?this._("catch.catchNodes",{number:this.scope.length}):this._("catch.catch"));
},
labelStyle: function() {
return this.name?"node_label_italic":"";

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2015 IBM Corp.
Copyright 2015, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -97,7 +97,7 @@
outputs:1,
icon: "alert.png",
label: function() {
return this.name||this.scope?this._("status.statusNodes",{number:this.scope.length}):this._("status.status");
return this.name||(this.scope?this._("status.statusNodes",{number:this.scope.length}):this._("status.status"));
},
labelStyle: function() {
return this.name?"node_label_italic":"";

View File

@@ -19,15 +19,16 @@
<div class="form-row">
<label for="node-input-pauseType"><i class="fa fa-tasks"></i> <span data-i18n="delay.action"></span></label>
<select id="node-input-pauseType" style="width:270px !important">
<option value="delay" data-i18n="delay.delaymsg"></option>
<option value="random" data-i18n="delay.randomdelay"></option>
<option value="rate" data-i18n="delay.limitrate"></option>
<option value="queue" data-i18n="delay.fairqueue"></option>
<option value="delay" data-i18n="delay.delaymsg"></option>
<option value="random" data-i18n="delay.randomdelay"></option>
<option value="rate" data-i18n="delay.limitrate"></option>
<option value="queue" data-i18n="delay.fairqueue"></option>
<option value="timed" data-i18n="delay.timedqueue"></option>
</select>
</div>
<div id="delay-details" class="form-row">
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="delay.for"></span></label>
<input type="text" id="node-input-timeout" placeholder="Time" style="text-align:right; width:50px !important">
<input type="text" id="node-input-timeout" placeholder="Time" style="text-align:end; width:50px !important">
<select id="node-input-timeoutUnits" style="width:200px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
@@ -39,7 +40,7 @@
<div id="rate-details" class="form-row">
<label for="node-input-rate"><i class="fa fa-clock-o"></i> <span data-i18n="delay.rate"></span></label>
<input type="text" id="node-input-rate" placeholder="1" style="text-align:right; width:30px !important">
<input type="text" id="node-input-rate" placeholder="1" style="text-align:end; width:30px !important">
<label for="node-input-rateUnits"><span data-i18n="delay.msgper"></span></label>
<select id="node-input-rateUnits" style="width:140px !important">
<option value="second" data-i18n="delay.sec"></option>
@@ -54,9 +55,9 @@
<div id="random-details" class="form-row">
<label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> <span data-i18n="delay.between"></span></label>
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:right; width:30px !important">
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:30px !important">
&nbsp;&nbsp;&amp;&nbsp;&nbsp;
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:right; width:30px !important">
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:30px !important">
<select id="node-input-randomUnits" style="width:140px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
@@ -82,6 +83,9 @@
At each "tick", derived from the rate, the next "topic" is released.
Any messages arriving on the same topic before release replace those in that position in the queue.
So each "topic" gets a turn - but the most recent value is always the one sent.</p>
<p>The "timed release queue" adds messages to an array based on their <code>msg.topic</code> property.
At each "tick", all the latest messages are released. Any new messages arriving before release will
replace those of the same topic.</p>
</script>
<script type="text/javascript">
@@ -114,6 +118,9 @@
} else if (this.pauseType == "random") {
return this.name || this._("delay.label.random");
}
else if (this.pauseType == "timed") {
return this.name || this.rate+" "+this._("delay.label.timed")+" "+this.rateUnits;
}
else {
var units = this.rateUnits ? this.rateUnits.charAt(0) : "s";
return this.name || this._("delay.label.queue")+" "+this.rate+" msg/"+units;
@@ -149,6 +156,11 @@
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
} else if (this.pauseType == "timed") {
$("#delay-details").hide();
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
}
if (!this.timeoutUnits) {
@@ -184,6 +196,11 @@
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
} else if (this.value == "timed") {
$("#delay-details").hide();
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
}
});
}

View File

@@ -147,10 +147,17 @@ module.exports = function(RED) {
node.status({});
});
} else if (this.pauseType === "queue") {
} else if ((this.pauseType === "queue") || (this.pauseType === "timed")) {
this.intervalID = setInterval(function() {
if (node.buffer.length > 0) {
node.send(node.buffer.shift()); // send the first on the queue
if (this.pauseType === "queue") {
if (node.buffer.length > 0) {
node.send(node.buffer.shift()); // send the first on the queue
}
}
else {
while (node.buffer.length > 0) { // send the whole queue
node.send(node.buffer.shift());
}
}
node.status({text:node.buffer.length});
},node.rate);

View File

@@ -32,7 +32,7 @@
<option value="wait" data-i18n="trigger.wait-for"></option>
</select>
<span class="node-type-wait">
<input type="text" id="node-input-duration" style="text-align:right; width:70px !important">
<input type="text" id="node-input-duration" style="text-align:end; width:70px !important">
<select id="node-input-units" style="width:140px !important">
<option value="ms" data-i18n="trigger.duration.ms"></option>
<option value="s" data-i18n="trigger.duration.s"></option>
@@ -60,7 +60,7 @@
<br/>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></input>
</div>
<div class="form-tips" data-i18n="[html]trigger.tip"></div>
</script>

View File

@@ -73,7 +73,7 @@ module.exports = function(RED) {
node.child.stdout.on('data', function (data) {
data = data.toString().trim();
if (data.length > 0) {
if (node.buttonState !== -1) {
if (node.running && node.buttonState !== -1) {
node.send({ topic:"pi/"+node.pin, payload:Number(data) });
}
node.buttonState = data;
@@ -87,8 +87,8 @@ module.exports = function(RED) {
});
node.child.on('close', function (code) {
node.child = null;
node.running = false;
node.child = null;
if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); }
if (node.done) {
node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"});
@@ -279,8 +279,8 @@ module.exports = function(RED) {
});
node.child.on('close', function (code) {
node.child = null;
node.running = false;
node.child = null;
if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); }
if (node.done) {
node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"});

View File

@@ -16,6 +16,9 @@
var util = require("util");
var mqtt = require("mqtt");
var events = require("events");
util.log("[warn] nodes/core/io/lib/mqtt.js is deprecated and will be removed in a future release of Node-RED. Please report this usage to the Node-RED mailing list.");
//var inspect = require("util").inspect;
//var Client = module.exports.Client = function(

View File

@@ -17,6 +17,8 @@ var util = require("util");
var mqtt = require("./mqtt");
var settings = require(process.env.NODE_RED_HOME+"/red/red").settings;
util.log("[warn] nodes/core/io/lib/mqttConnectionPool.js is deprecated and will be removed in a future release of Node-RED. Please report this usage to the Node-RED mailing list.");
var connections = {};
function matchTopic(ts,t) {
@@ -60,7 +62,7 @@ module.exports = {
subscribe: function(topic,qos,callback,ref) {
ref = ref||0;
subscriptions[topic] = subscriptions[topic]||{};
var sub = {
topic:topic,
qos:qos,
@@ -104,7 +106,7 @@ module.exports = {
}
},
disconnect: function(ref) {
this._instances -= 1;
if (this._instances == 0) {
client.disconnect();

View File

@@ -152,6 +152,7 @@
"randomdelay": "Random delay",
"limitrate": "Limit rate to",
"fairqueue": "Topic based fair queue",
"timedqueue": "Timed release queue",
"milisecs": "Miliseconds",
"secs": "Seconds",
"sec": "Second",
@@ -169,7 +170,8 @@
"delay": "delay",
"limit": "limit",
"random": "random",
"queue": "queue"
"queue": "queue",
"timed": "releases per"
},
"error": {
"buffer": "buffer exceeded 1000 messages"

View File

@@ -128,21 +128,37 @@
selectField.append($("<option></option>").val(selectOptions[i].v).text(selectOptions[i].l));
}
var propertyName = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-name",type:"text"}).appendTo(row1).typedInput({types:['msg','flow','global']});
var finalspan = $('<span/>',{style:"float: right; margin-right: 10px;"}).appendTo(row1);
var deleteButton = $('<a/>',{href:"#",class:"editor-button editor-button-small", style:"margin-top: 7px; margin-left: 5px;"}).appendTo(finalspan);
var propertyName = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-name",type:"text"})
.appendTo(row1)
.typedInput({types:['msg','flow','global']});
var finalspan = $('<span/>',{style:"float: right; margin-right: 10px;"})
.appendTo(row1);
var deleteButton = $('<a/>',{href:"#",class:"editor-button editor-button-small", style:"margin-top: 7px; margin-left: 5px;"})
.appendTo(finalspan);
$('<i/>',{class:"fa fa-remove"}).appendTo(deleteButton);
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"}).text(to).appendTo(row2);
var propertyValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-value",type:"text"}).appendTo(row2).typedInput({default:'str',types:['msg','flow','global','str','num','bool','json']});
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"})
.text(to)
.appendTo(row2);
var propertyValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-value",type:"text"})
.appendTo(row2)
.typedInput({default:'str',types:['msg','flow','global','str','num','bool','json']});
var row3_1 = $('<div/>').appendTo(row3);
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"}).text(search).appendTo(row3_1);
var fromValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-search-value",type:"text"}).appendTo(row3_1).typedInput({default:'str',types:['msg','flow','global','str','re']});
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"})
.text(search)
.appendTo(row3_1);
var fromValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-search-value",type:"text"})
.appendTo(row3_1)
.typedInput({default:'str',types:['msg','flow','global','str','re','num','bool']});
var row3_2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(row3);
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"}).text(replace).appendTo(row3_2);
var toValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-replace-value",type:"text"}).appendTo(row3_2).typedInput({default:'str',types:['msg','flow','global','str','num','json']});
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"})
.text(replace)
.appendTo(row3_2);
var toValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-replace-value",type:"text"})
.appendTo(row3_2)
.typedInput({default:'str',types:['msg','flow','global','str','num','bool','json']});
selectField.change(function() {
var width = $("#node-input-rule-container").width();

View File

@@ -64,12 +64,13 @@ module.exports = function(RED) {
if (!rule.fromt) {
rule.fromt = "str";
}
if (rule.t === "change") {
if (rule.t === "change" && rule.fromt !== 'msg' && rule.fromt !== 'flow' && rule.fromt !== 'global') {
rule.fromRE = rule.from;
if (rule.fromt !== 're') {
rule.from = rule.from.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
rule.fromRE = rule.fromRE.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
try {
rule.from = new RegExp(rule.from, "g");
rule.fromRE = new RegExp(rule.fromRE, "g");
} catch (e) {
valid = false;
this.error(RED._("change.errors.invalid-from",{error:e.message}));
@@ -93,6 +94,10 @@ module.exports = function(RED) {
try {
var property = rule.p;
var value = rule.to;
var current;
var fromValue;
var fromType;
var fromRE;
if (rule.tot === "msg") {
value = RED.util.getMessageProperty(msg,rule.to);
} else if (rule.tot === 'flow') {
@@ -100,16 +105,68 @@ module.exports = function(RED) {
} else if (rule.tot === 'global') {
value = node.context().global.get(rule.to);
}
if (rule.t === 'change') {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
if (rule.fromt === "msg") {
fromValue = RED.util.getMessageProperty(msg,rule.from);
} else if (rule.tot === 'flow') {
fromValue = node.context().flow.get(rule.from);
} else if (rule.tot === 'global') {
fromValue = node.context().global.get(rule.from);
}
if (typeof fromValue === 'number' || fromValue instanceof Number) {
fromType = 'num';
} else if (typeof fromValue === 'boolean') {
fromType = 'bool'
} else if (fromValue instanceof RegExp) {
fromType = 're';
fromRE = fromValue;
} else if (typeof fromValue === 'string') {
fromType = 'str';
fromRE = fromValue.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
try {
fromRE = new RegExp(fromRE, "g");
} catch (e) {
valid = false;
node.error(RED._("change.errors.invalid-from",{error:e.message}));
return
}
} else {
node.error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}));
return
}
} else {
fromType = rule.fromt;
fromValue = rule.from;
fromRE = rule.fromRE;
}
}
if (rule.pt === 'msg') {
if (rule.t === 'delete') {
RED.util.setMessageProperty(msg,property,undefined);
} else if (rule.t === 'set') {
RED.util.setMessageProperty(msg,property,value);
} else if (rule.t === 'change') {
var current = RED.util.getMessageProperty(msg,property);
current = RED.util.getMessageProperty(msg,property);
if (typeof current === 'string') {
current = current.replace(rule.from,value);
RED.util.setMessageProperty(msg,property,current);
if ((fromType === 'num' || fromType === 'bool') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
RED.util.setMessageProperty(msg,property,value);
} else {
current = current.replace(fromRE,value);
RED.util.setMessageProperty(msg,property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
RED.util.setMessageProperty(msg,property,value);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
RED.util.setMessageProperty(msg,property,value);
}
}
}
} else {
@@ -125,16 +182,30 @@ module.exports = function(RED) {
} else if (rule.t === 'set') {
target.set(property,value);
} else if (rule.t === 'change') {
var current = target.get(msg,property);
current = target.get(msg,property);
if (typeof current === 'string') {
current = current.replace(rule.from,value);
target.set(property,current);
if ((fromType === 'num' || fromType === 'bool') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
target.set(property,value);
} else {
current = current.replace(fromRE,value);
target.set(property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
target.set(property,value);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
target.set(property,value);
}
}
}
}
}
} catch(err) {console.log(err.stack)}
} catch(err) {/*console.log(err.stack)*/}
return msg;
}
if (valid) {
@@ -142,6 +213,9 @@ module.exports = function(RED) {
this.on('input', function(msg) {
for (var i=0;i<this.rules.length;i++) {
msg = applyRule(msg,this.rules[i]);
if (msg === null) {
return;
}
}
node.send(msg);
});

View File

@@ -1,6 +1,6 @@
{
"name" : "node-red",
"version" : "0.13.3",
"version" : "0.13.4",
"description" : "A visual tool for wiring the Internet of Things",
"homepage" : "http://nodered.org",
"license" : "Apache-2.0",
@@ -34,12 +34,12 @@
"cron":"1.1.0",
"express": "4.13.4",
"follow-redirects":"0.0.7",
"fs-extra": "0.26.5",
"fs-extra": "0.26.7",
"fs.notify":"0.0.4",
"i18next":"1.10.6",
"is-utf8":"0.2.1",
"media-typer": "0.3.0",
"mqtt": "1.7.2",
"mqtt": "1.7.4",
"mustache": "2.2.1",
"nopt": "3.0.6",
"oauth2orize":"1.2.2",
@@ -47,10 +47,10 @@
"passport":"0.3.2",
"passport-http-bearer":"1.0.1",
"passport-oauth2-client-password":"0.1.2",
"raw-body":"2.1.5",
"raw-body":"2.1.6",
"semver": "5.1.0",
"sentiment":"1.0.6",
"uglify-js":"2.6.1",
"uglify-js":"2.6.2",
"when": "3.7.7",
"ws": "0.8.1",
"xml2js":"0.4.16",

View File

@@ -54,7 +54,6 @@ module.exports = {
log.audit({event: "flow.update",id:id},req);
res.json({id:id});
}).otherwise(function(err) {
console.log(err.stack);
log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
})
@@ -63,7 +62,6 @@ module.exports = {
log.audit({event: "flow.update",id:id,error:"not_found"},req);
res.status(404).end();
} else {
console.log(err.stack);
log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2014, 2015 IBM Corp.
* Copyright 2014, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -40,6 +40,7 @@ var log;
var adminApp;
var nodeApp;
var server;
var runtime;
var errorHandler = function(err,req,res,next) {
if (err.message === "request entity too large") {
@@ -51,8 +52,18 @@ var errorHandler = function(err,req,res,next) {
res.status(400).json({error:"unexpected_error", message:err.toString()});
};
function init(_server,runtime) {
var ensureRuntimeStarted = function(req,res,next) {
if (!runtime.isStarted()) {
log.error("Node-RED runtime not started");
res.status(503).send("Not started");
} else {
next();
}
}
function init(_server,_runtime) {
server = _server;
runtime = _runtime;
var settings = runtime.settings;
i18n = runtime.i18n;
log = runtime.log;
@@ -75,7 +86,7 @@ function init(_server,runtime) {
if (!settings.disableEditor) {
ui.init(runtime);
var editorApp = express();
editorApp.get("/",ui.ensureSlash,ui.editor);
editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor);
editorApp.get("/icons/:icon",ui.icon);
theme.init(runtime);
if (settings.editorTheme) {

View File

@@ -13,6 +13,9 @@
* 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 redApp = null;
var storage;
@@ -68,40 +71,135 @@ 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);
},
register: createLibrary,
getAll: function(req,res) {
storage.getAllFlows().then(function(flows) {
log.audit({event: "library.get.all",type:"flow"},req);
if (exampleCount > 0) {
flows.d = flows.d||{};
flows.d._examples_ = exampleFlows;
}
res.json(flows);
});
},
get: function(req,res) {
storage.getFlow(req.params[0]).then(function(data) {
// data is already a JSON string
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
res.set('Content-Type', 'application/json');
res.send(data);
}).otherwise(function(err) {
if (err) {
log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req);
res.status(403).end();
return;
if (req.params[0].indexOf("_examples_/") === 0) {
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);
try {
fs.statSync(fullPath);
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
return res.sendFile(fullPath,{
headers:{
'Content-Type': 'application/json'
}
})
} catch(err) {
console.log(err);
}
}
}
// IF we get here, we didn't find the file
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req);
res.status(404).end();
});
return res.status(404).end();
} else {
storage.getFlow(req.params[0]).then(function(data) {
// data is already a JSON string
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
res.set('Content-Type', 'application/json');
res.send(data);
}).otherwise(function(err) {
if (err) {
log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req);
res.status(403).end();
return;
}
}
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req);
res.status(404).end();
});
}
},
post: function(req,res) {
// if (req.params[0].indexOf("_examples_/") === 0) {
// log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:"forbidden"}));
// log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req);
// return res.status(403).send({error:"unexpected_error", message:"forbidden"});
// }
var flow = JSON.stringify(req.body);
storage.saveFlow(req.params[0],flow).then(function() {
log.audit({event: "library.set",type:"flow",path:req.params[0]},req);

View File

@@ -31,6 +31,7 @@
"export": "Export",
"clipboard": "Clipboard",
"library": "Library",
"examples": "Examples",
"subflows": "Subflows",
"createSubflow": "Create Subflow",
"selectionToSubflow": "Selection to Subflow",

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@ var os = require("os");
var runtimeMetricInterval = null;
var started = false;
var stubbedExpressApp = {
get: function() {},
post: function() {},
@@ -142,6 +144,7 @@ function start() {
log.info(log._("runtime.paths.settings",{path:settings.settingsFile}));
}
redNodes.loadFlows().then(redNodes.startFlows);
started = true;
}).otherwise(function(err) {
console.log(err);
});
@@ -173,6 +176,7 @@ function stop() {
clearInterval(runtimeMetricInterval);
runtimeMetricInterval = null;
}
started = false;
return redNodes.stopFlows();
}
@@ -190,5 +194,8 @@ var runtime = module.exports = {
events: events,
nodes: redNodes,
util: require("./util"),
get adminApi() { return adminApi }
get adminApi() { return adminApi },
isStarted: function() {
return started;
}
}

View File

@@ -121,7 +121,11 @@
"localfilesystem": {
"user-dir": "User directory : __path__",
"flows-file": "Flows file : __path__",
"create": "Creating new flow file"
"create": "Creating new flow file",
"empty": "Existing flow file is empty",
"invalid": "Existing flow file is not valid json",
"restore": "Restoring flow file backup : __path__",
"restore-fail": "Restoring flow file backup failed : __message__"
}
}
}

View File

@@ -128,7 +128,7 @@ function setConfig(_config,type,muteLog) {
function getNode(id) {
var node;
if (activeNodesToFlow[id]) {
if (activeNodesToFlow[id] && activeFlows[activeNodesToFlow[id]]) {
return activeFlows[activeNodesToFlow[id]].getNode(id);
}
for (var flowId in activeFlows) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2015 IBM Corp.
* Copyright 2015, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -174,6 +174,8 @@ function uninstallModule(module) {
} else {
log.info(log._("server.install.uninstalled",{name:module}));
reportRemovedModules(list);
// TODO: tidy up internal event names
events.emit("node-module-uninstalled",module)
resolve(list);
}
}

View File

@@ -94,7 +94,9 @@ function createNodeApi(node) {
}
red["_"] = function() {
var args = Array.prototype.slice.call(arguments, 0);
args[0] = node.namespace+":"+args[0];
if (args[0].indexOf(":") === -1) {
args[0] = node.namespace+":"+args[0];
}
return runtime.i18n._.apply(null,args);
}
return red;

View File

@@ -184,6 +184,12 @@ function getModuleNodeFiles(module) {
}
}
}
var examplesDir = path.join(moduleDir,"examples");
try {
fs.statSync(examplesDir)
events.emit("node-examples-dir",{name:pkg.name,path:examplesDir});
} catch(err) {
}
return results;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2014 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -135,7 +135,7 @@ var localfilesystem = {
} catch(err) {
settings.userDir = fspath.join(process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE || process.env.NODE_RED_HOME,".node-red");
if (!settings.readOnly) {
promises.push(promiseDir(settings.userDir));
promises.push(promiseDir(fspath.join(settings.userDir,"node_modules")));
}
}
}
@@ -199,10 +199,42 @@ var localfilesystem = {
}
fs.readFile(flowsFullPath,'utf8',function(err,data) {
if (!err) {
return resolve(JSON.parse(data));
if (data.length === 0) {
log.warn(log._("storage.localfilesystem.empty"));
try {
var backupStat = fs.statSync(flowsFileBackup);
if (backupStat.size === 0) {
// Empty flows, empty backup - return empty flow
return resolve([]);
}
// Empty flows, restore backup
log.warn(log._("storage.localfilesystem.restore",{path:flowsFileBackup}));
fs.copy(flowsFileBackup,flowsFullPath,function(backupCopyErr) {
if (backupCopyErr) {
// Restore backup failed
log.warn(log._("storage.localfilesystem.restore-fail",{message:backupCopyErr.toString()}));
resolve([]);
} else {
// Loop back in to load the restored backup
resolve(localfilesystem.getFlows());
}
});
return;
} catch(backupStatErr) {
// Empty flow file, no back-up file
return resolve([]);
}
}
try {
return resolve(JSON.parse(data));
} catch(parseErr) {
log.warn(log._("storage.localfilesystem.invalid"));
return resolve([]);
}
} else {
log.info(log._("storage.localfilesystem.create"));
resolve([]);
}
log.info(log._("storage.localfilesystem.create"));
resolve([]);
});
});
},

View File

@@ -477,4 +477,45 @@ describe('delay Node', function() {
});
});
it('handles timed queue', function(done) {
this.timeout(6000);
var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"timed","timeout":5,"timeoutUnits":"seconds","rate":1000,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(delayNode, flow, function() {
var delayNode1 = helper.getNode("delayNode1");
var helperNode1 = helper.getNode("helperNode1");
var messages = 2;
var c = 0;
helperNode1.on("input", function(msg) {
c += 1;
if (c === 1) {
msg.should.have.property("topic","A");
msg.should.have.property("payload",3);
}
else if (c === 2) {
msg.should.have.property("topic","_none_");
msg.should.have.property("payload",2);
}
else if (c === 3) {
msg.should.have.property("topic","_none_");
msg.should.have.property("payload","Biscuit");
done();
}
});
// send test messages
delayNode1.receive({payload:1,topic:"A"});
delayNode1.receive({payload:1});
delayNode1.receive({payload:2,topic:"A"});
delayNode1.receive({payload:3,topic:"A"}); // should get this
delayNode1.receive({payload:2}); // and this
setTimeout( function() {
delayNode1.receive({payload:"Biscuit"}); // and then this
},2000);
});
});
});

View File

@@ -51,7 +51,7 @@ describe('change Node', function() {
helper.load(changeNode, flow, function() {
//console.log(helper.getNode("c1"));
helper.getNode("c1").should.have.property("name", "change1");
helper.getNode("c1").should.have.property("rules", [ { from: /(?:)/g,fromt: 'str', p: '',pt: 'msg', re: undefined, t: 'change', to: '',tot: 'str' } ]);
helper.getNode("c1").should.have.property("rules", [ { from: '', fromRE:/(?:)/g,fromt: 'str', p: '',pt: 'msg', re: undefined, t: 'change', to: '',tot: 'str' } ]);
done();
});
});
@@ -424,7 +424,6 @@ describe('change Node', function() {
});
});
it('changes the value - new rule format', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"ABC","to":"123","fromt":"str","tot":"str"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
@@ -442,6 +441,96 @@ describe('change Node', function() {
changeNode1.receive({payload:"abcABCabc"});
});
});
it('changes the value using msg property', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"topic","to":"123","fromt":"msg","tot":"str"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("abc123abc");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"abcABCabc",topic:"ABC"});
});
});
it('changes the value using number - string payload', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"123","to":"456","fromt":"num","tot":"str"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("456");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"123"});
});
});
it('changes the value using number - number payload', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"123","to":"abc","fromt":"num","tot":"str"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("abc");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:123});
});
});
it('changes the value using boolean - string payload', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"true","to":"xxx","fromt":"bool","tot":"str"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("xxx");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"true"});
});
});
it('changes the value using boolean - boolean payload', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"true","to":"xxx","fromt":"bool","tot":"str"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("xxx");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:true});
});
});
});
describe("#delete", function() {

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2014 IBM Corp.
* Copyright 2014, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,16 +21,259 @@ var bodyParser = require('body-parser');
var sinon = require('sinon');
var when = require('when');
var flows = require("../../../red/api/flows");
var flow = require("../../../red/api/flow");
describe("flow api", function() {
var app;
it.skip('add flow');
it.skip('update flow');
it.skip('remove flow');
it.skip('get flow');
before(function() {
app = express();
app.use(bodyParser.json());
app.get("/flow/:id",flow.get);
app.post("/flow",flow.post);
app.put("/flow/:id",flow.put);
app.delete("/flow/:id",flow.delete);
});
describe("get", function() {
before(function() {
flow.init({
settings:{},
nodes: {
getFlow: function(id) {
if (id === '123') {
return {id:'123'}
} else {
return null;
}
}
},
log:{ audit: sinon.stub() }
});
})
it('gets a known flow', function(done) {
request(app)
.get('/flow/123')
.set('Accept', 'application/json')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('id','123');
done();
});
})
it('404s an unknown flow', function(done) {
request(app)
.get('/flow/456')
.set('Accept', 'application/json')
.expect(404)
.end(done);
})
});
describe("add", function() {
before(function() {
flow.init({
settings:{},
nodes: {
addFlow: function(f) {
if (f.id === "123") {
return when.resolve('123')
} else {
return when.reject(new Error("test error"));
}
}
},
log:{ audit: sinon.stub() }
});
})
it('adds a new flow', function(done) {
request(app)
.post('/flow')
.set('Accept', 'application/json')
.send({id:'123'})
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('id','123');
done();
});
})
it('400 an invalid flow', function(done) {
request(app)
.post('/flow')
.set('Accept', 'application/json')
.send({id:'error'})
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('error','unexpected_error');
res.body.should.has.a.property('message','Error: test error');
done();
});
})
})
describe("update", function() {
var nodes;
before(function() {
nodes = {
updateFlow: function(id,f) {
var err;
if (id === "123") {
return when.resolve()
} else if (id === "unknown") {
err = new Error();
err.code = 404;
throw err;
} else if (id === "unexpected") {
err = new Error();
err.code = 500;
throw err;
} else {
return when.reject(new Error("test error"));
}
}
};
flow.init({
settings:{},
nodes: nodes,
log:{ audit: sinon.stub() }
});
})
it('updates an existing flow', function(done) {
sinon.spy(nodes,"updateFlow");
request(app)
.put('/flow/123')
.set('Accept', 'application/json')
.send({id:'123'})
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('id','123');
nodes.updateFlow.calledOnce.should.be.true;
nodes.updateFlow.lastCall.args[0].should.eql('123');
nodes.updateFlow.lastCall.args[1].should.eql({id:'123'});
nodes.updateFlow.restore();
done();
});
})
it('404s on an unknown flow', function(done) {
request(app)
.put('/flow/unknown')
.set('Accept', 'application/json')
.send({id:'123'})
.expect(404)
.end(done);
})
it('400 on async update error', function(done) {
request(app)
.put('/flow/async_error')
.set('Accept', 'application/json')
.send({id:'123'})
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('error','unexpected_error');
res.body.should.has.a.property('message','Error: test error');
done();
});
})
it('400 on sync update error', function(done) {
request(app)
.put('/flow/unexpected')
.set('Accept', 'application/json')
.send({id:'123'})
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('error',500);
res.body.should.has.a.property('message','Error');
done();
});
})
})
describe("delete", function() {
var nodes;
before(function() {
nodes = {
removeFlow: function(id) {
var err;
if (id === "123") {
return when.resolve()
} else if (id === "unknown") {
err = new Error();
err.code = 404;
throw err;
} else if (id === "unexpected") {
err = new Error();
err.code = 500;
throw err;
}
}
};
flow.init({
settings:{},
nodes: nodes,
log:{ audit: sinon.stub() }
});
})
it('updates an existing flow', function(done) {
sinon.spy(nodes,"removeFlow");
request(app)
.delete('/flow/123')
.expect(204)
.end(function(err,res) {
if (err) {
return done(err);
}
nodes.removeFlow.calledOnce.should.be.true;
nodes.removeFlow.lastCall.args[0].should.eql('123');
nodes.removeFlow.restore();
done();
});
})
it('404s on an unknown flow', function(done) {
request(app)
.delete('/flow/unknown')
.expect(404)
.end(done);
})
it('400 on remove error', function(done) {
request(app)
.delete('/flow/unexpected')
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.has.a.property('error',500);
res.body.should.has.a.property('message','Error');
done();
});
})
})
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2014 IBM Corp.
* Copyright 2014, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -87,6 +87,45 @@ describe("api index", function() {
});
});
describe("editor warns if runtime not started", function() {
var mockList = [
'nodes','flows','library','info','theme','locales','credentials'
]
before(function() {
mockList.forEach(function(m) {
sinon.stub(require("../../../red/api/"+m),"init",function(){});
});
});
after(function() {
mockList.forEach(function(m) {
require("../../../red/api/"+m).init.restore();
})
});
it('serves the editor', function(done) {
var errorLog = sinon.spy();
api.init({},{
log:{audit:function(){},error:errorLog},
settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false},
events:{on:function(){},removeListener:function(){}},
isStarted: function() { return false; } // <-----
});
app = api.adminApp;
request(app)
.get("/")
.expect(503)
.end(function(err,res) {
if (err) {
return done(err);
}
res.text.should.eql("Not started");
errorLog.calledOnce.should.be.true;
done();
});
});
});
describe("enables editor", function() {
var mockList = [
@@ -107,7 +146,8 @@ describe("api index", function() {
api.init({},{
log:{audit:function(){}},
settings:{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false},
events:{on:function(){},removeListener:function(){}}
events:{on:function(){},removeListener:function(){}},
isStarted: function() { return true; }
});
app = api.adminApp;
});

View File

@@ -80,6 +80,10 @@ describe("library api", function() {
libraryEntries[type][path] = body;
return when.resolve();
}
},
events: {
on: function(){},
removeListener: function(){}
}
});
}

View File

@@ -428,6 +428,8 @@ describe("red/nodes/registry/registry",function() {
});
describe('#registerNodeConstructor', function() {
function TestNodeConstructor() {
}
beforeEach(function() {
sinon.stub(events,'emit');
});
@@ -435,19 +437,19 @@ describe("red/nodes/registry/registry",function() {
events.emit.restore();
});
it('registers a node constructor', function() {
typeRegistry.registerNodeConstructor('node-type',{});
typeRegistry.registerNodeConstructor('node-type',TestNodeConstructor);
events.emit.calledOnce.should.be.true;
events.emit.lastCall.args[0].should.eql('type-registered');
events.emit.lastCall.args[1].should.eql('node-type');
})
it('throws error on duplicate node registration', function() {
typeRegistry.registerNodeConstructor('node-type',{});
typeRegistry.registerNodeConstructor('node-type',TestNodeConstructor);
events.emit.calledOnce.should.be.true;
events.emit.lastCall.args[0].should.eql('type-registered');
events.emit.lastCall.args[1].should.eql('node-type');
/*jshint immed: false */
(function(){
typeRegistry.registerNodeConstructor('node-type',{});
typeRegistry.registerNodeConstructor('node-type',TestNodeConstructor);
}).should.throw("node-type already registered");
events.emit.calledOnce.should.be.true;
})

View File

@@ -106,6 +106,45 @@ describe('LocalFileSystem', function() {
});
});
it('should handle empty flow file, no backup',function(done) {
localfilesystem.init({userDir:userDir}).then(function() {
var flowFile = 'flows_'+require('os').hostname()+'.json';
var flowFilePath = path.join(userDir,flowFile);
var flowFileBackupPath = path.join(userDir,"."+flowFile+".backup");
fs.closeSync(fs.openSync(flowFilePath, 'w'));
fs.existsSync(flowFilePath).should.be.true;
localfilesystem.getFlows().then(function(flows) {
flows.should.eql([]);
done();
}).otherwise(function(err) {
done(err);
});
}).otherwise(function(err) {
done(err);
});
});
it('should handle empty flow file, restores backup',function(done) {
localfilesystem.init({userDir:userDir}).then(function() {
var flowFile = 'flows_'+require('os').hostname()+'.json';
var flowFilePath = path.join(userDir,flowFile);
var flowFileBackupPath = path.join(userDir,"."+flowFile+".backup");
fs.closeSync(fs.openSync(flowFilePath, 'w'));
fs.existsSync(flowFilePath).should.be.true;
fs.existsSync(flowFileBackupPath).should.be.false;
fs.writeFileSync(flowFileBackupPath,JSON.stringify(testFlow));
fs.existsSync(flowFileBackupPath).should.be.true;
localfilesystem.getFlows().then(function(flows) {
flows.should.eql(testFlow);
done();
}).otherwise(function(err) {
done(err);
});
}).otherwise(function(err) {
done(err);
});
});
it('should save flows to the default file',function(done) {
localfilesystem.init({userDir:userDir}).then(function() {
var flowFile = 'flows_'+require('os').hostname()+'.json';