1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Add status node

This commit is contained in:
Nick O'Leary 2015-08-19 21:14:45 +01:00
parent 658746d2a3
commit a6644ad5ff
6 changed files with 395 additions and 36 deletions

View File

@ -0,0 +1,281 @@
<!--
Copyright 2015 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.
-->
<script type="text/x-red" data-template-name="status">
<div class="form-row">
<label style="width: auto" for="node-input-scope" data-i18n="status.label.source"></label>
<select id="node-input-scope-select">
<option value="all" data-i18n="status.scope.all"></option>
<option value="target" data-i18n="status.scope.selected"></options>
</select>
</div>
<div class="form-row node-input-target-row" style="display: none;">
<div id="node-input-status-target-container-div" style="position: relative; box-sizing: border-box; border-radius: 2px; height: 180px; border: 1px solid #ccc;overflow:hidden; ">
<div style="box-sizing: border-box; line-height: 20px; font-size: 0.8em; border-bottom: 1px solid #ddd; height: 20px;">
<input type="checkbox" data-i18n="[title]status.label.selectAll" id="node-input-target-node-checkbox-all" style="width: 30px; margin: 0 2px 1px 2px;">
<div style="display: inline-block;"><a id="node-input-target-sort-label" href="#" data-i18n="[title]status.label.sortByLabel"><span data-i18n="status.label.node"></span> <i class="node-input-status-sort-label-a fa fa-caret-down"></i><i class="node-input-status-sort-label-d fa fa-caret-up"></i></a></div>
<div style="position: absolute; right: 10px; width: 50px; display: inline-block; text-align: right;"><a id="node-input-target-sort-type" href="#" data-i18n="[title]status.label.sortByType"><i class="node-input-status-sort-sublabel-a fa fa-caret-down"></i><i class="node-input-status-sort-sublabel-d fa fa-caret-up"></i> <span data-i18n="status.label.type"></span></a></div>
</div>
<div style="background: #fbfbfb; box-sizing: border-box; position:absolute; top:20px;bottom:0;left:0px;right:0px; overflow-y: scroll; overflow-x: hidden;">
<ul id="node-input-status-target-container" style=" list-style-type:none; margin: 0;"></ul>
</div>
</div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/x-red" data-help-name="status">
<p>Send status messages from other nodes on the same tab.</p>
<p>The message sent by this node will have a <code>status</code> property
with the following attributes:
<ul>
<li><code>text</code> : the status text</li>
<li><code>source.type</code> : the type of the node that reported status</li>
<li><code>source.id</code> : the id of the node that reported status</li>
<li><code>source.name</code> : the name, if set, of the node that reported status</li>
</ul>
</p>
</script>
<style>
#node-input-status-target-container {
position: relative;
}
#node-input-status-target-container li {
padding: 2px 5px;
background: none;
font-size: 0.8em;
margin:0;
white-space: nowrap;
}
#node-input-status-target-container li label {
margin-bottom: 0;
width: 100%;
}
#node-input-status-target-container li label input {
vertical-align: top;
width:15px;
margin-right: 10px;
}
#node-input-status-target-container li:hover,
#node-input-status-target-container li:hover .node-input-target-node-sublabel {
background: #f0f0f0;
}
.node-input-target-node-sublabel {
position:absolute;
right: 0px;
padding-right: 10px;
padding-left: 10px;
font-size: 0.8em;
background: #fbfbfb;
}
</style>
<script type="text/javascript">
RED.nodes.registerType('status',{
category: 'input',
color:"#c0edc0",
defaults: {
name: {value:""},
scope: {value:null}
},
inputs:0,
outputs:1,
icon: "alert.png",
label: function() {
return this.name||this.scope?this._("status.statusNodes",{number:this.scope.length}):this._("status.status");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var nodeList = $("#node-input-status-target-container");
var node = this;
function createNodeList() {
node.scope = node.scope || [];
nodeList.empty();
var candidateNodes = RED.nodes.filterNodes({z:node.z});
var allChecked = true;
candidateNodes.forEach(function(n) {
if (n.id === node.id) {
return;
}
var isChecked = node.scope.indexOf(n.id) !== -1;
allChecked = allChecked && isChecked;
var container = $('<li/>',{class:"node-input-target-node"});
var row = $('<label/>',{for:"node-input-target-node-"+n.id}).appendTo(container);
$('<input>',{type:"checkbox",class:"node-input-target-node-checkbox",id:"node-input-target-node-"+n.id})
.data('node-id',n.id)
.prop('checked', isChecked)
.appendTo(row);
container.on('mouseover',function(e) {
n.highlighted = true;
n.dirty = true;
RED.view.redraw();
});
container.on('mouseout',function(e) {
n.highlighted = false;
n.dirty = true;
RED.view.redraw();
});
var labelSpan = $('<span>');
var nodeDef = RED.nodes.getType(n.type);
var label;
var sublabel;
if (nodeDef) {
var l = nodeDef.label;
label = (typeof l === "function" ? l.call(n) : l)||"";
sublabel = n.type;
if (sublabel.indexOf("subflow:") === 0) {
var subflowId = sublabel.substring(8);
var subflow = RED.nodes.subflow(subflowId);
sublabel = "subflow : "+subflow.name;
}
}
if (!nodeDef || !label) {
label = n.type;
}
$('<span>',{class:"node-input-target-node-label",style:"white-space:nowrap"}).text(label).appendTo(row);
if (sublabel) {
$('<span>',{class:"node-input-target-node-sublabel"}).text(sublabel).appendTo(row);
}
container.appendTo(nodeList);
});
$(".node-input-target-node-checkbox").change(function() {
if (!this.checked) {
$("#node-input-target-node-checkbox-all").prop('checked',false);
}
});
$("#node-input-target-node-checkbox-all").prop('checked',allChecked);
sortNodeList('label');
}
function sortNodeList(sortOn) {
var currentSort = nodeList.data('currentSort');
var currentSortOrder = nodeList.data('currentSortOrder');
if (!currentSort) {
currentSort = sortOn;
currentSortOrder = 'a';
} else {
if (currentSort === sortOn) {
currentSortOrder = (currentSortOrder === 'a'?'d':'a');
} else {
currentSortOrder = 'a';
}
currentSort = sortOn;
}
nodeList.data('currentSort',currentSort);
nodeList.data('currentSortOrder',currentSortOrder);
$("#node-input-status-target-container-div .fa").hide();
$(".node-input-status-sort-"+currentSort+"-"+currentSortOrder).show();
var items = nodeList.find("li").get();
items.sort(function(a,b) {
var labelA = $(a).find(".node-input-target-node-"+currentSort).text().toLowerCase();
var labelB = $(b).find(".node-input-target-node-"+currentSort).text().toLowerCase();
if (labelA < labelB) { return currentSortOrder==='a'?-1:1; }
if (labelA > labelB) { return currentSortOrder==='a'?1:-1; }
return 0;
});
$.each(items, function(i, li){
nodeList.append(li);
});
}
$("#node-input-target-sort-label").click(function(e) {
e.preventDefault();
sortNodeList('label');
});
$("#node-input-target-sort-type").click(function(e) {
e.preventDefault();
sortNodeList('sublabel')
});
$("#node-input-target-node-checkbox-all").change(function() {
$(".node-input-target-node-checkbox").prop('checked',this.checked);
})
$("#node-input-scope-select").change(function(e) {
var scope = $(this).children("option:selected").val();
if (scope === "target") {
createNodeList();
$(".node-input-target-row").show();
} else {
$(".node-input-target-row").hide();
}
});
if (this.scope == null) {
$("#node-input-scope-select").val("all");
} else {
$("#node-input-scope-select").val("target");
}
$("#node-input-scope-select").change();
function dialogResize() {
var rows = $("#dialog-form>div:not(.node-input-target-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-target-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-status-target-container-div").css("height",height+"px");
};
$( "#dialog" ).on("dialogresize", dialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-status');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
dialogResize();
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
$( "#dialog" ).off("dialogresize",dialogResize);
});
},
oneditsave: function() {
var scope = $("#node-input-scope-select").children("option:selected").val();
if (scope === 'all') {
this.scope = null;
} else {
var node = this;
node.scope = [];
$(".node-input-target-node-checkbox").each(function(n) {
if ($(this).prop("checked")) {
node.scope.push($(this).data('node-id'));
}
})
}
}
});
</script>

