mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
Rework Function node module integration
This commit is contained in:
@@ -1,20 +1,67 @@
|
||||
<script type="text/html" data-template-name="function">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
|
||||
</div>
|
||||
<style>
|
||||
#node-input-libs-container-row .red-ui-editableList-container {
|
||||
padding: 0px;
|
||||
}
|
||||
#node-input-libs-container-row .red-ui-editableList-container li {
|
||||
padding:5px;
|
||||
}
|
||||
#node-input-libs-container-row .red-ui-editableList-item-remove {
|
||||
right: 5px;
|
||||
}
|
||||
.node-libs-entry {
|
||||
display: flex;
|
||||
}
|
||||
.node-libs-entry .node-input-libs-var, .node-libs-entry .red-ui-typedInput-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.node-libs-entry > code,.node-libs-entry > span {
|
||||
line-height: 30px;
|
||||
}
|
||||
.node-libs-entry > input[type=text] {
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
<div class="form-row">
|
||||
.node-libs-entry > span > i {
|
||||
display: none;
|
||||
}
|
||||
.node-libs-entry > span.input-error > i {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
</style>
|
||||
<input type="hidden" id="node-input-func">
|
||||
<input type="hidden" id="node-input-noerr">
|
||||
<input type="hidden" id="node-input-finalize">
|
||||
<input type="hidden" id="node-input-initialize">
|
||||
|
||||
<div class="form-row" style="margin-top: -20px">
|
||||
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
|
||||
</div>
|
||||
|
||||
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
|
||||
|
||||
<div id="func-tab-init" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<input type="hidden" id="node-input-initialize" autofocus="autofocus">
|
||||
<div id="func-tab-config" style="display:none">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
||||
<input id="node-input-outputs" style="width: 60px;" value="1">
|
||||
</div>
|
||||
|
||||
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
|
||||
<label><i class="fa fa-cubes"></i> <span>Modules</span></label>
|
||||
</div>
|
||||
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
|
||||
<ol id="node-input-libs-container"></ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="func-tab-init" style="display:none">
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-init-editor" ></div>
|
||||
@@ -22,46 +69,77 @@
|
||||
</div>
|
||||
|
||||
<div id="func-tab-body" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<input type="hidden" id="node-input-func" autofocus="autofocus">
|
||||
<input type="hidden" id="node-input-noerr">
|
||||
</div>
|
||||
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="height: 220px; min-height:120px; margin-top: 30px;" class="node-text-editor" id="node-input-func-editor" ></div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-bottom: 0px">
|
||||
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
||||
<input id="node-input-outputs" style="width: 60px;" value="1">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="func-tab-finalize" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<input type="hidden" id="node-input-finalize" autofocus="autofocus">
|
||||
</div>
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="func-tab-config" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<label><i class="fa fa-wrench"></i> <span>Use Library</span></label>
|
||||
</div>
|
||||
<div class="form-row" style="height: 250px; min-height: 150px">
|
||||
<ol id="node-input-libs-container"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
(function() {
|
||||
|
||||
var knownFunctionNodes = {};
|
||||
RED.events.on("nodes:add", function(n) {
|
||||
if (n.type === "function") {
|
||||
knownFunctionNodes[n.id] = n;
|
||||
}
|
||||
})
|
||||
RED.events.on("nodes:remove", function(n) {
|
||||
if (n.type === "function") {
|
||||
delete knownFunctionNodes[n.id];
|
||||
}
|
||||
})
|
||||
|
||||
var missingModules = [];
|
||||
var missingModuleReasons = {};
|
||||
RED.events.on("runtime-state", function(event) {
|
||||
if (event.error === "missing-modules") {
|
||||
missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module });
|
||||
for (var id in knownFunctionNodes) {
|
||||
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
|
||||
RED.editor.validateNode(knownFunctionNodes[id])
|
||||
}
|
||||
}
|
||||
} else if (!event.text) {
|
||||
missingModuleReasons = {};
|
||||
missingModules = [];
|
||||
for (var id in knownFunctionNodes) {
|
||||
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
|
||||
RED.editor.validateNode(knownFunctionNodes[id])
|
||||
}
|
||||
}
|
||||
}
|
||||
RED.view.redraw();
|
||||
});
|
||||
|
||||
var installAllowList = ['*'];
|
||||
var installDenyList = [];
|
||||
|
||||
var modulesEnabled = true;
|
||||
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
|
||||
modulesEnabled = false;
|
||||
}
|
||||
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
|
||||
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
|
||||
if (settingsAllowList || settingsDenyList) {
|
||||
installAllowList = settingsAllowList;
|
||||
installDenyList = settingsDenyList
|
||||
}
|
||||
installAllowList = RED.utils.parseModuleList(installAllowList);
|
||||
installDenyList = RED.utils.parseModuleList(installDenyList);
|
||||
|
||||
|
||||
// object that maps from library name to its descriptor
|
||||
var allLibs = [];
|
||||
|
||||
@@ -73,523 +151,123 @@
|
||||
return [module, undefined];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add library descriptor
|
||||
* @param {string} name - library name
|
||||
*/
|
||||
function addLib(info) {
|
||||
allLibs.push(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate library spec including conflicts with other specs
|
||||
* @param {string} name - library name
|
||||
*/
|
||||
function checkLib(spec) {
|
||||
var [name, ver] = moduleName(spec);
|
||||
var libs = $("#node-input-libs-container").editableList("items");
|
||||
var count = 0;
|
||||
libs.each(function(i) {
|
||||
if (count > 1) {
|
||||
return;
|
||||
}
|
||||
var item = $(this);
|
||||
var n = item.find(".node-input-libs-val").val();
|
||||
if (n && (n !== "")) {
|
||||
var [name1, ver1] = moduleName(n);
|
||||
if (name1 === name) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
return (count <= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get library list
|
||||
*/
|
||||
function getModules() {
|
||||
var root = RED.settings.apiRootUrl || "";
|
||||
var promise = $.ajax({
|
||||
url: root + "/modules",
|
||||
type: "GET",
|
||||
cache: false
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and update in progress state of modules
|
||||
*/
|
||||
function checkInProgress(allLibs) {
|
||||
var waitTime = 1000;
|
||||
|
||||
function update() {
|
||||
map = {};
|
||||
allLibs.forEach(function (lib) {
|
||||
if (lib.inProgress) {
|
||||
map[lib.name] = lib;
|
||||
}
|
||||
});
|
||||
getModules().then(function (data,textStatus,xhr) {
|
||||
var count = 0;
|
||||
data.forEach(lib => {
|
||||
var name = lib.name;
|
||||
if (lib.inProgress) {
|
||||
count++;
|
||||
}
|
||||
else if (name in map) {
|
||||
var lib2 = map[name];
|
||||
if (lib2) {
|
||||
delete map[name];
|
||||
var cb = lib2.onReady;
|
||||
if (cb) {
|
||||
lib2.version = lib.version;
|
||||
lib2.status["installed"] = true;
|
||||
lib2.inProgress = false;
|
||||
cb();
|
||||
}
|
||||
function getAllUsedModules() {
|
||||
var moduleSet = new Set();
|
||||
for (var id in knownFunctionNodes) {
|
||||
if (knownFunctionNodes.hasOwnProperty(id)) {
|
||||
if (knownFunctionNodes[id].libs) {
|
||||
for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) {
|
||||
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) {
|
||||
moduleSet.add(knownFunctionNodes[id].libs[i].module);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (count > 0) {
|
||||
setTimeout(update, waitTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
setTimeout(update, waitTime);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update library list
|
||||
*/
|
||||
function updateLib(node, done) {
|
||||
var names = {};
|
||||
var specs = {};
|
||||
var libs = node.libs || [];
|
||||
allLibs = [];
|
||||
libs.forEach(lib => {
|
||||
var vname = lib.vname;
|
||||
var spec = lib.spec;
|
||||
var [name, ver] = moduleName(spec);
|
||||
var item = {
|
||||
vname: vname,
|
||||
name: name,
|
||||
spec: spec,
|
||||
status: { "edit":true }
|
||||
};
|
||||
addLib(item);
|
||||
specs[spec] = true;
|
||||
names[name] = true;
|
||||
});
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if ((n.type === "function") && (n !== node)) {
|
||||
var libs = n.libs || [];
|
||||
libs.forEach(lib => {
|
||||
var spec = lib.spec;
|
||||
var [name, ver] = moduleName(spec);
|
||||
if (name in names) {
|
||||
var items = allLibs.filter(item => (item.name === name));
|
||||
items.forEach(item => {
|
||||
item.status["used"] = true;
|
||||
});
|
||||
if (spec in specs) {
|
||||
// olready found
|
||||
return;
|
||||
}
|
||||
// spec conflict
|
||||
}
|
||||
var item = {
|
||||
name: name,
|
||||
spec: spec,
|
||||
status: { "used": true }
|
||||
};
|
||||
addLib(item);
|
||||
names[name] = true;
|
||||
specs[spec] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
var inProgressCount = 0;
|
||||
getModules().then(function(data,textStatus,xhr) {
|
||||
data.forEach(lib => {
|
||||
var name = lib.name;
|
||||
var version = lib.version;
|
||||
var preinstalled = (lib.status == "preinstalled");
|
||||
var spec = lib.spec;
|
||||
var items = allLibs.filter(item => (item.name === name));
|
||||
if (items.length > 0) {
|
||||
items.forEach(item => {
|
||||
item.version = version;
|
||||
item.status["installed"] = true;
|
||||
item.status["preinstalled"] = preinstalled;
|
||||
item.inProgress = lib.inProgress;
|
||||
});
|
||||
}
|
||||
else {
|
||||
var item = {
|
||||
name: name,
|
||||
spec: spec,
|
||||
version: version,
|
||||
status: {
|
||||
"installed": true,
|
||||
"preinstalled": preinstalled
|
||||
},
|
||||
inProgress: lib.inProgress
|
||||
};
|
||||
addLib(item);
|
||||
names[name] = true;
|
||||
specs[spec] = true;
|
||||
}
|
||||
if (lib.inProgress) {
|
||||
inProgressCount++;
|
||||
}
|
||||
});
|
||||
$("#node-input-libs-container").editableList("empty");
|
||||
allLibs.forEach(function(lib) {
|
||||
$("#node-input-libs-container").editableList("addItem", lib);
|
||||
});
|
||||
if (inProgressCount > 0) {
|
||||
checkInProgress(allLibs);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall NPM module
|
||||
*/
|
||||
function uninstallModule(node, spec, name, version,
|
||||
moduleInfo, installButton,
|
||||
installInfoRow, installedInfo,
|
||||
removeButton,
|
||||
progressRow,
|
||||
done) {
|
||||
var root = RED.settings.apiRootUrl || "";
|
||||
progressRow.show();
|
||||
$.ajax({
|
||||
url: root + "/modules/" +spec,
|
||||
type: "DELETE",
|
||||
}).then(function(data,textStatus,xhr) {
|
||||
moduleInfo.attr("disabled", false);
|
||||
installButton.attr("disabled", false);
|
||||
installInfoRow.hide();
|
||||
installedInfo.val("");
|
||||
removeButton.show();
|
||||
progressRow.hide();
|
||||
|
||||
RED.notify("Successfully uninstalled:" +name);
|
||||
if(done) {
|
||||
done();
|
||||
}
|
||||
}).fail(function(xhr,textStatus,err) {
|
||||
progressRow.hide();
|
||||
RED.notify("Failed to uninstall: " +name);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updaate or Install NPM module
|
||||
*/
|
||||
function updateModule(node, spec, update,
|
||||
moduleInfo, installButton,
|
||||
installInfoRow, installedInfo, removeButton,
|
||||
progressRow,
|
||||
done) {
|
||||
var [name, version] = moduleName(spec);
|
||||
var root = RED.settings.apiRootUrl || "";
|
||||
var errorMessage = "failed to "+(update ? "update": "install") +": " +name;
|
||||
progressRow.show();
|
||||
$.ajax({
|
||||
url: root + "/modules",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
cache: false,
|
||||
data: JSON.stringify({
|
||||
spec: spec,
|
||||
update: update
|
||||
})
|
||||
}).then(function(data) {
|
||||
getModules().then(function(data,textStatus,xhr) {
|
||||
var item = data.find(lib => (lib.name === name));
|
||||
progressRow.hide();
|
||||
if (item) {
|
||||
moduleInfo.attr("disabled", true);
|
||||
installButton.attr("disabled", true);
|
||||
installInfoRow.show();
|
||||
installedInfo.val(item.name+"@"+item.version);
|
||||
removeButton.hide();
|
||||
|
||||
var msg = "Successfully "
|
||||
+(update ? "updated": "installed")
|
||||
+": " +name;
|
||||
|
||||
RED.notify(msg);
|
||||
}
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}).fail(function(xhr,textStatus,err) {
|
||||
progressRow.hide();
|
||||
RED.notify(errorMessage);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}).fail(function(xhr,textStatus,err) {
|
||||
progressRow.hide();
|
||||
RED.notify(errorMessage);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function withNotification(msg, text, cb) {
|
||||
var notification = RED.notify(msg, {
|
||||
modal: true,
|
||||
fixed: true,
|
||||
buttons: [
|
||||
{
|
||||
text: RED._("common.label.cancel"),
|
||||
click: function() {
|
||||
notification.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: text,
|
||||
class: "primary red-ui-palette-module-install-confirm-button-update",
|
||||
click: function() {
|
||||
notification.close();
|
||||
cb();
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
var modules = Array.from(moduleSet);
|
||||
modules.sort();
|
||||
return modules;
|
||||
}
|
||||
|
||||
function prepareLibraryConfig(node) {
|
||||
$("#node-input-libs-container").css('min-height','250px').css('min-width','450px').editableList({
|
||||
$(".node-input-libs-row").show();
|
||||
var usedModules = getAllUsedModules();
|
||||
var typedModules = usedModules.map(function(l) {
|
||||
return {icon:"fa fa-cube", value:l,label:l,hasValue:false}
|
||||
})
|
||||
typedModules.push({
|
||||
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
|
||||
})
|
||||
|
||||
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
|
||||
addItem: function(container,i,opt) {
|
||||
var parent = container.parent();
|
||||
var removeButton = parent.find(".red-ui-editableList-item-remove");
|
||||
var disabled = (opt && !!opt.unused);
|
||||
var row0 = $("<div/>").appendTo(container);
|
||||
var fvar = $("<input/>", {
|
||||
class: "node-input-libs-var",
|
||||
placeholder: RED._("node-red:function.require.var"),
|
||||
type: "text",
|
||||
disabled: disabled
|
||||
}).css({
|
||||
width: "90px",
|
||||
"margin-left": "5px"
|
||||
}).appendTo(row0);
|
||||
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
|
||||
var fieldWidth = "260px";
|
||||
$('<code>const </code>').appendTo(row0);
|
||||
var fvar = $("<input/>", {
|
||||
class: "node-input-libs-var red-ui-font-code",
|
||||
placeholder: RED._("node-red:function.require.var"),
|
||||
type: "text"
|
||||
}).css({
|
||||
width: "120px",
|
||||
"margin-left": "5px"
|
||||
}).appendTo(row0).val(opt.var);
|
||||
$('<code> = require(</code>').appendTo(row0);
|
||||
var fmodule = $("<input/>", {
|
||||
class: "node-input-libs-val",
|
||||
placeholder: RED._("node-red:function.require.module"),
|
||||
type: "text",
|
||||
disabled: disabled
|
||||
type: "text"
|
||||
}).css({
|
||||
width: fieldWidth,
|
||||
"margin-left": "5px"
|
||||
}).appendTo(row0);
|
||||
width: "180px",
|
||||
}).appendTo(row0).typedInput({
|
||||
types: typedModules,
|
||||
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
|
||||
});
|
||||
if (usedModules.indexOf(opt.module) === -1) {
|
||||
fmodule.typedInput('value', opt.module);
|
||||
}
|
||||
|
||||
var buttonWidth = "70px";
|
||||
var install = $("<button/>", {
|
||||
class: "red-ui-button red-ui-button-small"
|
||||
}).css({
|
||||
"margin-left": "10px",
|
||||
width: buttonWidth
|
||||
}).appendTo(row0);;
|
||||
$("<i/>", {
|
||||
class: "fa fa-cube"
|
||||
}).appendTo(install);
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text("Install").appendTo(install);
|
||||
$('<code>)</code>').appendTo(row0);
|
||||
|
||||
var warn = $("<span/>").css({
|
||||
"margin-left": "10px"
|
||||
}).appendTo(row0);
|
||||
$("<i/>", {
|
||||
class: "fa fa-warning"
|
||||
}).appendTo(warn);
|
||||
RED.popover.tooltip(warn, "module spec conflict");
|
||||
warn.hide();
|
||||
|
||||
var row1 = $("<div/>").css({
|
||||
"margin-top": "5px"
|
||||
}).appendTo(container);
|
||||
|
||||
var linstalled = $("<label/>").css({
|
||||
width: "90px",
|
||||
"text-align": "right",
|
||||
"margin-right": "10px"
|
||||
}).appendTo(row1);
|
||||
$("<i/>", {
|
||||
class: "fa fa-cube"
|
||||
}).appendTo(linstalled);
|
||||
var status = "Installed";
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text(status).appendTo(linstalled);
|
||||
|
||||
var finstalled = $("<input/>", {
|
||||
type: "text",
|
||||
disabled: true
|
||||
}).css({
|
||||
width: fieldWidth
|
||||
}).appendTo(row1);
|
||||
|
||||
var uninstall = $("<button/>", {
|
||||
class: "red-ui-button red-ui-button-small"
|
||||
}).css({
|
||||
"margin-left": "10px",
|
||||
width: buttonWidth
|
||||
}).appendTo(row1);
|
||||
$("<i/>", {
|
||||
class: "fa fa-trash"
|
||||
}).appendTo(uninstall);
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text("Uninstall").appendTo(uninstall);
|
||||
|
||||
var update = $("<button/>", {
|
||||
class: "red-ui-button red-ui-button-small"
|
||||
}).css({
|
||||
"margin-left": "10px",
|
||||
width: buttonWidth
|
||||
}).appendTo(row1);
|
||||
$("<i/>", {
|
||||
class: "fa fa-refresh"
|
||||
}).appendTo(update);
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text("Update").appendTo(update);
|
||||
|
||||
var row2 = $("<div/>", {
|
||||
class: "red-ui-palette-module-shade"
|
||||
}).appendTo(container);
|
||||
$("<img/>", {
|
||||
src: "red/images/spin.svg",
|
||||
class: "red-ui-palette-spinner"
|
||||
}).appendTo(row2);
|
||||
row2.hide();
|
||||
var warning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
|
||||
RED.popover.tooltip(warning.find("i"),function() {
|
||||
var val = fmodule.typedInput("type");
|
||||
if (val === "_custom_") {
|
||||
val = fmodule.val();
|
||||
}
|
||||
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
|
||||
return "Module not allowed"
|
||||
} else {
|
||||
return "Module not installed: "+missingModuleReasons[val]
|
||||
}
|
||||
})
|
||||
|
||||
fvar.on("change", function (e) {
|
||||
opt.vname = $(this).val();
|
||||
var v = $(this).val().trim();
|
||||
if (v === "" || / /.test(v)) {
|
||||
fvar.addClass("input-error");
|
||||
} else {
|
||||
fvar.removeClass("input-error");
|
||||
}
|
||||
});
|
||||
|
||||
fmodule.on("change", function (e) {
|
||||
var val = $(this).val();
|
||||
if (!checkLib(val, opt.id)) {
|
||||
$(this).addClass("input-error");
|
||||
warn.show();
|
||||
var val = $(this).typedInput("type");
|
||||
if (val === "_custom_") {
|
||||
val = $(this).val();
|
||||
}
|
||||
else {
|
||||
$(this).removeClass("input-error");
|
||||
warn.hide();
|
||||
if (val && (val !== "") &&
|
||||
(!opt.version || (opt.version === ""))) {
|
||||
install.attr("disabled", false);
|
||||
}
|
||||
}
|
||||
opt.spec = val;
|
||||
});
|
||||
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() });
|
||||
fvar.val(varName);
|
||||
fvar.trigger("change");
|
||||
|
||||
install.on("click", function (e) {
|
||||
withNotification(
|
||||
"Install NPM Module " +opt.spec,
|
||||
"Install",
|
||||
function (cb) {
|
||||
updateModule(
|
||||
node, opt.spec, false,
|
||||
fmodule, install,
|
||||
row1, finstalled, removeButton,
|
||||
row2,
|
||||
cb);
|
||||
});
|
||||
});
|
||||
uninstall.on("click", function (e) {
|
||||
withNotification(
|
||||
"Uninstall NPM Module "+opt.spec,
|
||||
"Uninstall",
|
||||
function (cb) {
|
||||
uninstallModule(
|
||||
node, opt.spec, opt.name, opt.version,
|
||||
fmodule, install,
|
||||
row1, finstalled,
|
||||
removeButton,
|
||||
row2,
|
||||
cb);
|
||||
});
|
||||
});
|
||||
update.on("click", function (e) {
|
||||
withNotification(
|
||||
"Update NPM Module " +opt.spec,
|
||||
"Update",
|
||||
function (cb) {
|
||||
updateModule(
|
||||
node, opt.spec, true,
|
||||
fmodule, install,
|
||||
row1, finstalled, removeButton,
|
||||
row2,
|
||||
cb);
|
||||
});
|
||||
});
|
||||
|
||||
install.attr("disabled", true);
|
||||
if (opt) {
|
||||
function updateData(data) {
|
||||
if (data.vname && (data.vname !== "")) {
|
||||
fvar.val(data.vname);
|
||||
}
|
||||
if (data.spec && (data.spec !== "")) {
|
||||
fmodule.val(data.spec);
|
||||
}
|
||||
if (data.version && (data.version !== "")) {
|
||||
fmodule.prop("disabled", true);
|
||||
row1.show();
|
||||
removeButton.hide();
|
||||
finstalled.val(data.name+"@"+data.version)
|
||||
if (data.status["preinstalled"]) {
|
||||
uninstall.attr("disabled", true);
|
||||
update.attr("disabled", true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
install.attr("disabled", false);
|
||||
row1.hide();
|
||||
removeButton.show();
|
||||
}
|
||||
}
|
||||
updateData(opt);
|
||||
if (opt.inProgress) {
|
||||
row2.show();
|
||||
opt.onReady = function () {
|
||||
row2.hide();
|
||||
updateData(opt);
|
||||
opt.onReady = null;
|
||||
};
|
||||
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
|
||||
fmodule.removeClass("input-error");
|
||||
warning.removeClass("input-error");
|
||||
} else {
|
||||
fmodule.addClass("input-error");
|
||||
warning.addClass("input-error");
|
||||
}
|
||||
});
|
||||
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
|
||||
fmodule.removeClass("input-error");
|
||||
warning.removeClass("input-error");
|
||||
} else {
|
||||
fmodule.addClass("input-error");
|
||||
warning.addClass("input-error");
|
||||
}
|
||||
if (opt.var) {
|
||||
fvar.trigger("change");
|
||||
}
|
||||
},
|
||||
removable: true,
|
||||
sortable: true
|
||||
});
|
||||
updateLib(node, function () {
|
||||
removable: true
|
||||
});
|
||||
|
||||
var libs = node.libs || [];
|
||||
for (var i=0,l=libs.length;i<l; i++) {
|
||||
libList.editableList('addItem',libs[i])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RED.nodes.registerType('function',{
|
||||
@@ -602,7 +280,22 @@
|
||||
noerr: {value:0,required:true,validate:function(v) { return !v; }},
|
||||
initialize: {value:""},
|
||||
finalize: {value:""},
|
||||
libs: {value: []}
|
||||
libs: {value: [], validate: function(v) {
|
||||
if (!v) { return true; }
|
||||
for (var i=0,l=v.length;i<l;i++) {
|
||||
var m = v[i];
|
||||
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
|
||||
return false
|
||||
}
|
||||
if (m.var === "" || / /.test(m.var)) {
|
||||
return false;
|
||||
}
|
||||
if (missingModules.indexOf(m.module) > -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
@@ -623,21 +316,24 @@
|
||||
$("#" + tab.id).show();
|
||||
}
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-config",
|
||||
iconClass: "fa fa-cog",
|
||||
maximumTabWidth: 38
|
||||
|
||||
});
|
||||
|
||||
tabs.addTab({
|
||||
id: "func-tab-init",
|
||||
label: that._("function.label.initialize")
|
||||
label: "On Start", //that._("function.label.initialize")
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-body",
|
||||
label: that._("function.label.function")
|
||||
label: "On Message"//that._("function.label.function")
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-finalize",
|
||||
label: that._("function.label.finalize")
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-config",
|
||||
label: "Config"
|
||||
label: "On Stop"//that._("function.label.finalize")
|
||||
});
|
||||
|
||||
tabs.activateTab("func-tab-body");
|
||||
@@ -748,7 +444,9 @@
|
||||
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
|
||||
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
|
||||
|
||||
prepareLibraryConfig(that);
|
||||
if (RED.settings.functionExternalModules !== false) {
|
||||
prepareLibraryConfig(that);
|
||||
}
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
@@ -780,24 +478,28 @@
|
||||
|
||||
$("#node-input-noerr").val(noerr);
|
||||
this.noerr = noerr;
|
||||
|
||||
var libs = $("#node-input-libs-container").editableList("items");
|
||||
var oldLibs = node.libs || [];
|
||||
var newLibs = [];
|
||||
node.libs = newLibs;
|
||||
libs.each(function(i) {
|
||||
var item = $(this);
|
||||
var v = item.find(".node-input-libs-var").val();
|
||||
var n = item.find(".node-input-libs-val").val();
|
||||
if ((!v || (v === "")) ||
|
||||
(!n || (n === ""))) {
|
||||
return;
|
||||
}
|
||||
newLibs.push({
|
||||
vname: v,
|
||||
spec: n
|
||||
if (RED.settings.functionExternalModules !== false) {
|
||||
var libs = $("#node-input-libs-container").editableList("items");
|
||||
node.libs = [];
|
||||
libs.each(function(i) {
|
||||
var item = $(this);
|
||||
var v = item.find(".node-input-libs-var").val();
|
||||
var n = item.find(".node-input-libs-val").typedInput("type");
|
||||
if (n === "_custom_") {
|
||||
n = item.find(".node-input-libs-val").val();
|
||||
}
|
||||
if ((!v || (v === "")) ||
|
||||
(!n || (n === ""))) {
|
||||
return;
|
||||
}
|
||||
node.libs.push({
|
||||
var: v,
|
||||
module: n
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
node.libs = [];
|
||||
}
|
||||
},
|
||||
oneditcancel: function() {
|
||||
var node = this;
|
||||
@@ -823,15 +525,16 @@
|
||||
this.editor.resize();
|
||||
|
||||
var height = size.height;
|
||||
$("#node-input-init-editor").css("height", (height -105)+"px");
|
||||
$("#node-input-func-editor").css("height", (height -145)+"px");
|
||||
$("#node-input-finalize-editor").css("height", (height -105)+"px");
|
||||
$("#node-input-init-editor").css("height", (height -45)+"px");
|
||||
$("#node-input-func-editor").css("height", (height -45)+"px");
|
||||
$("#node-input-finalize-editor").css("height", (height -45)+"px");
|
||||
|
||||
this.initEditor.resize();
|
||||
this.editor.resize();
|
||||
this.finalizeEditor.resize();
|
||||
|
||||
$("#node-input-libs-container").css("height", (height -155)+"px");
|
||||
$("#node-input-libs-container").css("height", (height - 185)+"px");
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@@ -91,13 +91,17 @@ module.exports = function(RED) {
|
||||
function FunctionNode(n) {
|
||||
var libs = n.libs || [];
|
||||
n.modules = libs.map(x => x.spec).filter(x => (x && (x !== "")));
|
||||
var loadPromise = RED.nodes.createNode(this,n);
|
||||
RED.nodes.createNode(this,n);
|
||||
var node = this;
|
||||
node.name = n.name;
|
||||
node.func = n.func;
|
||||
node.ini = n.initialize ? n.initialize.trim() : "";
|
||||
node.fin = n.finalize ? n.finalize.trim() : "";
|
||||
node.libs = libs;
|
||||
node.libs = libs || [];
|
||||
|
||||
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
|
||||
throw new Error("Function node not allowed to load external modules");
|
||||
}
|
||||
|
||||
var handleNodeDoneCall = true;
|
||||
|
||||
@@ -287,173 +291,173 @@ module.exports = function(RED) {
|
||||
}
|
||||
});
|
||||
|
||||
// wait for module installation
|
||||
loadPromise.catch(()=>{
|
||||
}).finally(function () {
|
||||
if (node.hasOwnProperty("libs")) {
|
||||
var modules = node.libs;
|
||||
modules.forEach(module => {
|
||||
var vname = module.hasOwnProperty("vname") ? module.vname : null;
|
||||
if (vname && (vname !== "")) {
|
||||
sandbox[vname] = null;
|
||||
try {
|
||||
var spec = module.spec;
|
||||
if (spec && (spec !== "")) {
|
||||
var lib = RED.require(module.spec);
|
||||
sandbox[vname] = lib;
|
||||
if (node.hasOwnProperty("libs")) {
|
||||
var modules = node.libs;
|
||||
modules.forEach(module => {
|
||||
var vname = module.hasOwnProperty("var") ? module.var : null;
|
||||
if (vname && (vname !== "")) {
|
||||
sandbox[vname] = null;
|
||||
try {
|
||||
var spec = module.spec;
|
||||
if (spec && (spec !== "")) {
|
||||
var lib = RED.require(module.spec);
|
||||
sandbox[vname] = lib;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
node.warn("failed to load library: "+ module.spec);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
var context = vm.createContext(sandbox);
|
||||
try {
|
||||
var iniScript = null;
|
||||
var iniOpt = null;
|
||||
if (node.ini && (node.ini !== "")) {
|
||||
var iniText = `
|
||||
(async function(__send__) {
|
||||
var node = {
|
||||
id:__node__.id,
|
||||
name:__node__.name,
|
||||
log:__node__.log,
|
||||
error:__node__.error,
|
||||
warn:__node__.warn,
|
||||
debug:__node__.debug,
|
||||
trace:__node__.trace,
|
||||
status:__node__.status,
|
||||
send: function(msgs, cloneMsg) {
|
||||
__node__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
|
||||
}
|
||||
};
|
||||
`+ node.ini +`
|
||||
})(__initSend__);`;
|
||||
iniOpt = createVMOpt(node, " setup");
|
||||
iniScript = new vm.Script(iniText, iniOpt);
|
||||
}
|
||||
node.script = vm.createScript(functionText, createVMOpt(node, ""));
|
||||
if (node.fin && (node.fin !== "")) {
|
||||
var finText = "(function () {\n"+node.fin +"\n})();";
|
||||
finOpt = createVMOpt(node, " cleanup");
|
||||
finScript = new vm.Script(finText, finOpt);
|
||||
}
|
||||
var promise = Promise.resolve();
|
||||
if (iniScript) {
|
||||
context.__initSend__ = function(msgs) { node.send(msgs); };
|
||||
promise = iniScript.runInContext(context, iniOpt);
|
||||
}
|
||||
|
||||
processMessage = function (msg, send, done) {
|
||||
var start = process.hrtime();
|
||||
context.msg = msg;
|
||||
context.__send__ = send;
|
||||
context.__done__ = done;
|
||||
|
||||
node.script.runInContext(context);
|
||||
context.results.then(function(results) {
|
||||
sendResults(node,send,msg._msgid,results,false);
|
||||
if (handleNodeDoneCall) {
|
||||
done();
|
||||
}
|
||||
|
||||
var duration = process.hrtime(start);
|
||||
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
|
||||
node.metric("duration", msg, converted);
|
||||
if (process.env.NODE_RED_FUNCTION_TIME) {
|
||||
node.status({fill:"yellow",shape:"dot",text:""+converted});
|
||||
}
|
||||
}).catch(err => {
|
||||
if ((typeof err === "object") && err.hasOwnProperty("stack")) {
|
||||
//remove unwanted part
|
||||
var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/);
|
||||
err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n');
|
||||
var stack = err.stack.split(/\r?\n/);
|
||||
|
||||
//store the error in msg to be used in flows
|
||||
msg.error = err;
|
||||
|
||||
var line = 0;
|
||||
var errorMessage;
|
||||
if (stack.length > 0) {
|
||||
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
|
||||
line++;
|
||||
}
|
||||
|
||||
if (line < stack.length) {
|
||||
errorMessage = stack[line];
|
||||
var m = /:(\d+):(\d+)$/.exec(stack[line+1]);
|
||||
if (m) {
|
||||
var lineno = Number(m[1])-1;
|
||||
var cha = m[2];
|
||||
errorMessage += " (line "+lineno+", col "+cha+")";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
node.warn("failed to load library: "+ module.spec);
|
||||
if (!errorMessage) {
|
||||
errorMessage = err.toString();
|
||||
}
|
||||
done(errorMessage);
|
||||
}
|
||||
else if (typeof err === "string") {
|
||||
done(err);
|
||||
}
|
||||
else {
|
||||
done(JSON.stringify(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
var context = vm.createContext(sandbox);
|
||||
try {
|
||||
var iniScript = null;
|
||||
var iniOpt = null;
|
||||
if (node.ini && (node.ini !== "")) {
|
||||
var iniText = `
|
||||
(async function(__send__) {
|
||||
var node = {
|
||||
id:__node__.id,
|
||||
name:__node__.name,
|
||||
log:__node__.log,
|
||||
error:__node__.error,
|
||||
warn:__node__.warn,
|
||||
debug:__node__.debug,
|
||||
trace:__node__.trace,
|
||||
status:__node__.status,
|
||||
send: function(msgs, cloneMsg) {
|
||||
__node__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
|
||||
}
|
||||
};
|
||||
`+ node.ini +`
|
||||
})(__initSend__);`;
|
||||
iniOpt = createVMOpt(node, " setup");
|
||||
iniScript = new vm.Script(iniText, iniOpt);
|
||||
|
||||
node.on("close", function() {
|
||||
if (finScript) {
|
||||
try {
|
||||
finScript.runInContext(context, finOpt);
|
||||
}
|
||||
catch (err) {
|
||||
node.error(err);
|
||||
}
|
||||
}
|
||||
node.script = vm.createScript(functionText, createVMOpt(node, ""));
|
||||
if (node.fin && (node.fin !== "")) {
|
||||
var finText = "(function () {\n"+node.fin +"\n})();";
|
||||
finOpt = createVMOpt(node, " cleanup");
|
||||
finScript = new vm.Script(finText, finOpt);
|
||||
while (node.outstandingTimers.length > 0) {
|
||||
clearTimeout(node.outstandingTimers.pop());
|
||||
}
|
||||
var promise = Promise.resolve();
|
||||
if (iniScript) {
|
||||
context.__initSend__ = function(msgs) { node.send(msgs); };
|
||||
promise = iniScript.runInContext(context, iniOpt);
|
||||
while (node.outstandingIntervals.length > 0) {
|
||||
clearInterval(node.outstandingIntervals.pop());
|
||||
}
|
||||
if (node.clearStatus) {
|
||||
node.status({});
|
||||
}
|
||||
});
|
||||
|
||||
processMessage = function (msg, send, done) {
|
||||
var start = process.hrtime();
|
||||
context.msg = msg;
|
||||
context.__send__ = send;
|
||||
context.__done__ = done;
|
||||
|
||||
node.script.runInContext(context);
|
||||
context.results.then(function(results) {
|
||||
sendResults(node,send,msg._msgid,results,false);
|
||||
if (handleNodeDoneCall) {
|
||||
done();
|
||||
}
|
||||
|
||||
var duration = process.hrtime(start);
|
||||
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
|
||||
node.metric("duration", msg, converted);
|
||||
if (process.env.NODE_RED_FUNCTION_TIME) {
|
||||
node.status({fill:"yellow",shape:"dot",text:""+converted});
|
||||
}
|
||||
}).catch(err => {
|
||||
if ((typeof err === "object") && err.hasOwnProperty("stack")) {
|
||||
//remove unwanted part
|
||||
var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/);
|
||||
err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n');
|
||||
var stack = err.stack.split(/\r?\n/);
|
||||
|
||||
//store the error in msg to be used in flows
|
||||
msg.error = err;
|
||||
|
||||
var line = 0;
|
||||
var errorMessage;
|
||||
if (stack.length > 0) {
|
||||
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
|
||||
line++;
|
||||
}
|
||||
|
||||
if (line < stack.length) {
|
||||
errorMessage = stack[line];
|
||||
var m = /:(\d+):(\d+)$/.exec(stack[line+1]);
|
||||
if (m) {
|
||||
var lineno = Number(m[1])-1;
|
||||
var cha = m[2];
|
||||
errorMessage += " (line "+lineno+", col "+cha+")";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!errorMessage) {
|
||||
errorMessage = err.toString();
|
||||
}
|
||||
done(errorMessage);
|
||||
}
|
||||
else if (typeof err === "string") {
|
||||
done(err);
|
||||
}
|
||||
else {
|
||||
done(JSON.stringify(err));
|
||||
}
|
||||
promise.then(function (v) {
|
||||
var msgs = messages;
|
||||
messages = [];
|
||||
while (msgs.length > 0) {
|
||||
msgs.forEach(function (s) {
|
||||
processMessage(s.msg, s.send, s.done);
|
||||
});
|
||||
msgs = messages;
|
||||
messages = [];
|
||||
}
|
||||
state = RESOLVED;
|
||||
}).catch((error) => {
|
||||
messages = [];
|
||||
state = ERROR;
|
||||
node.error(error);
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
if (finScript) {
|
||||
try {
|
||||
finScript.runInContext(context, finOpt);
|
||||
}
|
||||
catch (err) {
|
||||
node.error(err);
|
||||
}
|
||||
}
|
||||
while (node.outstandingTimers.length > 0) {
|
||||
clearTimeout(node.outstandingTimers.pop());
|
||||
}
|
||||
while (node.outstandingIntervals.length > 0) {
|
||||
clearInterval(node.outstandingIntervals.pop());
|
||||
}
|
||||
if (node.clearStatus) {
|
||||
node.status({});
|
||||
}
|
||||
});
|
||||
|
||||
promise.then(function (v) {
|
||||
var msgs = messages;
|
||||
messages = [];
|
||||
while (msgs.length > 0) {
|
||||
msgs.forEach(function (s) {
|
||||
processMessage(s.msg, s.send, s.done);
|
||||
});
|
||||
msgs = messages;
|
||||
messages = [];
|
||||
}
|
||||
state = RESOLVED;
|
||||
}).catch((error) => {
|
||||
messages = [];
|
||||
state = ERROR;
|
||||
node.error(error);
|
||||
});
|
||||
|
||||
}
|
||||
catch(err) {
|
||||
// eg SyntaxError - which v8 doesn't include line number information
|
||||
// so we can't do better than this
|
||||
updateErrorInfo(err);
|
||||
node.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(err) {
|
||||
// eg SyntaxError - which v8 doesn't include line number information
|
||||
// so we can't do better than this
|
||||
updateErrorInfo(err);
|
||||
node.error(err);
|
||||
}
|
||||
}
|
||||
RED.nodes.registerType("function",FunctionNode, {
|
||||
dynamicModuleList: "modules"
|
||||
dynamicModuleList: "libs",
|
||||
settings: {
|
||||
functionExternalModules: { value: true, exportable: true }
|
||||
}
|
||||
});
|
||||
RED.library.register("functions");
|
||||
};
|
||||
|
Reference in New Issue
Block a user