Merge branch 'dev' into button-add-config-node

This commit is contained in:
Nick O'Leary 2024-04-03 14:02:40 +01:00 committed by GitHub
commit 6e7fa6f921
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 998 additions and 298 deletions

View File

@ -29,6 +29,15 @@ Nodes
- Let debug node status msg length be settable via settings (#4402) @dceejay
- Feat: Add ability to set headers for WebSocket client (#4436) @marcus-j-davies
#### 3.1.8: Maintenance Release
- Add validation and error handling on subflow instance properties (#4632) @knolleary
- Hide import/export context menu if disabled in theme (#4633) @knolleary
- Show change indicator on subflow tabs (#4631) @knolleary
- Bump dependencies (#4630) @knolleary
- Reset workspace index when clearing nodes (#4619) @knolleary
- Remove typo in global config (#4613) @kazuhitoyokoi
#### 3.1.7: Maintenance Release
- Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi

View File

@ -41,7 +41,7 @@
"cors": "2.8.5",
"cronosjs": "1.7.1",
"denque": "2.1.0",
"express": "4.18.2",
"express": "4.19.2",
"express-session": "1.17.3",
"form-data": "4.0.0",
"fs-extra": "11.1.1",
@ -64,7 +64,7 @@
"mqtt": "4.3.7",
"multer": "1.4.5-lts.1",
"mustache": "4.2.0",
"node-red-admin": "^3.1.2",
"node-red-admin": "^3.1.3",
"node-watch": "0.7.4",
"nopt": "5.0.0",
"oauth2orize": "1.11.1",
@ -112,7 +112,7 @@
"mermaid": "^10.4.0",
"minami": "1.2.3",
"mocha": "9.2.2",
"node-red-node-test-helper": "^0.3.2",
"node-red-node-test-helper": "^0.3.3",
"nodemon": "2.0.20",
"proxy": "^1.0.2",
"sass": "1.62.1",

View File

@ -91,6 +91,7 @@ module.exports = {
// Plugins
adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler);
adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler);
adminApp.get(/^\/plugins\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("plugins.read"),plugins.getConfig,apiUtil.errorHandler);
adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler);

View File

@ -40,5 +40,31 @@ module.exports = {
console.log(err.stack);
apiUtils.rejectHandler(req,res,err);
})
},
getConfig: function(req, res) {
let opts = {
user: req.user,
module: req.params[0],
req: apiUtils.getRequestLogObject(req)
}
if (req.get("accept") === "application/json") {
runtimeAPI.nodes.getNodeInfo(opts.module).then(function(result) {
res.send(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
} else {
opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages());
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
opts.lang = "en-US";
}
runtimeAPI.plugins.getPluginConfig(opts).then(function(result) {
return res.send(result);
}).catch(function(err) {
apiUtils.rejectHandler(req,res,err);
})
}
}
};

View File

@ -77,21 +77,35 @@ function CommsConnection(ws, user) {
log.trace("comms.close "+self.session);
removeActiveConnection(self);
});
ws.on('message', function(data,flags) {
var msg = null;
try {
msg = JSON.parse(data);
} catch(err) {
log.trace("comms received malformed message : "+err.toString());
return;
}
if (!pendingAuth) {
if (msg.subscribe) {
self.subscribe(msg.subscribe);
// handleRemoteSubscription(ws,msg.subscribe);
}
const handleAuthPacket = function(msg) {
Tokens.get(msg.auth).then(function(client) {
if (client) {
Users.get(client.user).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(msg, client.scope,msg.auth,true);
} else {
var completeConnection = function(userScope,session,sendAck) {
log.audit({event: "comms.auth.fail"});
completeConnection(msg, null,null,false);
}
});
} else {
Users.tokens(msg.auth).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(msg, user.permissions,msg.auth,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(msg, null,null,false);
}
});
}
});
}
const completeConnection = function(msg, userScope, session, sendAck) {
try {
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
ws.send(JSON.stringify({auth:"fail"}));
@ -110,44 +124,43 @@ function CommsConnection(ws, user) {
// to send anything.
}
}
ws.on('message', function(data,flags) {
var msg = null;
try {
msg = JSON.parse(data);
} catch(err) {
log.trace("comms received malformed message : "+err.toString());
return;
}
if (!pendingAuth) {
if (msg.auth) {
Tokens.get(msg.auth).then(function(client) {
if (client) {
Users.get(client.user).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(client.scope,msg.auth,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,null,false);
handleAuthPacket(msg)
} else if (msg.subscribe) {
self.subscribe(msg.subscribe);
// handleRemoteSubscription(ws,msg.subscribe);
} else if (msg.topic) {
runtimeAPI.comms.receive({
user: self.user,
client: self,
topic: msg.topic,
data: msg.data
})
}
});
} else {
Users.tokens(msg.auth).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(user.permissions,msg.auth,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,null,false);
}
});
}
});
if (msg.auth) {
handleAuthPacket(msg)
} else {
if (anonymousUser) {
log.audit({event: "comms.auth",user:anonymousUser});
self.user = anonymousUser;
completeConnection(anonymousUser.permissions,null,false);
completeConnection(msg, anonymousUser.permissions, null, false);
//TODO: duplicated code - pull non-auth message handling out
if (msg.subscribe) {
self.subscribe(msg.subscribe);
}
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,null,false);
completeConnection(msg, null,null,false);
}
}
}

View File

@ -23,7 +23,7 @@
"clone": "2.1.2",
"cors": "2.8.5",
"express-session": "1.17.3",
"express": "4.18.2",
"express": "4.19.2",
"memorystore": "1.6.7",
"mime": "3.0.0",
"multer": "1.4.5-lts.1",

View File

@ -590,6 +590,8 @@
},
"nodeCount": "__label__ Node",
"nodeCount_plural": "__label__ Nodes",
"pluginCount": "__count__ Plugin",
"pluginCount_plural": "__count__ Plugins",
"moduleCount": "__count__ Modul verfügbar",
"moduleCount_plural": "__count__ Module verfügbar",
"inuse": "In Gebrauch",

View File

@ -614,6 +614,8 @@
},
"nodeCount": "__label__ node",
"nodeCount_plural": "__label__ nodes",
"pluginCount": "__count__ plugin",
"pluginCount_plural": "__count__ plugins",
"moduleCount": "__count__ module available",
"moduleCount_plural": "__count__ modules available",
"inuse": "in use",

View File