View File

@ -0,0 +1,29 @@
/**
* Copyright 2015 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.
**/
module.exports = function(RED) {
"use strict";
function StatusNode(n) {
RED.nodes.createNode(this,n);
var node = this;
this.on("input", function(msg) {
this.send(msg);
});
}
RED.nodes.registerType("status",StatusNode);
}

View File

@ -83,6 +83,22 @@
"selected": "selected nodes" "selected": "selected nodes"
} }
}, },
"status": {
"status": "status (all)",
"statusNodes": "status (__number__)",
"label": {
"source": "Report status from",
"node": "node",
"type": "type",
"selectAll": "select all",
"sortByLabel": "sort by label",
"sortByType": "sort by type"
},
"scope": {
"all": "all nodes",
"selected": "selected nodes"
}
},
"debug": { "debug": {
"output": "Output", "output": "Output",
"msgprop": "message property", "msgprop": "message property",

View File

@ -68,7 +68,7 @@ function createSubflow(sf,sfn,subflows) {
node.z = sfn.id; node.z = sfn.id;
newNodes.push(node); newNodes.push(node);
} }
// Look for any catch nodes and update their scope ids // Look for any catch/status nodes and update their scope ids
// Update all subflow interior wiring to reflect new node IDs // Update all subflow interior wiring to reflect new node IDs
for (i=0;i<newNodes.length;i++) { for (i=0;i<newNodes.length;i++) {
node = newNodes[i]; node = newNodes[i];
@ -79,7 +79,7 @@ function createSubflow(sf,sfn,subflows) {
outputs[j][k] = node_map[outputs[j][k]].id outputs[j][k] = node_map[outputs[j][k]].id
} }
} }
if (node.type === 'catch' && node.scope) { if ((node.type === 'catch' || node.type === 'status') && node.scope) {
node.scope = node.scope.map(function(id) { node.scope = node.scope.map(function(id) {
return node_map[id]?node_map[id].id:"" return node_map[id]?node_map[id].id:""
}) })
@ -216,25 +216,6 @@ function diffNodeConfigs(oldNode,newNode) {
return false; return false;
} }
function createCatchNodeMap(nodes) {
var catchNodes = {};
var subflowInstances = {};
var id;
/*
- a catchNode with same z as error node
- if error occurs on a subflow without catchNode, look at z of subflow instance
*/
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].type === "catch") {
catchNodes[nodes[id].z] = catchNodes[nodes[id].z] || [];
catchNodes[nodes[id].z].push(nodes[id]);
}
}
}
return catchNodes;
}
var subflowInstanceRE = /^subflow:(.+)$/; var subflowInstanceRE = /^subflow:(.+)$/;
function Flow(config) { function Flow(config) {
@ -242,6 +223,7 @@ function Flow(config) {
this.activeNodes = {}; this.activeNodes = {};
this.subflowInstanceNodes = {}; this.subflowInstanceNodes = {};
this.catchNodeMap = {}; this.catchNodeMap = {};
this.statusNodeMap = {};
this.started = false; this.started = false;
this.parseConfig(config); this.parseConfig(config);
@ -306,7 +288,7 @@ Flow.prototype.parseConfig = function(config) {
} else { } else {
this.nodes[nodeConfig.id] = nodeInfo; this.nodes[nodeConfig.id] = nodeInfo;
} }
if (nodeConfig.type != "catch") { if (nodeConfig.type != "catch" && nodeConfig.type != "status") {
for (var prop in nodeConfig) { for (var prop in nodeConfig) {
if (nodeConfig.hasOwnProperty(prop) && if (nodeConfig.hasOwnProperty(prop) &&
prop != "type" && prop != "type" &&
@ -394,7 +376,22 @@ Flow.prototype.start = function(configDiff) {
} }
} }
this.catchNodeMap = createCatchNodeMap(this.activeNodes);
this.catchNodeMap = {};
this.statusNodeMap = {};
for (id in this.activeNodes) {
if (this.activeNodes.hasOwnProperty(id)) {
node = this.activeNodes[id];
if (node.type === "catch") {
this.catchNodeMap[node.z] = this.catchNodeMap[node.z] || [];
this.catchNodeMap[node.z].push(node);
} else if (node.type === "status") {
this.statusNodeMap[node.z] = this.statusNodeMap[node.z] || [];
this.statusNodeMap[node.z].push(node);
}
}
}
credentials.clean(this.config); credentials.clean(this.config);
events.emit("nodes-started"); events.emit("nodes-started");
@ -727,6 +724,38 @@ function diffFlow(flow,config) {
return diff; return diff;
} }
Flow.prototype.handleStatus = function(node,statusMessage) {
var targetStatusNodes = null;
var reportingNode = node;
var handled = false;
while(reportingNode && !handled) {
targetStatusNodes = this.statusNodeMap[reportingNode.z];
if (targetStatusNodes) {
targetStatusNodes.forEach(function(targetStatusNode) {
if (targetStatusNode.scope && targetStatusNode.scope.indexOf(node.id) === -1) {
return;
}
var message = {
status: {
source: {
id: node.id,
type: node.type,
name: node.name
}
}
};
if (statusMessage.text) {
message.status.text = statusMessage.text;
}
targetStatusNode.receive(message);
handled = true;
});
}
if (!handled) {
reportingNode = this.activeNodes[reportingNode.z];
}
}
}
Flow.prototype.handleError = function(node,logMessage,msg) { Flow.prototype.handleError = function(node,logMessage,msg) {
var count = 1; var count = 1;

View File

@ -189,7 +189,7 @@ Node.prototype.receive = function(msg) {
msg._msgid = redUtil.generateId(); msg._msgid = redUtil.generateId();
} }
this.metric("receive",msg); this.metric("receive",msg);
try { try {
this.emit("input", msg); this.emit("input", msg);
} catch(err) { } catch(err) {
this.error(err,msg); this.error(err,msg);
@ -247,5 +247,6 @@ Node.prototype.metric = function(eventname, msg, metricValue) {
*/ */
Node.prototype.status = function(status) { Node.prototype.status = function(status) {
comms.publish("status/" + this.id, status, true); comms.publish("status/" + this.id, status, true);
flows.handleStatus(this,status);
}; };
module.exports = Node; module.exports = Node;

View File

@ -46,7 +46,7 @@ var flowNodes = module.exports = {
settings = _settings; settings = _settings;
storage = _storage; storage = _storage;
}, },
/** /**
* Load the current activeConfig from storage and start it running * Load the current activeConfig from storage and start it running
* @return a promise for the loading of the config * @return a promise for the loading of the config
@ -62,7 +62,7 @@ var flowNodes = module.exports = {
console.log(err.stack); console.log(err.stack);
}); });
}, },
/** /**
* Get a node * Get a node
* @param i the node id * @param i the node id
@ -71,18 +71,18 @@ var flowNodes = module.exports = {
get: function(i) { get: function(i) {
return activeFlow.getNode(i); return activeFlow.getNode(i);
}, },
eachNode: function(cb) { eachNode: function(cb) {
activeFlow.eachNode(cb); activeFlow.eachNode(cb);
}, },
/** /**
* @return the active configuration * @return the active configuration
*/ */
getFlows: function() { getFlows: function() {
return activeFlow.getFlow(); return activeFlow.getFlow();
}, },
/** /**
* Sets the current active config. * Sets the current active config.
* @param config the configuration to enable * @param config the configuration to enable
@ -90,14 +90,14 @@ var flowNodes = module.exports = {
* @return a promise for the starting of the new flow * @return a promise for the starting of the new flow
*/ */
setFlows: function (config,type) { setFlows: function (config,type) {
type = type||"full"; type = type||"full";
var credentialsChanged = false; var credentialsChanged = false;
var credentialSavePromise = null; var credentialSavePromise = null;
// Clone config and extract credentials prior to saving // Clone config and extract credentials prior to saving
// Original config needs to retain credentials so that flow.applyConfig // Original config needs to retain credentials so that flow.applyConfig
// knows which nodes have had changes. // knows which nodes have had changes.
@ -108,7 +108,7 @@ var flowNodes = module.exports = {
credentialsChanged = true; credentialsChanged = true;
} }
}); });
if (credentialsChanged) { if (credentialsChanged) {
credentialSavePromise = credentials.save(); credentialSavePromise = credentials.save();
} else { } else {
@ -122,7 +122,7 @@ var flowNodes = module.exports = {
} else { } else {
return credentialSavePromise return credentialSavePromise
.then(function() { return storage.saveFlows(cleanConfig);}) .then(function() { return storage.saveFlows(cleanConfig);})
.then(function() { .then(function() {
var configDiff = activeFlow.diffConfig(config,type); var configDiff = activeFlow.diffConfig(config,type);
return flowNodes.stopFlows(configDiff).then(function() { return flowNodes.stopFlows(configDiff).then(function() {
activeFlow.parseConfig(config); activeFlow.parseConfig(config);
@ -190,6 +190,9 @@ var flowNodes = module.exports = {
}, },
handleError: function(node,logMessage,msg) { handleError: function(node,logMessage,msg) {
activeFlow.handleError(node,logMessage,msg); activeFlow.handleError(node,logMessage,msg);
},
handleStatus: function(node,statusMessage) {
activeFlow.handleStatus(node,statusMessage);
} }
}; };