update UI, Runtime API, metadata handling, and others

This commit is contained in:
Hiroyasu Nishiyama
2021-01-27 22:27:54 +09:00
parent d51aefa156
commit 4a1d66f210
9 changed files with 900 additions and 420 deletions

View File

@@ -1,280 +1,3 @@
<script type="text/html" data-template-name="library-config">
<div class="form-row">
<label for="node-config-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-config-input-name" data-i18n="[placeholder]common.label.name">
</div>
</div>
<div class="form-row" style="margin-bottom: 2px; margin-top: 15px;">
<label style="width: 65%;"><i class="fa fa-cog"></i> <span>Import Libraries</span></label>
<input type="checkbox" id="node-config-input-showAll" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-showAll" style="width: auto;">Show All Libraries</label>
</div>
<div class="form-row "">
<ol id="node-input-libs-container"></ol>
</div>
</script>
<script type="text/javascript">
// object that maps from library name to its descriptor
var allLibs = {};
/**
* Add library descriptor
* @param {string} name - library name
*/
function addLib(name) {
var item = allLibs[name]
if (item) {
item.count++;
}
else {
allLibs[name] = {
name: name,
count: 1
};
}
}
/**
* Remove library descriptor if reference count=0
* @param {string} name - library name
*/
function removeLib(name) {
var item = allLibs[name];
if (item) {
item.count--;
if (item.count === 0) {
delete allLibs[name];
}
}
}
function currentLibs() {
var result = [];
var libs = $("#node-input-libs-container").editableList("items");
libs.each(function(i) {
var item = $(this);
var n = item.find(".node-input-libs-val").val();
if (n && (n !== "")) {
result.push(n);
}
});
return result;
}
/**
* Validate library spec including conflicts with other specs
* @param {string} name - library name
*/
function checkLib(name) {
var m0 = name.match(/^([^@]+)(@.*)?$/);
if (m0) {
var lname0 = m0[1];
var ver0 = m0[2];
var ok = true;
var current = currentLibs(libs);
var libs = Object.keys(allLibs).concat(current);
libs.forEach(function(lib) {
if (name !== lib) {
var m1 = lib.match(/^([^@]+)(@.*)?$/);
if (m1) {
var lname1 = m1[1];
var ver1 = m1[2];
if ((lname1 === lname0) && (ver0 !== ver1)) {
ok = false;
return;
}
}
}
});
return ok;
}
return false;
}
/**
* Update library list
*/
function updateLib(done) {
allLibs = {};
RED.nodes.eachConfig(function(n) {
if (n.type === "library-config") {
var libs = n.libs || [];
libs.forEach(function(lib) { addLib(lib.name); });
}
});
var root = RED.settings.apiRootUrl || "";
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: root + "/function/modules",
success: function(data) {
data.forEach(function(lib) {
var name = lib.name;
if (lib.version && (lib.version !== "")) {
name += "@" +lib.version;
}
addLib(name);
});
done();
}
});
}
/**
* Register NPM library configuration node
*/
RED.nodes.registerType("library-config", {
category: "config",
defaults: {
name: {value: ""},
libs: {value: []},
showAll: {value: false}
},
label: function() {
if (this.name && (this.name !== "")) {
return this.name;
}
return "Library Set:"+this.id;
},
oneditprepare: function() {
var node = this;
var popovers = {};
$("#node-input-libs-container").css('min-height','250px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var disabled = (opt && !!opt.unused);
var row = $("<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(row);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: disabled
}).css({
width: "280px",
"margin-left": "5px"
}).appendTo(row);
fvar.on("change", function (e) {
opt.vname = $(this).val();
});
fmodule.on("change", function (e) {
var val = $(this).val();
if (!checkLib(val)) {
$(this).addClass("input-error");
var popover = RED.popover.tooltip($(this), "conflict in module spec");
popovers[i] = popover;
}
else {
$(this).removeClass("input-error");
var popover = popovers[i];
if (popover) {
popover.setContent(function () {
return null;
});
}
}
opt.name = val;
});
if (opt) {
if (opt.vname) {
fvar.val(opt.vname);
}
if (opt.name) {
fmodule.val(opt.name);
addLib(opt.name);
}
}
},
removeItem: function(item) {
removeLib(item.name);
},
removable: true,
sortable: true
});
var libs = node.libs ? node.libs : [];
libs.forEach(function(lib) {
var conf = {
vname: lib.vname,
name: lib.name
};
$("#node-input-libs-container").editableList("addItem", conf);
});
$("#node-config-input-showAll").on("change", function () {
var checked = $(this).prop("checked");
var fun = null;
if (checked) {
fun = null;
}
else {
fun = function(item) {
return (!item.unused) ||
((item.vname && (item.vname !== "")) &&
(item.name && (item.name !== "")));
};
}
$("#node-input-libs-container").editableList("filter", fun);
});
updateLib(function () {
Object.values(allLibs).forEach(function(lib) {
function pred(x) {
return (x.name === lib.name);
}
if (libs.find(pred)) {
return;
}
var conf = {
vname: "",
name: lib.name,
unused: true
};
$("#node-input-libs-container").editableList("addItem", conf);
});
$("#node-config-input-showAll").change();
});
},
oneditsave: function() {
var node = this;
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,
name: n
});
});
},
oneditresize: function(size) {
var height = size.height;
$("#node-input-libs-container").editableList("height", height -60);
}
});
</script>
<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>
@@ -327,12 +50,11 @@
</div>
<div id="func-tab-config" style="display:none">
<div class="form-row">
<div class="form-row" style="margin-bottom: 0px;">
<label><i class="fa fa-wrench"></i> <span>Use Library</span></label>
<input type="text" id="node-input-libs">
</div>
<div class="form-row" style="height: 250px; min-height: 150px">
<ol id="node-input-require-container"></ol>
<ol id="node-input-libs-container"></ol>
</div>
</div>
</div>
@@ -340,6 +62,536 @@
</script>
<script type="text/javascript">
// object that maps from library name to its descriptor
var allLibs = [];
function moduleName(module) {
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
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();
}
}
}
});
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();
}
},
]
});
}
function prepareLibraryConfig(node) {
$("#node-input-libs-container").css('min-height','250px').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 fieldWidth = "260px";
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: disabled
}).css({
width: fieldWidth,
"margin-left": "5px"
}).appendTo(row0);
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);
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();
fvar.on("change", function (e) {
opt.vname = $(this).val();
});
fmodule.on("change", function (e) {
var val = $(this).val();
if (!checkLib(val, opt.id)) {
$(this).addClass("input-error");
warn.show();
}
else {
$(this).removeClass("input-error");
warn.hide();
if (val && (val !== "") &&
(!opt.version || (opt.version === ""))) {
install.attr("disabled", false);
}
}
opt.spec = val;
});
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;
};
}
}
},
removable: true,
sortable: true
});
updateLib(node, function () {
});
}
RED.nodes.registerType('function',{
color:"#fdd0a2",
category: 'function',
@@ -350,7 +602,7 @@
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""},
libs: {value:"", type:"library-config", required:false}
libs: {value: []}
},
inputs:1,
outputs:1,
@@ -496,56 +748,7 @@
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"));
$("#node-input-require-container").css('min-height','250px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var row = $("<div/>").appendTo(container);
var fvar = $("<input/>", {
class: "node-input-require-var",
placeholder: RED._("node-red:function.require.var"),
type: "text",
disabled: true
}).css({
width: "130px",
"margin-left": "5px"
}).appendTo(row);
var fmodule = $("<input/>", {
class: "node-input-require-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: true
}).css({
width: "390px",
"margin-left": "5px"
}).appendTo(row);
if (opt) {
if (opt.vname) {
fvar.val(opt.vname);
}
if (opt.name) {
fmodule.val(opt.name);
}
}
},
addButton: false,
removable: false
});
function updateLibs() {
var id = $("#node-input-libs").val();
var node = RED.nodes.node(id);
if (node && node.libs) {
var libs = node.libs;
$("#node-input-require-container").editableList("empty");
libs.forEach(function (r) {
$("#node-input-require-container").editableList("addItem", r);
});
}
}
updateLibs();
$("#node-input-libs").on("change", function () {
updateLibs();
});
prepareLibraryConfig(that);
},
oneditsave: function() {
var node = this;
@@ -578,6 +781,23 @@
$("#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
});
});
},
oneditcancel: function() {
var node = this;
@@ -611,7 +831,7 @@
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-require-container").css("height", (height -155)+"px");
$("#node-input-libs-container").css("height", (height -155)+"px");
}
});
</script>

