mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #2690 from node-red/sf-module
[sf-modules] Support npm subflow modules
This commit is contained in:
commit
c40412d7c6
@ -60,6 +60,7 @@ module.exports = {
|
|||||||
runtimeAPI.nodes.addModule(opts).then(function(info) {
|
runtimeAPI.nodes.addModule(opts).then(function(info) {
|
||||||
res.json(info);
|
res.json(info);
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
|
console.log(err.stack);
|
||||||
apiUtils.rejectHandler(req,res,err);
|
apiUtils.rejectHandler(req,res,err);
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -339,6 +339,17 @@
|
|||||||
"deleteSubflow": "delete subflow",
|
"deleteSubflow": "delete subflow",
|
||||||
"info": "Description",
|
"info": "Description",
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
|
"module": "Module",
|
||||||
|
"license": "License",
|
||||||
|
"licenseOther": "Other",
|
||||||
|
"type": "Node Type",
|
||||||
|
"version": "Version",
|
||||||
|
"versionPlaceholder": "x.y.z",
|
||||||
|
"keys": "Keywords",
|
||||||
|
"keysPlaceholder": "Comma-separated keywords",
|
||||||
|
"author": "Author",
|
||||||
|
"authorPlaceholder": "Your Name <email@example.com>",
|
||||||
|
"desc": "Description",
|
||||||
"env": {
|
"env": {
|
||||||
"restore": "Restore to subflow default",
|
"restore": "Restore to subflow default",
|
||||||
"remove": "Remove environment variable"
|
"remove": "Remove environment variable"
|
||||||
@ -1079,6 +1090,7 @@
|
|||||||
"editor-tab": {
|
"editor-tab": {
|
||||||
"properties": "Properties",
|
"properties": "Properties",
|
||||||
"envProperties": "Environment Variables",
|
"envProperties": "Environment Variables",
|
||||||
|
"module": "Module Properties",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
"preview": "UI Preview",
|
"preview": "UI Preview",
|
||||||
|
@ -339,6 +339,12 @@
|
|||||||
"deleteSubflow": "サブフローを削除",
|
"deleteSubflow": "サブフローを削除",
|
||||||
"info": "詳細",
|
"info": "詳細",
|
||||||
"category": "カテゴリ",
|
"category": "カテゴリ",
|
||||||
|
"module": "モジュール",
|
||||||
|
"license": "ライセンス",
|
||||||
|
"version": "バージョン",
|
||||||
|
"keys": "キーワード",
|
||||||
|
"author": "作者",
|
||||||
|
"desc": "説明",
|
||||||
"env": {
|
"env": {
|
||||||
"restore": "デフォルト値に戻す",
|
"restore": "デフォルト値に戻す",
|
||||||
"remove": "環境変数を削除"
|
"remove": "環境変数を削除"
|
||||||
|
@ -670,6 +670,7 @@ RED.nodes = (function() {
|
|||||||
node.in = [];
|
node.in = [];
|
||||||
node.out = [];
|
node.out = [];
|
||||||
node.env = n.env;
|
node.env = n.env;
|
||||||
|
node.meta = n.meta;
|
||||||
|
|
||||||
if (exportCreds) {
|
if (exportCreds) {
|
||||||
var credentialSet = {};
|
var credentialSet = {};
|
||||||
@ -1920,6 +1921,18 @@ RED.nodes = (function() {
|
|||||||
RED.events.emit("groups:remove",group);
|
RED.events.emit("groups:remove",group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeHelp(type) {
|
||||||
|
var helpContent = "";
|
||||||
|
var helpElement = $("script[data-help-name='"+type+"']");
|
||||||
|
if (helpElement) {
|
||||||
|
helpContent = helpElement.html();
|
||||||
|
var helpType = helpElement.attr("type");
|
||||||
|
if (helpType === "text/markdown") {
|
||||||
|
helpContent = RED.utils.renderMarkdown(helpContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return helpContent;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: function() {
|
init: function() {
|
||||||
@ -1999,6 +2012,7 @@ RED.nodes = (function() {
|
|||||||
|
|
||||||
registerType: registry.registerNodeType,
|
registerType: registry.registerNodeType,
|
||||||
getType: registry.getNodeType,
|
getType: registry.getNodeType,
|
||||||
|
getNodeHelp: getNodeHelp,
|
||||||
convertNode: convertNode,
|
convertNode: convertNode,
|
||||||
|
|
||||||
add: addNode,
|
add: addNode,
|
||||||
|
@ -414,6 +414,7 @@ RED.editor = (function() {
|
|||||||
for (var cred in credDefinition) {
|
for (var cred in credDefinition) {
|
||||||
if (credDefinition.hasOwnProperty(cred)) {
|
if (credDefinition.hasOwnProperty(cred)) {
|
||||||
var input = $("#" + prefix + '-' + cred);
|
var input = $("#" + prefix + '-' + cred);
|
||||||
|
if (input.length > 0) {
|
||||||
var value = input.val();
|
var value = input.val();
|
||||||
if (credDefinition[cred].type == 'password') {
|
if (credDefinition[cred].type == 'password') {
|
||||||
node.credentials['has_' + cred] = (value !== "");
|
node.credentials['has_' + cred] = (value !== "");
|
||||||
@ -429,6 +430,7 @@ RED.editor = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,6 +467,7 @@ RED.editor = (function() {
|
|||||||
definition.oneditprepare.call(node);
|
definition.oneditprepare.call(node);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.log("oneditprepare",node.id,node.type,err.toString());
|
console.log("oneditprepare",node.id,node.type,err.toString());
|
||||||
|
console.log(err.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Now invoke any change handlers added to the fields - passing true
|
// Now invoke any change handlers added to the fields - passing true
|
||||||
@ -1192,7 +1195,7 @@ RED.editor = (function() {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
|
console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (d in editing_node._def.defaults) {
|
for (d in editing_node._def.defaults) {
|
||||||
@ -1890,7 +1893,7 @@ RED.editor = (function() {
|
|||||||
try {
|
try {
|
||||||
configTypeDef.oneditsave.call(editing_config_node);
|
configTypeDef.oneditsave.call(editing_config_node);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.log("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
|
console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2250,6 +2253,14 @@ RED.editor = (function() {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var newMeta = RED.subflow.exportSubflowModuleProperties(editing_node);
|
||||||
|
|
||||||
|
if (!isSameObj(editing_node.meta,newMeta)) {
|
||||||
|
changes.meta = editing_node.meta;
|
||||||
|
editing_node.meta = newMeta;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
var wasChanged = editing_node.changed;
|
var wasChanged = editing_node.changed;
|
||||||
editing_node.changed = true;
|
editing_node.changed = true;
|
||||||
@ -2356,6 +2367,16 @@ RED.editor = (function() {
|
|||||||
};
|
};
|
||||||
editorTabs.addTab(nodePropertiesTab);
|
editorTabs.addTab(nodePropertiesTab);
|
||||||
|
|
||||||
|
var moduleTab = {
|
||||||
|
id: "editor-tab-module",
|
||||||
|
label: RED._("editor-tab.module"),
|
||||||
|
name: RED._("editor-tab.module"),
|
||||||
|
content: $('<div>', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(),
|
||||||
|
iconClass: "fa fa-cube",
|
||||||
|
};
|
||||||
|
editorTabs.addTab(moduleTab);
|
||||||
|
RED.subflow.buildModuleForm(moduleTab.content, editing_node);
|
||||||
|
|
||||||
var descriptionTab = {
|
var descriptionTab = {
|
||||||
id: "editor-tab-description",
|
id: "editor-tab-description",
|
||||||
label: RED._("editor-tab.description"),
|
label: RED._("editor-tab.description"),
|
||||||
@ -2457,7 +2478,7 @@ RED.editor = (function() {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
|
console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (d in editing_node._def.defaults) {
|
for (d in editing_node._def.defaults) {
|
||||||
|
@ -147,7 +147,7 @@ RED.palette = (function() {
|
|||||||
var popOverContent;
|
var popOverContent;
|
||||||
try {
|
try {
|
||||||
var l = "<p><b>"+RED.text.bidi.enforceTextDirectionWithUCC(label)+"</b></p>";
|
var l = "<p><b>"+RED.text.bidi.enforceTextDirectionWithUCC(label)+"</b></p>";
|
||||||
popOverContent = $('<div></div>').append($(l+(info?info:$("script[data-help-name='"+type+"']").html()||"<p>"+RED._("palette.noInfo")+"</p>").trim())
|
popOverContent = $('<div></div>').append($(l+(info?info:RED.nodes.getNodeHelp(type)||"<p>"+RED._("palette.noInfo")+"</p>").trim())
|
||||||
.filter(function(n) {
|
.filter(function(n) {
|
||||||
return (this.nodeType == 1 && this.nodeName == "P") || (this.nodeType == 3 && this.textContent.trim().length > 0)
|
return (this.nodeType == 1 && this.nodeName == "P") || (this.nodeType == 3 && this.textContent.trim().length > 0)
|
||||||
}).slice(0,2));
|
}).slice(0,2));
|
||||||
@ -264,27 +264,6 @@ RED.palette = (function() {
|
|||||||
|
|
||||||
d.data('popover',popover);
|
d.data('popover',popover);
|
||||||
|
|
||||||
// $(d).popover({
|
|
||||||
// title:d.type,
|
|
||||||
// placement:"right",
|
|
||||||
// trigger: "hover",
|
|
||||||
// delay: { show: 750, hide: 50 },
|
|
||||||
// html: true,
|
|
||||||
// container:'body'
|
|
||||||
// });
|
|
||||||
// d.on("click", function() {
|
|
||||||
// RED.view.focus();
|
|
||||||
// var helpText;
|
|
||||||
// if (nt.indexOf("subflow:") === 0) {
|
|
||||||
// helpText = RED.utils.renderMarkdown(RED.nodes.subflow(nt.substring(8)).info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
|
|
||||||
// } else {
|
|
||||||
// helpText = $("script[data-help-name='"+d.attr("data-palette-type")+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
|
|
||||||
// }
|
|
||||||
// // Don't look too closely. RED.sidebar.info.set will set the 'Description'
|
|
||||||
// // section of the sidebar. Pass in the title of the Help section so it looks
|
|
||||||
// // right.
|
|
||||||
// RED.sidebar.type.show(helpText,RED._("sidebar.info.nodeHelp"));
|
|
||||||
// });
|
|
||||||
var chart = $("#red-ui-workspace-chart");
|
var chart = $("#red-ui-workspace-chart");
|
||||||
var chartSVG = $("#red-ui-workspace-chart>svg").get(0);
|
var chartSVG = $("#red-ui-workspace-chart>svg").get(0);
|
||||||
var activeSpliceLink;
|
var activeSpliceLink;
|
||||||
|
@ -47,6 +47,37 @@ RED.subflow = (function() {
|
|||||||
'</div>'+
|
'</div>'+
|
||||||
'</script>';
|
'</script>';
|
||||||
|
|
||||||
|
var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
|
||||||
|
'<div class="form-row">'+
|
||||||
|
'<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+
|
||||||
|
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="form-row">'+
|
||||||
|
'<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+
|
||||||
|
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="form-row">'+
|
||||||
|
'<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+
|
||||||
|
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="form-row">'+
|
||||||
|
'<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+
|
||||||
|
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="form-row">'+
|
||||||
|
'<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+
|
||||||
|
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="form-row">'+
|
||||||
|
'<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+
|
||||||
|
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="form-row">'+
|
||||||
|
'<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+
|
||||||
|
'<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+
|
||||||
|
'</div>'+
|
||||||
|
'</form>';
|
||||||
|
|
||||||
function findAvailableSubflowIOPosition(subflow,isInput) {
|
function findAvailableSubflowIOPosition(subflow,isInput) {
|
||||||
var pos = {x:50,y:30};
|
var pos = {x:50,y:30};
|
||||||
if (!isInput) {
|
if (!isInput) {
|
||||||
@ -993,6 +1024,7 @@ RED.subflow = (function() {
|
|||||||
icon: "",
|
icon: "",
|
||||||
type: "cred"
|
type: "cred"
|
||||||
}
|
}
|
||||||
|
opt.ui.type = "cred";
|
||||||
} else {
|
} else {
|
||||||
opt.ui = opt.ui || {
|
opt.ui = opt.ui || {
|
||||||
icon: "",
|
icon: "",
|
||||||
@ -1488,6 +1520,7 @@ RED.subflow = (function() {
|
|||||||
var locale = RED.i18n.lang();
|
var locale = RED.i18n.lang();
|
||||||
var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, locale);
|
var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, locale);
|
||||||
var label = $('<label>').appendTo(row);
|
var label = $('<label>').appendTo(row);
|
||||||
|
$('<span> </span>').appendTo(row);
|
||||||
var labelContainer = $('<span></span>').appendTo(label);
|
var labelContainer = $('<span></span>').appendTo(label);
|
||||||
if (ui.icon) {
|
if (ui.icon) {
|
||||||
var newPath = RED.utils.separateIconPath(ui.icon);
|
var newPath = RED.utils.separateIconPath(ui.icon);
|
||||||
@ -1723,8 +1756,6 @@ RED.subflow = (function() {
|
|||||||
parentEnv[env.name] = item;
|
parentEnv[env.name] = item;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (node.env) {
|
if (node.env) {
|
||||||
for (var i = 0; i < node.env.length; i++) {
|
for (var i = 0; i < node.env.length; i++) {
|
||||||
var env = node.env[i];
|
var env = node.env[i];
|
||||||
@ -1740,6 +1771,40 @@ RED.subflow = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (node._def.subflowModule) {
|
||||||
|
var keys = Object.keys(node._def.defaults);
|
||||||
|
keys.forEach(function(name) {
|
||||||
|
if (name !== 'name') {
|
||||||
|
var prop = node._def.defaults[name];
|
||||||
|
var nodeProp = node[name];
|
||||||
|
var nodePropType;
|
||||||
|
var nodePropValue = nodeProp;
|
||||||
|
if (prop.ui && prop.ui.type === "cred") {
|
||||||
|
nodePropType = "cred";
|
||||||
|
} else {
|
||||||
|
switch(typeof nodeProp) {
|
||||||
|
case "string": nodePropType = "str"; break;
|
||||||
|
case "number": nodePropType = "num"; break;
|
||||||
|
case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
|
||||||
|
default:
|
||||||
|
nodePropType = nodeProp.type;
|
||||||
|
nodePropValue = nodeProp.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var item = {
|
||||||
|
name: name,
|
||||||
|
type: nodePropType,
|
||||||
|
value: nodePropValue,
|
||||||
|
parent: {
|
||||||
|
type: prop.type,
|
||||||
|
value: prop.value
|
||||||
|
},
|
||||||
|
ui: $.extend(true,{},prop.ui)
|
||||||
|
}
|
||||||
|
envList.push(item);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
return envList;
|
return envList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1859,6 +1924,122 @@ RED.subflow = (function() {
|
|||||||
buildPropertiesList(list, node);
|
buildPropertiesList(list, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupInputValidation(input,validator) {
|
||||||
|
var errorTip;
|
||||||
|
var validateTimeout;
|
||||||
|
|
||||||
|
var validateFunction = function() {
|
||||||
|
if (validateTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validateTimeout = setTimeout(function() {
|
||||||
|
var error = validator(input.val());
|
||||||
|
// if (!error && errorTip) {
|
||||||
|
// errorTip.close();
|
||||||
|
// errorTip = null;
|
||||||
|
// } else if (error && !errorTip) {
|
||||||
|
// errorTip = RED.popover.create({
|
||||||
|
// tooltip: true,
|
||||||
|
// target:input,
|
||||||
|
// size: "small",
|
||||||
|
// direction: "bottom",
|
||||||
|
// content: error,
|
||||||
|
// }).open();
|
||||||
|
// }
|
||||||
|
input.toggleClass("input-error",!!error);
|
||||||
|
validateTimeout = null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
input.on("change keyup paste", validateFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModuleForm(container, node) {
|
||||||
|
$(_subflowModulePaneTemplate).appendTo(container);
|
||||||
|
var moduleProps = node.meta || {};
|
||||||
|
[
|
||||||
|
'module',
|
||||||
|
'type',
|
||||||
|
'version',
|
||||||
|
'author',
|
||||||
|
'desc',
|
||||||
|
'keywords',
|
||||||
|
'license'
|
||||||
|
].forEach(function(property) {
|
||||||
|
$("#subflow-input-module-"+property).val(moduleProps[property]||"")
|
||||||
|
})
|
||||||
|
$("#subflow-input-module-type").attr("placeholder",node.id);
|
||||||
|
|
||||||
|
setupInputValidation($("#subflow-input-module-module"), function(newValue) {
|
||||||
|
newValue = newValue.trim();
|
||||||
|
var isValid = newValue.length < 215;
|
||||||
|
isValid = isValid && !/^[._]/.test(newValue);
|
||||||
|
isValid = isValid && !/[A-Z]/.test(newValue);
|
||||||
|
if (newValue !== encodeURIComponent(newValue)) {
|
||||||
|
var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue);
|
||||||
|
if (m) {
|
||||||
|
isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2]))
|
||||||
|
} else {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isValid?"":"Invalid module name"
|
||||||
|
})
|
||||||
|
setupInputValidation($("#subflow-input-module-version"), function(newValue) {
|
||||||
|
newValue = newValue.trim();
|
||||||
|
var isValid = newValue === "" ||
|
||||||
|
/^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue);
|
||||||
|
return isValid?"":"Invalid version number"
|
||||||
|
})
|
||||||
|
|
||||||
|
var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"];
|
||||||
|
var typedLicenses = {
|
||||||
|
types: licenses.map(function(l) {
|
||||||
|
return {value:l,label:l,hasValue:false}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
typedLicenses.types.push({
|
||||||
|
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
|
||||||
|
})
|
||||||
|
if (!moduleProps.license) {
|
||||||
|
typedLicenses.default = "none";
|
||||||
|
} else if (licenses.indexOf(moduleProps.license) > -1) {
|
||||||
|
typedLicenses.default = moduleProps.license;
|
||||||
|
} else {
|
||||||
|
typedLicenses.default = "_custom_";
|
||||||
|
}
|
||||||
|
$("#subflow-input-module-license").typedInput(typedLicenses)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSubflowModuleProperties(node) {
|
||||||
|
var value;
|
||||||
|
var moduleProps = {};
|
||||||
|
[
|
||||||
|
'module',
|
||||||
|
'type',
|
||||||
|
'version',
|
||||||
|
'author',
|
||||||
|
'desc',
|
||||||
|
'keywords'
|
||||||
|
].forEach(function(property) {
|
||||||
|
value = $("#subflow-input-module-"+property).val().trim();
|
||||||
|
if (value) {
|
||||||
|
moduleProps[property] = value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var selectedLicenseType = $("#subflow-input-module-license").typedInput("type");
|
||||||
|
|
||||||
|
if (selectedLicenseType === '_custom_') {
|
||||||
|
value = $("#subflow-input-module-license").val();
|
||||||
|
if (value) {
|
||||||
|
moduleProps.license = value;
|
||||||
|
}
|
||||||
|
} else if (selectedLicenseType !== "none") {
|
||||||
|
moduleProps.license = selectedLicenseType;
|
||||||
|
}
|
||||||
|
return moduleProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: init,
|
init: init,
|
||||||
createSubflow: createSubflow,
|
createSubflow: createSubflow,
|
||||||
@ -1872,9 +2053,11 @@ RED.subflow = (function() {
|
|||||||
|
|
||||||
buildEditForm: buildEditForm,
|
buildEditForm: buildEditForm,
|
||||||
buildPropertiesForm: buildPropertiesForm,
|
buildPropertiesForm: buildPropertiesForm,
|
||||||
|
buildModuleForm: buildModuleForm,
|
||||||
|
|
||||||
exportSubflowTemplateEnv: exportEnvList,
|
exportSubflowTemplateEnv: exportEnvList,
|
||||||
exportSubflowInstanceEnv: exportSubflowInstanceEnv
|
exportSubflowInstanceEnv: exportSubflowInstanceEnv,
|
||||||
|
exportSubflowModuleProperties: exportSubflowModuleProperties
|
||||||
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -247,7 +247,7 @@ RED.sidebar.help = (function() {
|
|||||||
helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
|
helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
|
||||||
title = subflowNode.name || nodeType;
|
title = subflowNode.name || nodeType;
|
||||||
} else {
|
} else {
|
||||||
helpText = $("script[data-help-name='"+nodeType+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
|
helpText = RED.nodes.getNodeHelp(nodeType)||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
|
||||||
title = nodeType;
|
title = nodeType;
|
||||||
}
|
}
|
||||||
setInfoText(title, helpText, helpSection);
|
setInfoText(title, helpText, helpSection);
|
||||||
|
@ -382,20 +382,13 @@ RED.sidebar.info = (function() {
|
|||||||
var category = subflowNode.category||"subflows";
|
var category = subflowNode.category||"subflows";
|
||||||
$(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category}))
|
$(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category}))
|
||||||
$('<tr class="node-info-subflow-row"><td>'+RED._("sidebar.info.instances")+"</td><td>"+subflowUserCount+'</td></tr>').appendTo(tableBody);
|
$('<tr class="node-info-subflow-row"><td>'+RED._("sidebar.info.instances")+"</td><td>"+subflowUserCount+'</td></tr>').appendTo(tableBody);
|
||||||
|
if (subflowNode.meta) {
|
||||||
|
propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.module")+'</td><td></td></tr>').appendTo(tableBody);
|
||||||
|
$(propRow.children()[1]).text(subflowNode.meta.module||"")
|
||||||
|
propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.version")+'</td><td></td></tr>').appendTo(tableBody);
|
||||||
|
$(propRow.children()[1]).text(subflowNode.meta.version||"")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// var helpText = "";
|
|
||||||
// if (node.type === "tab" || node.type === "subflow") {
|
|
||||||
// } else {
|
|
||||||
// if (subflowNode && node.type !== "subflow") {
|
|
||||||
// // Selected a subflow instance node.
|
|
||||||
// // - The subflow template info goes into help
|
|
||||||
// helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
|
|
||||||
// } else {
|
|
||||||
// helpText = $("script[data-help-name='"+node.type+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
|
|
||||||
// }
|
|
||||||
// setInfoText(helpText, helpSection.content);
|
|
||||||
// }
|
|
||||||
|
|
||||||
var infoText = "";
|
var infoText = "";
|
||||||
|
|
||||||
@ -409,23 +402,6 @@ RED.sidebar.info = (function() {
|
|||||||
}
|
}
|
||||||
var infoSectionContainer = $("<div>").css("padding","0 6px 6px").appendTo(propertiesPanelContent)
|
var infoSectionContainer = $("<div>").css("padding","0 6px 6px").appendTo(propertiesPanelContent)
|
||||||
|
|
||||||
// var editInfo = $('<button class="red-ui-button red-ui-button-small" style="float: right"><i class="fa fa-file-text-o"></button>').appendTo(infoSectionContainer).on("click", function(evt) {
|
|
||||||
// //.text(RED._("sidebar.info.editDescription"))
|
|
||||||
// evt.preventDefault();
|
|
||||||
// evt.stopPropagation();
|
|
||||||
// if (node.type === 'tab') {
|
|
||||||
//
|
|
||||||
// } else if (node.type === 'subflow') {
|
|
||||||
//
|
|
||||||
// } else if (node.type === 'group') {
|
|
||||||
//
|
|
||||||
// } else if (node._def.category !== 'config') {
|
|
||||||
// RED.editor.edit(node,"editor-tab-description");
|
|
||||||
// } else {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
setInfoText(infoText, infoSectionContainer);
|
setInfoText(infoText, infoSectionContainer);
|
||||||
|
|
||||||
$(".red-ui-sidebar-info-stack").scrollTop(0);
|
$(".red-ui-sidebar-info-stack").scrollTop(0);
|
||||||
|
@ -765,6 +765,10 @@ button.red-ui-toggleButton.toggle {
|
|||||||
width: calc(100% - 10px);
|
width: calc(100% - 10px);
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
padding: 0 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
.placeholder-input {
|
.placeholder-input {
|
||||||
span:first-child {
|
span:first-child {
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
|
@ -139,6 +139,9 @@
|
|||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
.red-ui-flow-node-icon-group {
|
.red-ui-flow-node-icon-group {
|
||||||
|
text {
|
||||||
|
@include disable-selection;
|
||||||
|
}
|
||||||
.fa-lg {
|
.fa-lg {
|
||||||
@include disable-selection;
|
@include disable-selection;
|
||||||
stroke: none;
|
stroke: none;
|
||||||
|
@ -99,6 +99,8 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
get: registry.getNodeConstructor,
|
get: registry.getNodeConstructor,
|
||||||
|
|
||||||
|
registerSubflow: registry.registerSubflow,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a node's set information.
|
* Get a node's set information.
|
||||||
*
|
*
|
||||||
|
@ -94,20 +94,6 @@ function checkModulePath(folder) {
|
|||||||
version: moduleVersion
|
version: moduleVersion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkExistingModule(module,version) {
|
|
||||||
var info = registry.getModuleInfo(module);
|
|
||||||
if (info) {
|
|
||||||
if (!version || info.version === version) {
|
|
||||||
var err = new Error("Module already loaded");
|
|
||||||
err.code = "module_already_loaded";
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installModule(module,version,url) {
|
async function installModule(module,version,url) {
|
||||||
if (Buffer.isBuffer(module)) {
|
if (Buffer.isBuffer(module)) {
|
||||||
return installTarball(module)
|
return installTarball(module)
|
||||||
@ -118,6 +104,7 @@ async function installModule(module,version,url) {
|
|||||||
var installName = module;
|
var installName = module;
|
||||||
let isRegistryPackage = true;
|
let isRegistryPackage = true;
|
||||||
var isUpgrade = false;
|
var isUpgrade = false;
|
||||||
|
var isExisting = false;
|
||||||
if (url) {
|
if (url) {
|
||||||
if (pkgurlRe.test(url) || localtgzRe.test(url)) {
|
if (pkgurlRe.test(url) || localtgzRe.test(url)) {
|
||||||
// Git remote url or Tarball url - check the valid package url
|
// Git remote url or Tarball url - check the valid package url
|
||||||
@ -158,7 +145,21 @@ async function installModule(module,version,url) {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isUpgrade = checkExistingModule(module,version);
|
|
||||||
|
var info = registry.getModuleInfo(module);
|
||||||
|
if (info) {
|
||||||
|
if (!info.user) {
|
||||||
|
log.debug(`Installing existing module: ${module}`)
|
||||||
|
isExisting = true;
|
||||||
|
} else if (!version || info.version === version) {
|
||||||
|
var err = new Error("Module already loaded");
|
||||||
|
err.code = "module_already_loaded";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
isUpgrade = true;
|
||||||
|
} else {
|
||||||
|
isUpgrade = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isUpgrade) {
|
if (!isUpgrade) {
|
||||||
log.info(log._("server.install.installing",{name: module,version: version||"latest"}));
|
log.info(log._("server.install.installing",{name: module,version: version||"latest"}));
|
||||||
@ -172,7 +173,17 @@ async function installModule(module,version,url) {
|
|||||||
return exec.run(npmCommand,args,{
|
return exec.run(npmCommand,args,{
|
||||||
cwd: installDir
|
cwd: installDir
|
||||||
}, true).then(result => {
|
}, true).then(result => {
|
||||||
if (!isUpgrade) {
|
if (isExisting) {
|
||||||
|
// This is a module we already have installed as a non-user module.
|
||||||
|
// That means it was discovered when loading, but was not listed
|
||||||
|
// in package.json and has been hidden from the editor.
|
||||||
|
// The user has requested to install this module. Having run
|
||||||
|
// the npm install above, it will now be listed in package.json.
|
||||||
|
// Update the registry to mark it as a user module so it will
|
||||||
|
// be available to the editor.
|
||||||
|
log.info(log._("server.install.installed",{name:module}));
|
||||||
|
return require("./registry").setUserInstalled(module,true).then(reportAddedModules);
|
||||||
|
} else if (!isUpgrade) {
|
||||||
log.info(log._("server.install.installed",{name:module}));
|
log.info(log._("server.install.installed",{name:module}));
|
||||||
return require("./index").addModule(module).then(reportAddedModules);
|
return require("./index").addModule(module).then(reportAddedModules);
|
||||||
} else {
|
} else {
|
||||||
@ -212,7 +223,6 @@ async function installModule(module,version,url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reportAddedModules(info) {
|
function reportAddedModules(info) {
|
||||||
//comms.publish("node/added",info.nodes,false);
|
|
||||||
if (info.nodes.length > 0) {
|
if (info.nodes.length > 0) {
|
||||||
log.info(log._("server.added-types"));
|
log.info(log._("server.added-types"));
|
||||||
for (var i=0;i<info.nodes.length;i++) {
|
for (var i=0;i<info.nodes.length;i++) {
|
||||||
|
@ -158,13 +158,10 @@ async function loadNodeTemplate(node) {
|
|||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
node.types = [];
|
// ENOENT means no html file. We can live with that. But any other error
|
||||||
if (err.code === 'ENOENT') {
|
// should be fatal
|
||||||
if (!node.types) {
|
// node.err = "Error: "+node.template+" does not exist";
|
||||||
node.types = [];
|
if (err.code !== 'ENOENT') {
|
||||||
}
|
|
||||||
node.err = "Error: "+node.template+" does not exist";
|
|
||||||
} else {
|
|
||||||
node.types = [];
|
node.types = [];
|
||||||
node.err = err.toString();
|
node.err = err.toString();
|
||||||
}
|
}
|
||||||
@ -311,15 +308,37 @@ function addModule(module) {
|
|||||||
throw new Error("Settings unavailable");
|
throw new Error("Settings unavailable");
|
||||||
}
|
}
|
||||||
var nodes = [];
|
var nodes = [];
|
||||||
if (registry.getModuleInfo(module)) {
|
var existingInfo = registry.getModuleInfo(module);
|
||||||
|
if (existingInfo) {
|
||||||
// TODO: nls
|
// TODO: nls
|
||||||
var e = new Error("module_already_loaded");
|
var e = new Error("module_already_loaded");
|
||||||
e.code = "module_already_loaded";
|
e.code = "module_already_loaded";
|
||||||
return Promise.reject(e);
|
return Promise.reject(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var moduleFiles = localfilesystem.getModuleFiles(module);
|
var moduleFiles = {};
|
||||||
return loadNodeFiles(moduleFiles);
|
var moduleStack = [module];
|
||||||
|
while(moduleStack.length > 0) {
|
||||||
|
var moduleToLoad = moduleStack.shift();
|
||||||
|
var files = localfilesystem.getModuleFiles(moduleToLoad);
|
||||||
|
if (files[moduleToLoad]) {
|
||||||
|
moduleFiles[moduleToLoad] = files[moduleToLoad];
|
||||||
|
if (moduleFiles[moduleToLoad].dependencies) {
|
||||||
|
log.debug(`Loading dependencies for ${module}`)
|
||||||
|
for (var i=0; i<moduleFiles[moduleToLoad].dependencies.length; i++) {
|
||||||
|
var dep = moduleFiles[moduleToLoad].dependencies[i]
|
||||||
|
if (!registry.getModuleInfo(dep)) {
|
||||||
|
log.debug(` - load ${dep}`)
|
||||||
|
moduleStack.push(dep);
|
||||||
|
} else {
|
||||||
|
log.debug(` - already loaded ${dep}`)
|
||||||
|
registry.addModuleDependency(dep,moduleToLoad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loadNodeFiles(moduleFiles).then(() => module)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ let loadDenyList = [];
|
|||||||
var settings;
|
var settings;
|
||||||
var disableNodePathScan = false;
|
var disableNodePathScan = false;
|
||||||
var iconFileExtensions = [".png", ".gif", ".svg"];
|
var iconFileExtensions = [".png", ".gif", ".svg"];
|
||||||
|
var packageList = {};
|
||||||
|
|
||||||
function init(_settings) {
|
function init(_settings) {
|
||||||
settings = _settings;
|
settings = _settings;
|
||||||
@ -187,9 +188,17 @@ function scanTreeForNodesModules(moduleName) {
|
|||||||
var userDir;
|
var userDir;
|
||||||
|
|
||||||
if (settings.userDir) {
|
if (settings.userDir) {
|
||||||
|
packageList = getPackageList();
|
||||||
userDir = path.join(settings.userDir,"node_modules");
|
userDir = path.join(settings.userDir,"node_modules");
|
||||||
results = scanDirForNodesModules(userDir,moduleName);
|
results = scanDirForNodesModules(userDir,moduleName);
|
||||||
results.forEach(function(r) { r.local = true; });
|
results.forEach(function(r) {
|
||||||
|
// If it was found in <userDir>/node_modules then it is considered
|
||||||
|
// a local module.
|
||||||
|
// Also check to see if it is listed in the package.json file as a user-installed
|
||||||
|
// module. This distinguishes modules installed as a dependency
|
||||||
|
r.local = true;
|
||||||
|
r.user = !!packageList[r.package.name];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dir) {
|
if (dir) {
|
||||||
@ -288,20 +297,19 @@ function getNodeFiles(disableNodePathScan) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeList = {
|
var nodeList = {};
|
||||||
"node-red": {
|
var coreNodeEntry = {
|
||||||
name: "node-red",
|
name: "node-red",
|
||||||
version: settings.version,
|
version: settings.version,
|
||||||
nodes: {},
|
nodes: {},
|
||||||
icons: iconList
|
icons: iconList
|
||||||
}
|
}
|
||||||
}
|
|
||||||
nodeFiles.forEach(function(node) {
|
nodeFiles.forEach(function(node) {
|
||||||
nodeList["node-red"].nodes[node.name] = node;
|
coreNodeEntry.nodes[node.name] = node;
|
||||||
});
|
});
|
||||||
if (settings.coreNodesDir) {
|
if (settings.coreNodesDir) {
|
||||||
var examplesDir = path.join(settings.coreNodesDir,"examples");
|
var examplesDir = path.join(settings.coreNodesDir,"examples");
|
||||||
nodeList["node-red"].examples = {path: examplesDir};
|
coreNodeEntry.examples = {path: examplesDir};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disableNodePathScan) {
|
if (!disableNodePathScan) {
|
||||||
@ -310,7 +318,6 @@ function getNodeFiles(disableNodePathScan) {
|
|||||||
// Filter the module list to ignore global modules
|
// Filter the module list to ignore global modules
|
||||||
// that have also been installed locally - allowing the user to
|
// that have also been installed locally - allowing the user to
|
||||||
// update a module they may not otherwise be able to touch
|
// update a module they may not otherwise be able to touch
|
||||||
|
|
||||||
moduleFiles.sort(function(A,B) {
|
moduleFiles.sort(function(A,B) {
|
||||||
if (A.local && !B.local) {
|
if (A.local && !B.local) {
|
||||||
return -1
|
return -1
|
||||||
@ -323,7 +330,7 @@ function getNodeFiles(disableNodePathScan) {
|
|||||||
moduleFiles = moduleFiles.filter(function(mod) {
|
moduleFiles = moduleFiles.filter(function(mod) {
|
||||||
var result;
|
var result;
|
||||||
if (!knownModules[mod.package.name]) {
|
if (!knownModules[mod.package.name]) {
|
||||||
knownModules[mod.package.name] = true;
|
knownModules[mod.package.name] = mod;
|
||||||
result = true;
|
result = true;
|
||||||
} else {
|
} else {
|
||||||
result = false;
|
result = false;
|
||||||
@ -332,48 +339,62 @@ function getNodeFiles(disableNodePathScan) {
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
moduleFiles.forEach(function(moduleFile) {
|
// Do a second pass to check we have all the declared node dependencies
|
||||||
var nodeModuleFiles = getModuleNodeFiles(moduleFile);
|
// As this is only done as part of the initial palette load, `knownModules` will
|
||||||
nodeList[moduleFile.package.name] = {
|
// contain a list of everything discovered during this phase. This means
|
||||||
name: moduleFile.package.name,
|
// we can check for missing dependencies here.
|
||||||
version: moduleFile.package.version,
|
moduleFiles = moduleFiles.filter(function(mod) {
|
||||||
path: moduleFile.dir,
|
if (Array.isArray(mod.package["node-red"].dependencies)) {
|
||||||
local: moduleFile.local||false,
|
const deps = mod.package["node-red"].dependencies;
|
||||||
nodes: {},
|
const missingDeps = mod.package["node-red"].dependencies.filter(dep => {
|
||||||
icons: nodeModuleFiles.icons,
|
if (knownModules[dep]) {
|
||||||
examples: nodeModuleFiles.examples
|
knownModules[dep].usedBy = knownModules[dep].usedBy || [];
|
||||||
};
|
knownModules[dep].usedBy.push(mod.package.name)
|
||||||
if (moduleFile.package['node-red'].version) {
|
} else {
|
||||||
nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;
|
return true;
|
||||||
}
|
}
|
||||||
nodeModuleFiles.files.forEach(function(node) {
|
})
|
||||||
node.local = moduleFile.local||false;
|
if (missingDeps.length > 0) {
|
||||||
nodeList[moduleFile.package.name].nodes[node.name] = node;
|
log.error(`Module: ${mod.package.name} missing dependencies:`);
|
||||||
});
|
missingDeps.forEach(m => { log.error(` - ${m}`)});
|
||||||
nodeFiles = nodeFiles.concat(nodeModuleFiles.files);
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
nodeList = convertModuleFileListToObject(moduleFiles);
|
||||||
} else {
|
} else {
|
||||||
// console.log("node path scan disabled");
|
// console.log("node path scan disabled");
|
||||||
}
|
}
|
||||||
|
nodeList["node-red"] = coreNodeEntry;
|
||||||
return nodeList;
|
return nodeList;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModuleFiles(module) {
|
function getModuleFiles(module) {
|
||||||
var nodeList = {};
|
// Update the package list
|
||||||
|
|
||||||
var moduleFiles = scanTreeForNodesModules(module);
|
var moduleFiles = scanTreeForNodesModules(module);
|
||||||
if (moduleFiles.length === 0) {
|
if (moduleFiles.length === 0) {
|
||||||
var err = new Error(log._("nodes.registry.localfilesystem.module-not-found", {module:module}));
|
var err = new Error(log._("nodes.registry.localfilesystem.module-not-found", {module:module}));
|
||||||
err.code = 'MODULE_NOT_FOUND';
|
err.code = 'MODULE_NOT_FOUND';
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
// Unlike when doing the initial palette load, this call cannot verify the
|
||||||
|
// dependencies of the new module as it doesn't have visiblity of what
|
||||||
|
// is in the registry. That will have to be done be the caller in loader.js
|
||||||
|
return convertModuleFileListToObject(moduleFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertModuleFileListToObject(moduleFiles) {
|
||||||
|
const nodeList = {};
|
||||||
moduleFiles.forEach(function(moduleFile) {
|
moduleFiles.forEach(function(moduleFile) {
|
||||||
|
|
||||||
var nodeModuleFiles = getModuleNodeFiles(moduleFile);
|
var nodeModuleFiles = getModuleNodeFiles(moduleFile);
|
||||||
nodeList[moduleFile.package.name] = {
|
nodeList[moduleFile.package.name] = {
|
||||||
name: moduleFile.package.name,
|
name: moduleFile.package.name,
|
||||||
version: moduleFile.package.version,
|
version: moduleFile.package.version,
|
||||||
path: moduleFile.dir,
|
path: moduleFile.dir,
|
||||||
|
local: moduleFile.local||false,
|
||||||
|
user: moduleFile.user||false,
|
||||||
nodes: {},
|
nodes: {},
|
||||||
icons: nodeModuleFiles.icons,
|
icons: nodeModuleFiles.icons,
|
||||||
examples: nodeModuleFiles.examples
|
examples: nodeModuleFiles.examples
|
||||||
@ -381,7 +402,14 @@ function getModuleFiles(module) {
|
|||||||
if (moduleFile.package['node-red'].version) {
|
if (moduleFile.package['node-red'].version) {
|
||||||
nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;
|
nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;
|
||||||
}
|
}
|
||||||
|
if (moduleFile.package['node-red'].dependencies) {
|
||||||
|
nodeList[moduleFile.package.name].dependencies = moduleFile.package['node-red'].dependencies;
|
||||||
|
}
|
||||||
|
if (moduleFile.usedBy) {
|
||||||
|
nodeList[moduleFile.package.name].usedBy = moduleFile.usedBy;
|
||||||
|
}
|
||||||
nodeModuleFiles.files.forEach(function(node) {
|
nodeModuleFiles.files.forEach(function(node) {
|
||||||
|
node.local = moduleFile.local||false;
|
||||||
nodeList[moduleFile.package.name].nodes[node.name] = node;
|
nodeList[moduleFile.package.name].nodes[node.name] = node;
|
||||||
nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false;
|
nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false;
|
||||||
});
|
});
|
||||||
@ -412,6 +440,23 @@ function scanIconDir(dir) {
|
|||||||
})
|
})
|
||||||
return iconList;
|
return iconList;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Gets the list of modules installed in this runtime as reported by package.json
|
||||||
|
* Note: these may include non-Node-RED modules
|
||||||
|
*/
|
||||||
|
function getPackageList() {
|
||||||
|
var list = {};
|
||||||
|
if (settings.userDir) {
|
||||||
|
try {
|
||||||
|
var userPackage = path.join(settings.userDir,"package.json");
|
||||||
|
var pkg = JSON.parse(fs.readFileSync(userPackage,"utf-8"));
|
||||||
|
return pkg.dependencies;
|
||||||
|
} catch(err) {
|
||||||
|
log.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init: init,
|
init: init,
|
||||||
|
@ -20,6 +20,7 @@ var fs = require("fs");
|
|||||||
|
|
||||||
var library = require("./library");
|
var library = require("./library");
|
||||||
const {events} = require("@node-red/util")
|
const {events} = require("@node-red/util")
|
||||||
|
var subflows = require("./subflow");
|
||||||
var settings;
|
var settings;
|
||||||
var loader;
|
var loader;
|
||||||
|
|
||||||
@ -27,6 +28,8 @@ var nodeConfigCache = {};
|
|||||||
var moduleConfigs = {};
|
var moduleConfigs = {};
|
||||||
var nodeList = [];
|
var nodeList = [];
|
||||||
var nodeConstructors = {};
|
var nodeConstructors = {};
|
||||||
|
var subflowModules = {};
|
||||||
|
|
||||||
var nodeTypeToId = {};
|
var nodeTypeToId = {};
|
||||||
var moduleNodes = {};
|
var moduleNodes = {};
|
||||||
|
|
||||||
@ -36,6 +39,7 @@ function init(_settings,_loader) {
|
|||||||
moduleNodes = {};
|
moduleNodes = {};
|
||||||
nodeTypeToId = {};
|
nodeTypeToId = {};
|
||||||
nodeConstructors = {};
|
nodeConstructors = {};
|
||||||
|
subflowModules = {};
|
||||||
nodeList = [];
|
nodeList = [];
|
||||||
nodeConfigCache = {};
|
nodeConfigCache = {};
|
||||||
}
|
}
|
||||||
@ -54,7 +58,8 @@ function filterNodeInfo(n) {
|
|||||||
name: n.name,
|
name: n.name,
|
||||||
types: n.types,
|
types: n.types,
|
||||||
enabled: n.enabled,
|
enabled: n.enabled,
|
||||||
local: n.local||false
|
local: n.local||false,
|
||||||
|
user: n.user || false
|
||||||
};
|
};
|
||||||
if (n.hasOwnProperty("module")) {
|
if (n.hasOwnProperty("module")) {
|
||||||
r.module = n.module;
|
r.module = n.module;
|
||||||
@ -90,6 +95,7 @@ function saveNodeList() {
|
|||||||
name: module,
|
name: module,
|
||||||
version: moduleConfigs[module].version,
|
version: moduleConfigs[module].version,
|
||||||
local: moduleConfigs[module].local||false,
|
local: moduleConfigs[module].local||false,
|
||||||
|
user: moduleConfigs[module].user||false,
|
||||||
nodes: {}
|
nodes: {}
|
||||||
};
|
};
|
||||||
if (moduleConfigs[module].hasOwnProperty('pending_version')) {
|
if (moduleConfigs[module].hasOwnProperty('pending_version')) {
|
||||||
@ -175,6 +181,7 @@ function loadNodeConfigs() {
|
|||||||
function addModule(module) {
|
function addModule(module) {
|
||||||
moduleNodes[module.name] = [];
|
moduleNodes[module.name] = [];
|
||||||
moduleConfigs[module.name] = module;
|
moduleConfigs[module.name] = module;
|
||||||
|
// console.log("registry.js.addModule",module.name,"user?",module.user,"usedBy",module.usedBy,"dependencies",module.dependencies)
|
||||||
for (var setName in module.nodes) {
|
for (var setName in module.nodes) {
|
||||||
if (module.nodes.hasOwnProperty(setName)) {
|
if (module.nodes.hasOwnProperty(setName)) {
|
||||||
var set = module.nodes[setName];
|
var set = module.nodes[setName];
|
||||||
@ -225,6 +232,7 @@ function removeNode(id) {
|
|||||||
config.types.forEach(function(t) {
|
config.types.forEach(function(t) {
|
||||||
var typeId = nodeTypeToId[t];
|
var typeId = nodeTypeToId[t];
|
||||||
if (typeId === id) {
|
if (typeId === id) {
|
||||||
|
delete subflowModules[t];
|
||||||
delete nodeConstructors[t];
|
delete nodeConstructors[t];
|
||||||
delete nodeTypeToId[t];
|
delete nodeTypeToId[t];
|
||||||
}
|
}
|
||||||
@ -235,21 +243,47 @@ function removeNode(id) {
|
|||||||
return filterNodeInfo(config);
|
return filterNodeInfo(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeModule(module) {
|
function removeModule(name,skipSave) {
|
||||||
if (!settings.available()) {
|
if (!settings.available()) {
|
||||||
throw new Error("Settings unavailable");
|
throw new Error("Settings unavailable");
|
||||||
}
|
}
|
||||||
var nodes = moduleNodes[module];
|
|
||||||
if (!nodes) {
|
|
||||||
throw new Error("Unrecognised module: "+module);
|
|
||||||
}
|
|
||||||
var infoList = [];
|
var infoList = [];
|
||||||
for (var i=0;i<nodes.length;i++) {
|
var module = moduleConfigs[name];
|
||||||
infoList.push(removeNode(module+"/"+nodes[i]));
|
var nodes = moduleNodes[name];
|
||||||
|
if (!nodes) {
|
||||||
|
throw new Error("Unrecognised module: "+name);
|
||||||
}
|
}
|
||||||
delete moduleNodes[module];
|
if (module.usedBy && module.usedBy > 0) {
|
||||||
delete moduleConfigs[module];
|
// We are removing a module that is used by other modules... so whilst
|
||||||
|
// this module should be removed from the editor palette, it needs to
|
||||||
|
// stay in the runtime... for now.
|
||||||
|
module.user = false;
|
||||||
|
for (var i=0;i<nodes.length;i++) {
|
||||||
|
infoList.push(filterNodeInfo(nodes[i]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (module.dependencies) {
|
||||||
|
module.dependencies.forEach(function(dep) {
|
||||||
|
// Check each dependency of this module to see if it is a non-user-installed
|
||||||
|
// module that we can expect to disappear once npm uninstall is run
|
||||||
|
if (!moduleConfigs[dep].user) {
|
||||||
|
moduleConfigs[dep].usedBy = moduleConfigs[dep].usedBy.filter(m => m !== name);
|
||||||
|
if (moduleConfigs[dep].usedBy.length === 0) {
|
||||||
|
// Remove the dependency
|
||||||
|
removeModule(dep,true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (var i=0;i<nodes.length;i++) {
|
||||||
|
infoList.push(removeNode(name+"/"+nodes[i]));
|
||||||
|
}
|
||||||
|
delete moduleNodes[name];
|
||||||
|
delete moduleConfigs[name];
|
||||||
|
}
|
||||||
|
if (!skipSave) {
|
||||||
saveNodeList();
|
saveNodeList();
|
||||||
|
}
|
||||||
return infoList;
|
return infoList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,6 +336,9 @@ function getNodeList(filter) {
|
|||||||
for (var module in moduleConfigs) {
|
for (var module in moduleConfigs) {
|
||||||
/* istanbul ignore else */
|
/* istanbul ignore else */
|
||||||
if (moduleConfigs.hasOwnProperty(module)) {
|
if (moduleConfigs.hasOwnProperty(module)) {
|
||||||
|
if (!moduleConfigs[module].user && (moduleConfigs[module].usedBy && moduleConfigs[module].usedBy.length > 0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var nodes = moduleConfigs[module].nodes;
|
var nodes = moduleConfigs[module].nodes;
|
||||||
for (var node in nodes) {
|
for (var node in nodes) {
|
||||||
/* istanbul ignore else */
|
/* istanbul ignore else */
|
||||||
@ -341,9 +378,13 @@ function getModuleInfo(module) {
|
|||||||
name: module,
|
name: module,
|
||||||
version: moduleConfigs[module].version,
|
version: moduleConfigs[module].version,
|
||||||
local: moduleConfigs[module].local,
|
local: moduleConfigs[module].local,
|
||||||
|
user: moduleConfigs[module].user,
|
||||||
path: moduleConfigs[module].path,
|
path: moduleConfigs[module].path,
|
||||||
nodes: []
|
nodes: []
|
||||||
};
|
};
|
||||||
|
if (moduleConfigs[module].dependencies) {
|
||||||
|
m.dependencies = moduleConfigs[module].dependencies;
|
||||||
|
}
|
||||||
if (moduleConfigs[module] && moduleConfigs[module].pending_version) {
|
if (moduleConfigs[module] && moduleConfigs[module].pending_version) {
|
||||||
m.pending_version = moduleConfigs[module].pending_version;
|
m.pending_version = moduleConfigs[module].pending_version;
|
||||||
}
|
}
|
||||||
@ -393,13 +434,40 @@ function registerNodeConstructor(nodeSet,type,constructor) {
|
|||||||
events.emit("type-registered",type);
|
events.emit("type-registered",type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerSubflow(nodeSet, subflow) {
|
||||||
|
var nodeSetInfo = getFullNodeInfo(nodeSet);
|
||||||
|
|
||||||
|
const result = subflows.register(nodeSet,subflow);
|
||||||
|
|
||||||
|
if (subflowModules.hasOwnProperty(result.type)) {
|
||||||
|
throw new Error(result.type+" already registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeSetInfo) {
|
||||||
|
if (nodeSetInfo.types.indexOf(result.type) === -1) {
|
||||||
|
nodeSetInfo.types.push(result.type);
|
||||||
|
nodeTypeToId[result.type] = nodeSetInfo.id;
|
||||||
|
}
|
||||||
|
nodeSetInfo.config = result.config;
|
||||||
|
}
|
||||||
|
subflowModules[result.type] = result;
|
||||||
|
events.emit("type-registered",result.type);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function getAllNodeConfigs(lang) {
|
function getAllNodeConfigs(lang) {
|
||||||
if (!nodeConfigCache[lang]) {
|
if (!nodeConfigCache[lang]) {
|
||||||
var result = "";
|
var result = "";
|
||||||
var script = "";
|
var script = "";
|
||||||
for (var i=0;i<nodeList.length;i++) {
|
for (var i=0;i<nodeList.length;i++) {
|
||||||
var id = nodeList[i];
|
var id = nodeList[i];
|
||||||
var config = moduleConfigs[getModule(id)].nodes[getNode(id)];
|
|
||||||
|
var module = moduleConfigs[getModule(id)]
|
||||||
|
if (!module.user && (module.usedBy && module.usedBy.length > 0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = module.nodes[getNode(id)];
|
||||||
if (config.enabled && !config.err) {
|
if (config.enabled && !config.err) {
|
||||||
result += "\n<!-- --- [red-module:"+id+"] --- -->\n";
|
result += "\n<!-- --- [red-module:"+id+"] --- -->\n";
|
||||||
result += config.config;
|
result += config.config;
|
||||||
@ -447,7 +515,7 @@ function getNodeConstructor(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!config || (config.enabled && !config.err)) {
|
if (!config || (config.enabled && !config.err)) {
|
||||||
return nodeConstructors[type];
|
return nodeConstructors[type] || subflowModules[type];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -457,6 +525,7 @@ function clear() {
|
|||||||
moduleConfigs = {};
|
moduleConfigs = {};
|
||||||
nodeList = [];
|
nodeList = [];
|
||||||
nodeConstructors = {};
|
nodeConstructors = {};
|
||||||
|
subflowModules = {};
|
||||||
nodeTypeToId = {};
|
nodeTypeToId = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,6 +629,17 @@ function setModulePendingUpdated(module,version) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setUserInstalled(module,userInstalled) {
|
||||||
|
moduleConfigs[module].user = userInstalled;
|
||||||
|
return saveNodeList().then(function() {
|
||||||
|
return getModuleInfo(module);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function addModuleDependency(module,usedBy) {
|
||||||
|
moduleConfigs[module].usedBy = moduleConfigs[module].usedBy || [];
|
||||||
|
moduleConfigs[module].usedBy.push(usedBy);
|
||||||
|
}
|
||||||
|
|
||||||
var icon_paths = { };
|
var icon_paths = { };
|
||||||
var iconCache = {};
|
var iconCache = {};
|
||||||
|
|
||||||
@ -613,6 +693,7 @@ var registry = module.exports = {
|
|||||||
registerNodeConstructor: registerNodeConstructor,
|
registerNodeConstructor: registerNodeConstructor,
|
||||||
getNodeConstructor: getNodeConstructor,
|
getNodeConstructor: getNodeConstructor,
|
||||||
|
|
||||||
|
registerSubflow: registerSubflow,
|
||||||
|
|
||||||
addModule: addModule,
|
addModule: addModule,
|
||||||
|
|
||||||
@ -620,6 +701,9 @@ var registry = module.exports = {
|
|||||||
disableNodeSet: disableNodeSet,
|
disableNodeSet: disableNodeSet,
|
||||||
|
|
||||||
setModulePendingUpdated: setModulePendingUpdated,
|
setModulePendingUpdated: setModulePendingUpdated,
|
||||||
|
setUserInstalled: setUserInstalled,
|
||||||
|
addModuleDependency:addModuleDependency,
|
||||||
|
|
||||||
removeModule: removeModule,
|
removeModule: removeModule,
|
||||||
|
|
||||||
getNodeInfo: getNodeInfo,
|
getNodeInfo: getNodeInfo,
|
||||||
|
127
packages/node_modules/@node-red/registry/lib/subflow.js
vendored
Normal file
127
packages/node_modules/@node-red/registry/lib/subflow.js
vendored
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
function getSubflowType(subflow) {
|
||||||
|
if (subflow.meta && subflow.meta.type) {
|
||||||
|
return subflow.meta.type
|
||||||
|
}
|
||||||
|
return "sf:"+subflow.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSubflowConfig(subflow) {
|
||||||
|
|
||||||
|
const subflowType = getSubflowType(subflow)
|
||||||
|
const label = subflow.name || subflowType;
|
||||||
|
const category = subflow.category || "function";
|
||||||
|
const color = subflow.color || "#C0DEED";
|
||||||
|
const inputCount = subflow.in?subflow.in.length:0;
|
||||||
|
const outputCount = subflow.out?subflow.out.length:0;
|
||||||
|
const icon = subflow.icon || "arrow-in.svg";
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
name: {value: ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = {}
|
||||||
|
|
||||||
|
if (subflow.env) {
|
||||||
|
subflow.env.forEach(prop => {
|
||||||
|
var defaultValue;
|
||||||
|
|
||||||
|
switch(prop.type) {
|
||||||
|
case "cred": defaultValue = ""; break;
|
||||||
|
case "str": defaultValue = prop.value||""; break;
|
||||||
|
case "bool": defaultValue = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
|
||||||
|
case "num": defaultValue = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
|
||||||
|
default:
|
||||||
|
defaultValue = {
|
||||||
|
type: prop.type,
|
||||||
|
value: prop.value||""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
defaults[prop.name] = {
|
||||||
|
value: defaultValue,
|
||||||
|
ui: prop.ui
|
||||||
|
}
|
||||||
|
if (prop.type === 'cred') {
|
||||||
|
defaults[prop.name].ui.type = "cred";
|
||||||
|
credentials[prop.name] = {type:"password"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const defaultString = JSON.stringify(defaults);
|
||||||
|
const credentialsString = JSON.stringify(credentials);
|
||||||
|
|
||||||
|
let nodeHelp = "";
|
||||||
|
if (subflow.info) {
|
||||||
|
nodeHelp = `<script type="text/markdown" data-help-name="${subflowType}">${subflow.info}</script>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<script type="text/javascript">
|
||||||
|
RED.nodes.registerType("${subflowType}",{
|
||||||
|
subflowModule: true,
|
||||||
|
category: "${category}",
|
||||||
|
color: "${color}",
|
||||||
|
defaults: ${defaultString},
|
||||||
|
credentials: ${credentialsString},
|
||||||
|
inputs:${inputCount},
|
||||||
|
outputs:${outputCount},
|
||||||
|
icon: "${icon}",
|
||||||
|
paletteLabel: "${label}",
|
||||||
|
label: function() {
|
||||||
|
return this.name||"${label}";
|
||||||
|
},
|
||||||
|
labelStyle: function() {
|
||||||
|
return this.name?"node_label_italic":"";
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
RED.subflow.buildEditForm('subflow', this);
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
var props = RED.subflow.exportSubflowInstanceEnv(this);
|
||||||
|
var i=0,l=props.length;
|
||||||
|
for (;i<l;i++) {
|
||||||
|
var prop = props[i];
|
||||||
|
if (this._def.defaults[prop.name].ui && this._def.defaults[prop.name].ui.type === "cred") {
|
||||||
|
this[prop.name] = "";
|
||||||
|
this.credentials[prop.name] = prop.value || "";
|
||||||
|
this.credentials['has_' + prop.name] = (this.credentials[prop.name] !== "");
|
||||||
|
} else {
|
||||||
|
switch(prop.type) {
|
||||||
|
case "str": this[prop.name] = prop.value||""; break;
|
||||||
|
case "bool": this[prop.name] = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
|
||||||
|
case "num": this[prop.name] = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
|
||||||
|
default:
|
||||||
|
this[prop.name] = {
|
||||||
|
type: prop.type,
|
||||||
|
value: prop.value||""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/x-red" data-template-name="${subflowType}">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>
|
||||||
|
<input type="text" id="node-input-name" data-i18n="[placeholder]editor:common.label.name">
|
||||||
|
</div>
|
||||||
|
<div id="subflow-input-ui"></div>
|
||||||
|
</script>
|
||||||
|
${nodeHelp}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function register(id,subflow) {
|
||||||
|
return {
|
||||||
|
subflow: subflow,
|
||||||
|
type: getSubflowType(subflow),
|
||||||
|
config: generateSubflowConfig(subflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register: register
|
||||||
|
}
|
@ -83,6 +83,9 @@ function createNodeApi(node) {
|
|||||||
red.nodes.registerType = function(type,constructor,opts) {
|
red.nodes.registerType = function(type,constructor,opts) {
|
||||||
runtime.nodes.registerType(node.id,type,constructor,opts);
|
runtime.nodes.registerType(node.id,type,constructor,opts);
|
||||||
}
|
}
|
||||||
|
red.nodes.registerSubflow = function(subflowDef) {
|
||||||
|
runtime.nodes.registerSubflow(node.id,subflowDef)
|
||||||
|
}
|
||||||
copyObjectProperties(log,red.log,null,["init"]);
|
copyObjectProperties(log,red.log,null,["init"]);
|
||||||
copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]);
|
copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]);
|
||||||
if (runtime.adminApi) {
|
if (runtime.adminApi) {
|
||||||
|
@ -194,7 +194,7 @@ var api = module.exports = {
|
|||||||
}
|
}
|
||||||
if (opts.module) {
|
if (opts.module) {
|
||||||
var existingModule = runtime.nodes.getModuleInfo(opts.module);
|
var existingModule = runtime.nodes.getModuleInfo(opts.module);
|
||||||
if (existingModule) {
|
if (existingModule && existingModule.user) {
|
||||||
if (!opts.version || existingModule.version === opts.version) {
|
if (!opts.version || existingModule.version === opts.version) {
|
||||||
runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"}, opts.req);
|
runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"}, opts.req);
|
||||||
var err = new Error("Module already loaded");
|
var err = new Error("Module already loaded");
|
||||||
|
@ -93,7 +93,7 @@ class Flow {
|
|||||||
* @param {[type]} msg [description]
|
* @param {[type]} msg [description]
|
||||||
* @return {[type]} [description]
|
* @return {[type]} [description]
|
||||||
*/
|
*/
|
||||||
log(msg) {
|
info(msg) {
|
||||||
Log.log({
|
Log.log({
|
||||||
id: this.id||"global",
|
id: this.id||"global",
|
||||||
level: Log.INFO,
|
level: Log.INFO,
|
||||||
@ -116,6 +116,17 @@ class Flow {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [log description]
|
||||||
|
* @param {[type]} msg [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
log(msg) {
|
||||||
|
if (!msg.path) {
|
||||||
|
msg.path = this.path;
|
||||||
|
}
|
||||||
|
this.parent.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start this flow.
|
* Start this flow.
|
||||||
@ -303,22 +314,14 @@ class Flow {
|
|||||||
if (node) {
|
if (node) {
|
||||||
delete this.activeNodes[stopList[i]];
|
delete this.activeNodes[stopList[i]];
|
||||||
if (this.subflowInstanceNodes[stopList[i]]) {
|
if (this.subflowInstanceNodes[stopList[i]]) {
|
||||||
try {
|
|
||||||
(function(subflow) {
|
|
||||||
promises.push(stopNode(node,false).then(() => subflow.stop()));
|
|
||||||
})(this.subflowInstanceNodes[stopList[i]]);
|
|
||||||
} catch(err) {
|
|
||||||
node.error(err);
|
|
||||||
}
|
|
||||||
delete this.subflowInstanceNodes[stopList[i]];
|
delete this.subflowInstanceNodes[stopList[i]];
|
||||||
} else {
|
}
|
||||||
try {
|
try {
|
||||||
var removed = removedMap[stopList[i]];
|
var removed = removedMap[stopList[i]];
|
||||||
promises.push(stopNode(node,removed).catch(()=>{}));
|
promises.push(stopNode(node,removed).catch(()=>{}));
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
node.error(err);
|
node.error(err);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (removedMap[stopList[i]]) {
|
if (removedMap[stopList[i]]) {
|
||||||
events.emit("node-status",{
|
events.emit("node-status",{
|
||||||
id: node.id
|
id: node.id
|
||||||
|
@ -208,7 +208,12 @@ class Subflow extends Flow {
|
|||||||
|
|
||||||
this.node = new Node(subflowInstanceConfig);
|
this.node = new Node(subflowInstanceConfig);
|
||||||
this.node.on("input", function(msg) { this.send(msg);});
|
this.node.on("input", function(msg) { this.send(msg);});
|
||||||
this.node.on("close", function() { this.status({}); })
|
// Called when the subflow instance node is being stopped
|
||||||
|
this.node.on("close", function(done) {
|
||||||
|
this.status({});
|
||||||
|
// Stop the complete subflow
|
||||||
|
self.stop().finally(done)
|
||||||
|
})
|
||||||
this.node.status = status => this.parent.handleStatus(this.node,status);
|
this.node.status = status => this.parent.handleStatus(this.node,status);
|
||||||
// Create a context instance
|
// Create a context instance
|
||||||
// console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z)
|
// console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z)
|
||||||
@ -499,11 +504,49 @@ function remapSubflowNodes(nodes,nodeMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SubflowModule extends Subflow {
|
||||||
|
/**
|
||||||
|
* Create a Subflow Module object.
|
||||||
|
* This is a node that has been published as a subflow.
|
||||||
|
* @param {[type]} parent [description]
|
||||||
|
* @param {[type]} globalFlow [description]
|
||||||
|
* @param {[type]} subflowDef [description]
|
||||||
|
* @param {[type]} subflowInstance [description]
|
||||||
|
*/
|
||||||
|
constructor(type, parent,globalFlow,subflowDef,subflowInstance) {
|
||||||
|
super(parent,globalFlow,subflowDef,subflowInstance);
|
||||||
|
this.TYPE = `module:${type}`;
|
||||||
|
this.subflowType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [log description]
|
||||||
|
* @param {[type]} msg [description]
|
||||||
|
* @return {[type]} [description]
|
||||||
|
*/
|
||||||
|
log(msg) {
|
||||||
|
if (msg.id) {
|
||||||
|
msg.id = this.id
|
||||||
|
}
|
||||||
|
if (msg.type) {
|
||||||
|
msg.type = this.subflowType
|
||||||
|
}
|
||||||
|
super.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
|
function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
|
||||||
return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
|
return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createModuleInstance(type, parent,globalFlow,subflowDef,subflowInstance) {
|
||||||
|
return new SubflowModule(type, parent,globalFlow,subflowDef,subflowInstance);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init: function(runtime) {},
|
init: function(runtime) {},
|
||||||
create: createSubflow
|
create: createSubflow,
|
||||||
|
createModuleInstance: createModuleInstance
|
||||||
}
|
}
|
||||||
|
@ -698,7 +698,8 @@ const flowAPI = {
|
|||||||
getNode: getNode,
|
getNode: getNode,
|
||||||
handleError: () => false,
|
handleError: () => false,
|
||||||
handleStatus: () => false,
|
handleStatus: () => false,
|
||||||
getSetting: k => flowUtil.getEnvVar(k)
|
getSetting: k => flowUtil.getEnvVar(k),
|
||||||
|
log: m => log.log(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ var Log = require("@node-red/util").log;
|
|||||||
var subflowInstanceRE = /^subflow:(.+)$/;
|
var subflowInstanceRE = /^subflow:(.+)$/;
|
||||||
var typeRegistry = require("@node-red/registry");
|
var typeRegistry = require("@node-red/registry");
|
||||||
|
|
||||||
|
|
||||||
var envVarExcludes = {};
|
var envVarExcludes = {};
|
||||||
|
|
||||||
function diffNodes(oldNode,newNode) {
|
function diffNodes(oldNode,newNode) {
|
||||||
@ -66,20 +67,78 @@ function mapEnvVarProperties(obj,prop,flow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init: function(runtime) {
|
|
||||||
envVarExcludes = {};
|
|
||||||
if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) {
|
|
||||||
runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getEnvVar: function(k) {
|
|
||||||
return !envVarExcludes[k]?process.env[k]:undefined
|
|
||||||
},
|
|
||||||
diffNodes: diffNodes,
|
|
||||||
mapEnvVarProperties: mapEnvVarProperties,
|
|
||||||
|
|
||||||
parseConfig: function(config) {
|
function createNode(flow,config) {
|
||||||
|
var newNode = null;
|
||||||
|
var type = config.type;
|
||||||
|
try {
|
||||||
|
var nodeTypeConstructor = typeRegistry.get(type);
|
||||||
|
if (typeof nodeTypeConstructor === "function") {
|
||||||
|
var conf = clone(config);
|
||||||
|
delete conf.credentials;
|
||||||
|
for (var p in conf) {
|
||||||
|
if (conf.hasOwnProperty(p)) {
|
||||||
|
mapEnvVarProperties(conf,p,flow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
|
||||||
|
newNode = new nodeTypeConstructor(conf);
|
||||||
|
} catch (err) {
|
||||||
|
Log.log({
|
||||||
|
level: Log.ERROR,
|
||||||
|
id:conf.id,
|
||||||
|
type: type,
|
||||||
|
msg: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (nodeTypeConstructor) {
|
||||||
|
// console.log(nodeTypeConstructor)
|
||||||
|
var subflowConfig = parseConfig([nodeTypeConstructor.subflow].concat(nodeTypeConstructor.subflow.flow));
|
||||||
|
var instanceConfig = clone(config);
|
||||||
|
instanceConfig.env = clone(nodeTypeConstructor.subflow.env);
|
||||||
|
|
||||||
|
instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => {
|
||||||
|
var nodePropType;
|
||||||
|
var nodePropValue = config[nodeProp.name];
|
||||||
|
if (nodeProp.type === "cred") {
|
||||||
|
nodePropType = "cred";
|
||||||
|
} else {
|
||||||
|
switch(typeof config[nodeProp.name]) {
|
||||||
|
case "string": nodePropType = "str"; break;
|
||||||
|
case "number": nodePropType = "num"; break;
|
||||||
|
case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
|
||||||
|
default:
|
||||||
|
nodePropType = config[nodeProp.name].type;
|
||||||
|
nodePropValue = config[nodeProp.name].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: nodeProp.name,
|
||||||
|
type: nodePropType,
|
||||||
|
value: nodePropValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var subflow = require("./Subflow").createModuleInstance(
|
||||||
|
nodeTypeConstructor.type,
|
||||||
|
flow,
|
||||||
|
flow.global,
|
||||||
|
subflowConfig.subflows[nodeTypeConstructor.subflow.id],
|
||||||
|
instanceConfig
|
||||||
|
);
|
||||||
|
subflow.start();
|
||||||
|
return subflow.node;
|
||||||
|
|
||||||
|
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
Log.error(err);
|
||||||
|
}
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfig(config) {
|
||||||
var flow = {};
|
var flow = {};
|
||||||
flow.allNodes = {};
|
flow.allNodes = {};
|
||||||
flow.subflows = {};
|
flow.subflows = {};
|
||||||
@ -195,7 +254,22 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return flow;
|
return flow;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init: function(runtime) {
|
||||||
|
envVarExcludes = {};
|
||||||
|
if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) {
|
||||||
|
runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
getEnvVar: function(k) {
|
||||||
|
return !envVarExcludes[k]?process.env[k]:undefined
|
||||||
|
},
|
||||||
|
diffNodes: diffNodes,
|
||||||
|
mapEnvVarProperties: mapEnvVarProperties,
|
||||||
|
|
||||||
|
parseConfig: parseConfig,
|
||||||
|
|
||||||
diffConfigs: function(oldConfig, newConfig) {
|
diffConfigs: function(oldConfig, newConfig) {
|
||||||
var id;
|
var id;
|
||||||
@ -475,36 +549,5 @@ module.exports = {
|
|||||||
* @param {object} config The node configuration object
|
* @param {object} config The node configuration object
|
||||||
* @return {Node} The instance of the node
|
* @return {Node} The instance of the node
|
||||||
*/
|
*/
|
||||||
createNode: function(flow,config) {
|
createNode: createNode
|
||||||
var newNode = null;
|
|
||||||
var type = config.type;
|
|
||||||
try {
|
|
||||||
var nodeTypeConstructor = typeRegistry.get(type);
|
|
||||||
if (nodeTypeConstructor) {
|
|
||||||
var conf = clone(config);
|
|
||||||
delete conf.credentials;
|
|
||||||
for (var p in conf) {
|
|
||||||
if (conf.hasOwnProperty(p)) {
|
|
||||||
mapEnvVarProperties(conf,p,flow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
|
|
||||||
newNode = new nodeTypeConstructor(conf);
|
|
||||||
} catch (err) {
|
|
||||||
Log.log({
|
|
||||||
level: Log.ERROR,
|
|
||||||
id:conf.id,
|
|
||||||
type: type,
|
|
||||||
msg: err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
Log.error(err);
|
|
||||||
}
|
|
||||||
return newNode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -486,16 +486,14 @@ function log_helper(self, level, msg) {
|
|||||||
if (self._alias) {
|
if (self._alias) {
|
||||||
o._alias = self._alias;
|
o._alias = self._alias;
|
||||||
}
|
}
|
||||||
if (self._flow) {
|
|
||||||
o.path = self._flow.path;
|
|
||||||
}
|
|
||||||
if (self.z) {
|
if (self.z) {
|
||||||
o.z = self.z;
|
o.z = self.z;
|
||||||
}
|
}
|
||||||
if (self.name) {
|
if (self.name) {
|
||||||
o.name = self.name;
|
o.name = self.name;
|
||||||
}
|
}
|
||||||
Log.log(o);
|
self._flow.log(o);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Log an INFO level message
|
* Log an INFO level message
|
||||||
|
@ -112,6 +112,25 @@ function createNode(node,def) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerSubflow(nodeSet, subflow) {
|
||||||
|
// TODO: extract credentials definition from subflow properties
|
||||||
|
var registeredType = registry.registerSubflow(nodeSet,subflow);
|
||||||
|
|
||||||
|
if (subflow.env) {
|
||||||
|
var creds = {};
|
||||||
|
var hasCreds = false;
|
||||||
|
subflow.env.forEach(e => {
|
||||||
|
if (e.type === "cred") {
|
||||||
|
creds[e.name] = {type: "password"};
|
||||||
|
hasCreds = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (hasCreds) {
|
||||||
|
credentials.register(registeredType.type,creds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function init(runtime) {
|
function init(runtime) {
|
||||||
settings = runtime.settings;
|
settings = runtime.settings;
|
||||||
log = runtime.log;
|
log = runtime.log;
|
||||||
@ -162,11 +181,12 @@ function installModule(module,version,url) {
|
|||||||
|
|
||||||
function uninstallModule(module) {
|
function uninstallModule(module) {
|
||||||
var info = registry.getModuleInfo(module);
|
var info = registry.getModuleInfo(module);
|
||||||
if (!info) {
|
if (!info || !info.user) {
|
||||||
throw new Error(log._("nodes.index.unrecognised-module", {module:module}));
|
throw new Error(log._("nodes.index.unrecognised-module", {module:module}));
|
||||||
} else {
|
} else {
|
||||||
for (var i=0;i<info.nodes.length;i++) {
|
var nodeTypesToCheck = info.nodes.map(n => `${module}/${n.name}`);
|
||||||
flows.checkTypeInUse(module+"/"+info.nodes[i].name);
|
for (var i=0;i<nodeTypesToCheck.length;i++) {
|
||||||
|
flows.checkTypeInUse(nodeTypesToCheck[i]);
|
||||||
}
|
}
|
||||||
return registry.uninstallModule(module).then(function(list) {
|
return registry.uninstallModule(module).then(function(list) {
|
||||||
events.emit("runtime-event",{id:"node/removed",retain:false,payload:list});
|
events.emit("runtime-event",{id:"node/removed",retain:false,payload:list});
|
||||||
@ -196,6 +216,7 @@ module.exports = {
|
|||||||
|
|
||||||
// Node type registry
|
// Node type registry
|
||||||
registerType: registerType,
|
registerType: registerType,
|
||||||
|
registerSubflow: registerSubflow,
|
||||||
getType: registry.get,
|
getType: registry.get,
|
||||||
|
|
||||||
getNodeInfo: registry.getNodeInfo,
|
getNodeInfo: registry.getNodeInfo,
|
||||||
|
3
test/resources/subflow/package/README.md
Normal file
3
test/resources/subflow/package/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
curl -H "Content-Type: application/json" --request POST --data '{"url":"/Users/nol/code/node-red/node-red/test/resources/subflow/test-subflow-mod-1.0.0.tgz","module":"test-subflow-mod"}' http://localhost:1880/nodes
|
||||||
|
|
||||||
|
curl --request DELETE http://localhost:1880/nodes/test-subflow-mod
|
18
test/resources/subflow/package/package.json
Normal file
18
test/resources/subflow/package/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "test-subflow-mod",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"license": "ISC",
|
||||||
|
"node-red": {
|
||||||
|
"nodes": {
|
||||||
|
"test-subflow": "subflow.js"
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
"node-red-node-random"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-red-node-random": "*"
|
||||||
|
}
|
||||||
|
}
|
4
test/resources/subflow/package/subflow.js
Normal file
4
test/resources/subflow/package/subflow.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
module.exports = function(RED) {
|
||||||
|
RED.nodes.registerSubflow(JSON.parse(require('fs').readFileSync(require("path").join(__dirname,"subflow.json"))))
|
||||||
|
}
|
268
test/resources/subflow/package/subflow.json
Normal file
268
test/resources/subflow/package/subflow.json
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
{
|
||||||
|
"id": "caf258cc.4e2c48",
|
||||||
|
"type": "subflow",
|
||||||
|
"name": "Test Subflow",
|
||||||
|
"info":"This is my exportable module subflow\n\nI hope this shows as help!",
|
||||||
|
"category": "common",
|
||||||
|
"in": [
|
||||||
|
{
|
||||||
|
"x": 120,
|
||||||
|
"y": 100,
|
||||||
|
"wires": [
|
||||||
|
{
|
||||||
|
"id": "2f1d674f.a02d28"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"out": [
|
||||||
|
{
|
||||||
|
"x": 560,
|
||||||
|
"y": 100,
|
||||||
|
"wires": [
|
||||||
|
{
|
||||||
|
"id": "1497236e.07f12d",
|
||||||
|
"port": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 360,
|
||||||
|
"y": 200,
|
||||||
|
"wires": [
|
||||||
|
{
|
||||||
|
"id": "f4334f5f.4905c",
|
||||||
|
"port": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "FOO",
|
||||||
|
"type": "cred",
|
||||||
|
"ui": {
|
||||||
|
"icon": "font-awesome/fa-thermometer-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BAR",
|
||||||
|
"type": "str",
|
||||||
|
"value": "1",
|
||||||
|
"ui": {
|
||||||
|
"icon": "font-awesome/fa-thermometer-2",
|
||||||
|
"type": "select",
|
||||||
|
"opts": {
|
||||||
|
"opts": [
|
||||||
|
{
|
||||||
|
"l": {
|
||||||
|
"en-US": "option 1"
|
||||||
|
},
|
||||||
|
"v": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"l": {
|
||||||
|
"en-US": "option 2"
|
||||||
|
},
|
||||||
|
"v": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"l": {
|
||||||
|
"en-US": "option 3"
|
||||||
|
},
|
||||||
|
"v": "3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "onewithaverylongname",
|
||||||
|
"type": "str",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BARRY",
|
||||||
|
"type": "bool",
|
||||||
|
"value": "true",
|
||||||
|
"ui": {
|
||||||
|
"icon": "font-awesome/fa-thermometer-4",
|
||||||
|
"type": "checkbox"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WILMA",
|
||||||
|
"type": "num",
|
||||||
|
"value": "10",
|
||||||
|
"ui": {
|
||||||
|
"icon": "font-awesome/fbomb",
|
||||||
|
"type": "spinner"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "awg",
|
||||||
|
"type": "num",
|
||||||
|
"value": "",
|
||||||
|
"ui": {
|
||||||
|
"icon": "font-awesome/fa-address-book-o",
|
||||||
|
"type": "spinner"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "awf",
|
||||||
|
"type": "str",
|
||||||
|
"value": "",
|
||||||
|
"ui": {
|
||||||
|
"type": "select",
|
||||||
|
"opts": {
|
||||||
|
"opts": [
|
||||||
|
{
|
||||||
|
"l": {
|
||||||
|
"en-US": "one"
|
||||||
|
},
|
||||||
|
"v": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"l": {
|
||||||
|
"en-US": "two"
|
||||||
|
},
|
||||||
|
"v": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"l": {
|
||||||
|
"en-US": "three"
|
||||||
|
},
|
||||||
|
"v": "3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aawf",
|
||||||
|
"type": "bool",
|
||||||
|
"value": "true",
|
||||||
|
"ui": {
|
||||||
|
"type": "checkbox"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "awgawgawg",
|
||||||
|
"type": "str",
|
||||||
|
"value": "",
|
||||||
|
"ui": {
|
||||||
|
"type": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "seagseg",
|
||||||
|
"type": "str",
|
||||||
|
"value": "",
|
||||||
|
"ui": {
|
||||||
|
"type": "hide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"type": "fly-a-plane"
|
||||||
|
},
|
||||||
|
"color": "#A6BBCF",
|
||||||
|
"icon": "font-awesome/fa-space-shuttle",
|
||||||
|
"status": {
|
||||||
|
"x": 500,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [
|
||||||
|
{
|
||||||
|
"id": "8252d1cc.54f94",
|
||||||
|
"port": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"flow": [
|
||||||
|
{
|
||||||
|
"id": "2f1d674f.a02d28",
|
||||||
|
"type": "function",
|
||||||
|
"z": "caf258cc.4e2c48",
|
||||||
|
"name": "",
|
||||||
|
"func": "node.error(\"subflow error \"+msg.payload,msg);\nmsg.payload = {\n FOO: env.get(\"FOO\"),\n BAR: env.get(\"BAR\"),\n WILMA: env.get(\"WILMA\"),\n BARRY: env.get(\"BARRY\")\n}\nnode.warn(\"warning\");\n\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"x": 240,
|
||||||
|
"y": 100,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"1497236e.07f12d"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f4334f5f.4905c",
|
||||||
|
"type": "catch",
|
||||||
|
"z": "caf258cc.4e2c48",
|
||||||
|
"name": "",
|
||||||
|
"scope": null,
|
||||||
|
"uncaught": false,
|
||||||
|
"x": 220,
|
||||||
|
"y": 200,
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"8252d1cc.54f94"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8252d1cc.54f94",
|
||||||
|
"type": "change",
|
||||||
|
"z": "caf258cc.4e2c48",
|
||||||
|
"name": "",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"t": "set",
|
||||||
|
"p": "payload",
|
||||||
|
"pt": "msg",
|
||||||
|
"to": "error.message",
|
||||||
|
"tot": "msg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": "",
|
||||||
|
"property": "",
|
||||||
|
"from": "",
|
||||||
|
"to": "",
|
||||||
|
"reg": false,
|
||||||
|
"x": 350,
|
||||||
|
"y": 300,
|
||||||
|
"wires": [
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1497236e.07f12d",
|
||||||
|
"type": "random",
|
||||||
|
"z": "caf258cc.4e2c48",
|
||||||
|
"name": "",
|
||||||
|
"low": "1",
|
||||||
|
"high": "10",
|
||||||
|
"inte": "true",
|
||||||
|
"property": "random",
|
||||||
|
"x": 420,
|
||||||
|
"y": 100,
|
||||||
|
"wires": [
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "876fc49e.f15268",
|
||||||
|
"type": "subflow:caf258cc.4e2c48",
|
||||||
|
"z": "d607ce33.4fa5a",
|
||||||
|
"name": "",
|
||||||
|
"x": 200,
|
||||||
|
"y": 760,
|
||||||
|
"wires": [
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
test/resources/subflow/test-subflow-mod-1.0.1.tgz
Normal file
BIN
test/resources/subflow/test-subflow-mod-1.0.1.tgz
Normal file
Binary file not shown.
@ -131,6 +131,7 @@ describe('nodes/registry/installer', function() {
|
|||||||
it("rejects when update requested to existing version", function(done) {
|
it("rejects when update requested to existing version", function(done) {
|
||||||
sinon.stub(typeRegistry,"getModuleInfo", function() {
|
sinon.stub(typeRegistry,"getModuleInfo", function() {
|
||||||
return {
|
return {
|
||||||
|
user: true,
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -142,6 +143,7 @@ describe('nodes/registry/installer', function() {
|
|||||||
it("rejects when update requested to existing version and url", function(done) {
|
it("rejects when update requested to existing version and url", function(done) {
|
||||||
sinon.stub(typeRegistry,"getModuleInfo", function() {
|
sinon.stub(typeRegistry,"getModuleInfo", function() {
|
||||||
return {
|
return {
|
||||||
|
user: true,
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -414,60 +414,61 @@ describe("red/nodes/registry/loader",function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("load core node files scanned by lfs - missing html file", function(done) {
|
// it("load core node files scanned by lfs - missing html file", function(done) {
|
||||||
stubs.push(sinon.stub(localfilesystem,"getNodeFiles", function(){
|
// // This is now an okay situation
|
||||||
var result = {};
|
// stubs.push(sinon.stub(localfilesystem,"getNodeFiles", function(){
|
||||||
result["node-red"] = {
|
// var result = {};
|
||||||
"name": "node-red",
|
// result["node-red"] = {
|
||||||
"version": "1.2.3",
|
// "name": "node-red",
|
||||||
"nodes": {
|
// "version": "1.2.3",
|
||||||
"DuffNode": {
|
// "nodes": {
|
||||||
"file": path.join(resourcesDir,"DuffNode","DuffNode.js"),
|
// "DuffNode": {
|
||||||
"module": "node-red",
|
// "file": path.join(resourcesDir,"DuffNode","DuffNode.js"),
|
||||||
"name": "DuffNode"
|
// "module": "node-red",
|
||||||
}
|
// "name": "DuffNode"
|
||||||
}
|
// }
|
||||||
};
|
// }
|
||||||
return result;
|
// };
|
||||||
}));
|
// return result;
|
||||||
|
// }));
|
||||||
stubs.push(sinon.stub(registry,"saveNodeList", function(){ return }));
|
//
|
||||||
stubs.push(sinon.stub(registry,"addModule", function(){ return }));
|
// stubs.push(sinon.stub(registry,"saveNodeList", function(){ return }));
|
||||||
// This module isn't already loaded
|
// stubs.push(sinon.stub(registry,"addModule", function(){ return }));
|
||||||
stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; }));
|
// // This module isn't already loaded
|
||||||
|
// stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; }));
|
||||||
stubs.push(sinon.stub(nodes,"registerType"));
|
//
|
||||||
loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
|
// stubs.push(sinon.stub(nodes,"registerType"));
|
||||||
loader.load().then(function(result) {
|
// loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
|
||||||
|
// loader.load().then(function(result) {
|
||||||
registry.addModule.called.should.be.true();
|
//
|
||||||
var module = registry.addModule.lastCall.args[0];
|
// registry.addModule.called.should.be.true();
|
||||||
module.should.have.property("name","node-red");
|
// var module = registry.addModule.lastCall.args[0];
|
||||||
module.should.have.property("version","1.2.3");
|
// module.should.have.property("name","node-red");
|
||||||
module.should.have.property("nodes");
|
// module.should.have.property("version","1.2.3");
|
||||||
module.nodes.should.have.property("DuffNode");
|
// module.should.have.property("nodes");
|
||||||
module.nodes.DuffNode.should.have.property("id","node-red/DuffNode");
|
// module.nodes.should.have.property("DuffNode");
|
||||||
module.nodes.DuffNode.should.have.property("module","node-red");
|
// module.nodes.DuffNode.should.have.property("id","node-red/DuffNode");
|
||||||
module.nodes.DuffNode.should.have.property("name","DuffNode");
|
// module.nodes.DuffNode.should.have.property("module","node-red");
|
||||||
module.nodes.DuffNode.should.have.property("file");
|
// module.nodes.DuffNode.should.have.property("name","DuffNode");
|
||||||
module.nodes.DuffNode.should.have.property("template");
|
// module.nodes.DuffNode.should.have.property("file");
|
||||||
module.nodes.DuffNode.should.have.property("enabled",true);
|
// module.nodes.DuffNode.should.have.property("template");
|
||||||
module.nodes.DuffNode.should.have.property("loaded",false);
|
// module.nodes.DuffNode.should.have.property("enabled",true);
|
||||||
module.nodes.DuffNode.should.have.property("types");
|
// module.nodes.DuffNode.should.have.property("loaded",false);
|
||||||
module.nodes.DuffNode.types.should.have.a.length(0);
|
// module.nodes.DuffNode.should.have.property("types");
|
||||||
module.nodes.DuffNode.should.have.property("config","");
|
// module.nodes.DuffNode.types.should.have.a.length(0);
|
||||||
module.nodes.DuffNode.should.have.property("help",{});
|
// module.nodes.DuffNode.should.have.property("config","");
|
||||||
module.nodes.DuffNode.should.have.property("namespace","node-red");
|
// module.nodes.DuffNode.should.have.property("help",{});
|
||||||
module.nodes.DuffNode.should.have.property('err');
|
// module.nodes.DuffNode.should.have.property("namespace","node-red");
|
||||||
module.nodes.DuffNode.err.should.endWith("DuffNode.html does not exist");
|
// module.nodes.DuffNode.should.have.property('err');
|
||||||
|
// module.nodes.DuffNode.err.should.endWith("DuffNode.html does not exist");
|
||||||
nodes.registerType.called.should.be.false();
|
//
|
||||||
|
// nodes.registerType.called.should.be.false();
|
||||||
done();
|
//
|
||||||
}).catch(function(err) {
|
// done();
|
||||||
done(err);
|
// }).catch(function(err) {
|
||||||
});
|
// done(err);
|
||||||
});
|
// });
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#addModule",function() {
|
describe("#addModule",function() {
|
||||||
@ -527,7 +528,7 @@ describe("red/nodes/registry/loader",function() {
|
|||||||
stubs.push(sinon.stub(nodes,"registerType"));
|
stubs.push(sinon.stub(nodes,"registerType"));
|
||||||
loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
|
loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
|
||||||
loader.addModule("TestNodeModule").then(function(result) {
|
loader.addModule("TestNodeModule").then(function(result) {
|
||||||
result.should.eql("a node list");
|
result.should.eql("TestNodeModule");
|
||||||
|
|
||||||
registry.addModule.called.should.be.true();
|
registry.addModule.called.should.be.true();
|
||||||
var module = registry.addModule.lastCall.args[0];
|
var module = registry.addModule.lastCall.args[0];
|
||||||
@ -584,7 +585,7 @@ describe("red/nodes/registry/loader",function() {
|
|||||||
stubs.push(sinon.stub(nodes,"registerType"));
|
stubs.push(sinon.stub(nodes,"registerType"));
|
||||||
loader.init({log:{"_":function(){},warn:function(){}},nodes:nodes,version: function() { return "0.12.0"}, settings:{available:function(){return true;}}});
|
loader.init({log:{"_":function(){},warn:function(){}},nodes:nodes,version: function() { return "0.12.0"}, settings:{available:function(){return true;}}});
|
||||||
loader.addModule("TestNodeModule").then(function(result) {
|
loader.addModule("TestNodeModule").then(function(result) {
|
||||||
result.should.eql("a node list");
|
result.should.eql("TestNodeModule");
|
||||||
registry.addModule.called.should.be.false();
|
registry.addModule.called.should.be.false();
|
||||||
nodes.registerType.called.should.be.false();
|
nodes.registerType.called.should.be.false();
|
||||||
done();
|
done();
|
||||||
|
3
test/unit/@node-red/registry/lib/subflow_spec.js
Normal file
3
test/unit/@node-red/registry/lib/subflow_spec.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
describe("red/nodes/registry/subflow",function() {
|
||||||
|
it.skip("NEEDS TESTS");
|
||||||
|
});
|
@ -653,42 +653,30 @@ describe('Node', function() {
|
|||||||
|
|
||||||
describe('#log', function() {
|
describe('#log', function() {
|
||||||
it('produces a log message', function(done) {
|
it('produces a log message', function(done) {
|
||||||
var n = new RedNode({id:'123',type:'abc',z:'789'});
|
var n = new RedNode({id:'123',type:'abc',z:'789', _flow: {log:function(msg) { loginfo = msg;}}});
|
||||||
var loginfo = {};
|
var loginfo = {};
|
||||||
sinon.stub(Log, 'log', function(msg) {
|
|
||||||
loginfo = msg;
|
|
||||||
});
|
|
||||||
n.log("a log message");
|
n.log("a log message");
|
||||||
should.deepEqual({level:Log.INFO, id:n.id,
|
should.deepEqual({level:Log.INFO, id:n.id,
|
||||||
type:n.type, msg:"a log message",z:'789'}, loginfo);
|
type:n.type, msg:"a log message",z:'789'}, loginfo);
|
||||||
Log.log.restore();
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
it('produces a log message with a name', function(done) {
|
it('produces a log message with a name', function(done) {
|
||||||
var n = new RedNode({id:'123', type:'abc', name:"barney", z:'789'});
|
var n = new RedNode({id:'123', type:'abc', name:"barney", z:'789', _flow: {log:function(msg) { loginfo = msg;}}});
|
||||||
var loginfo = {};
|
var loginfo = {};
|
||||||
sinon.stub(Log, 'log', function(msg) {
|
|
||||||
loginfo = msg;
|
|
||||||
});
|
|
||||||
n.log("a log message");
|
n.log("a log message");
|
||||||
should.deepEqual({level:Log.INFO, id:n.id, name: "barney",
|
should.deepEqual({level:Log.INFO, id:n.id, name: "barney",
|
||||||
type:n.type, msg:"a log message",z:'789'}, loginfo);
|
type:n.type, msg:"a log message",z:'789'}, loginfo);
|
||||||
Log.log.restore();
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#warn', function() {
|
describe('#warn', function() {
|
||||||
it('produces a warning message', function(done) {
|
it('produces a warning message', function(done) {
|
||||||
var n = new RedNode({id:'123',type:'abc',z:'789'});
|
var n = new RedNode({id:'123',type:'abc',z:'789', _flow: {log:function(msg) { loginfo = msg;}}});
|
||||||
var loginfo = {};
|
var loginfo = {};
|
||||||
sinon.stub(Log, 'log', function(msg) {
|
|
||||||
loginfo = msg;
|
|
||||||
});
|
|
||||||
n.warn("a warning");
|
n.warn("a warning");
|
||||||
should.deepEqual({level:Log.WARN, id:n.id,
|
should.deepEqual({level:Log.WARN, id:n.id,
|
||||||
type:n.type, msg:"a warning",z:'789'}, loginfo);
|
type:n.type, msg:"a warning",z:'789'}, loginfo);
|
||||||
Log.log.restore();
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -696,7 +684,8 @@ describe('Node', function() {
|
|||||||
describe('#error', function() {
|
describe('#error', function() {
|
||||||
it('handles a null error message', function(done) {
|
it('handles a null error message', function(done) {
|
||||||
var flow = {
|
var flow = {
|
||||||
handleError: sinon.stub()
|
handleError: sinon.stub(),
|
||||||
|
log:sinon.stub()
|
||||||
}
|
}
|
||||||
var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'});
|
var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'});
|
||||||
var message = {a:1};
|
var message = {a:1};
|
||||||
@ -712,7 +701,8 @@ describe('Node', function() {
|
|||||||
|
|
||||||
it('produces an error message', function(done) {
|
it('produces an error message', function(done) {
|
||||||
var flow = {
|
var flow = {
|
||||||
handleError: sinon.stub()
|
handleError: sinon.stub(),
|
||||||
|
log:sinon.stub()
|
||||||
}
|
}
|
||||||
var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'});
|
var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'});
|
||||||
var message = {a:2};
|
var message = {a:2};
|
||||||
|
Loading…
Reference in New Issue
Block a user