@ -924,7 +924,14 @@
"date": "horodatage",
"jsonata": "expression",
"env": "variable d'environnement",
"cred": "identifiant"
"cred": "identifiant",
"conf-types": "noeud de configuration"
},
"date": {
"format": {
"timestamp": "millisecondes depuis l'époque",
"object": "Objet de date JavaScript"
}
}
},
"editableList": {

View File

@ -26,6 +26,15 @@ RED.comms = (function() {
var reconnectAttempts = 0;
var active = false;
RED.events.on('login', function(username) {
// User has logged in
// Need to upgrade the connection to be authenticated
if (ws && ws.readyState == 1) {
const auth_tokens = RED.settings.get("auth-tokens");
ws.send(JSON.stringify({auth:auth_tokens.access_token}))
}
})
function connectWS() {
active = true;
var wspath;
@ -56,6 +65,7 @@ RED.comms = (function() {
ws.send(JSON.stringify({subscribe:t}));
}
}
emit('connect')
}
ws = new WebSocket(wspath);
@ -180,9 +190,53 @@ RED.comms = (function() {
}
}
function send(topic, msg) {
if (ws && ws.readyState == 1) {
ws.send(JSON.stringify({
topic,
data: msg
}))
}
}
const eventHandlers = {};
function on(evt,func) {
eventHandlers[evt] = eventHandlers[evt]||[];
eventHandlers[evt].push(func);
}
function off(evt,func) {
const handler = eventHandlers[evt];
if (handler) {
for (let i=0;i<handler.length;i++) {
if (handler[i] === func) {
handler.splice(i,1);
return;
}
}
}
}
function emit() {
const evt = arguments[0]
const args = Array.prototype.slice.call(arguments,1);
if (eventHandlers[evt]) {
let cpyHandlers = [...eventHandlers[evt]];
for (let i=0;i<cpyHandlers.length;i++) {
try {
cpyHandlers[i].apply(null, args);
} catch(err) {
console.warn("RED.comms.emit error: ["+evt+"] "+(err.toString()));
console.warn(err);
}
}
}
}
return {
connect: connectWS,
subscribe: subscribe,
unsubscribe:unsubscribe
unsubscribe:unsubscribe,
on,
off,
send
}
})();

View File

@ -149,6 +149,8 @@ RED.nodes = (function() {
},
removeNodeSet: function(id) {
var ns = nodeSets[id];
if (!ns) { return {} }
for (var j=0;j<ns.types.length;j++) {
delete typeToId[ns.types[j]];
}
@ -572,12 +574,16 @@ RED.nodes = (function() {
* @param {String} z tab id
*/
checkTabState: function (z) {
const ws = workspaces[z]
const ws = workspaces[z] || subflows[z]
if (ws) {
const contentsChanged = tabDirtyMap[z].size > 0 || tabDeletedNodesMap[z].size > 0
if (Boolean(ws.contentsChanged) !== contentsChanged) {
ws.contentsChanged = contentsChanged
if (ws.type === 'tab') {
RED.events.emit("flows:change", ws);
} else {
RED.events.emit("subflows:change", ws);
}
}
}
}
@ -1050,7 +1056,22 @@ RED.nodes = (function() {
RED.nodes.registerType("subflow:"+sf.id, {
defaults:{
name:{value:""},
env:{value:[]}
env:{value:[], validate: function(value) {
const errors = []
if (value) {
value.forEach(env => {
const r = RED.utils.validateTypedProperty(env.value, env.type)
if (r !== true) {
errors.push(env.name+': '+r)
}
})
}
if (errors.length === 0) {
return true
} else {
return errors
}
}}
},
icon: function() { return sf.icon||"subflow.svg" },
category: sf.category || "subflows",

View File

@ -1,6 +1,7 @@
RED.plugins = (function() {
var plugins = {};
var pluginsByType = {};
var moduleList = {};
function registerPlugin(id,definition) {
plugins[id] = definition;
@ -38,9 +39,43 @@ RED.plugins = (function() {
function getPluginsByType(type) {
return pluginsByType[type] || [];
}
function setPluginList(list) {
for(let i=0;i<list.length;i++) {
let p = list[i];
addPlugin(p);
}
}
function addPlugin(p) {
moduleList[p.module] = moduleList[p.module] || {
name:p.module,
version:p.version,
local:p.local,
sets:{},
plugin: true,
id: p.id
};
if (p.pending_version) {
moduleList[p.module].pending_version = p.pending_version;
}
moduleList[p.module].sets[p.name] = p;
RED.events.emit("registry:plugin-module-added",p.module);
}
function getModule(module) {
return moduleList[module];
}
return {
registerPlugin: registerPlugin,
getPlugin: getPlugin,
getPluginsByType: getPluginsByType
getPluginsByType: getPluginsByType,
setPluginList: setPluginList,
addPlugin: addPlugin,
getModule: getModule
}
})();

View File

@ -25,6 +25,7 @@ var RED = (function() {
cache: false,
url: 'plugins',
success: function(data) {
RED.plugins.setPluginList(data);
loader.reportProgress(RED._("event.loadPlugins"), 13)
RED.i18n.loadPluginCatalogs(function() {
loadPlugins(function() {
@ -534,6 +535,41 @@ var RED = (function() {
RED.view.redrawStatus(node);
}
});
RED.comms.subscribe("notification/plugin/#",function(topic,msg) {
if (topic == "notification/plugin/added") {
RED.settings.refreshSettings(function(err, data) {
let addedPlugins = [];
msg.forEach(function(m) {
let id = m.id;
RED.plugins.addPlugin(m);
m.plugins.forEach((p) => {
addedPlugins.push(p.id);
})
RED.i18n.loadNodeCatalog(id, function() {
var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage();
$.ajax({
headers: {
"Accept":"text/html",
"Accept-Language": lang
},
cache: false,
url: 'plugins/'+id,
success: function(data) {
appendPluginConfig(data);
}
});
});
});
if (addedPlugins.length) {
let pluginList = "<ul><li>"+addedPlugins.map(RED.utils.sanitize).join("</li><li>")+"</li></ul>";
// ToDo: Adapt notification (node -> plugin)
RED.notify(RED._("palette.event.nodeAdded", {count:addedPlugins.length})+pluginList,"success");
}
})
}
});
let pendingNodeRemovedNotifications = []
let pendingNodeRemovedTimeout

View File

@ -118,10 +118,16 @@ RED.contextMenu = (function () {
onselect: 'core:split-wire-with-link-nodes',
disabled: !canEdit || !hasLinks
},
null,
null
)
if (RED.settings.theme("menu.menu-item-import-library", true)) {
insertOptions.push(
{ onselect: 'core:show-import-dialog', label: RED._('common.label.import')},
{ onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') }
)
}
if (hasSelection && canEdit) {
const nodeOptions = []
if (!hasMultipleSelection && !isGroup) {
@ -194,8 +200,14 @@ RED.contextMenu = (function () {
{ onselect: 'core:paste-from-internal-clipboard', label: RED._("keyboard.pasteNode"), disabled: !canEdit || !RED.view.clipboard() },
{ onselect: 'core:delete-selection', label: RED._('keyboard.deleteSelected'), disabled: !canEdit || !canDelete },
{ onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), disabled: !canEdit || !canDelete },
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") },
{ onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") },
)
if (RED.settings.theme("menu.menu-item-export-library", true)) {
menuItems.push(
{ onselect: 'core:show-export-dialog', label: RED._("menu.label.export") }
)
}
menuItems.push(
{ onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") }
)
}

View File

@ -153,10 +153,6 @@ RED.envVar = (function() {
}
function init(done) {
if (!RED.user.hasPermission("settings.write")) {
RED.notify(RED._("user.errors.settings"),"error");
return;
}
RED.userSettings.add({
id:'envvar',
title: RED._("env-var.environment"),

View File

@ -248,6 +248,23 @@ RED.palette.editor = (function() {
var moduleInfo = nodeEntries[module].info;
var nodeEntry = nodeEntries[module].elements;
if (nodeEntry) {
if (moduleInfo.plugin) {
nodeEntry.enableButton.hide();
nodeEntry.removeButton.show();
let pluginCount = 0;
for (let setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
let set = moduleInfo.sets[setName];
if (set.plugins) {
pluginCount += set.plugins.length;
}
}
}
nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount}));
} else {
var activeTypeCount = 0;
var typeCount = 0;
var errorCount = 0;
@ -258,8 +275,9 @@ RED.palette.editor = (function() {
for (var setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
var inUseCount = 0;
var set = moduleInfo.sets[setName];
var setElements = nodeEntry.sets[setName];
const set = moduleInfo.sets[setName];
const setElements = nodeEntry.sets[setName]
if (set.err) {
errorCount++;
var errMessage = set.err;
@ -277,18 +295,18 @@ RED.palette.editor = (function() {
for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
var t = moduleInfo.sets[setName].types[i];
inUseCount += (typesInUse[t]||0);
var swatch = setElements.swatches[t];
if (set.enabled) {
if (setElements && set.enabled) {
var def = RED.nodes.getType(t);
if (def && def.color) {
swatch.css({background:RED.utils.getNodeColor(t,def)});
swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))})
setElements.swatches[t].css({background:RED.utils.getNodeColor(t,def)});
setElements.swatches[t].css({border: "1px solid "+getContrastingBorder(setElements.swatches[t].css('backgroundColor'))})
}
}
}
nodeEntries[module].setUseCount[setName] = inUseCount;
nodeEntries[module].totalUseCount += inUseCount;
if (setElements) {
if (inUseCount > 0) {
setElements.enableButton.text(RED._('palette.editor.inuse'));
setElements.enableButton.addClass('disabled');
@ -303,6 +321,7 @@ RED.palette.editor = (function() {
setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
}
}
}
if (errorCount === 0) {
nodeEntry.errorRow.hide()
@ -330,6 +349,7 @@ RED.palette.editor = (function() {
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
}
}
}
if (moduleInfo.pending_version) {
nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
nodeEntry.updateButton.text(RED._('palette.editor.updated')).addClass('disabled').css('display', 'inline-block');
@ -678,6 +698,33 @@ RED.palette.editor = (function() {
}
}
})
RED.events.on("registry:plugin-module-added", function(module) {
if (!nodeEntries.hasOwnProperty(module)) {
nodeEntries[module] = {info:RED.plugins.getModule(module)};
var index = [module];
for (var s in nodeEntries[module].info.sets) {
if (nodeEntries[module].info.sets.hasOwnProperty(s)) {
index.push(s);
index = index.concat(nodeEntries[module].info.sets[s].types)
}
}
nodeEntries[module].index = index.join(",").toLowerCase();
nodeList.editableList('addItem', nodeEntries[module]);
} else {
_refreshNodeModule(module);
}
for (var i=0;i<filteredList.length;i++) {
if (filteredList[i].info.id === module) {
var installButton = filteredList[i].elements.installButton;
installButton.addClass('disabled');
installButton.text(RED._('palette.editor.installed'));
break;
}
}
});
}
var settingsPane;
@ -804,6 +851,7 @@ RED.palette.editor = (function() {
errorRow: errorRow,
errorList: errorList,
setCount: setCount,
setButton: setButton,
container: container,
shade: shade,
versionSpan: versionSpan,
@ -814,12 +862,17 @@ RED.palette.editor = (function() {
if (container.hasClass('expanded')) {
container.removeClass('expanded');
contentRow.slideUp();
setTimeout(() => {
contentRow.empty()
}, 200)
object.elements.sets = {}
} else {
container.addClass('expanded');
populateSetList()
contentRow.slideDown();
}
})
const populateSetList = function () {
var setList = Object.keys(entry.sets)
setList.sort(function(A,B) {
return A.toLowerCase().localeCompare(B.toLowerCase());
@ -829,12 +882,21 @@ RED.palette.editor = (function() {
var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
var typeSwatches = {};
let enableButton;
if (set.types) {
set.types.forEach(function(t) {
var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
if (set.enabled) {
var def = RED.nodes.getType(t);
if (def && def.color) {
typeSwatches[t].css({background:RED.utils.getNodeColor(t,def)});
typeSwatches[t].css({border: "1px solid "+getContrastingBorder(typeSwatches[t].css('backgroundColor'))})
}
}
$('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
})
var enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup);
enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup);
enableButton.on("click", function(evt) {
evt.preventDefault();
if (object.setUseCount[setName] === 0) {
@ -851,12 +913,37 @@ RED.palette.editor = (function() {
}
})
if (object.setUseCount[setName] > 0) {
enableButton.text(RED._('palette.editor.inuse'));
enableButton.addClass('disabled');
} else {
enableButton.removeClass('disabled');
if (set.enabled) {
enableButton.text(RED._('palette.editor.disable'));
} else {
enableButton.text(RED._('palette.editor.enable'));
}
}
setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
}
if (set.plugins) {
set.plugins.forEach(function(p) {
var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
// typeSwatches[p.id] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
$('<span><i class="fa fa-puzzle-piece" aria-hidden="true"></i> </span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
$('<span>',{class:"red-ui-palette-module-type-node"}).text(p.id).appendTo(typeDiv);
})
}
object.elements.sets[set.name] = {
setRow: setRow,
enableButton: enableButton,
swatches: typeSwatches
};
});
}
enableButton.on("click", function(evt) {
evt.preventDefault();
if (object.totalUseCount === 0) {
@ -1226,7 +1313,55 @@ RED.palette.editor = (function() {
}
}
]
}); }
});
}
} else {
// dedicated list management for plugins
if (entry.plugin) {
let e = nodeEntries[entry.name];
if (e) {
nodeList.editableList('removeItem', e);
delete nodeEntries[entry.name];
}
// We assume that a plugin that implements onremove
// cleans the editor accordingly of its left-overs.
let found_onremove = true;
let keys = Object.keys(entry.sets);
keys.forEach((key) => {
let set = entry.sets[key];
for (let i=0; i<set.plugins?.length; i++) {
let plgn = RED.plugins.getPlugin(set.plugins[i].id);
if (plgn && plgn.onremove && typeof plgn.onremove === 'function') {
plgn.onremove();
} else {
if (plgn && plgn.onadd && typeof plgn.onadd === 'function') {
// if there's no 'onadd', there shouldn't be any left-overs
found_onremove = false;
}
}
}
});
if (!found_onremove) {
let removeNotify = RED.notify("Removed plugin " + entry.name + ". Please reload the editor to clear left-overs.",{
modal: true,
fixed: true,
type: 'warning',
buttons: [
{
text: "Understood",
class:"primary",
click: function(e) {
removeNotify.close();
}
}
]
});
}
}
}
})
notification.close();

View File

@ -35,6 +35,10 @@ RED.palette = (function() {
var categoryContainers = {};
var sidebarControls;
let paletteState = { filter: "", collapsed: [] };
let filterRefreshTimeout
function createCategory(originalCategory,rootCategory,category,ns) {
if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
@ -60,20 +64,57 @@ RED.palette = (function() {
catDiv.data('label',label);
categoryContainers[category] = {
container: catDiv,
close: function() {
hide: function (instant) {
if (instant) {
catDiv.hide()
} else {
catDiv.slideUp()
}
},
show: function () {
catDiv.show()
},
isOpen: function () {
return !!catDiv.hasClass("red-ui-palette-open")
},
getNodeCount: function (visibleOnly) {
const nodes = catDiv.find(".red-ui-palette-node")
if (visibleOnly) {
return nodes.filter(function() { return $(this).css('display') !== 'none'}).length
} else {
return nodes.length
}
},
close: function(instant, skipSaveState) {
catDiv.removeClass("red-ui-palette-open");
catDiv.addClass("red-ui-palette-closed");
if (instant) {
$("#red-ui-palette-base-category-"+category).hide();
} else {
$("#red-ui-palette-base-category-"+category).slideUp();
}
$("#red-ui-palette-header-"+category+" i").removeClass("expanded");
if (!skipSaveState) {
if (!paletteState.collapsed.includes(category)) {
paletteState.collapsed.push(category);
savePaletteState();
}
}
},
open: function() {
open: function(skipSaveState) {
catDiv.addClass("red-ui-palette-open");
catDiv.removeClass("red-ui-palette-closed");
$("#red-ui-palette-base-category-"+category).slideDown();
$("#red-ui-palette-header-"+category+" i").addClass("expanded");
if (!skipSaveState) {
if (paletteState.collapsed.includes(category)) {
paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1);
savePaletteState();
}
}
},
toggle: function() {
if (catDiv.hasClass("red-ui-palette-open")) {
if (categoryContainers[category].isOpen()) {
categoryContainers[category].close();
} else {
categoryContainers[category].open();
@ -415,8 +456,16 @@ RED.palette = (function() {
var categoryNode = $("#red-ui-palette-container-"+rootCategory);
if (categoryNode.find(".red-ui-palette-node").length === 1) {
if (!paletteState?.collapsed?.includes(rootCategory)) {
categoryContainers[rootCategory].open();
} else {
categoryContainers[rootCategory].close(true);
}
}
clearTimeout(filterRefreshTimeout)
filterRefreshTimeout = setTimeout(() => {
refreshFilter()
}, 200)
}
}
@ -516,7 +565,8 @@ RED.palette = (function() {
paletteNode.css("backgroundColor", sf.color);
}
function filterChange(val) {
function refreshFilter() {
const val = $("#red-ui-palette-search input").val()
var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
$("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
var currentLabel = $(el).attr("data-palette-label");
@ -528,16 +578,26 @@ RED.palette = (function() {
}
});
for (var category in categoryContainers) {
for (let category in categoryContainers) {
if (categoryContainers.hasOwnProperty(category)) {
if (categoryContainers[category].container
.find(".red-ui-palette-node")
.filter(function() { return $(this).css('display') !== 'none'}).length === 0) {
categoryContainers[category].close();
categoryContainers[category].container.slideUp();
const categorySection = categoryContainers[category]
if (categorySection.getNodeCount(true) === 0) {
categorySection.hide()
} else {
categoryContainers[category].open();
categoryContainers[category].container.show();
categorySection.show()
if (val) {
// There is a filter being applied and it has matched
// something in this category - show the contents
categorySection.open(true)
} else {
// No filter. Only show the category if it isn't in lastState
if (!paletteState.collapsed.includes(category)) {
categorySection.open(true)
} else if (categorySection.isOpen()) {
// This section should be collapsed but isn't - so make it so
categorySection.close(true, true)
}
}
}
}
}
@ -553,6 +613,9 @@ RED.palette = (function() {
$("#red-ui-palette > .red-ui-palette-spinner").show();
RED.events.on('logout', function () {
RED.settings.removeLocal('palette-state')
})
RED.events.on('registry:node-type-added', function(nodeType) {
var def = RED.nodes.getType(nodeType);
@ -596,14 +659,14 @@ RED.palette = (function() {
RED.events.on("subflows:change",refreshSubflow);
$("#red-ui-palette-search input").searchBox({
delay: 100,
change: function() {
filterChange($(this).val());
refreshFilter();
paletteState.filter = $(this).val();
savePaletteState();
}
})
});
sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");
@ -669,7 +732,23 @@ RED.palette = (function() {
togglePalette(state);
}
});
try {
paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
if (paletteState.filter) {
// Apply the category filter
$("#red-ui-palette-search input").searchBox("value", paletteState.filter);
}
} catch (error) {
console.error("Unexpected error loading palette state from localStorage: ", error);
}
setTimeout(() => {
// Lazily tidy up any categories that haven't been reloaded
paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
savePaletteState()
}, 10000)
}
function togglePalette(state) {
if (!state) {
$("#red-ui-main-container").addClass("red-ui-palette-closed");
@ -689,6 +768,15 @@ RED.palette = (function() {
})
return categories;
}
function savePaletteState() {
try {
RED.settings.setLocal("palette-state", JSON.stringify(paletteState));
} catch (error) {
console.error("Unexpected error saving palette state to localStorage: ", error);
}
}
return {
init: init,
add:addNodeType,

View File

@ -1280,14 +1280,20 @@ RED.subflow = (function() {
var nodePropValue = nodeProp;
if (prop.ui && prop.ui.type === "cred") {
nodePropType = "cred";
} else if (prop.ui && prop.ui.type === "conf-types") {
nodePropType = prop.value.type
} else {
switch(typeof nodeProp) {
case "string": nodePropType = "str"; break;
case "number": nodePropType = "num"; break;
case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
default:
if (nodeProp) {
nodePropType = nodeProp.type;
nodePropValue = nodeProp.value;
} else {
nodePropType = 'str'
}
}
}
var item = {

View File

@ -158,9 +158,11 @@ RED.sidebar.help = (function() {
function refreshSubflow(sf) {
var item = treeList.treeList('get',"node-type:subflow:"+sf.id);
if (item) {
item.subflowLabel = sf._def.label().toLowerCase();
item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()}));
}
}
function hideTOC() {
var tocButton = $('#red-ui-sidebar-help-show-toc')

View File

@ -491,6 +491,11 @@ RED.workspaces = (function() {
createWorkspaceTabs();
RED.events.on("sidebar:resize",workspace_tabs.resize);
RED.events.on("workspace:clear", () => {
// Reset the index used to generate new flow names
workspaceIndex = 0
})
RED.actions.add("core:show-next-tab",function() {
var oldActive = activeWorkspace;
workspace_tabs.nextTab();
@ -657,6 +662,9 @@ RED.workspaces = (function() {
RED.events.on("flows:change", (ws) => {
$("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
})
RED.events.on("subflows:change", (ws) => {
$("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added));
})
hideWorkspace();
}

View File

@ -187,6 +187,7 @@ RED.user = (function() {
}
function logout() {
RED.events.emit('logout')
var tokens = RED.settings.get("auth-tokens");
var token = tokens?tokens.access_token:"";
$.ajax({
@ -225,6 +226,7 @@ RED.user = (function() {
});
}
});
$('<i class="fa fa-user"></i>').appendTo("#red-ui-header-button-user");
} else {
RED.menu.addItem("red-ui-header-button-user",{
id:"usermenu-item-username",
@ -237,6 +239,15 @@ RED.user = (function() {
RED.user.logout();
}
});
const userMenu = $("#red-ui-header-button-user")
userMenu.empty()
if (RED.settings.user.image) {
$('<span class="user-profile"></span>').css({
backgroundImage: "url("+RED.settings.user.image+")",
}).appendTo(userMenu);
} else {
$('<i class="fa fa-user"></i>').appendTo(userMenu);
}
}
}
@ -247,14 +258,6 @@ RED.user = (function() {
var userMenu = $('<li><a id="red-ui-header-button-user" class="button hide" href="#"></a></li>')
.prependTo(".red-ui-header-toolbar");
if (RED.settings.user.image) {
$('<span class="user-profile"></span>').css({
backgroundImage: "url("+RED.settings.user.image+")",
}).appendTo(userMenu.find("a"));
} else {
$('<i class="fa fa-user"></i>').appendTo(userMenu.find("a"));
}
RED.menu.init({id:"red-ui-header-button-user",
options: []
});

View File

@ -63,25 +63,29 @@
}
.red-ui-header-toolbar {
display: flex;
align-items: stretch;
padding: 0;
margin: 0;
list-style: none;
float: right;
> li {
display: inline-block;
display: inline-flex;
align-items: stretch;
padding: 0;
margin: 0;
position: relative;
}
}
.button {
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
text-align: center;
line-height: 40px;
display: inline-block;
font-size: 20px;
padding: 0px 12px;
text-decoration: none;
@ -271,13 +275,13 @@
color: var(--red-ui-header-menu-heading-color);
}
#red-ui-header-button-user .user-profile {
.user-profile {
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
display: inline-block;
width: 40px;
height: 35px;
width: 30px;
height: 30px;
vertical-align: middle;
}
}

View File

@ -17,7 +17,8 @@ export default {
{
title: {
"en-US": "Timestamp formatting options",
"ja": "タイムスタンプの形式の項目"
"ja": "タイムスタンプの形式の項目",
"fr": "Options de formatage de l'horodatage"
},
image: 'images/nr4-timestamp-formatting.png',
description: {
@ -34,13 +35,21 @@ export default {
<li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li>
<li>ISO 8601 - 多くのシステムで使用されている共通の形式</li>
<li>JavaScript日付オブジェクト</li>
</ul>`,
"fr": `<p>Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.</p>
<p>Nous gardons les choses simples en proposant trois options :<p>
<ul>
<li>Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage</li>
<li>ISO 8601 : un format commun utilisé par de nombreux systèmes</li>
<li>Objet Date JavaScript</li>
</ul>`
}
},
{
title: {
"en-US": "Auto-complete of flow/global and env types",
"ja": "フロー/グローバル、環境変数の型の自動補完"
"ja": "フロー/グローバル、環境変数の型の自動補完",
"fr": "Saisie automatique des types de flux/global et env"
},
image: 'images/nr4-auto-complete.png',
description: {
@ -48,13 +57,17 @@ export default {
now all include auto-complete suggestions based on the live state of your flows.</p>
`,
"ja": `<p><code>flow</code>/<code>global</code>コンテキストや<code>env</code>の入力を、現在のフローの状態をもとに自動補完で提案するようになりました。</p>
`
`,
"fr": `<p>Les entrées contextuelles <code>flow</code>/<code>global</code> et l'entrée <code>env</code>
incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.</p>
`,
}
},
{
title: {
"en-US": "Config node customisation in Subflows",
"ja": "サブフローでの設定ノードのカスタマイズ"
"ja": "サブフローでの設定ノードのカスタマイズ",
"fr": "Personnalisation du noeud de configuration dans les sous-flux"
},
image: 'images/nr4-sf-config.png',
description: {
@ -65,6 +78,11 @@ export default {
`,
"ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p>
<p>例えばMQTTブローカへ接続しメッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です</p>
`,
"fr": `<p>Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un
noeud de configuration d'un type sélectionné.</p>
<p>Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement
des messages reçus peut être pointée vers un autre courtier.</p>
`
}
},
@ -90,6 +108,14 @@ export default {
<li>WebSocketードのカスタマイズ可能なヘッダ</li>
<li>Splitードはメッセージプロパティで操作できるようになりました</li>
<li>他にも沢山あります...</li>
</ul>`,
"fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour.
Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :</p>
<ul>
<li>Un mode CSV entièrement conforme à la norme RFC4180</li>
<li>En-têtes personnalisables pour le noeud WebSocket</li>
<li>Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message</li>
<li>Et bien plus encore...</li>
</ul>`
}
}

View File

@ -378,7 +378,7 @@
return { id: id, label: RED.nodes.workspace(id).label } //flow id + name
} else {
const instanceNode = RED.nodes.node(id)
const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8)).name)
const pathLabel = (instanceNode.name || RED.nodes.subflow(instanceNode.type.substring(8))?.name || instanceNode.type)
return { id: id, label: pathLabel }
}
})

View File

@ -233,9 +233,12 @@ module.exports = function(RED) {
// only replace if they match exactly
RED.util.setMessageProperty(msg,property,value);
} else {
// if target is boolean then just replace it
if (rule.tot === "bool") { current = value; }
else { current = current.replace(fromRE,value); }
current = current.replace(fromRE,value);
if (rule.tot === "bool" && current === ""+value) {
// If the target type is boolean, and the replace call has resulted in "true"/"false",
// convert to boolean type (which 'value' already is)
current = value
}
RED.util.setMessageProperty(msg,property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {

View File

@ -0,0 +1,3 @@
<script type="text/html" data-help-name="global-config">
<p>Un noeud pour contenir la configuration globale des flux.</p>
</script>

View File

@ -94,6 +94,7 @@
},
"catch": {
"catch": "catch : tout",
"catchGroup": "catch: groupe",
"catchNodes": "catch : __number__",
"catchUncaught": "catch : non capturé",
"label": {
@ -109,6 +110,7 @@
},
"status": {
"status": "statut : tout",
"statusGroup": "statut: groupe",
"statusNodes": "statut : __number__",
"label": {
"source": "Signaler l'état de",
@ -250,7 +252,8 @@
"initialize": "Au démarrage",
"finalize": "À l'arrêt",
"outputs": "Sorties",
"modules": "Modules"
"modules": "Modules",
"timeout": "Délai d'attente"
},
"text": {
"initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\n",
@ -847,7 +850,13 @@
"newline": "Nouvelle ligne",
"usestrings": "Analyser les valeurs numériques",
"include_empty_strings": "Inclure les chaînes vides",
"include_null_values": "Inclure les valeurs nulles"
"include_null_values": "Inclure les valeurs nulles",
"spec": "Analyseur"
},
"spec": {
"rfc": "RFC4180",
"legacy": "Hérité (Legacy)",
"legacy_warning": "Le mode hérité sera supprimé dans une prochaine version."
},
"placeholder": {
"columns": "noms de colonnes séparés par des virgules"
@ -876,6 +885,7 @@
"once": "envoyer les en-têtes une fois, jusqu'à msg.reset"
},
"errors": {
"bad_template": "Colonnes du modèle mal formées.",
"csv_js": "Ce noeud ne gère que les chaînes CSV ou les objets js.",
"obj_csv": "Aucun modèle de colonnes spécifié pour l'objet -> CSV.",
"bad_csv": "Données CSV mal formées - sortie probablement corrompue."
@ -885,12 +895,14 @@
"label": {
"select": "Sélecteur",
"output": "Sortie",
"in": "dans"
"in": "dans",
"prefix": "Nom de la propriété pour le contenu HTML"
},
"output": {
"html": "le contenu html des éléments",
"text": "uniquement le contenu textuel des éléments",
"attr": "un objet de n'importe quel attribut des éléments"
"attr": "un objet de n'importe quel attribut des éléments",
"compl": "un objet pour tous les attributs de tous les éléments ainsi que du contenu HTML"
},
"format": {
"single": "comme un seul message contenant un tableau",

View File

@ -30,6 +30,8 @@
avant d'être envoyé.</p>
<p>Si <code>msg._session</code> n'est pas présent, la charge utile est
envoyé à <b>tous</b> les clients connectés.</p>
<p>En mode Répondre à, définir <code>msg.reset = true</code> réinitialisera la connexion
spécifiée par _session.id ou toutes les connexions si aucun _session.id n'est spécifié.</p>
<p><b>Remarque</b> : Sur certains systèmes, vous aurez peut-être besoin d'un accès root ou administrateur
pour accéder aux ports inférieurs à 1024.</p>
</script>
@ -40,6 +42,8 @@
caractères renvoyés dans un tampon fixe, correspondant à un caractère spécifié avant de revenir,
attendre un délai fixe à partir de la première réponse, puis revenir, s'installer et attender les données, ou envoie puis ferme la connexion
immédiatement, sans attendre de réponse.</p>
<p>Dans le cas du mode veille (maintien de la connexion), vous pouvez envoyer <code>msg.reset = true</code> ou <code>msg.reset = "host:port"</code> pour forcer une interruption
de la connexion et une reconnexion automatique.</p>
<p>La réponse sortira dans <code>msg.payload</code> en tant que tampon, vous pouvez alors utiliser la fonction .toString().</p>
<p>Si vous laissez l'hôte ou le port tcp vide, ils doivent être définis à l'aide des propriétés <code>msg.host</code> et <code>msg.port</code> dans chaque message envoyé au noeud.</ p>
</script>

View File

@ -36,7 +36,9 @@
</dl>
<h3>Détails</h3>
<p>Le modèle de colonne peut contenir une liste ordonnée de noms de colonnes. Lors de la conversion de CSV en objet, les noms de colonne
seront utilisés comme noms de propriété. Alternativement, les noms de colonne peuvent être tirés de la première ligne du CSV.</p>
seront utilisés comme noms de propriété. Alternativement, les noms de colonne peuvent être tirés de la première ligne du CSV.
<p>Lorsque l'analyseur RFC est sélectionné, le modèle de colonne doit être conforme à la norme RFC4180.</p>
</p>
<p>Lors de la conversion au format CSV, le modèle de colonnes est utilisé pour identifier les propriétés à extraire de l'objet et dans quel ordre.</p>
<p>Si le modèle de colonnes est vide, vous pouvez utiliser une simple liste de propriétés séparées par des virgules fournies dans <code>msg.columns</code> pour
déterminer quoi extraire et dans quel ordre. Si ni l'un ni l'autre n'est présent, toutes les propriétés de l'objet sont sorties dans l'ordre

View File

@ -319,6 +319,7 @@ module.exports = {
getPluginsByType: plugins.getPluginsByType,
getPluginList: plugins.getPluginList,
getPluginConfigs: plugins.getPluginConfigs,
getPluginConfig: plugins.getPluginConfig,
exportPluginSettings: plugins.exportPluginSettings,

View File

@ -28,6 +28,8 @@ const child_process = require('child_process');
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
let installerEnabled = false;
const plugins = require("./plugins");
let settings;
const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/;
const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/;
@ -330,10 +332,18 @@ function reportRemovedModules(removedNodes) {
//comms.publish("node/removed",removedNodes,false);
log.info(log._("server.removed-types"));
for (var j=0;j<removedNodes.length;j++) {
for (var i=0;i<removedNodes[j].types.length;i++) {
for (var i=0;i<removedNodes[j].types?.length;i++) {
log.info(" - "+(removedNodes[j].module?removedNodes[j].module+":":"")+removedNodes[j].types[i]);
}
}
log.info(log._("server.removed-plugins"));
for (let j=0;j<removedNodes.length;j++) {
for (var i=0;i<removedNodes[j].plugins?.length;i++) {
log.info(" - "+(removedNodes[j].module?removedNodes[j].module+":":"")+removedNodes[j].plugins[i].id);
}
}
return removedNodes;
}
@ -496,7 +506,11 @@ function uninstallModule(module) {
return reject(new Error(log._("server.install.uninstall-failed",{name:module})));
}
var list = registry.removeModule(module);
// need to remove the plugins first,
// as registry data necessary to perform this operation
var list = plugins.removeModule(module);
list = list.concat(registry.removeModule(module));
log.info(log._("server.install.uninstalling",{name:module}));
let triggerPayload = {

View File

@ -39,6 +39,8 @@ function registerPlugin(nodeSetId,id,definition) {
pluginSettings[id] = definition.settings;
}
// reset the cache when a new plugin is incoming!
pluginConfigCache = {};
if (definition.onadd && typeof definition.onadd === 'function') {
definition.onadd();
@ -55,29 +57,47 @@ function getPluginsByType(type) {
}
function getPluginConfigs(lang) {
// we're not re-using getPluginConfig() here,
// to avoid calling registry.getModuleList() multiple times!
if (!pluginConfigCache[lang]) {
var result = "";
var script = "";
var moduleConfigs = registry.getModuleList();
for (var module in moduleConfigs) {
/* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(module)) {
var plugins = moduleConfigs[module].plugins;
for (var plugin in plugins) {
if (plugins.hasOwnProperty(plugin)) {
var config = plugins[plugin];
if (config.enabled && !config.err && config.config) {
result += "\n<!-- --- [red-plugin:"+config.id+"] --- -->\n";
result += config.config;
}
}
}
result += generateModulePluginConfig(moduleConfigs[module]);
}
}
pluginConfigCache[lang] = result;
}
return pluginConfigCache[lang];
}
function getPluginConfig(id, lang) {
let result = '';
let moduleConfigs = registry.getModuleList();
if (moduleConfigs.hasOwnProperty(id)) {
result = generateModulePluginConfig(moduleConfigs[id]);
}
return result;
}
function generateModulePluginConfig(module) {
let result = '';
const plugins = module.plugins
for (let plugin in plugins) {
if (plugins.hasOwnProperty(plugin)) {
let config = plugins[plugin];
if (config.enabled && !config.err && config.config) {
result += "\n<!-- --- [red-plugin:"+config.id+"] --- -->\n";
result += config.config;
}
}
}
return result;
}
function getPluginList() {
var list = [];
var moduleConfigs = registry.getModuleList();
@ -142,12 +162,51 @@ function exportPluginSettings(safeSettings) {
return safeSettings;
}
function removeModule(moduleId) {
// clean the (plugin) registry when a module is removed / uninstalled
let pluginList = [];
let module = registry.getModule(moduleId);
let keys = Object.keys(module.plugins ?? {});
keys.forEach( key => {
let _plugins = module.plugins[key].plugins ?? [];
_plugins.forEach( plugin => {
let id = plugin.id;
if (plugin.onremove && typeof plugin.onremove === 'function') {
plugin.onremove();
}
delete pluginToId[id];
delete plugins[id];
delete pluginSettings[id];
pluginConfigCache = {};
let psbtype = pluginsByType[plugin.type] ?? [];
for (let i=psbtype.length; i>0; i--) {
let pbt = psbtype[i-1];
if (pbt.id == id) {
psbtype.splice(i-1, 1);
}
}
})
pluginList.push(registry.filterNodeInfo(module.plugins[key]));
})
return pluginList;
}
module.exports = {
init,
registerPlugin,
getPlugin,
getPluginsByType,
getPluginConfigs,
getPluginConfig,
getPluginList,
exportPluginSettings
exportPluginSettings,
removeModule
}

View File

@ -65,7 +65,13 @@ function filterNodeInfo(n) {
r.err = n.err;
}
if (n.hasOwnProperty("plugins")) {
r.plugins = n.plugins;
r.plugins = n.plugins.map(p => {
return {
id: p.id,
type: p.type,
module: p.module
}
});
}
if (n.type === "plugin") {
r.editor = !!n.template;
@ -386,7 +392,8 @@ function getModuleInfo(module) {
local: moduleConfigs[module].local,
user: moduleConfigs[module].user,
path: moduleConfigs[module].path,
nodes: []
nodes: [],
plugins: []
};
if (moduleConfigs[module].dependencies) {
m.dependencies = moduleConfigs[module].dependencies;
@ -399,6 +406,14 @@ function getModuleInfo(module) {
nodeInfo.version = m.version;
m.nodes.push(nodeInfo);
}
let plugins = Object.values(moduleConfigs[module].plugins ?? {});
plugins.forEach((plugin) => {
let nodeInfo = filterNodeInfo(plugin);
nodeInfo.version = m.version;
m.plugins.push(nodeInfo);
});
return m;
} else {
return null;

View File

@ -36,7 +36,7 @@ var connections = [];
const events = require("@node-red/util").events;
function handleCommsEvent(event) {
publish(event.topic,event.data,event.retain);
publish(event.topic,event.data,event.retain,event.session,event.excludeSession);
}
function handleStatusEvent(event) {
if (!event.status) {
@ -74,13 +74,17 @@ function handleEventLog(event) {
publish("event-log/"+event.id,event.payload||{});
}
function publish(topic,data,retain) {
function publish(topic, data, retain, session, excludeSession) {
if (retain) {
retained[topic] = data;
} else {
delete retained[topic];
}
connections.forEach(connection => connection.send(topic,data))
connections.forEach(connection => {
if ((!session || connection.session === session) && (!excludeSession || connection.session !== excludeSession)) {
connection.send(topic,data)
}
})
}
@ -109,6 +113,10 @@ var api = module.exports = {
*/
addConnection: async function(opts) {
connections.push(opts.client);
events.emit('comms:connection-added', {
session: opts.client.session,
user: opts.client.user
})
},
/**
@ -126,6 +134,9 @@ var api = module.exports = {
break;
}
}
events.emit('comms:connection-removed', {
session: opts.client.session
})
},
/**
@ -157,5 +168,23 @@ var api = module.exports = {
* @return {Promise<Object>} - resolves when complete
* @memberof @node-red/runtime_comms
*/
unsubscribe: async function(opts) {}
unsubscribe: async function(opts) {},
/**
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {CommsConnection} opts.client - the client connection
* @param {String} opts.topic - the message topic
* @param {String} opts.data - the message data
* @return {Promise<Object>} - resolves when complete
*/
receive: async function (opts) {
if (opts.topic) {
events.emit('comms:message:' + opts.topic, {
session: opts.client.session,
user: opts.user,
data: opts.data
})
}
}
};

View File

@ -65,6 +65,25 @@ var api = module.exports = {
runtime.log.audit({event: "plugins.configs.get"}, opts.req);
return runtime.plugins.getPluginConfigs(opts.lang);
},
/**
* Gets the editor content for one registered plugin
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {User} opts.user - the user calling the api
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<NodeInfo>} - the plugin information
* @memberof @node-red/runtime_plugins
*/
getPluginConfig: async function(opts) {
if (/[^0-9a-z=\-\*]/i.test(opts.lang)) {
throw new Error("Invalid language: "+opts.lang)
return;
}
runtime.log.audit({event: "plugins.configs.get"}, opts.req);
return runtime.plugins.getPluginConfig(opts.module, opts.lang);
},
/**
* Gets all registered module message catalogs
* @param {Object} opts

View File

@ -106,15 +106,23 @@ async function evaluateEnvProperties(flow, env, credentials) {
result = { value: result, __clone__: true}
}
evaluatedEnv[name] = result
} else {
evaluatedEnv[name] = undefined
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
}
resolve()
});
}))
} else {
try {
value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null);
if (typeof value === 'object') {
value = { value: value, __clone__: true}
}
} catch (err) {
value = undefined
flow.error(`Error evaluating env property '${name}': ${err.toString()}`)
}
}
evaluatedEnv[name] = value
}

View File

@ -172,9 +172,13 @@ function installModule(module,version,url) {
return registry.installModule(module,version,url).then(function(info) {
if (info.pending_version) {
events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:info.name,version:info.pending_version}});
} else {
if (!info.nodes.length && info.plugins.length) {
events.emit("runtime-event",{id:"plugin/added",retain:false,payload:info.plugins});
} else {
events.emit("runtime-event",{id:"node/added",retain:false,payload:info.nodes});
}
}
return info;
});
}

View File

@ -7,5 +7,6 @@ module.exports = {
getPluginsByType: registry.getPluginsByType,
getPluginList: registry.getPluginList,
getPluginConfigs: registry.getPluginConfigs,
getPluginConfig: registry.getPluginConfig,
exportPluginSettings: registry.exportPluginSettings
}

View File

@ -25,6 +25,7 @@
"removing-modules": "Removing modules from config",
"added-types": "Added node types:",
"removed-types": "Removed node types:",
"removed-plugins": "Removed plugins:",
"install": {
"invalid": "Invalid module name",
"installing": "Installing module: __name__, version: __version__",

View File

@ -20,7 +20,7 @@
"@node-red/util": "4.0.0-beta.1",
"async-mutex": "0.4.0",
"clone": "2.1.2",
"express": "4.18.2",
"express": "4.19.2",
"fs-extra": "11.1.1",
"json-stringify-safe": "5.0.1"
}

View File

@ -37,9 +37,9 @@
"@node-red/nodes": "4.0.0-beta.1",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.18.2",
"express": "4.19.2",
"fs-extra": "11.1.1",
"node-red-admin": "^3.1.2",
"node-red-admin": "^3.1.3",
"nopt": "5.0.0",
"semver": "7.5.4"
},

View File

@ -918,7 +918,7 @@ describe('change Node', function() {
});
});
it('changes the value and type of the message property if a complete match', function(done) {
it('changes the value and type of the message property if a complete match - number', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{ "t": "change", "p": "payload", "pt": "msg", "from": "123", "fromt": "str", "to": "456", "tot": "num" }],"reg":false,"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
@ -938,6 +938,25 @@ describe('change Node', function() {
});
});
it('changes the value and type of the message property if a complete match - boolean', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{ "t": "change", "p": "payload.a", "pt": "msg", "from": "123", "fromt": "str", "to": "true", "tot": "bool" }, { "t": "change", "p": "payload.b", "pt": "msg", "from": "456", "fromt": "str", "to": "false", "tot": "bool" }],"reg":false,"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.a.should.equal(true);
msg.payload.b.should.equal(false);
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload: { a: "123", b: "456" }});
});
});
it('changes the value of a multi-level message property', function(done) {
var flow = [{"id":"changeNode1","type":"change","action":"change","property":"foo.bar","from":"Hello","to":"Goodbye","reg":false,"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
@ -993,20 +1012,28 @@ describe('change Node', function() {
});
it('changes the value of the message property based on a regex', function(done) {
var flow = [{"id":"changeNode1","type":"change","action":"change","property":"payload","from":"\\d+","to":"NUMBER","reg":true,"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
const flow = [
{"id":"changeNode1","type":"change",rules:[
{ "t": "change", "p": "payload.a", "pt": "msg", "from": "\\d+", "fromt": "re", "to": "NUMBER", "tot": "str" },
{ "t": "change", "p": "payload.b", "pt": "msg", "from": "on", "fromt": "re", "to": "true", "tot": "bool" },
{ "t": "change", "p": "payload.c", "pt": "msg", "from": "off", "fromt": "re", "to": "false", "tot": "bool" }
],"reg":false,"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}
];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.payload.should.equal("Replace all numbers NUMBER and NUMBER");
msg.payload.a.should.equal("Replace all numbers NUMBER and NUMBER");
msg.payload.b.should.equal(true)
msg.payload.c.should.equal(false)
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"Replace all numbers 12 and 14"});
changeNode1.receive({payload:{ a: "Replace all numbers 12 and 14", b: 'on', c: 'off' } });
});
});

View File

@ -60,6 +60,7 @@ describe('HTTP Request Node', function() {
function startServer(done) {
testPort += 1;
testServer = stoppable(http.createServer(testApp));
const promises = []
testServer.listen(testPort,function(err) {
testSslPort += 1;
console.log("ssl port", testSslPort);
@ -81,13 +82,17 @@ describe('HTTP Request Node', function() {
*/
};
testSslServer = stoppable(https.createServer(sslOptions,testApp));
console.log('> start testSslServer')
promises.push(new Promise((resolve, reject) => {
testSslServer.listen(testSslPort, function(err){
console.log(' done testSslServer')
if (err) {
console.log(err);
reject(err)
} else {
console.log("started testSslServer");
resolve()
}
});
}))
testSslClientPort += 1;
var sslClientOptions = {
@ -97,10 +102,17 @@ describe('HTTP Request Node', function() {
requestCert: true
};
testSslClientServer = stoppable(https.createServer(sslClientOptions, testApp));
console.log('> start testSslClientServer')
promises.push(new Promise((resolve, reject) => {
testSslClientServer.listen(testSslClientPort, function(err){
console.log("ssl-client", err)
console.log(' done testSslClientServer')
if (err) {
reject(err)
} else {
resolve()
}
});
}))
testProxyPort += 1;
testProxyServer = stoppable(httpProxy(http.createServer()))
@ -109,7 +121,17 @@ describe('HTTP Request Node', function() {
res.setHeader("x-testproxy-header", "foobar")
}
})
testProxyServer.listen(testProxyPort)
console.log('> testProxyServer')
promises.push(new Promise((resolve, reject) => {
testProxyServer.listen(testProxyPort, function(err) {
console.log(' done testProxyServer')
if (err) {
reject(err)
} else {
resolve()
}
})
}))
testProxyAuthPort += 1
testProxyServerAuth = stoppable(httpProxy(http.createServer()))
@ -131,9 +153,19 @@ describe('HTTP Request Node', function() {
res.setHeader("x-testproxy-header", "foobar")
}
})
testProxyServerAuth.listen(testProxyAuthPort)
console.log('> testProxyServerAuth')
promises.push(new Promise((resolve, reject) => {
testProxyServerAuth.listen(testProxyAuthPort, function(err) {
console.log(' done testProxyServerAuth')
if (err) {
reject(err)
} else {
resolve()
}
})
}))
done(err);
Promise.all(promises).then(() => { done() }).catch(done)
});
}
@ -429,7 +461,11 @@ describe('HTTP Request Node', function() {
if (err) {
done(err);
}
helper.startServer(done);
console.log('> helper.startServer')
helper.startServer(function(err) {
console.log('> helper started')
done(err)
});
});
});

View File

@ -25,6 +25,7 @@ var NR_TEST_UTILS = require("nr-test-utils");
var installer = NR_TEST_UTILS.require("@node-red/registry/lib/installer");
var registry = NR_TEST_UTILS.require("@node-red/registry/lib/index");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry/lib/registry");
let pluginRegistry = NR_TEST_UTILS.require("@node-red/registry/lib/plugins");
const { events, exec, log, hooks } = NR_TEST_UTILS.require("@node-red/util");
describe('nodes/registry/installer', function() {
@ -66,6 +67,9 @@ describe('nodes/registry/installer', function() {
if (typeRegistry.setModulePendingUpdated.restore) {
typeRegistry.setModulePendingUpdated.restore();
}
if (pluginRegistry.removeModule.restore) {
pluginRegistry.removeModule.restore();
}
if (fs.statSync.restore) {
fs.statSync.restore();
}
@ -502,6 +506,9 @@ describe('nodes/registry/installer', function() {
var removeModule = sinon.stub(typeRegistry,"removeModule").callsFake(function(md) {
return nodeInfo;
});
let removePluginModule = sinon.stub(pluginRegistry,"removeModule").callsFake(function(md) {
return [];
});
var getModuleInfo = sinon.stub(registry,"getModuleInfo").callsFake(function(md) {
return {nodes:[]};
});

View File

@ -115,38 +115,7 @@ test-module-config`)
let pluginList = plugins.getPluginList();
JSON.stringify(pluginList).should.eql(JSON.stringify(
[
{
"id": "test-module/test-set",
"enabled": true,
"local": false,
"user": false,
"plugins": [
{
"type": "foo",
"id": "a-plugin",
"module": "test-module"
},
{
"type": "bar",
"id": "a-plugin2",
"module": "test-module"
},
{
"type": "foo",
"id": "a-plugin3",
"module": "test-module"
}
]
},
{
"id": "test-module/test-disabled-set",
"enabled": false,
"local": false,
"user": false,
"plugins": []
}
]
[{"id":"test-module/test-set","enabled":true,"local":false,"user":false,"plugins":[{"id":"a-plugin","type":"foo","module":"test-module"},{"id":"a-plugin2","type":"bar","module":"test-module"},{"id":"a-plugin3","type":"foo","module":"test-module"}]},{"id":"test-module/test-disabled-set","enabled":false,"local":false,"user":false,"plugins":[]}]
))
})
})