View File

@@ -17,18 +17,6 @@
module.exports = function(RED) {
"use strict";
function LibsConfigNode(n) {
RED.nodes.createNode(this, n);
this.name = n.name;
this.libs = n.libs;
}
RED.nodes.registerType("library-config", LibsConfigNode);
RED.httpNode.get("/function/modules", function (req, res) {
var list = RED.nodes.listNPMModules();
res.send(list);
});
var util = require("util");
var vm = require("vm");
@@ -101,9 +89,8 @@ module.exports = function(RED) {
}
function FunctionNode(n) {
var libConf = RED.nodes.getNode(n.libs);
var libs = libConf ? libConf.libs : [];
n.modules = libs.map(x => x.name).filter(x => (x && (x !== "")));
var libs = n.libs || [];
n.modules = libs.map(x => x.spec).filter(x => (x && (x !== "")));
var loadPromise = RED.nodes.createNode(this,n);
var node = this;
node.name = n.name;
@@ -301,7 +288,8 @@ module.exports = function(RED) {
});
// wait for module installation
loadPromise.then(function () {
loadPromise.catch(()=>{
}).finally(function () {
if (node.hasOwnProperty("libs")) {
var modules = node.libs;
modules.forEach(module => {
@@ -309,11 +297,14 @@ module.exports = function(RED) {
if (vname && (vname !== "")) {
sandbox[vname] = null;
try {
var lib = RED.require(module.name);
sandbox[vname] = lib;
var spec = module.spec;
if (spec && (spec !== "")) {
var lib = RED.require(module.spec);
sandbox[vname] = lib;
}
}
catch (e) {
node.warn("failed to load library: "+ module.name);
node.warn("failed to load library: "+ module.spec);
}
}
});