From 9994df960164e7a6a6028e36b18c25a19e4f6944 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Mon, 19 Mar 2018 15:45:52 -0700 Subject: [PATCH 001/208] tcprequest tests: normalize indents --- test/nodes/core/io/31-tcprequest_spec.js | 33 ++++++++++++------------ 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/test/nodes/core/io/31-tcprequest_spec.js b/test/nodes/core/io/31-tcprequest_spec.js index bc9a627e0..481e11464 100644 --- a/test/nodes/core/io/31-tcprequest_spec.js +++ b/test/nodes/core/io/31-tcprequest_spec.js @@ -27,12 +27,12 @@ describe('TCP Request Node', function() { function startServer(done) { port += 1; server = net.createServer(function(c) { - c.on('data', function(data) { - var rdata = "ACK:"+data.toString(); - c.write(rdata); - }); + c.on('data', function(data) { + var rdata = "ACK:"+data.toString(); + c.write(rdata); + }); c.on('error', function(err) { - startServer(done); + startServer(done); }); }).listen(port, "127.0.0.1", function(err) { done(); @@ -63,48 +63,47 @@ describe('TCP Request Node', function() { done(err); } }); - if((typeof val0) === 'object') { - n1.receive(val0); - } else { - n1.receive({payload:val0}); - } + if((typeof val0) === 'object') { + n1.receive(val0); + } else { + n1.receive({payload:val0}); + } }); } it('should send & recv data', function(done) { var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] }, {id:"n2", type:"helper"}]; - testTCP(flow, "foo", "ACK:foo", done) + testTCP(flow, "foo", "ACK:foo", done) }); it('should send & recv data when specified character received', function(done) { var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] }, {id:"n2", type:"helper"}]; - testTCP(flow, "foo0bar0", "ACK:foo0", done); + testTCP(flow, "foo0bar0", "ACK:foo0", done); }); it('should send & recv data after fixed number of chars received', function(done) { var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] }, {id:"n2", type:"helper"}]; - testTCP(flow, "foo bar", "ACK:foo", done); + testTCP(flow, "foo bar", "ACK:foo", done); }); it('should send & receive, then keep connection', function(done) { var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] }, {id:"n2", type:"helper"}]; - testTCP(flow, "foo", "ACK:foo", done); + testTCP(flow, "foo", "ACK:foo", done); }); it('should send & close', function(done) { var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] }, {id:"n2", type:"helper"}]; - testTCP(flow, "foo", "ACK:foo", done); + testTCP(flow, "foo", "ACK:foo", done); }); it('should send & recv data to/from server:port from msg', function(done) { var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] }, {id:"n2", type:"helper"}]; - testTCP(flow, {payload:"foo", host:"localhost", port:port}, "ACK:foo", done) + testTCP(flow, {payload:"foo", host:"localhost", port:port}, "ACK:foo", done) }); - }); From 6e2e36e7a02a4a85f1039bb9da5946007cad1564 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Mon, 19 Mar 2018 16:22:14 -0700 Subject: [PATCH 002/208] tcp: queue messages while connecting; closes #1414 - queues messages on a per-client basis while waiting for TCP server connection - add `denque` package for performance (`shift()` happens in constant instead of `Array`'s linear time) - add tests - remove a duplicate test in `31-tcp_request.spec.js` - cap queue at value specified in settings (`tcpMsgQueueSize`); default to 1000 - add `tcpMsgQueueSize` to `settings.js` Signed-off-by: Christopher Hiller --- nodes/core/io/31-tcpin.js | 89 ++++++++++++----- package.json | 1 + settings.js | 4 + test/nodes/core/io/31-tcprequest_spec.js | 118 +++++++++++++++++------ 4 files changed, 161 insertions(+), 51 deletions(-) diff --git a/nodes/core/io/31-tcpin.js b/nodes/core/io/31-tcpin.js index 5853d4f42..2bcab4976 100644 --- a/nodes/core/io/31-tcpin.js +++ b/nodes/core/io/31-tcpin.js @@ -18,10 +18,36 @@ module.exports = function(RED) { "use strict"; var reconnectTime = RED.settings.socketReconnectTime||10000; var socketTimeout = RED.settings.socketTimeout||null; + const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000; + const Denque = require('denque'); var net = require('net'); var connectionPool = {}; + /** + * Enqueue `item` in `queue` + * @param {Denque} queue - Queue + * @param {*} item - Item to enqueue + * @private + * @returns {Denque} `queue` + */ + const enqueue = (queue, item) => { + // drop msgs from front of queue if size is going to be exceeded + if (queue.size() === msgQueueSize) { + queue.shift(); + } + queue.push(item); + return queue; + }; + + /** + * Shifts item off front of queue + * @param {Deque} queue - Queue + * @private + * @returns {*} Item previously at front of queue + */ + const dequeue = queue => queue.shift(); + function TcpIn(n) { RED.nodes.createNode(this,n); this.host = n.host; @@ -430,11 +456,14 @@ module.exports = function(RED) { // the clients object will have: // clients[id].client, clients[id].msg, clients[id].timeout var connection_id = host + ":" + port; - clients[connection_id] = clients[connection_id] || {}; - clients[connection_id].msg = msg; - clients[connection_id].connected = clients[connection_id].connected || false; + clients[connection_id] = clients[connection_id] || { + msgQueue: new Denque(), + connected: false, + connecting: false + }; + enqueue(clients[connection_id].msgQueue, msg); - if (!clients[connection_id].connected) { + if (!clients[connection_id].connecting && !clients[connection_id].connected) { var buf; if (this.out == "count") { if (this.splitc === 0) { buf = Buffer.alloc(1); } @@ -446,14 +475,19 @@ module.exports = function(RED) { if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);} if (host && port) { + clients[connection_id].connecting = true; clients[connection_id].client.connect(port, host, function() { //node.log(RED._("tcpin.errors.client-connected")); node.status({fill:"green",shape:"dot",text:"common.status.connected"}); if (clients[connection_id] && clients[connection_id].client) { clients[connection_id].connected = true; - clients[connection_id].client.write(clients[connection_id].msg.payload); + clients[connection_id].connecting = false; + let msg; + while (msg = dequeue(clients[connection_id].msgQueue)) { + clients[connection_id].client.write(msg.payload); + } if (node.out === "time" && node.splitc < 0) { - clients[connection_id].connected = false; + clients[connection_id].connected = clients[connection_id].connecting = false; clients[connection_id].client.end(); delete clients[connection_id]; node.status({}); @@ -468,9 +502,10 @@ module.exports = function(RED) { clients[connection_id].client.on('data', function(data) { if (node.out === "sit") { // if we are staying connected just send the buffer if (clients[connection_id]) { - if (!clients[connection_id].hasOwnProperty("msg")) { clients[connection_id].msg = {}; } - clients[connection_id].msg.payload = data; - node.send(RED.util.cloneMessage(clients[connection_id].msg)); + let msg = dequeue(clients[connection_id].msgQueue) || {}; + clients[connection_id].msgQueue.unshift(msg); + msg.payload = data; + node.send(RED.util.cloneMessage(msg)); } } // else if (node.splitc === 0) { @@ -490,9 +525,11 @@ module.exports = function(RED) { clients[connection_id].timeout = setTimeout(function () { if (clients[connection_id]) { clients[connection_id].timeout = null; - clients[connection_id].msg.payload = Buffer.alloc(i+1); - buf.copy(clients[connection_id].msg.payload,0,0,i+1); - node.send(clients[connection_id].msg); + let msg = dequeue(clients[connection_id].msgQueue) || {}; + clients[connection_id].msgQueue.unshift(msg); + msg.payload = Buffer.alloc(i+1); + buf.copy(msg.payload,0,0,i+1); + node.send(msg); if (clients[connection_id].client) { node.status({}); clients[connection_id].client.destroy(); @@ -511,9 +548,11 @@ module.exports = function(RED) { i += 1; if ( i >= node.splitc) { if (clients[connection_id]) { - clients[connection_id].msg.payload = Buffer.alloc(i); - buf.copy(clients[connection_id].msg.payload,0,0,i); - node.send(clients[connection_id].msg); + let msg = dequeue(clients[connection_id].msgQueue) || {}; + clients[connection_id].msgQueue.unshift(msg); + msg.payload = Buffer.alloc(i); + buf.copy(msg.payload,0,0,i); + node.send(msg); if (clients[connection_id].client) { node.status({}); clients[connection_id].client.destroy(); @@ -529,9 +568,11 @@ module.exports = function(RED) { i += 1; if (data[j] == node.splitc) { if (clients[connection_id]) { - clients[connection_id].msg.payload = Buffer.alloc(i); - buf.copy(clients[connection_id].msg.payload,0,0,i); - node.send(clients[connection_id].msg); + let msg = dequeue(clients[connection_id].msgQueue) || {}; + clients[connection_id].msgQueue.unshift(msg); + msg.payload = Buffer.alloc(i); + buf.copy(msg.payload,0,0,i); + node.send(msg); if (clients[connection_id].client) { node.status({}); clients[connection_id].client.destroy(); @@ -549,7 +590,7 @@ module.exports = function(RED) { //console.log("END"); node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"}); if (clients[connection_id] && clients[connection_id].client) { - clients[connection_id].connected = false; + clients[connection_id].connected = clients[connection_id].connecting = false; clients[connection_id].client = null; } }); @@ -557,7 +598,7 @@ module.exports = function(RED) { clients[connection_id].client.on('close', function() { //console.log("CLOSE"); if (clients[connection_id]) { - clients[connection_id].connected = false; + clients[connection_id].connected = clients[connection_id].connecting = false; } var anyConnected = false; @@ -587,21 +628,23 @@ module.exports = function(RED) { clients[connection_id].client.on('timeout',function() { //console.log("TIMEOUT"); if (clients[connection_id]) { - clients[connection_id].connected = false; + clients[connection_id].connected = clients[connection_id].connecting = false; node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"}); //node.warn(RED._("tcpin.errors.connect-timeout")); if (clients[connection_id].client) { + clients[connection_id].connecting = true; clients[connection_id].client.connect(port, host, function() { clients[connection_id].connected = true; + clients[connection_id].connecting = false; node.status({fill:"green",shape:"dot",text:"common.status.connected"}); }); } } }); } - else { + else if (!clients[connection_id].connecting && clients[connection_id].connected) { if (clients[connection_id] && clients[connection_id].client) { - clients[connection_id].client.write(clients[connection_id].msg.payload); + clients[connection_id].client.write(dequeue(clients[connection_id].msgQueue)); } } }); diff --git a/package.json b/package.json index 2c816569a..c7fe330e2 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "cookie-parser": "1.4.3", "cors": "2.8.4", "cron": "1.3.0", + "denque": "^1.2.3", "express": "4.16.2", "express-session": "1.15.6", "follow-redirects": "1.3.0", diff --git a/settings.js b/settings.js index c3bd898ec..1a6daa336 100644 --- a/settings.js +++ b/settings.js @@ -40,6 +40,10 @@ module.exports = { // defaults to no timeout //socketTimeout: 120000, + // Maximum number of messages to wait in queue while attempting to connect to TCP socket + // defaults to 1000 + //tcpMsgQueueSize: 2000, + // Timeout in milliseconds for HTTP request connections // defaults to 120 seconds //httpRequestTimeout: 120000, diff --git a/test/nodes/core/io/31-tcprequest_spec.js b/test/nodes/core/io/31-tcprequest_spec.js index 481e11464..2af28e382 100644 --- a/test/nodes/core/io/31-tcprequest_spec.js +++ b/test/nodes/core/io/31-tcprequest_spec.js @@ -18,6 +18,7 @@ var net = require("net"); var should = require("should"); var helper = require("../../helper.js"); var tcpinNode = require("../../../../nodes/core/io/31-tcpin.js"); +const RED = require("../../../../red/red.js"); describe('TCP Request Node', function() { @@ -70,40 +71,101 @@ describe('TCP Request Node', function() { } }); } + + function testTCPMany(flow, values, result, done) { + helper.load(tcpinNode, flow, () => { + const n1 = helper.getNode("n1"); + const n2 = helper.getNode("n2"); + n2.on("input", msg => { + try { + msg.should.have.property('payload', Buffer(result)); + done(); + } catch(err) { + done(err); + } + }); + values.forEach(value => { + n1.receive(typeof value === 'object' ? value : {payload: value}); + }); + }); + } + + describe('single message', function () { + it('should send & recv data', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCP(flow, "foo", "ACK:foo", done) + }); - it('should send & recv data', function(done) { - var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] }, - {id:"n2", type:"helper"}]; - testTCP(flow, "foo", "ACK:foo", done) + it('should send & recv data when specified character received', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCP(flow, "foo0bar0", "ACK:foo0", done); + }); + + it('should send & recv data after fixed number of chars received', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCP(flow, "foo bar", "ACK:foo", done); + }); + + it('should send & receive, then keep connection', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCP(flow, "foo", "ACK:foo", done); + }); + + it('should send & recv data to/from server:port from msg', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCP(flow, {payload:"foo", host:"localhost", port:port}, "ACK:foo", done) + }); }); - it('should send & recv data when specified character received', function(done) { - var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] }, - {id:"n2", type:"helper"}]; - testTCP(flow, "foo0bar0", "ACK:foo0", done); - }); + describe('many messages', function () { + it('should send & recv data', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; - it('should send & recv data after fixed number of chars received', function(done) { - var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] }, - {id:"n2", type:"helper"}]; - testTCP(flow, "foo bar", "ACK:foo", done); - }); + testTCPMany(flow, ['f', 'o', 'o'], 'ACK:foo', done); + }); - it('should send & receive, then keep connection', function(done) { - var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] }, - {id:"n2", type:"helper"}]; - testTCP(flow, "foo", "ACK:foo", done); - }); + it('should send & recv data when specified character received', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCPMany(flow, ["foo0","bar0"], "ACK:foo0", done); + }); - it('should send & close', function(done) { - var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] }, - {id:"n2", type:"helper"}]; - testTCP(flow, "foo", "ACK:foo", done); - }); + it('should send & recv data after fixed number of chars received', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCPMany(flow, ["fo", "ob", "ar"], "ACK:foo", done); + }); - it('should send & recv data to/from server:port from msg', function(done) { - var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] }, - {id:"n2", type:"helper"}]; - testTCP(flow, {payload:"foo", host:"localhost", port:port}, "ACK:foo", done) + + it('should send & receive, then keep connection', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCPMany(flow, ["foo", "bar", "baz"], "ACK:foobarbaz", done); + }); + + it('should send & recv data to/from server:port from msg', function(done) { + var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + testTCPMany(flow, [ + {payload:"f", host:"localhost", port:port}, + {payload:"o", host:"localhost", port:port}, + {payload:"o", host:"localhost", port:port}], "ACK:foo", done); + }); + + it('should limit the queue size', function (done) { + RED.settings.tcpMsgQueueSize = 10; + var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] }, + {id:"n2", type:"helper"}]; + // create one more msg than is allowed + const msgs = new Array(RED.settings.tcpMsgQueueSize + 1).fill('x'); + const expected = msgs.slice(0, -1); + testTCPMany(flow, msgs, "ACK:" + expected.join(''), done); + }); }); }); From b761904424fa99300a0259cc6205ff4f2a91c7ba Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Tue, 22 May 2018 15:48:24 +0100 Subject: [PATCH 003/208] let Pi nodes be visible/editable on all platforms even where they are not physically available. --- nodes/core/hardware/36-rpi-gpio.js | 343 ++++++++++++++----------- nodes/core/locales/en-US/messages.json | 2 +- 2 files changed, 189 insertions(+), 156 deletions(-) diff --git a/nodes/core/hardware/36-rpi-gpio.js b/nodes/core/hardware/36-rpi-gpio.js index cc1a8ab2a..40c3d0f9b 100644 --- a/nodes/core/hardware/36-rpi-gpio.js +++ b/nodes/core/hardware/36-rpi-gpio.js @@ -6,35 +6,36 @@ module.exports = function(RED) { var fs = require('fs'); var gpioCommand = __dirname+'/nrgpio'; + var allOK = true; try { var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); - if (cpuinfo.indexOf(": BCM") === -1) { throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); } - } catch(err) { - throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); - } - - try { - fs.statSync("/usr/share/doc/python-rpi.gpio"); // test on Raspbian - // /usr/lib/python2.7/dist-packages/RPi/GPIO - } catch(err) { + if (cpuinfo.indexOf(": BCM") === -1) { + allOK = false; + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.ignorenode")); + } try { - fs.statSync("/usr/lib/python2.7/site-packages/RPi/GPIO"); // test on Arch - } - catch(err) { + fs.statSync("/usr/share/doc/python-rpi.gpio"); // test on Raspbian + // /usr/lib/python2.7/dist-packages/RPi/GPIO + } catch(err) { 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"); + fs.statSync("/usr/lib/python2.7/site-packages/RPi/GPIO"); // test on Arch + } catch(err) { + try { + fs.statSync("/usr/lib/python2.7/dist-packages/RPi/GPIO"); // test on Hypriot + } catch(err) { + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.libnotfound")); + allOK = false; + } } } - } - - if ( !(1 & parseInt((fs.statSync(gpioCommand).mode & parseInt("777", 8)).toString(8)[0]) )) { - RED.log.error(RED._("rpi-gpio.errors.needtobeexecutable",{command:gpioCommand})); - throw "Error : "+RED._("rpi-gpio.errors.mustbeexecutable"); + if ( !(1 & parseInt((fs.statSync(gpioCommand).mode & parseInt("777", 8)).toString(8)[0]) )) { + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.needtobeexecutable",{command:gpioCommand})); + allOK = false; + } + } catch(err) { + allOK = false; + RED.log.warn("rpi-gpio : "+RED._("rpi-gpio.errors.ignorenode")); } // the magic to make python print stuff immediately @@ -61,48 +62,62 @@ module.exports = function(RED) { } } - if (node.pin !== undefined) { - node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]); - node.running = true; - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); + if (allOK === true) { + if (node.pin !== undefined) { + node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]); + node.running = true; + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - node.child.stdout.on('data', function (data) { - var d = data.toString().trim().split("\n"); - for (var i = 0; i < d.length; i++) { - if (d[i] === '') { return; } - if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i])) && node.buttonState !== d[i]) { - node.send({ topic:"pi/"+node.pin, payload:Number(d[i]) }); + node.child.stdout.on('data', function (data) { + var d = data.toString().trim().split("\n"); + for (var i = 0; i < d.length; i++) { + if (d[i] === '') { return; } + if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i])) && node.buttonState !== d[i]) { + node.send({ topic:"pi/"+node.pin, payload:Number(d[i]) }); + } + node.buttonState = d[i]; + node.status({fill:"green",shape:"dot",text:d[i]}); + if (RED.settings.verbose) { node.log("out: "+d[i]+" :"); } } - node.buttonState = d[i]; - node.status({fill:"green",shape:"dot",text:d[i]}); - if (RED.settings.verbose) { node.log("out: "+d[i]+" :"); } - } - }); + }); - node.child.stderr.on('data', function (data) { - if (RED.settings.verbose) { node.log("err: "+data+" :"); } - }); + node.child.stderr.on('data', function (data) { + if (RED.settings.verbose) { node.log("err: "+data+" :"); } + }); - node.child.on('close', function (code) { - 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"}); - node.done(); - } - else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } - }); + node.child.on('close', function (code) { + 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"}); + node.done(); + } + else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } + }); - node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error",{error:err.errno})) } - }); + node.child.on('error', function (err) { + if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } + else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } + else { node.error(RED._("rpi-gpio.errors.error",{error:err.errno})) } + }); + } + else { + node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + } } else { - node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); + if (node.read === true) { + var val; + if (node.intype == "up") { val = 1; } + if (node.intype == "down") { val = 0; } + setTimeout(function(){ + node.send({ topic:"pi/"+node.pin, payload:val }); + node.status({fill:"grey",shape:"dot",text:RED._("rpi-gpio.status.na",{value:val})}); + },250); + } } node.on("close", function(done) { @@ -155,20 +170,83 @@ module.exports = function(RED) { else { node.warn(RED._("rpi-gpio.errors.invalidinput")+": "+out); } } - if (node.pin !== undefined) { - if (node.set && (node.out === "out")) { - node.child = spawn(gpioCommand, [node.out,node.pin,node.level]); - node.status({fill:"green",shape:"dot",text:node.level}); - } else { - node.child = spawn(gpioCommand, [node.out,node.pin,node.freq]); - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - } - node.running = true; + if (allOK === true) { + if (node.pin !== undefined) { + if (node.set && (node.out === "out")) { + node.child = spawn(gpioCommand, [node.out,node.pin,node.level]); + node.status({fill:"green",shape:"dot",text:node.level}); + } else { + node.child = spawn(gpioCommand, [node.out,node.pin,node.freq]); + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); + } + node.running = true; - node.on("input", inputlistener); + node.on("input", inputlistener); + + node.child.stdout.on('data', function (data) { + if (RED.settings.verbose) { node.log("out: "+data+" :"); } + }); + + node.child.stderr.on('data', function (data) { + if (RED.settings.verbose) { node.log("err: "+data+" :"); } + }); + + node.child.on('close', function (code) { + node.child = null; + node.running = false; + 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"}); + node.done(); + } + else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } + }); + + node.child.on('error', function (err) { + if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } + else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } + else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } + }); + + } + else { + node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + } + } + else { + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); + node.on("input", function(msg){ + node.status({fill:"grey",shape:"dot",text:RED._("rpi-gpio.status.na",{value:msg.payload.toString()})}); + }); + } + + node.on("close", function(done) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + delete pinsInUse[node.pin]; + if (node.child != null) { + node.done = done; + node.child.stdin.write("close "+node.pin); + node.child.kill('SIGKILL'); + } + else { done(); } + }); + + } + RED.nodes.registerType("rpi-gpio out",GPIOOutNode); + + function PiMouseNode(n) { + RED.nodes.createNode(this,n); + this.butt = n.butt || 7; + var node = this; + + if (allOK === true) { + node.child = spawn(gpioCommand+".py", ["mouse",node.butt]); + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); node.child.stdout.on('data', function (data) { - if (RED.settings.verbose) { node.log("out: "+data+" :"); } + data = Number(data); + if (data !== 0) { node.send({ topic:"pi/mouse", button:data, payload:1 }); } + else { node.send({ topic:"pi/mouse", button:data, payload:0 }); } }); node.child.stderr.on('data', function (data) { @@ -192,69 +270,19 @@ module.exports = function(RED) { else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } }); + node.on("close", function(done) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + if (node.child != null) { + node.done = done; + node.child.kill('SIGINT'); + node.child = null; + } + else { done(); } + }); } else { - node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); } - - node.on("close", function(done) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - delete pinsInUse[node.pin]; - if (node.child != null) { - node.done = done; - node.child.stdin.write("close "+node.pin); - node.child.kill('SIGKILL'); - } - else { done(); } - }); - - } - RED.nodes.registerType("rpi-gpio out",GPIOOutNode); - - function PiMouseNode(n) { - RED.nodes.createNode(this,n); - this.butt = n.butt || 7; - var node = this; - - node.child = spawn(gpioCommand+".py", ["mouse",node.butt]); - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - - node.child.stdout.on('data', function (data) { - data = Number(data); - if (data === 1) { node.send({ topic:"pi/mouse", button:data, payload:1 }); } - else { node.send({ topic:"pi/mouse", button:data, payload:0 }); } - }); - - node.child.stderr.on('data', function (data) { - if (RED.settings.verbose) { node.log("err: "+data+" :"); } - }); - - node.child.on('close', function (code) { - node.child = null; - node.running = false; - 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"}); - node.done(); - } - else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } - }); - - node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } - }); - - node.on("close", function(done) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - if (node.child != null) { - node.done = done; - node.child.kill('SIGINT'); - node.child = null; - } - else { done(); } - }); } RED.nodes.registerType("rpi-mouse",PiMouseNode); @@ -262,39 +290,40 @@ module.exports = function(RED) { RED.nodes.createNode(this,n); var node = this; - node.child = spawn(gpioCommand+".py", ["kbd","0"]); - node.status({fill:"green",shape:"dot",text:"common.status.ok"}); + if (allOK === true) { + node.child = spawn(gpioCommand+".py", ["kbd","0"]); + node.status({fill:"green",shape:"dot",text:"common.status.ok"}); - node.child.stdout.on('data', function (data) { - var b = data.toString().trim().split(","); - var act = "up"; - if (b[1] === "1") { act = "down"; } - if (b[1] === "2") { act = "repeat"; } - node.send({ topic:"pi/key", payload:Number(b[0]), action:act }); - }); + node.child.stdout.on('data', function (data) { + var b = data.toString().trim().split(","); + var act = "up"; + if (b[1] === "1") { act = "down"; } + if (b[1] === "2") { act = "repeat"; } + node.send({ topic:"pi/key", payload:Number(b[0]), action:act }); + }); - node.child.stderr.on('data', function (data) { - if (RED.settings.verbose) { node.log("err: "+data+" :"); } - }); + node.child.stderr.on('data', function (data) { + if (RED.settings.verbose) { node.log("err: "+data+" :"); } + }); - node.child.on('close', function (code) { - 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"}); - node.done(); - } - else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } - }); + node.child.on('close', function (code) { + 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"}); + node.done(); + } + else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } + }); - node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } - }); + node.child.on('error', function (err) { + if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } + else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } + else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } + }); - node.on("close", function(done) { + node.on("close", function(done) { node.status({}); if (node.child != null) { node.done = done; @@ -303,6 +332,10 @@ module.exports = function(RED) { } else { done(); } }); + } + else { + node.status({fill:"grey",shape:"dot",text:"node-red:rpi-gpio.status.not-available"}); + } } RED.nodes.registerType("rpi-keyboard",PiKeyboardNode); diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index 74ec0bcd4..15d66c9dc 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -787,7 +787,7 @@ }, "errors": { "ignorenode": "Ignoring Raspberry Pi specific node", - "version": "Version command failed", + "version": "Failed to get version from Pi", "sawpitype": "Saw Pi Type", "libnotfound": "Cannot find Pi RPi.GPIO python library", "alreadyset": "GPIO pin __pin__ already set as type: __type__", From 7dd329b5ee589f9477e7a2e838e3cdbb066b24a9 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Tue, 22 May 2018 17:26:52 +0100 Subject: [PATCH 004/208] Add basic loading tests for GPIO nodes --- nodes/core/hardware/36-rpi-gpio.js | 26 +++--- nodes/core/locales/en-US/messages.json | 2 +- test/nodes/core/hardware/36-rpi-gpio_spec.js | 90 ++++++++++++++++++++ 3 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 test/nodes/core/hardware/36-rpi-gpio_spec.js diff --git a/nodes/core/hardware/36-rpi-gpio.js b/nodes/core/hardware/36-rpi-gpio.js index 40c3d0f9b..0a21f578a 100644 --- a/nodes/core/hardware/36-rpi-gpio.js +++ b/nodes/core/hardware/36-rpi-gpio.js @@ -340,20 +340,22 @@ module.exports = function(RED) { RED.nodes.registerType("rpi-keyboard",PiKeyboardNode); var pitype = { type:"" }; - exec(gpioCommand+" info", function(err,stdout,stderr) { - if (err) { - RED.log.info(RED._("rpi-gpio.errors.version")); - } - else { - try { - var info = JSON.parse( stdout.trim().replace(/\'/g,"\"") ); - pitype.type = info["TYPE"]; + if (allOK === true) { + exec(gpioCommand+" info", function(err,stdout,stderr) { + if (err) { + RED.log.info(RED._("rpi-gpio.errors.version")); } - catch(e) { - RED.log.info(RED._("rpi-gpio.errors.sawpitype"),stdout.trim()); + else { + try { + var info = JSON.parse( stdout.trim().replace(/\'/g,"\"") ); + pitype.type = info["TYPE"]; + } + catch(e) { + RED.log.info(RED._("rpi-gpio.errors.sawpitype"),stdout.trim()); + } } - } - }); + }); + } RED.httpAdmin.get('/rpi-gpio/:id', RED.auth.needsPermission('rpi-gpio.read'), function(req,res) { res.json(pitype); diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index 15d66c9dc..d59f3d1bf 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -786,7 +786,7 @@ "na": "N/A : __value__" }, "errors": { - "ignorenode": "Ignoring Raspberry Pi specific node", + "ignorenode": "Raspberry Pi specific node set inactive", "version": "Failed to get version from Pi", "sawpitype": "Saw Pi Type", "libnotfound": "Cannot find Pi RPi.GPIO python library", diff --git a/test/nodes/core/hardware/36-rpi-gpio_spec.js b/test/nodes/core/hardware/36-rpi-gpio_spec.js new file mode 100644 index 000000000..3933154c0 --- /dev/null +++ b/test/nodes/core/hardware/36-rpi-gpio_spec.js @@ -0,0 +1,90 @@ +/** + * 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 should = require("should"); +var rpi = require("../../../../nodes/core/hardware/36-rpi-gpio.js"); +var helper = require("node-red-node-test-helper"); +var fs = require("fs"); + +describe('RPI GPIO Node', function() { + + before(function(done) { + helper.startServer(done); + }); + + after(function(done) { + helper.stopServer(done); + }); + + afterEach(function() { + helper.unload(); + }); + + var checkIgnore = function(done) { + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return ((evt[0].level == 30) && (evt[0].msg.indexOf("rpi-gpio")===0)); + }); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("rpi-gpio : rpi-gpio.errors.ignorenode"); + done(); + } catch(err) { + done(err); + } + },25); + } + + it('should load Input node', function(done) { + var flow = [{id:"n1", type:"rpi-gpio in", name:"rpi-gpio in" }]; + helper.load(rpi, flow, function() { + var n1 = helper.getNode("n1"); + n1.should.have.property('name', 'rpi-gpio in'); + try { + var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); + if (cpuinfo.indexOf(": BCM") === 1) { + done(); // It's ON a PI ... should really do more tests ! + } else { + checkIgnore(done); + } + } + catch(e) { + checkIgnore(done); + } + }); + }); + + it('should load Output node', function(done) { + var flow = [{id:"n1", type:"rpi-gpio out", name:"rpi-gpio out" }]; + helper.load(rpi, flow, function() { + var n1 = helper.getNode("n1"); + n1.should.have.property('name', 'rpi-gpio out'); + try { + var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString(); + if (cpuinfo.indexOf(": BCM") === 1) { + done(); // It's ON a PI ... should really do more tests ! + } else { + checkIgnore(done); + } + } + catch(e) { + checkIgnore(done); + } + }); + }); + +}); From 1d05b4c9814c656c2be9683d88eac4efae96eb41 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Wed, 23 May 2018 08:58:04 +0100 Subject: [PATCH 005/208] relax test spec slightly --- test/nodes/core/hardware/36-rpi-gpio_spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/nodes/core/hardware/36-rpi-gpio_spec.js b/test/nodes/core/hardware/36-rpi-gpio_spec.js index 3933154c0..c11ea254c 100644 --- a/test/nodes/core/hardware/36-rpi-gpio_spec.js +++ b/test/nodes/core/hardware/36-rpi-gpio_spec.js @@ -39,7 +39,6 @@ describe('RPI GPIO Node', function() { var logEvents = helper.log().args.filter(function(evt) { return ((evt[0].level == 30) && (evt[0].msg.indexOf("rpi-gpio")===0)); }); - logEvents.should.have.length(1); logEvents[0][0].should.have.a.property('msg'); logEvents[0][0].msg.toString().should.startWith("rpi-gpio : rpi-gpio.errors.ignorenode"); done(); From b204b183de8e85a1a0621a133041f7894ce38680 Mon Sep 17 00:00:00 2001 From: KatsuyaHoshii Date: Fri, 1 Jun 2018 14:33:20 +0900 Subject: [PATCH 006/208] Add logic nodes test cases --- test/nodes/core/logic/17-split_spec.js | 229 ++++++++++++++++++++++++- test/nodes/core/logic/18-sort_spec.js | 4 - 2 files changed, 228 insertions(+), 5 deletions(-) diff --git a/test/nodes/core/logic/17-split_spec.js b/test/nodes/core/logic/17-split_spec.js index dfc57fc3b..9fed82cd4 100644 --- a/test/nodes/core/logic/17-split_spec.js +++ b/test/nodes/core/logic/17-split_spec.js @@ -20,6 +20,8 @@ var joinNode = require("../../../../nodes/core/logic/17-split.js"); var helper = require("node-red-node-test-helper"); var RED = require("../../../../red/red.js"); +var TimeoutForErrorCase = 20; + describe('SPLIT node', function() { before(function(done) { @@ -264,6 +266,126 @@ describe('SPLIT node', function() { }); }); + it('should handle invalid spltType (not an array)', function (done) { + var flow = [{ id: "sn1", type: "split", splt: "1", spltType: "bin", wires: [["sn2"]] }, + { id: "sn2", type: "helper" }]; + helper.load(splitNode, flow, function () { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + setTimeout(function () { + done(); + }, TimeoutForErrorCase); + sn2.on("input", function (msg) { + done(new Error("This path does not go through.")); + }); + sn1.receive({ payload: "123" }); + }); + }); + + it('should handle invalid splt length', function (done) { + var flow = [{ id: "sn1", type: "split", splt: 0, spltType: "len", wires: [["sn2"]] }, + { id: "sn2", type: "helper" }]; + helper.load(splitNode, flow, function () { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + setTimeout(function () { + done(); + }, TimeoutForErrorCase); + sn2.on("input", function (msg) { + done(new Error("This path does not go through.")); + }); + sn1.receive({ payload: "123" }); + }); + }); + + it('should handle invalid array splt length', function (done) { + var flow = [{ id: "sn1", type: "split", arraySplt: 0, arraySpltType: "len", wires: [["sn2"]] }, + { id: "sn2", type: "helper" }]; + helper.load(splitNode, flow, function () { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + setTimeout(function () { + done(); + }, TimeoutForErrorCase); + sn2.on("input", function (msg) { + done(new Error("This path does not go through.")); + }); + sn1.receive({ payload: "123" }); + }); + }); + + it('should ceil count value when msg.payload type is string', function (done) { + var flow = [{ id: "sn1", type: "split", splt: "2", spltType: "len", wires: [["sn2"]] }, + { id: "sn2", type: "helper" }]; + helper.load(splitNode, flow, function () { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + sn2.on("input", function (msg) { + msg.should.have.property("parts"); + msg.parts.should.have.property("count", 2); + msg.parts.should.have.property("index"); + if (msg.parts.index === 0) { msg.payload.length.should.equal(2); } + if (msg.parts.index === 1) { msg.payload.length.should.equal(1); done(); } + }); + sn1.receive({ payload: "123" }); + }); + }); + + it('should handle spltBufferString value of undefined', function (done) { + var flow = [{ id: "sn1", type: "split", wires: [["sn2"]], splt: "[52]", spltType: "bin" }, + { id: "sn2", type: "helper" }]; + helper.load(splitNode, flow, function () { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + sn2.on("input", function (msg) { + try { + msg.should.have.property("parts"); + msg.parts.should.have.property("index"); + if (msg.parts.index === 0) { msg.payload.toString().should.equal("123"); done(); } + } catch (err) { + done(err); + } + }); + sn1.receive({ payload: "123" }); + }); + }); + + it('should ceil count value when msg.payload type is Buffer', function (done) { + var flow = [{ id: "sn1", type: "split", splt: "2", spltType: "len", wires: [["sn2"]] }, + { id: "sn2", type: "helper" }]; + helper.load(splitNode, flow, function () { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + sn2.on("input", function (msg) { + msg.should.have.property("parts"); + msg.parts.should.have.property("count", 2); + msg.parts.should.have.property("index"); + if (msg.parts.index === 0) { msg.payload.length.should.equal(2); } + if (msg.parts.index === 1) { msg.payload.length.should.equal(1); done(); } + }); + var b = new Buffer.from("123"); + sn1.receive({ payload: b }); + }); + }); + + it('should set msg.parts.ch when node.spltType is str', function (done) { + var flow = [{ id: "sn1", type: "split", splt: "2", spltType: "str", stream: false, wires: [["sn2"]] }, + { id: "sn2", type: "helper" }]; + helper.load(splitNode, flow, function () { + var sn1 = helper.getNode("sn1"); + var sn2 = helper.getNode("sn2"); + sn2.on("input", function (msg) { + msg.should.have.property("parts"); + msg.parts.should.have.property("count", 2); + msg.parts.should.have.property("index"); + if (msg.parts.index === 0) { msg.payload.length.should.equal(2); } + if (msg.parts.index === 1) { msg.payload.length.should.equal(1); done(); } + }); + var b = new Buffer.from("123"); + sn1.receive({ payload: b }); + }); + }); + }); describe('JOIN node', function() { @@ -942,7 +1064,7 @@ describe('JOIN node', function() { evt.should.have.property('type', "join"); evt.should.have.property('msg', "join.too-many"); done(); - }, 150); + }, TimeoutForErrorCase); n1.receive({payload:3, parts:{index:2, count:4, id:222}}); n1.receive({payload:2, parts:{index:1, count:4, id:222}}); n1.receive({payload:4, parts:{index:3, count:4, id:222}}); @@ -950,4 +1072,109 @@ describe('JOIN node', function() { }); }); + it('should handle invalid JSON expression"', function (done) { + var flow = [{ + id: "n1", type: "join", mode: "reduce", + reduceRight: false, + reduceExp: "invalid expr", + reduceInit: "0", + reduceInitType: "num", + reduceFixup: undefined, + wires: [["n2"]] + }, + { id: "n2", type: "helper" }]; + helper.load(joinNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + setTimeout(function () { + done(); + }, TimeoutForErrorCase); + n2.on("input", function (msg) { + done(new Error("This path does not go through.")); + }); + n1.receive({ payload: "A", parts: { id: 1, type: "string", ch: ",", index: 0, count: 1 } }); + }); + }); + + it('should concat payload when group.type is array', function (done) { + var flow = [{ id: "n1", type: "join", wires: [["n2"]], build: "array", mode: "auto" }, + { id: "n2", type: "helper" }]; + helper.load(joinNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload"); + msg.payload.should.be.an.Array(); + msg.payload[0].should.equal("ab"); + msg.payload[1].should.equal("cd"); + msg.payload[2].should.equal("ef"); + done(); + } + catch (e) { done(e); } + }); + n1.receive({ payload: "ab", parts: { id: 1, type: "array", ch: ",", index: 0, count: 3, len:2}}); + n1.receive({ payload: "cd", parts: { id: 1, type: "array", ch: ",", index: 1, count: 3, len:2}}); + n1.receive({ payload: "ef", parts: { id: 1, type: "array", ch: ",", index: 2, count: 3, len:2}}); + }); + }); + + it('should concat payload when group.type is buffer and group.joinChar is undefined', function (done) { + var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: ",", build: "buffer", mode: "auto" }, + { id: "n2", type: "helper" }]; + helper.load(joinNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload"); + Buffer.isBuffer(msg.payload).should.be.true(); + msg.payload.toString().should.equal("ABC"); + done(); + } + catch (e) { done(e); } + }); + n1.receive({ payload: Buffer.from("A"), parts: { id: 1, type: "buffer", index: 0, count: 3 } }); + n1.receive({ payload: Buffer.from("B"), parts: { id: 1, type: "buffer", index: 1, count: 3 } }); + n1.receive({ payload: Buffer.from("C"), parts: { id: 1, type: "buffer", index: 2, count: 3 } }); + }); + }); + + it('should concat payload when group.type is string and group.joinChar is not string', function (done) { + var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: ",", build: "buffer", mode: "auto" }, + { id: "n2", type: "helper" }]; + helper.load(joinNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload"); + msg.payload.toString().should.equal("A0B0C"); + done(); + } + catch (e) { done(e); } + }); + n1.receive({ payload: Buffer.from("A"), parts: { id: 1, type: "string", ch: Buffer.from("0"), index: 0, count: 3 } }); + n1.receive({ payload: Buffer.from("B"), parts: { id: 1, type: "string", ch: Buffer.from("0"), index: 1, count: 3 } }); + n1.receive({ payload: Buffer.from("C"), parts: { id: 1, type: "string", ch: Buffer.from("0"), index: 2, count: 3 } }); + }); + }); + + it('should handle msg.parts property when mode is auto and parts or id are missing', function (done) { + var flow = [{ id: "n1", type: "join", wires: [["n2"]], joiner: "[44]", joinerType: "bin", build: "string", mode: "auto" }, + { id: "n2", type: "helper" }]; + helper.load(joinNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + done(new Error("This path does not go through.")); + }); + n1.receive({ payload: "A", parts: { type: "string", ch: ",", index: 0, count: 2 } }); + n1.receive({ payload: "B", parts: { type: "string", ch: ",", index: 1, count: 2 } }); + setTimeout(function () { + done(); + }, TimeoutForErrorCase); + }); + }); + }); diff --git a/test/nodes/core/logic/18-sort_spec.js b/test/nodes/core/logic/18-sort_spec.js index a585d7dd5..e92974523 100644 --- a/test/nodes/core/logic/18-sort_spec.js +++ b/test/nodes/core/logic/18-sort_spec.js @@ -213,8 +213,6 @@ describe('SORT node', function() { check_sort1C(flow, "$substring(payload,1)", data_in, data_out, done); }); })(); - - return; (function() { var flow = [{id:"n1", type:"sort", order:"descending", as_num:false, wires:[["n2"]]}, @@ -250,8 +248,6 @@ describe('SORT node', function() { n1.receive(msg1); }); }); - - return; it('should handle too many pending messages', function(done) { var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, target:"payload", targetType:"seq", seqKey:"payload", seqKeyType:"msg", wires:[["n2"]]}, From dcf44fed5820d76611200492159f21d1c549773a Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Tue, 5 Jun 2018 20:18:40 +0900 Subject: [PATCH 007/208] allow multi-line category name in editor --- editor/sass/palette.scss | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/editor/sass/palette.scss b/editor/sass/palette.scss index 34890b8b5..10fd60cad 100644 --- a/editor/sass/palette.scss +++ b/editor/sass/palette.scss @@ -90,13 +90,12 @@ text-align: left; padding: 9px; font-weight: bold; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - + padding-left: 30px; } .palette-header > i { - margin: 3px 10px 3px 3px; + position: absolute; + left: 11px; + top: 12px; -webkit-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out; From 17c5fdf0d502a76497e4f907320108c50c1fae4d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 8 Jun 2018 23:32:17 +0100 Subject: [PATCH 008/208] Add flow navigator widget --- Gruntfile.js | 1 + editor/js/ui/view-navigator.js | 164 +++++++++++++++++++++++++++++++++ editor/js/ui/view.js | 11 ++- editor/sass/mixins.scss | 3 +- editor/sass/workspace.scss | 4 +- editor/templates/index.mst | 1 + 6 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 editor/js/ui/view-navigator.js diff --git a/Gruntfile.js b/Gruntfile.js index 3738e60b4..6d53e4d29 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -144,6 +144,7 @@ module.exports = function(grunt) { "editor/js/ui/keyboard.js", "editor/js/ui/workspaces.js", "editor/js/ui/view.js", + "editor/js/ui/view-navigator.js", "editor/js/ui/sidebar.js", "editor/js/ui/palette.js", "editor/js/ui/tab-info.js", diff --git a/editor/js/ui/view-navigator.js b/editor/js/ui/view-navigator.js new file mode 100644 index 000000000..da1908982 --- /dev/null +++ b/editor/js/ui/view-navigator.js @@ -0,0 +1,164 @@ +/** + * Copyright 2016 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + + RED.view.navigator = (function() { + + var nav_scale = 25; + var nav_width = 5000/nav_scale; + var nav_height = 5000/nav_scale; + + var navContainer; + var navBox; + var navBorder; + var navVis; + var scrollPos; + var scaleFactor; + var chartSize; + var dimensions; + var isDragging; + var isShowing = false; + + function refreshNodes() { + if (!isShowing) { + return; + } + var navNode = navVis.selectAll(".navnode").data(RED.view.getActiveNodes(),function(d){return d.id}); + navNode.exit().remove(); + navNode.enter().insert("rect") + .attr('class','navnode') + .attr("pointer-events", "none"); + navNode.each(function(d) { + d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale }) + .attr("y",function(d) { return (d.y-d.h/2)/nav_scale }) + .attr("width",function(d) { return Math.max(9,d.w/nav_scale) }) + .attr("height",function(d) { return Math.max(3,d.h/nav_scale) }) + .attr("fill",function(d) { return d._def.color;}) + }); + } + function onScroll() { + if (!isDragging) { + resizeNavBorder(); + } + } + function resizeNavBorder() { + if (navBorder) { + scaleFactor = RED.view.scale(); + chartSize = [ $("#chart").width(), $("#chart").height()]; + scrollPos = [$("#chart").scrollLeft(),$("#chart").scrollTop()]; + navBorder.attr('x',scrollPos[0]/nav_scale) + .attr('y',scrollPos[1]/nav_scale) + .attr('width',chartSize[0]/nav_scale/scaleFactor) + .attr('height',chartSize[1]/nav_scale/scaleFactor) + } + } + + return { + init: function() { + + $(window).resize(resizeNavBorder); + RED.events.on("sidebar:resize",resizeNavBorder); + + var hideTimeout; + + navContainer = $('
').css({ + "position":"absolute", + "bottom":$("#workspace-footer").height(), + "right":0, + zIndex: 1 + }).appendTo("#workspace").hide(); + + navBox = d3.select(navContainer[0]) + .append("svg:svg") + .attr("width", nav_width) + .attr("height", nav_height) + .attr("pointer-events", "all") + .style({ + position: "absolute", + bottom: 0, + right:0, + zIndex: 101, + "border-left": "1px solid #ccc", + "border-top": "1px solid #ccc", + background: "rgba(245,245,245,0.5)", + "box-shadow": "-1px 0 3px rgba(0,0,0,0.1)" + }); + + navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({ + fill:"none", + stroke:"none", + pointerEvents:"all" + }).on("mousedown", function() { + // Update these in case they have changed + scaleFactor = RED.view.scale(); + chartSize = [ $("#chart").width(), $("#chart").height()]; + dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor]; + var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]); + var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]); + navBorder.attr('x',newX).attr('y',newY); + isDragging = true; + $("#chart").scrollLeft(newX*nav_scale*scaleFactor); + $("#chart").scrollTop(newY*nav_scale*scaleFactor); + }).on("mousemove", function() { + if (!isDragging) { return } + if (d3.event.buttons === 0) { + isDragging = false; + return; + } + var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]); + var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]); + navBorder.attr('x',newX).attr('y',newY); + $("#chart").scrollLeft(newX*nav_scale*scaleFactor); + $("#chart").scrollTop(newY*nav_scale*scaleFactor); + }).on("mouseup", function() { + isDragging = false; + }) + + navBorder = navBox.append("rect") + .attr("stroke-dasharray","5,5") + .attr("pointer-events", "none") + .style({ + stroke: "#999", + strokeWidth: 1, + fill: "white", + }); + + navVis = navBox.append("svg:g") + + + $("#btn-navigate").click(function(evt) { + evt.preventDefault(); + if (!isShowing) { + isShowing = true; + $("#btn-navigate").addClass("selected"); + resizeNavBorder(); + refreshNodes(); + $("#chart").on("scroll",onScroll); + navContainer.fadeIn(200); + } else { + isShowing = false; + navContainer.fadeOut(100); + $("#chart").off("scroll",onScroll); + $("#btn-navigate").removeClass("selected"); + } + }) + }, + refresh: refreshNodes, + resize: resizeNavBorder + } + + +})(); diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index d6c1bfa8c..6a8dd77c4 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -332,6 +332,8 @@ RED.view = (function() { redraw(); }); + RED.view.navigator.init(); + $("#btn-zoom-out").click(function() {zoomOut();}); $("#btn-zoom-zero").click(function() {zoomZero();}); $("#btn-zoom-in").click(function() {zoomIn();}); @@ -1060,17 +1062,20 @@ RED.view = (function() { function zoomIn() { if (scaleFactor < 2) { scaleFactor += 0.1; + RED.view.navigator.resize(); redraw(); } } function zoomOut() { if (scaleFactor > 0.3) { scaleFactor -= 0.1; + RED.view.navigator.resize(); redraw(); } } function zoomZero() { scaleFactor = 1; + RED.view.navigator.resize(); redraw(); } @@ -2542,7 +2547,7 @@ RED.view = (function() { } ).classed("link_selected", false); } - + RED.view.navigator.refresh(); if (d3.event) { d3.event.preventDefault(); } @@ -2816,7 +2821,9 @@ RED.view = (function() { gridSize = Math.max(5,v); updateGrid(); } + }, + getActiveNodes: function() { + return activeNodes; } - }; })(); diff --git a/editor/sass/mixins.scss b/editor/sass/mixins.scss index 7201ef544..36d08c948 100644 --- a/editor/sass/mixins.scss +++ b/editor/sass/mixins.scss @@ -203,7 +203,7 @@ height: 25px; line-height: 23px; padding: 0 10px; - + user-select: none; .button-group:not(:last-child) { margin-right: 5px; @@ -227,6 +227,7 @@ font-size: 11px; line-height: 17px; height: 18px; + width: 18px; &.text-button { width: auto; padding: 0 5px; diff --git a/editor/sass/workspace.scss b/editor/sass/workspace.scss index 59b8fcf75..14b6eebb0 100644 --- a/editor/sass/workspace.scss +++ b/editor/sass/workspace.scss @@ -47,7 +47,9 @@ .workspace-footer-button { @include component-footer-button; } - +.workspace-footer-button-toggle { + @include component-footer-button-toggle; +} #workspace-footer { @include component-footer; } diff --git a/editor/templates/index.mst b/editor/templates/index.mst index b8d0ac0c0..972258a55 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -51,6 +51,7 @@ +
From 2a122ed28358cc34130b8f9ab4338b364e007ac0 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 12 Jun 2018 12:54:32 +0100 Subject: [PATCH 009/208] Allow subflows to be put in any palette category --- editor/js/nodes.js | 5 +- editor/js/ui/editor.js | 44 +++++++++++++- editor/js/ui/palette.js | 75 +++++++++++++++++------- editor/js/ui/tab-info.js | 7 +++ editor/templates/index.mst | 4 ++ red/api/editor/locales/en-US/editor.json | 2 + 6 files changed, 113 insertions(+), 24 deletions(-) diff --git a/editor/js/nodes.js b/editor/js/nodes.js index 433e400a2..12ff3e682 100644 --- a/editor/js/nodes.js +++ b/editor/js/nodes.js @@ -133,7 +133,7 @@ RED.nodes = (function() { registerNodeType: function(nt,def) { nodeDefinitions[nt] = def; def.type = nt; - if (def.category != "subflows") { + if (nt.substring(0,8) != "subflow:") { def.set = nodeSets[typeToId[nt]]; nodeSets[typeToId[nt]].added = true; nodeSets[typeToId[nt]].enabled = true; @@ -356,7 +356,7 @@ RED.nodes = (function() { defaults:{name:{value:""}}, info: sf.info, icon: function() { return sf.icon||"subflow.png" }, - category: "subflows", + category: sf.category || "subflows", inputs: sf.in.length, outputs: sf.out.length, color: "#da9", @@ -519,6 +519,7 @@ RED.nodes = (function() { node.type = n.type; node.name = n.name; node.info = n.info; + node.category = n.category; node.in = []; node.out = []; diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index df50db725..380521333 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -1701,6 +1701,21 @@ RED.editor = (function() { editing_node.icon = icon; changed = true; } + var newCategory = $("#subflow-input-category").val().trim(); + if (newCategory === "_custom_") { + newCategory = $("#subflow-input-custom-category").val().trim(); + if (newCategory === "") { + newCategory = editing_node.category; + } + } + if (newCategory === 'subflows') { + newCategory = ''; + } + if (newCategory != editing_node.category) { + changes['category'] = editing_node.category; + editing_node.category = newCategory; + changed = true; + } RED.palette.refresh(); @@ -1773,8 +1788,6 @@ RED.editor = (function() { }); portLabels.content.addClass("editor-tray-content"); - - if (editing_node) { RED.sidebar.info.refresh(editing_node); } @@ -1787,6 +1800,33 @@ RED.editor = (function() { $("#subflow-input-name").val(subflow.name); RED.text.bidi.prepareInput($("#subflow-input-name")); + + $("#subflow-input-category").empty(); + var categories = RED.palette.getCategories(); + categories.sort(function(A,B) { + return A.label.localeCompare(B.label); + }) + categories.forEach(function(cat) { + $("#subflow-input-category").append($("").val(cat.id).text(cat.label)); + }) + $("#subflow-input-category").append($("").attr('disabled',true).text("---")); + $("#subflow-input-category").append($("").val("_custom_").text(RED._("palette.addCategory"))); + + + $("#subflow-input-category").change(function() { + var val = $(this).val(); + if (val === "_custom_") { + $("#subflow-input-category").width(120); + $("#subflow-input-custom-category").show(); + } else { + $("#subflow-input-category").width(250); + $("#subflow-input-custom-category").hide(); + } + }) + + + $("#subflow-input-category").val(subflow.category||"subflows"); + subflowEditor.getSession().setValue(subflow.info||"",-1); var userCount = 0; var subflowType = "subflow:"+editing_node.id; diff --git a/editor/js/ui/palette.js b/editor/js/ui/palette.js index 38ebc1091..c72ed4f7a 100644 --- a/editor/js/ui/palette.js +++ b/editor/js/ui/palette.js @@ -21,7 +21,18 @@ RED.palette = (function() { var categoryContainers = {}; - function createCategoryContainer(category, label) { + + function createCategory(originalCategory,rootCategory,category,ns) { + if ($("#palette-base-category-"+rootCategory).length === 0) { + createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory); + } + $("#palette-container-"+rootCategory).show(); + if ($("#palette-"+category).length === 0) { + $("#palette-base-category-"+rootCategory).append('
'); + } + } + function createCategoryContainer(originalCategory,category, labelId) { + var label = RED._(labelId, {defaultValue:category}); label = (label || category).replace(/_/g, " "); var catDiv = $('
'+ '
'+label+'
'+ @@ -31,7 +42,8 @@ RED.palette = (function() { '
'+ '
'+ '').appendTo("#palette-container"); - + catDiv.data('category',originalCategory); + catDiv.data('label',label); categoryContainers[category] = { container: catDiv, close: function() { @@ -133,6 +145,7 @@ RED.palette = (function() { } if (exclusion.indexOf(def.category)===-1) { + var originalCategory = def.category; var category = def.category.replace(/ /g,"_"); var rootCategory = category.split("-")[0]; @@ -153,7 +166,6 @@ RED.palette = (function() { d.className="palette_node"; - if (def.icon) { var icon_url = RED.utils.getNodeIcon(def); var iconContainer = $('
',{class:"palette_icon_container"+(def.align=="right"?" palette_icon_container_right":"")}).appendTo(d); @@ -174,21 +186,12 @@ RED.palette = (function() { d.appendChild(portIn); } - if ($("#palette-base-category-"+rootCategory).length === 0) { - if(coreCategories.indexOf(rootCategory) !== -1){ - createCategoryContainer(rootCategory, RED._("node-red:palette.label."+rootCategory, {defaultValue:rootCategory})); - } else { - var ns = def.set.id; - createCategoryContainer(rootCategory, RED._(ns+":palette.label."+rootCategory, {defaultValue:rootCategory})); - } - } - $("#palette-container-"+rootCategory).show(); - - if ($("#palette-"+category).length === 0) { - $("#palette-base-category-"+rootCategory).append('
'); - } + createCategory(def.category,rootCategory,category,(coreCategories.indexOf(rootCategory) !== -1)?"node-red":def.set.id); $("#palette-"+category).append(d); + + $(d).data('category',rootCategory); + d.onmousedown = function(e) { e.preventDefault(); }; var popover = RED.popover.create({ @@ -308,7 +311,7 @@ RED.palette = (function() { }); var nodeInfo = null; - if (def.category == "subflows") { + if (nt.indexOf("subflow:") === 0) { $(d).dblclick(function(e) { RED.workspaces.show(nt.substring(8)); e.preventDefault(); @@ -382,6 +385,31 @@ RED.palette = (function() { } setLabel(sf.type+":"+sf.id,paletteNode,sf.name,marked(sf.info||"")); setIcon(paletteNode,sf); + + var currentCategory = paletteNode.data('category'); + var newCategory = (sf.category||"subflows"); + if (currentCategory !== newCategory) { + var category = newCategory.replace(/ /g,"_"); + createCategory(newCategory,category,category,"node-red"); + + var currentCategoryNode = paletteNode.closest(".palette-category"); + var newCategoryNode = $("#palette-"+category); + newCategoryNode.append(paletteNode); + if (newCategoryNode.find(".palette_node").length === 1) { + categoryContainers[category].open(); + } + + paletteNode.data('category',newCategory); + if (currentCategoryNode.find(".palette_node").length === 0) { + if (currentCategoryNode.find("i").hasClass("expanded")) { + currentCategoryNode.find(".palette-content").slideToggle(); + currentCategoryNode.find("i").toggleClass("expanded"); + } + } + + + + } }); } @@ -471,7 +499,7 @@ RED.palette = (function() { categoryList = coreCategories } categoryList.forEach(function(category){ - createCategoryContainer(category, RED._("palette.label."+category,{defaultValue:category})); + createCategoryContainer(category, category, "palette.label."+category); }); $("#palette-collapse-all").on("click", function(e) { @@ -491,13 +519,20 @@ RED.palette = (function() { } }); } - + function getCategories() { + var categories = []; + $("#palette-container .palette-category").each(function(i,d) { + categories.push({id:$(d).data('category'),label:$(d).data('label')}); + }) + return categories; + } return { init: init, add:addNodeType, remove:removeNodeType, hide:hideNodeType, show:showNodeType, - refresh:refreshNodeTypes + refresh:refreshNodeTypes, + getCategories: getCategories }; })(); diff --git a/editor/js/ui/tab-info.js b/editor/js/ui/tab-info.js index 340a6bd74..bb1bf37cb 100644 --- a/editor/js/ui/tab-info.js +++ b/editor/js/ui/tab-info.js @@ -170,6 +170,10 @@ RED.sidebar.info = (function() { if (node.type === "tab") { propRow = $(''+RED._("sidebar.info.status")+'').appendTo(tableBody); $(propRow.children()[1]).text((!!!node.disabled)?RED._("sidebar.info.enabled"):RED._("sidebar.info.disabled")) + } else if (node.type === "subflow") { + propRow = $(''+RED._("subflow.category")+'').appendTo(tableBody); + var category = node.category||"subflows"; + $(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category})) } } else { propRow = $(''+RED._("sidebar.info.node")+"").appendTo(tableBody); @@ -235,6 +239,9 @@ RED.sidebar.info = (function() { } } if (m) { + propRow = $(''+RED._("subflow.category")+'').appendTo(tableBody); + var category = subflowNode.category||"subflows"; + $(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category})) $(''+RED._("sidebar.info.instances")+""+subflowUserCount+'').appendTo(tableBody); } diff --git a/editor/templates/index.mst b/editor/templates/index.mst index 972258a55..23a87fde7 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -132,6 +132,10 @@
+
+ + +
diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json index af62248da..65681850f 100644 --- a/red/api/editor/locales/en-US/editor.json +++ b/red/api/editor/locales/en-US/editor.json @@ -240,6 +240,7 @@ "output": "outputs:", "deleteSubflow": "delete subflow", "info": "Description", + "category": "Category", "format":"markdown format", "errors": { "noNodesSelected": "Cannot create subflow: no nodes selected", @@ -318,6 +319,7 @@ "noInfo": "no information available", "filter": "filter nodes", "search": "search modules", + "addCategory": "Add new...", "label": { "subflows": "subflows", "input": "input", From 68779caa2e83013b83c74335b8d5b44afcb4519a Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 12 Jun 2018 15:34:08 +0100 Subject: [PATCH 010/208] Only edit nodes on dbl click on primary button with no modifiers --- editor/js/ui/view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js index 6a8dd77c4..71454153d 100644 --- a/editor/js/ui/view.js +++ b/editor/js/ui/view.js @@ -1697,7 +1697,9 @@ RED.view = (function() { clickElapsed = now-clickTime; clickTime = now; - dblClickPrimed = (lastClickNode == mousedown_node); + dblClickPrimed = (lastClickNode == mousedown_node && + d3.event.buttons === 1 && + !d3.event.shiftKey && !d3.event.metaKey && !d3.event.altKey && !d3.event.ctrlKey); lastClickNode = mousedown_node; var i; From 6cad80c4ad3fe913c6d6107470652214f61fa209 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 12 Jun 2018 23:46:06 +0100 Subject: [PATCH 011/208] Add node icon picker widget --- editor/js/ui/editor.js | 227 +++++++++++++++++++++------------------- editor/sass/editor.scss | 61 +++++++++++ 2 files changed, 179 insertions(+), 109 deletions(-) diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index 380521333..0f6050b8a 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -108,17 +108,6 @@ RED.editor = (function() { } } } - if (node.icon) { - var iconPath = RED.utils.separateIconPath(node.icon); - if (!iconPath.module) { - return isValid; - } - var iconSets = RED.nodes.getIconSets(); - var iconFileList = iconSets[iconPath.module]; - if (!iconFileList || iconFileList.indexOf(iconPath.file) === -1) { - isValid = false; - } - } return isValid; } @@ -170,27 +159,6 @@ RED.editor = (function() { } } } - validateIcon(node); - } - - function validateIcon(node) { - if (node._def.hasOwnProperty("defaults") && !node._def.defaults.hasOwnProperty("icon") && node.icon) { - var iconPath = RED.utils.separateIconPath(node.icon); - var iconSets = RED.nodes.getIconSets(); - var iconFileList = iconSets[iconPath.module]; - var iconModule = $("#node-settings-icon-module"); - var iconFile = $("#node-settings-icon-file"); - if (!iconFileList) { - iconModule.addClass("input-error"); - iconFile.removeClass("input-error"); - } else if (iconFileList.indexOf(iconPath.file) === -1) { - iconModule.removeClass("input-error"); - iconFile.addClass("input-error"); - } else { - iconModule.removeClass("input-error"); - iconFile.removeClass("input-error"); - } - } } function validateNodeEditorProperty(node,defaults,property,prefix) { @@ -711,6 +679,97 @@ RED.editor = (function() { } return result; } + function showIconPicker(container, node, iconPath, done) { + var containerPos = container.offset(); + var pickerBackground = $('
').css({ + position: "absolute",top:0,bottom:0,left:0,right:0,zIndex:20 + }).appendTo("body"); + + var top = containerPos.top - 30; + + if (top+280 > $( window ).height()) { + top = $( window ).height() - 280; + } + var picker = $('
').css({ + top: top+"px", + left: containerPos.left+"px", + }).appendTo("body"); + + var hide = function() { + pickerBackground.remove(); + picker.remove(); + RED.keyboard.remove("escape"); + } + RED.keyboard.add("*","escape",function(){hide()}); + pickerBackground.on("mousedown", hide); + + var searchDiv = $("
",{class:"red-ui-search-container"}).appendTo(picker); + searchInput = $('').attr("placeholder","Search icons").appendTo(searchDiv).searchBox({ + delay: 50, + change: function() { + var searchTerm = $(this).val().trim(); + if (searchTerm === "") { + iconList.find(".red-ui-icon-list-module").show(); + iconList.find(".red-ui-icon-list-icon").show(); + } else { + iconList.find(".red-ui-icon-list-module").hide(); + iconList.find(".red-ui-icon-list-icon").each(function(i,n) { + if ($(n).data('icon').indexOf(searchTerm) === -1) { + $(n).hide(); + } else { + $(n).show(); + } + }); + } + } + }); + + var row = $('
').appendTo(picker); + var iconList = $('
').appendTo(picker); + var metaRow = $('
').appendTo(picker); + var summary = $('').appendTo(metaRow); + var resetButton = $('').appendTo(metaRow).click(function(e) { + e.preventDefault(); + hide(); + done(null); + }); + var iconSets = RED.nodes.getIconSets(); + Object.keys(iconSets).forEach(function(moduleName) { + var icons = iconSets[moduleName]; + if (icons.length > 0) { + // selectIconModule.append($("").val(moduleName).text(moduleName)); + var header = $('
').text(moduleName).appendTo(iconList); + $('').prependTo(header); + icons.forEach(function(icon) { + var iconDiv = $('
',{class:"red-ui-icon-list-icon"}).appendTo(iconList); + var nodeDiv = $('
',{class:"red-ui-search-result-node"}).appendTo(iconDiv); + var colour = node._def.color; + var icon_url = "icons/"+moduleName+"/"+icon; + iconDiv.data('icon',icon_url) + nodeDiv.css('backgroundColor',colour); + var iconContainer = $('
',{class:"palette_icon_container"}).appendTo(nodeDiv); + $('
',{class:"palette_icon",style:"background-image: url("+icon_url+")"}).appendTo(iconContainer); + + if (iconPath.module === moduleName && iconPath.file === icon) { + iconDiv.addClass("selected"); + } + iconDiv.on("mouseover", function() { + summary.text(icon); + }) + iconDiv.on("mouseout", function() { + summary.html(" "); + }) + iconDiv.click(function() { + hide(); + done(moduleName+"/"+icon); + }) + }) + } + }); + picker.slideDown(100); + searchInput.focus(); + } + function buildLabelForm(container,node) { var dialogForm = $('
').appendTo(container); @@ -748,81 +807,36 @@ RED.editor = (function() { } if ((!node._def.defaults || !node._def.defaults.hasOwnProperty("icon"))) { - $('
').appendTo(dialogForm); - var iconDiv = $("#node-settings-icon"); - $('