diff --git a/public/index.html b/public/index.html
index f10b69a44..fe08b753e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -32,14 +32,13 @@
diff --git a/public/red/main.js b/public/red/main.js
index e77c684df..aa60f1f41 100644
--- a/public/red/main.js
+++ b/public/red/main.js
@@ -15,6 +15,14 @@
**/
var RED = (function() {
+ var deploymentTypes = {
+ "full":"Deploy",
+ "nodes":"Deploy changed nodes",
+ "flows":"Deploy changed flows"
+ }
+ var deploymentType = "full";
+
+
function hideDropTarget() {
$("#dropTarget").hide();
RED.keyboard.remove(/* ESCAPE */ 27);
@@ -82,7 +90,10 @@ var RED = (function() {
url:"flows",
type: "POST",
data: JSON.stringify(nns),
- contentType: "application/json; charset=utf-8"
+ contentType: "application/json; charset=utf-8",
+ headers: {
+ "Node-RED-Deployment-Type":deploymentType
+ }
}).done(function(data,textStatus,xhr) {
RED.notify("Successfully deployed","success");
RED.nodes.eachNode(function(node) {
@@ -280,6 +291,12 @@ var RED = (function() {
dialog.modal();
}
+
+ function changeDeploymentType(type) {
+ deploymentType = type;
+ $("#btn-deploy span").text(deploymentTypes[type]);
+ }
+
$(function() {
RED.menu.init({id:"btn-sidemenu",
options: [
@@ -312,6 +329,15 @@ var RED = (function() {
{id:"btn-help",icon:"fa fa-question",label:"Help...", href:"http://nodered.org/docs"}
]
});
+
+ //RED.menu.init({id:"btn-deploy-options",
+ // options: [
+ // {id:"btn-deploy-select",label:"Select deployment type"},
+ // {id:"btn-deploy-full",icon:null,label:"Full deploy",tip:"Deploys all nodes",onselect:function() { changeDeploymentType("full")}},
+ // {id:"btn-deploy-node",icon:null,label:"Deploy changed nodes",tip:"Deploys all nodes that have been changed",onselect:function() { changeDeploymentType("nodes")}},
+ // {id:"btn-deploy-flow",icon:null,label:"Deploy changed flows",tip:"Deploys all nodes in flows that contain changes",onselect:function() { changeDeploymentType("flows")}}
+ // ]
+ //});
RED.keyboard.add(/* ? */ 191,{shift:true},function(){showHelp();d3.event.preventDefault();});
loadSettings();
diff --git a/public/red/nodes.js b/public/red/nodes.js
index e0841bc87..aaab94243 100644
--- a/public/red/nodes.js
+++ b/public/red/nodes.js
@@ -394,7 +394,7 @@ RED.nodes = (function() {
var wires = links.filter(function(d) { return d.source === p });
for (var i=0;i
');
var link = $(''+
(opt.toggle?'':'')+
- (opt.icon?' ':'')+
- opt.label+
+ (opt.icon!==undefined?' ':"")+
+ ''+
'').appendTo(item);
menuItems[opt.id] = opt;
@@ -67,6 +67,11 @@ RED.menu = (function() {
setState();
} else if (opt.href) {
link.attr("target","_blank").attr("href",opt.href);
+ } else if (!opt.options) {
+ item.addClass("disabled");
+ link.click(function(event) {
+ event.preventDefault();
+ });
}
if (opt.options) {
item.addClass("dropdown-submenu pull-left");
@@ -79,6 +84,17 @@ RED.menu = (function() {
if (opt.disabled) {
item.addClass("disabled");
}
+ if (opt.tip) {
+ item.popover({
+ placement:"left",
+ trigger: "hover",
+ delay: { show: 350, hide: 20 },
+ html: true,
+ container:'body',
+ content: opt.tip
+ });
+ }
+
}
@@ -88,8 +104,8 @@ RED.menu = (function() {
function createMenu(options) {
var button = $("#"+options.id);
-
- var topMenu = $("",{class:"dropdown-menu"}).insertAfter(button);
+
+ var topMenu = $("",{id:options.id+"-submenu", class:"dropdown-menu pull-right"}).insertAfter(button);
for (var i=0;i li {
+ display: inline-block;
+ padding: 0;
+ margin: 0;
+ position: relative;
+}
+
.button {
-webkit-user-select: none;
-khtml-user-select: none;
@@ -83,6 +96,8 @@ span.logo img {
}
#header .button {
+ min-width: 20px;
+ text-align: center;
line-height: 22px;
display: inline-block;
font-size: 14px;
@@ -90,7 +105,7 @@ span.logo img {
text-decoration: none;
border-radius: 3px;
color: #ccc;
- margin: auto 10px;
+ margin: auto 5px;
vertical-align: middle;
}
#header .button:not(.disabled):hover {
diff --git a/red/api/flows.js b/red/api/flows.js
index 9be6763b0..9b43e20c0 100644
--- a/red/api/flows.js
+++ b/red/api/flows.js
@@ -28,11 +28,13 @@ module.exports = {
},
post: function(req,res) {
var flows = req.body;
- redNodes.setFlows(flows).then(function() {
+ var deploymentType = req.get("Node-RED-Deployment-Type")||"full";
+ redNodes.setFlows(flows,deploymentType).then(function() {
res.send(204);
}).otherwise(function(err) {
util.log("[red] Error saving flows : "+err);
res.send(500,err.message);
+ console.log(err.stack);
});
}
}
diff --git a/red/nodes/Flow.js b/red/nodes/Flow.js
new file mode 100644
index 000000000..57e23184e
--- /dev/null
+++ b/red/nodes/Flow.js
@@ -0,0 +1,638 @@
+/**
+ * Copyright 2014 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.
+ **/
+
+var util = require("util");
+var when = require("when");
+var clone = require("clone");
+
+var typeRegistry = require("./registry");
+var credentials = require("./credentials");
+var redUtil = require("../util");
+var events = require("../events");
+
+function getID() {
+ return (1+Math.random()*4294967295).toString(16);
+}
+
+function createNode(type,config) {
+ var nn = null;
+ var nt = typeRegistry.get(type);
+ if (nt) {
+ try {
+ nn = new nt(clone(config));
+ }
+ catch (err) {
+ util.log("[red] "+type+" : "+err);
+ }
+ } else {
+ util.log("[red] unknown type: "+type);
+ }
+ return nn;
+}
+
+
+function createSubflow(sf,sfn,subflows) {
+ //console.log("CREATE SUBFLOW",sf.config.id,sfn.id);
+ var nodes = [];
+ var node_map = {};
+ var newNodes = [];
+ var node;
+ var wires;
+ var i,j,k;
+
+ // Clone all of the subflow node definitions and give them new IDs
+ for (i=0;i 0) {
+ throw new Error("missing types");
+ }
+
+ events.emit("nodes-starting");
+
+ for (var id in this.nodes) {
+ if (this.nodes.hasOwnProperty(id)) {
+ var node = this.nodes[id];
+ if (!node.subflow) {
+ if (!this.activeNodes[id]) {
+ this.activeNodes[id] = createNode(node.type,node.config);
+ //console.log(id,"created");
+ } else {
+ //console.log(id,"already running");
+ }
+ } else {
+ if (!this.subflowInstanceNodes[id]) {
+ var nodes = createSubflow(this.subflows[node.subflow],node.config,this.subflows);
+ this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id});
+ for (var i=0;i 0) {
+ var i = this.missingTypes.indexOf(type);
+ if (i != -1) {
+ this.missingTypes.splice(i,1);
+ if (this.missingTypes.length === 0) {
+ this.start();
+ }
+ }
+ }
+
+}
+
+Flow.prototype.getNode = function(id) {
+ return this.activeNodes[id];
+}
+
+Flow.prototype.getFlow = function() {
+ //console.log(this.config);
+ return this.config;
+}
+
+Flow.prototype.applyConfig = function(config,type) {
+ var diff = this.diffFlow(config);
+ //console.log(diff);
+ //var diff = {
+ // deleted:[]
+ // changed:[]
+ // linked:[]
+ // wiringChanged: []
+ //}
+
+ var nodesToStop = [];
+ var nodesToCreate = [];
+ var nodesToRewire = diff.wiringChanged;
+
+ if (type == "nodes") {
+ nodesToStop = diff.deleted.concat(diff.changed);
+ nodesToCreate = diff.changed;
+ } else if (type == "flows") {
+ nodesToStop = diff.deleted.concat(diff.changed).concat(diff.linked);
+ nodesToCreate = diff.changed.concat(diff.linked);
+ }
+ var activeNodesToStop = [];
+
+ for (var i=0;i 0) {
+ var subflowId = changedSubflowStack.pop();
+
+ config.forEach(function(node) {
+ if (node.type == "subflow:"+subflowId) {
+ if (!changedNodes[node.id]) {
+ changedNodes[node.id] = node;
+ checkSubflowMembership(configNodes,node.id);
+ }
+ }
+ });
+
+ }
+
+ config.forEach(function(node) {
+ buildNodeLinks(newLinks,node,configNodes);
+ });
+
+ var markLinkedNodes = function(linkChanged,changedNodes,linkMap,allNodes) {
+ var stack = Object.keys(changedNodes);
+ var visited = {};
+
+ while(stack.length > 0) {
+ var id = stack.pop();
+
+ var linkedNodes = linkMap[id];
+ if (linkedNodes) {
+ for (var i=0;i 0) {
- var i = missingTypes.indexOf(type);
- if (i != -1) {
- missingTypes.splice(i,1);
- util.log("[red] Missing type registered: "+type);
- if (missingTypes.length === 0) {
- parseConfig();
- }
- }
+ if (activeFlow) {
+ if (activeFlow.typeRegistered(type)) {
+ util.log("[red] Missing type registered: "+type);
}
+ }
});
-
-function getID() {
- return (1+Math.random()*4294967295).toString(16);
-}
-
-function createSubflow(sf,sfn) {
- var node_map = {};
- var newNodes = [];
- var node;
- var wires;
- var i,j,k;
-
- // Clone all of the subflow node definitions and give them new IDs
- for (i=0;i 0) {
- util.log("[red] Waiting for missing types to be registered:");
- for (i=0;i 0) {
- util.log("[red] Stopping flows");
- }
- return flowNodes.clear();
-}
-
-
-function diffNodes(oldNode,newNode) {
- if (oldNode == null) {
- return true;
- } else {
- for (var p in newNode) {
- if (newNode.hasOwnProperty(p) && p != "x" && p != "y") {
- if (!redUtil.compareObjects(oldNode[p],newNode[p])) {
- return true;
- break;
- }
- }
- }
- }
- return false;
-}
var flowNodes = module.exports = {
init: function(_storage) {
@@ -261,13 +54,12 @@ var flowNodes = module.exports = {
load: function() {
return storage.getFlows().then(function(flows) {
return credentials.load().then(function() {
- activeConfig = flows;
- if (activeConfig && activeConfig.length > 0) {
- parseConfig();
- }
+ activeFlow = new Flow(flows);
+ flowNodes.startFlows();
});
}).otherwise(function(err) {
util.log("[red] Error loading flows : "+err);
+ console.log(err.stack);
});
},
@@ -276,7 +68,7 @@ var flowNodes = module.exports = {
* @param n the node to add
*/
add: function(n) {
- nodes[n.id] = n;
+ //console.log("ADDED NODE:",n.id,n.type,n.name||"");
n.on("log",log.log);
},
@@ -286,196 +78,114 @@ var flowNodes = module.exports = {
* @return the node
*/
get: function(i) {
- return nodes[i];
+ return activeFlow.getNode(i);
},
/**
* Stops all active nodes and clears the active set
* @return a promise for the stopping of all active nodes
*/
- clear: function() {
- return when.promise(function(resolve) {
- events.emit("nodes-stopping");
- var promises = [];
- for (var n in nodes) {
- if (nodes.hasOwnProperty(n)) {
- try {
- var p = nodes[n].close();
- if (p) {
- promises.push(p);
- }
- } catch(err) {
- nodes[n].error(err);
- }
- }
- }
- when.settle(promises).then(function() {
- events.emit("nodes-stopped");
- nodes = {};
- resolve();
- });
- });
- },
-
- /**
- * Provides an iterator over the active set of nodes
- * @param cb a function to be called for each node in the active set
- */
- each: function(cb) {
- for (var n in nodes) {
- if (nodes.hasOwnProperty(n)) {
- cb(nodes[n]);
- }
- }
- },
+ //clear: function(nodesToStop) {
+ // var stopList;
+ // if (nodesToStop == null) {
+ // stopList = Object.keys(nodes);
+ // } else {
+ // stopList = Object.keys(nodesToStop);
+ // }
+ // console.log(stopList);
+ // return when.promise(function(resolve) {
+ // events.emit("nodes-stopping");
+ // var promises = [];
+ // console.log("running nodes:",Object.keys(nodes).length);
+ // for (var i=0;i 0) {
- var newChangedSubflowsObj = {};
- changedSubflows.forEach(function(id) {
- changedNodes[id] = newNodes[id];
- });
-
- var subflowNameRE = /^subflow:(.*)$/;
- config.forEach(function(n) {
- var m = subflowNameRE.exec(n.type);
- if (m && changedSubflowsObj[m[1]]) {
- // This is an instance of a changed subflow
- changedNodes[n.id] = n;
- if (activeConfigNodes[n.z] && activeConfigNodes[n.z].type == "subflow" && !changedNodes[n.z]) {
- // This instance is itself in a subflow that has not yet been dealt with
- newChangedSubflowsObj[n.z] = true;
- }
- }
- });
- changedSubflowsObj = newChangedSubflowsObj;
- changedSubflows = Object.keys(newChangedSubflowsObj);
+ if (type=="full") {
+ return credentialSavePromise
+ .then(function() { return storage.saveFlows(config);})
+ .then(function() { return flowNodes.stopFlows(); })
+ .then(function() { activeFlow = new Flow(config); flowNodes.startFlows();});
+ } else {
+ return credentialSavePromise
+ .then(function() { return storage.saveFlows(config);})
+ .then(function() { return activeFlow.applyConfig(config,type); });
}
-
- // Build the list of what each node is connected to
- config.forEach(function(n) {
- nodeLinks[n.id] = nodeLinks[n.id] || [];
- if (n.wires) {
- for (var j=0;j 0) {
- var nid = changedNodeStack.pop();
- var n = newNodes[nid];
- if (!visited[nid]) {
- visited[nid] = true;
- if (nodeLinks[nid]) {
- nodeLinks[nid].forEach(function(id) {
- var nn = newNodes[id];
- if (!changedNodes[id]) {
- linkChangedNodes[id] = nn;
- changedNodeStack.push(nn.id);
- }
- });
- }
- }
- }
-
- config.forEach(function(n) {
- if (changedNodes[n.id]|| linkChangedNodes[n.id]) {
- console.log(changedNodes[n.id]!=null,linkChangedNodes[n.id]!=null,n.id,n.type,n.name);
- }
- });
- deletedNodes.forEach(function(n) {
- console.log("Deleted:",n);
- });
-
- return credentials.save()
- .then(function() { return storage.saveFlows(config);})
- .then(function() { return stopFlows();})
- .then(function () {
- activeConfig = config;
- parseConfig();
- });
},
- stopFlows: stopFlows
+ startFlows: function() {
+ util.log("[red] Starting flows");
+ try {
+ activeFlow.start();
+ } catch(err) {
+ var missingTypes = activeFlow.getMissingTypes();
+ if (missingTypes.length > 0) {
+ util.log("[red] Waiting for missing types to be registered:");
+ for (i=0;i