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 - 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 - 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 #### 3.1.7: Maintenance Release
- Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi - Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi

View File

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

View File

@ -91,6 +91,7 @@ module.exports = {
// Plugins // Plugins
adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler);
adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, 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); adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler);

View File

@ -40,5 +40,31 @@ module.exports = {
console.log(err.stack); console.log(err.stack);
apiUtils.rejectHandler(req,res,err); 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,6 +77,53 @@ function CommsConnection(ws, user) {
log.trace("comms.close "+self.session); log.trace("comms.close "+self.session);
removeActiveConnection(self); removeActiveConnection(self);
}); });
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 {
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"}));
ws.close();
} else {
pendingAuth = false;
addActiveConnection(self);
self.token = msg.auth;
if (sendAck) {
ws.send(JSON.stringify({auth:"ok"}));
}
}
} catch(err) {
console.log(err.stack);
// Just in case the socket closes before we attempt
// to send anything.
}
}
ws.on('message', function(data,flags) { ws.on('message', function(data,flags) {
var msg = null; var msg = null;
try { try {
@ -86,68 +133,34 @@ function CommsConnection(ws, user) {
return; return;
} }
if (!pendingAuth) { if (!pendingAuth) {
if (msg.subscribe) { if (msg.auth) {
handleAuthPacket(msg)
} else if (msg.subscribe) {
self.subscribe(msg.subscribe); self.subscribe(msg.subscribe);
// handleRemoteSubscription(ws,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 { } else {
var completeConnection = function(userScope,session,sendAck) {
try {
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
ws.send(JSON.stringify({auth:"fail"}));
ws.close();
} else {
pendingAuth = false;
addActiveConnection(self);
self.token = msg.auth;
if (sendAck) {
ws.send(JSON.stringify({auth:"ok"}));
}
}
} catch(err) {
console.log(err.stack);
// Just in case the socket closes before we attempt
// to send anything.
}
}
if (msg.auth) { if (msg.auth) {
Tokens.get(msg.auth).then(function(client) { handleAuthPacket(msg)
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);
}
});
} 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);
}
});
}
});
} else { } else {
if (anonymousUser) { if (anonymousUser) {
log.audit({event: "comms.auth",user:anonymousUser}); log.audit({event: "comms.auth",user:anonymousUser});
self.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 //TODO: duplicated code - pull non-auth message handling out
if (msg.subscribe) { if (msg.subscribe) {
self.subscribe(msg.subscribe); self.subscribe(msg.subscribe);
} }
} else { } else {
log.audit({event: "comms.auth.fail"}); 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", "clone": "2.1.2",
"cors": "2.8.5", "cors": "2.8.5",
"express-session": "1.17.3", "express-session": "1.17.3",
"express": "4.18.2", "express": "4.19.2",
"memorystore": "1.6.7", "memorystore": "1.6.7",
"mime": "3.0.0", "mime": "3.0.0",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",

View File

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

View File

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

View File

@ -924,7 +924,14 @@
"date": "horodatage", "date": "horodatage",
"jsonata": "expression", "jsonata": "expression",
"env": "variable d'environnement", "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": { "editableList": {

View File

@ -26,6 +26,15 @@ RED.comms = (function() {
var reconnectAttempts = 0; var reconnectAttempts = 0;
var active = false; 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() { function connectWS() {
active = true; active = true;
var wspath; var wspath;
@ -56,6 +65,7 @@ RED.comms = (function() {
ws.send(JSON.stringify({subscribe:t})); ws.send(JSON.stringify({subscribe:t}));
} }
} }
emit('connect')
} }
ws = new WebSocket(wspath); 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 { return {
connect: connectWS, connect: connectWS,
subscribe: subscribe, subscribe: subscribe,
unsubscribe:unsubscribe unsubscribe:unsubscribe,
on,
off,
send
} }
})(); })();

View File

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

View File

@ -1,6 +1,7 @@
RED.plugins = (function() { RED.plugins = (function() {
var plugins = {}; var plugins = {};
var pluginsByType = {}; var pluginsByType = {};
var moduleList = {};
function registerPlugin(id,definition) { function registerPlugin(id,definition) {
plugins[id] = definition; plugins[id] = definition;
@ -38,9 +39,43 @@ RED.plugins = (function() {
function getPluginsByType(type) { function getPluginsByType(type) {
return pluginsByType[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 { return {
registerPlugin: registerPlugin, registerPlugin: registerPlugin,
getPlugin: getPlugin, getPlugin: getPlugin,
getPluginsByType: getPluginsByType getPluginsByType: getPluginsByType,
setPluginList: setPluginList,
addPlugin: addPlugin,
getModule: getModule
} }
})(); })();

View File

@ -25,6 +25,7 @@ var RED = (function() {
cache: false, cache: false,
url: 'plugins', url: 'plugins',
success: function(data) { success: function(data) {
RED.plugins.setPluginList(data);
loader.reportProgress(RED._("event.loadPlugins"), 13) loader.reportProgress(RED._("event.loadPlugins"), 13)
RED.i18n.loadPluginCatalogs(function() { RED.i18n.loadPluginCatalogs(function() {
loadPlugins(function() { loadPlugins(function() {
@ -534,6 +535,41 @@ var RED = (function() {
RED.view.redrawStatus(node); 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 pendingNodeRemovedNotifications = []
let pendingNodeRemovedTimeout let pendingNodeRemovedTimeout

View File

@ -118,10 +118,16 @@ RED.contextMenu = (function () {
onselect: 'core:split-wire-with-link-nodes', onselect: 'core:split-wire-with-link-nodes',
disabled: !canEdit || !hasLinks disabled: !canEdit || !hasLinks
}, },
null, null
{ onselect: 'core:show-import-dialog', label: RED._('common.label.import')},
{ onselect: 'core:show-examples-import-dialog', label: RED._('menu.label.importExample') }
) )
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) { if (hasSelection && canEdit) {
const nodeOptions = [] const nodeOptions = []
if (!hasMultipleSelection && !isGroup) { 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: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', label: RED._('keyboard.deleteSelected'), disabled: !canEdit || !canDelete },
{ onselect: 'core:delete-selection-and-reconnect', label: RED._('keyboard.deleteReconnect'), 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) { function init(done) {
if (!RED.user.hasPermission("settings.write")) {
RED.notify(RED._("user.errors.settings"),"error");
return;
}
RED.userSettings.add({ RED.userSettings.add({
id:'envvar', id:'envvar',
title: RED._("env-var.environment"), title: RED._("env-var.environment"),

View File

@ -248,86 +248,106 @@ RED.palette.editor = (function() {
var moduleInfo = nodeEntries[module].info; var moduleInfo = nodeEntries[module].info;
var nodeEntry = nodeEntries[module].elements; var nodeEntry = nodeEntries[module].elements;
if (nodeEntry) { if (nodeEntry) {
var activeTypeCount = 0; if (moduleInfo.plugin) {
var typeCount = 0; nodeEntry.enableButton.hide();
var errorCount = 0; nodeEntry.removeButton.show();
nodeEntry.errorList.empty();
nodeEntries[module].totalUseCount = 0;
nodeEntries[module].setUseCount = {};
for (var setName in moduleInfo.sets) { let pluginCount = 0;
if (moduleInfo.sets.hasOwnProperty(setName)) { for (let setName in moduleInfo.sets) {
var inUseCount = 0; if (moduleInfo.sets.hasOwnProperty(setName)) {
var set = moduleInfo.sets[setName]; let set = moduleInfo.sets[setName];
var setElements = nodeEntry.sets[setName]; if (set.plugins) {
if (set.err) { pluginCount += set.plugins.length;
errorCount++;
var errMessage = set.err;
if (set.err.message) {
errMessage = set.err.message;
} else if (set.err.code) {
errMessage = set.err.code;
} }
$("<li>").text(errMessage).appendTo(nodeEntry.errorList);
} }
if (set.enabled) { }
activeTypeCount += set.types.length;
} nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount}));
typeCount += set.types.length;
for (var i=0;i<moduleInfo.sets[setName].types.length;i++) { } else {
var t = moduleInfo.sets[setName].types[i]; var activeTypeCount = 0;
inUseCount += (typesInUse[t]||0); var typeCount = 0;
var swatch = setElements.swatches[t]; var errorCount = 0;
nodeEntry.errorList.empty();
nodeEntries[module].totalUseCount = 0;
nodeEntries[module].setUseCount = {};
for (var setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
var inUseCount = 0;
const set = moduleInfo.sets[setName];
const setElements = nodeEntry.sets[setName]
if (set.err) {
errorCount++;
var errMessage = set.err;
if (set.err.message) {
errMessage = set.err.message;
} else if (set.err.code) {
errMessage = set.err.code;
}
$("<li>").text(errMessage).appendTo(nodeEntry.errorList);
}
if (set.enabled) { if (set.enabled) {
var def = RED.nodes.getType(t); activeTypeCount += set.types.length;
if (def && def.color) { }
swatch.css({background:RED.utils.getNodeColor(t,def)}); typeCount += set.types.length;
swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))}) for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
var t = moduleInfo.sets[setName].types[i];
inUseCount += (typesInUse[t]||0);
if (setElements && set.enabled) {
var def = RED.nodes.getType(t);
if (def && def.color) {
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].setUseCount[setName] = inUseCount; nodeEntries[module].totalUseCount += inUseCount;
nodeEntries[module].totalUseCount += inUseCount;
if (inUseCount > 0) { if (setElements) {
setElements.enableButton.text(RED._('palette.editor.inuse')); if (inUseCount > 0) {
setElements.enableButton.addClass('disabled'); setElements.enableButton.text(RED._('palette.editor.inuse'));
} else { setElements.enableButton.addClass('disabled');
setElements.enableButton.removeClass('disabled'); } else {
if (set.enabled) { setElements.enableButton.removeClass('disabled');
setElements.enableButton.text(RED._('palette.editor.disable')); if (set.enabled) {
} else { setElements.enableButton.text(RED._('palette.editor.disable'));
setElements.enableButton.text(RED._('palette.editor.enable')); } else {
setElements.enableButton.text(RED._('palette.editor.enable'));
}
}
setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
} }
} }
setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
} }
}
if (errorCount === 0) { if (errorCount === 0) {
nodeEntry.errorRow.hide() nodeEntry.errorRow.hide()
} else {
nodeEntry.errorRow.show();
}
var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
if (nodeEntries[module].totalUseCount > 0) {
nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
nodeEntry.enableButton.addClass('disabled');
nodeEntry.removeButton.hide();
} else {
nodeEntry.enableButton.removeClass('disabled');
if (moduleInfo.local) {
nodeEntry.removeButton.css('display', 'inline-block');
}
if (activeTypeCount === 0) {
nodeEntry.enableButton.text(RED._('palette.editor.enableall'));
} else { } else {
nodeEntry.enableButton.text(RED._('palette.editor.disableall')); nodeEntry.errorRow.show();
}
var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
if (nodeEntries[module].totalUseCount > 0) {
nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
nodeEntry.enableButton.addClass('disabled');
nodeEntry.removeButton.hide();
} else {
nodeEntry.enableButton.removeClass('disabled');
if (moduleInfo.local) {
nodeEntry.removeButton.css('display', 'inline-block');
}
if (activeTypeCount === 0) {
nodeEntry.enableButton.text(RED._('palette.editor.enableall'));
} else {
nodeEntry.enableButton.text(RED._('palette.editor.disableall'));
}
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
} }
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
} }
} }
if (moduleInfo.pending_version) { if (moduleInfo.pending_version) {
@ -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; var settingsPane;
@ -804,6 +851,7 @@ RED.palette.editor = (function() {
errorRow: errorRow, errorRow: errorRow,
errorList: errorList, errorList: errorList,
setCount: setCount, setCount: setCount,
setButton: setButton,
container: container, container: container,
shade: shade, shade: shade,
versionSpan: versionSpan, versionSpan: versionSpan,
@ -814,49 +862,88 @@ RED.palette.editor = (function() {
if (container.hasClass('expanded')) { if (container.hasClass('expanded')) {
container.removeClass('expanded'); container.removeClass('expanded');
contentRow.slideUp(); contentRow.slideUp();
setTimeout(() => {
contentRow.empty()
}, 200)
object.elements.sets = {}
} else { } else {
container.addClass('expanded'); container.addClass('expanded');
populateSetList()
contentRow.slideDown(); contentRow.slideDown();
} }
}) })
const populateSetList = function () {
var setList = Object.keys(entry.sets) var setList = Object.keys(entry.sets)
setList.sort(function(A,B) { setList.sort(function(A,B) {
return A.toLowerCase().localeCompare(B.toLowerCase()); return A.toLowerCase().localeCompare(B.toLowerCase());
}); });
setList.forEach(function(setName) { setList.forEach(function(setName) {
var set = entry.sets[setName]; var set = entry.sets[setName];
var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow); 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 buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
var typeSwatches = {}; var typeSwatches = {};
set.types.forEach(function(t) { let enableButton;
var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow); if (set.types) {
typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv); set.types.forEach(function(t) {
$('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv); var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
}) typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
var enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup); if (set.enabled) {
enableButton.on("click", function(evt) { var def = RED.nodes.getType(t);
evt.preventDefault(); if (def && def.color) {
if (object.setUseCount[setName] === 0) { typeSwatches[t].css({background:RED.utils.getNodeColor(t,def)});
var currentSet = RED.nodes.registry.getNodeSet(set.id); typeSwatches[t].css({border: "1px solid "+getContrastingBorder(typeSwatches[t].css('backgroundColor'))})
shade.show();
var newState = !currentSet.enabled
changeNodeState(set.id,newState,shade,function(xhr){
if (xhr) {
if (xhr.responseJSON) {
RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message}));
} }
} }
}); $('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
} })
}) 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) {
var currentSet = RED.nodes.registry.getNodeSet(set.id);
shade.show();
var newState = !currentSet.enabled
changeNodeState(set.id,newState,shade,function(xhr){
if (xhr) {
if (xhr.responseJSON) {
RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message}));
}
}
});
}
})
object.elements.sets[set.name] = { if (object.setUseCount[setName] > 0) {
setRow: setRow, enableButton.text(RED._('palette.editor.inuse'));
enableButton: enableButton, enableButton.addClass('disabled');
swatches: typeSwatches } 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) { enableButton.on("click", function(evt) {
evt.preventDefault(); evt.preventDefault();
if (object.totalUseCount === 0) { 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(); notification.close();

View File

@ -35,6 +35,10 @@ RED.palette = (function() {
var categoryContainers = {}; var categoryContainers = {};
var sidebarControls; var sidebarControls;
let paletteState = { filter: "", collapsed: [] };
let filterRefreshTimeout
function createCategory(originalCategory,rootCategory,category,ns) { function createCategory(originalCategory,rootCategory,category,ns) {
if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) { if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory); createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
@ -60,20 +64,57 @@ RED.palette = (function() {
catDiv.data('label',label); catDiv.data('label',label);
categoryContainers[category] = { categoryContainers[category] = {
container: catDiv, 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.removeClass("red-ui-palette-open");
catDiv.addClass("red-ui-palette-closed"); catDiv.addClass("red-ui-palette-closed");
$("#red-ui-palette-base-category-"+category).slideUp(); 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"); $("#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.addClass("red-ui-palette-open");
catDiv.removeClass("red-ui-palette-closed"); catDiv.removeClass("red-ui-palette-closed");
$("#red-ui-palette-base-category-"+category).slideDown(); $("#red-ui-palette-base-category-"+category).slideDown();
$("#red-ui-palette-header-"+category+" i").addClass("expanded"); $("#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() { toggle: function() {
if (catDiv.hasClass("red-ui-palette-open")) { if (categoryContainers[category].isOpen()) {
categoryContainers[category].close(); categoryContainers[category].close();
} else { } else {
categoryContainers[category].open(); categoryContainers[category].open();
@ -415,8 +456,16 @@ RED.palette = (function() {
var categoryNode = $("#red-ui-palette-container-"+rootCategory); var categoryNode = $("#red-ui-palette-container-"+rootCategory);
if (categoryNode.find(".red-ui-palette-node").length === 1) { if (categoryNode.find(".red-ui-palette-node").length === 1) {
categoryContainers[rootCategory].open(); 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); 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'); var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
$("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) { $("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
var currentLabel = $(el).attr("data-palette-label"); 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.hasOwnProperty(category)) {
if (categoryContainers[category].container const categorySection = categoryContainers[category]
.find(".red-ui-palette-node") if (categorySection.getNodeCount(true) === 0) {
.filter(function() { return $(this).css('display') !== 'none'}).length === 0) { categorySection.hide()
categoryContainers[category].close();
categoryContainers[category].container.slideUp();
} else { } else {
categoryContainers[category].open(); categorySection.show()
categoryContainers[category].container.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-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) { RED.events.on('registry:node-type-added', function(nodeType) {
var def = RED.nodes.getType(nodeType); var def = RED.nodes.getType(nodeType);
@ -596,14 +659,14 @@ RED.palette = (function() {
RED.events.on("subflows:change",refreshSubflow); RED.events.on("subflows:change",refreshSubflow);
$("#red-ui-palette-search input").searchBox({ $("#red-ui-palette-search input").searchBox({
delay: 100, delay: 100,
change: function() { 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")); 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"); RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");
@ -669,7 +732,23 @@ RED.palette = (function() {
togglePalette(state); 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) { function togglePalette(state) {
if (!state) { if (!state) {
$("#red-ui-main-container").addClass("red-ui-palette-closed"); $("#red-ui-main-container").addClass("red-ui-palette-closed");
@ -689,6 +768,15 @@ RED.palette = (function() {
}) })
return categories; 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 { return {
init: init, init: init,
add:addNodeType, add:addNodeType,

View File

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

View File

@ -158,8 +158,10 @@ RED.sidebar.help = (function() {
function refreshSubflow(sf) { function refreshSubflow(sf) {
var item = treeList.treeList('get',"node-type:subflow:"+sf.id); var item = treeList.treeList('get',"node-type:subflow:"+sf.id);
item.subflowLabel = sf._def.label().toLowerCase(); if (item) {
item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()})); item.subflowLabel = sf._def.label().toLowerCase();
item.treeList.replaceElement(getNodeLabel({_def:sf._def,type:sf._def.label()}));
}
} }
function hideTOC() { function hideTOC() {

View File

@ -491,6 +491,11 @@ RED.workspaces = (function() {
createWorkspaceTabs(); createWorkspaceTabs();
RED.events.on("sidebar:resize",workspace_tabs.resize); 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() { RED.actions.add("core:show-next-tab",function() {
var oldActive = activeWorkspace; var oldActive = activeWorkspace;
workspace_tabs.nextTab(); workspace_tabs.nextTab();
@ -657,6 +662,9 @@ RED.workspaces = (function() {
RED.events.on("flows:change", (ws) => { RED.events.on("flows:change", (ws) => {
$("#red-ui-tab-"+(ws.id.replace(".","-"))).toggleClass('red-ui-workspace-changed',!!(ws.contentsChanged || ws.changed || ws.added)); $("#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(); hideWorkspace();
} }

View File

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

View File

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

View File

@ -17,7 +17,8 @@ export default {
{ {
title: { title: {
"en-US": "Timestamp formatting options", "en-US": "Timestamp formatting options",
"ja": "タイムスタンプの形式の項目" "ja": "タイムスタンプの形式の項目",
"fr": "Options de formatage de l'horodatage"
}, },
image: 'images/nr4-timestamp-formatting.png', image: 'images/nr4-timestamp-formatting.png',
description: { description: {
@ -34,13 +35,21 @@ export default {
<li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li> <li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li>
<li>ISO 8601 - 多くのシステムで使用されている共通の形式</li> <li>ISO 8601 - 多くのシステムで使用されている共通の形式</li>
<li>JavaScript日付オブジェクト</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>` </ul>`
} }
}, },
{ {
title: { title: {
"en-US": "Auto-complete of flow/global and env types", "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', image: 'images/nr4-auto-complete.png',
description: { description: {
@ -48,13 +57,17 @@ export default {
now all include auto-complete suggestions based on the live state of your flows.</p> 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> "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: { title: {
"en-US": "Config node customisation in Subflows", "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', image: 'images/nr4-sf-config.png',
description: { description: {
@ -65,6 +78,11 @@ export default {
`, `,
"ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p> "ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p>
<p>例えばMQTTブローカへ接続しメッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です</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>WebSocketードのカスタマイズ可能なヘッダ</li>
<li>Splitードはメッセージプロパティで操作できるようになりました</li> <li>Splitードはメッセージプロパティで操作できるようになりました</li>
<li>他にも沢山あります...</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>` </ul>`
} }
} }

View File

@ -378,7 +378,7 @@
return { id: id, label: RED.nodes.workspace(id).label } //flow id + name return { id: id, label: RED.nodes.workspace(id).label } //flow id + name
} else { } else {
const instanceNode = RED.nodes.node(id) 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 } return { id: id, label: pathLabel }
} }
}) })

View File

@ -233,9 +233,12 @@ module.exports = function(RED) {
// only replace if they match exactly // only replace if they match exactly
RED.util.setMessageProperty(msg,property,value); RED.util.setMessageProperty(msg,property,value);
} else { } else {
// if target is boolean then just replace it current = current.replace(fromRE,value);
if (rule.tot === "bool") { current = value; } if (rule.tot === "bool" && current === ""+value) {
else { current = current.replace(fromRE,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); RED.util.setMessageProperty(msg,property,current);
} }
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') { } 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": "catch : tout", "catch": "catch : tout",
"catchGroup": "catch: groupe",
"catchNodes": "catch : __number__", "catchNodes": "catch : __number__",
"catchUncaught": "catch : non capturé", "catchUncaught": "catch : non capturé",
"label": { "label": {
@ -109,6 +110,7 @@
}, },
"status": { "status": {
"status": "statut : tout", "status": "statut : tout",
"statusGroup": "statut: groupe",
"statusNodes": "statut : __number__", "statusNodes": "statut : __number__",
"label": { "label": {
"source": "Signaler l'état de", "source": "Signaler l'état de",
@ -250,7 +252,8 @@
"initialize": "Au démarrage", "initialize": "Au démarrage",
"finalize": "À l'arrêt", "finalize": "À l'arrêt",
"outputs": "Sorties", "outputs": "Sorties",
"modules": "Modules" "modules": "Modules",
"timeout": "Délai d'attente"
}, },
"text": { "text": {
"initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\n", "initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\n",
@ -847,7 +850,13 @@
"newline": "Nouvelle ligne", "newline": "Nouvelle ligne",
"usestrings": "Analyser les valeurs numériques", "usestrings": "Analyser les valeurs numériques",
"include_empty_strings": "Inclure les chaînes vides", "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": { "placeholder": {
"columns": "noms de colonnes séparés par des virgules" "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" "once": "envoyer les en-têtes une fois, jusqu'à msg.reset"
}, },
"errors": { "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.", "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.", "obj_csv": "Aucun modèle de colonnes spécifié pour l'objet -> CSV.",
"bad_csv": "Données CSV mal formées - sortie probablement corrompue." "bad_csv": "Données CSV mal formées - sortie probablement corrompue."
@ -885,12 +895,14 @@
"label": { "label": {
"select": "Sélecteur", "select": "Sélecteur",
"output": "Sortie", "output": "Sortie",
"in": "dans" "in": "dans",
"prefix": "Nom de la propriété pour le contenu HTML"
}, },
"output": { "output": {
"html": "le contenu html des éléments", "html": "le contenu html des éléments",
"text": "uniquement le contenu textuel 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": { "format": {
"single": "comme un seul message contenant un tableau", "single": "comme un seul message contenant un tableau",

View File

@ -30,6 +30,8 @@
avant d'être envoyé.</p> avant d'être envoyé.</p>
<p>Si <code>msg._session</code> n'est pas présent, la charge utile est <p>Si <code>msg._session</code> n'est pas présent, la charge utile est
envoyé à <b>tous</b> les clients connectés.</p> 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 <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> pour accéder aux ports inférieurs à 1024.</p>
</script> </script>
@ -40,6 +42,8 @@
caractères renvoyés dans un tampon fixe, correspondant à un caractère spécifié avant de revenir, 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 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> 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>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> <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> </script>

View File

@ -36,7 +36,9 @@
</dl> </dl>
<h3>Détails</h3> <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 <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>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 <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 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, getPluginsByType: plugins.getPluginsByType,
getPluginList: plugins.getPluginList, getPluginList: plugins.getPluginList,
getPluginConfigs: plugins.getPluginConfigs, getPluginConfigs: plugins.getPluginConfigs,
getPluginConfig: plugins.getPluginConfig,
exportPluginSettings: plugins.exportPluginSettings, exportPluginSettings: plugins.exportPluginSettings,

View File

@ -28,6 +28,8 @@ const child_process = require('child_process');
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
let installerEnabled = false; let installerEnabled = false;
const plugins = require("./plugins");
let settings; let settings;
const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/; const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/;
const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/;
@ -330,10 +332,18 @@ function reportRemovedModules(removedNodes) {
//comms.publish("node/removed",removedNodes,false); //comms.publish("node/removed",removedNodes,false);
log.info(log._("server.removed-types")); log.info(log._("server.removed-types"));
for (var j=0;j<removedNodes.length;j++) { 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(" - "+(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; return removedNodes;
} }
@ -495,8 +505,12 @@ function uninstallModule(module) {
} catch(err) { } catch(err) {
return reject(new Error(log._("server.install.uninstall-failed",{name:module}))); return reject(new Error(log._("server.install.uninstall-failed",{name: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));
var list = registry.removeModule(module);
log.info(log._("server.install.uninstalling",{name:module})); log.info(log._("server.install.uninstalling",{name:module}));
let triggerPayload = { let triggerPayload = {

View File

@ -39,6 +39,8 @@ function registerPlugin(nodeSetId,id,definition) {
pluginSettings[id] = definition.settings; pluginSettings[id] = definition.settings;
} }
// reset the cache when a new plugin is incoming!
pluginConfigCache = {};
if (definition.onadd && typeof definition.onadd === 'function') { if (definition.onadd && typeof definition.onadd === 'function') {
definition.onadd(); definition.onadd();
@ -55,29 +57,47 @@ function getPluginsByType(type) {
} }
function getPluginConfigs(lang) { function getPluginConfigs(lang) {
// we're not re-using getPluginConfig() here,
// to avoid calling registry.getModuleList() multiple times!
if (!pluginConfigCache[lang]) { if (!pluginConfigCache[lang]) {
var result = ""; var result = "";
var script = "";
var moduleConfigs = registry.getModuleList(); var moduleConfigs = registry.getModuleList();
for (var module in moduleConfigs) { for (var module in moduleConfigs) {
/* istanbul ignore else */ /* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(module)) { if (moduleConfigs.hasOwnProperty(module)) {
var plugins = moduleConfigs[module].plugins; result += generateModulePluginConfig(moduleConfigs[module]);
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;
}
}
}
} }
} }
pluginConfigCache[lang] = result; pluginConfigCache[lang] = result;
} }
return pluginConfigCache[lang]; 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() { function getPluginList() {
var list = []; var list = [];
var moduleConfigs = registry.getModuleList(); var moduleConfigs = registry.getModuleList();
@ -142,12 +162,51 @@ function exportPluginSettings(safeSettings) {
return 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 = { module.exports = {
init, init,
registerPlugin, registerPlugin,
getPlugin, getPlugin,
getPluginsByType, getPluginsByType,
getPluginConfigs, getPluginConfigs,
getPluginConfig,
getPluginList, getPluginList,
exportPluginSettings exportPluginSettings,
removeModule
} }

View File

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

View File

@ -36,7 +36,7 @@ var connections = [];
const events = require("@node-red/util").events; const events = require("@node-red/util").events;
function handleCommsEvent(event) { function handleCommsEvent(event) {
publish(event.topic,event.data,event.retain); publish(event.topic,event.data,event.retain,event.session,event.excludeSession);
} }
function handleStatusEvent(event) { function handleStatusEvent(event) {
if (!event.status) { if (!event.status) {
@ -74,13 +74,17 @@ function handleEventLog(event) {
publish("event-log/"+event.id,event.payload||{}); publish("event-log/"+event.id,event.payload||{});
} }
function publish(topic,data,retain) { function publish(topic, data, retain, session, excludeSession) {
if (retain) { if (retain) {
retained[topic] = data; retained[topic] = data;
} else { } else {
delete retained[topic]; 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) { addConnection: async function(opts) {
connections.push(opts.client); 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; break;
} }
} }
events.emit('comms:connection-removed', {
session: opts.client.session
})
}, },
/** /**
@ -157,5 +168,23 @@ var api = module.exports = {
* @return {Promise<Object>} - resolves when complete * @return {Promise<Object>} - resolves when complete
* @memberof @node-red/runtime_comms * @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); runtime.log.audit({event: "plugins.configs.get"}, opts.req);
return runtime.plugins.getPluginConfigs(opts.lang); 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 * Gets all registered module message catalogs
* @param {Object} opts * @param {Object} opts

View File

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

View File

@ -173,7 +173,11 @@ function installModule(module,version,url) {
if (info.pending_version) { if (info.pending_version) {
events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:info.name,version:info.pending_version}}); events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:info.name,version:info.pending_version}});
} else { } else {
events.emit("runtime-event",{id:"node/added",retain:false,payload:info.nodes}); 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; return info;
}); });

View File

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

View File

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

View File

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

View File

@ -37,9 +37,9 @@
"@node-red/nodes": "4.0.0-beta.1", "@node-red/nodes": "4.0.0-beta.1",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"express": "4.18.2", "express": "4.19.2",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"node-red-admin": "^3.1.2", "node-red-admin": "^3.1.3",
"nopt": "5.0.0", "nopt": "5.0.0",
"semver": "7.5.4" "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"]]}, 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:[]}]; {id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() { 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) { 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"]]}, 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:[]}]; {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) { 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"]]}, const flow = [
{id:"helperNode1", type:"helper", wires:[]}]; {"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() { helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1"); var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1"); var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) { helperNode1.on("input", function(msg) {
try { 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(); done();
} catch(err) { } catch(err) {
done(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) { function startServer(done) {
testPort += 1; testPort += 1;
testServer = stoppable(http.createServer(testApp)); testServer = stoppable(http.createServer(testApp));
const promises = []
testServer.listen(testPort,function(err) { testServer.listen(testPort,function(err) {
testSslPort += 1; testSslPort += 1;
console.log("ssl port", testSslPort); console.log("ssl port", testSslPort);
@ -81,13 +82,17 @@ describe('HTTP Request Node', function() {
*/ */
}; };
testSslServer = stoppable(https.createServer(sslOptions,testApp)); testSslServer = stoppable(https.createServer(sslOptions,testApp));
testSslServer.listen(testSslPort, function(err){ console.log('> start testSslServer')
if (err) { promises.push(new Promise((resolve, reject) => {
console.log(err); testSslServer.listen(testSslPort, function(err){
} else { console.log(' done testSslServer')
console.log("started testSslServer"); if (err) {
} reject(err)
}); } else {
resolve()
}
});
}))
testSslClientPort += 1; testSslClientPort += 1;
var sslClientOptions = { var sslClientOptions = {
@ -97,10 +102,17 @@ describe('HTTP Request Node', function() {
requestCert: true requestCert: true
}; };
testSslClientServer = stoppable(https.createServer(sslClientOptions, testApp)); testSslClientServer = stoppable(https.createServer(sslClientOptions, testApp));
testSslClientServer.listen(testSslClientPort, function(err){ console.log('> start testSslClientServer')
console.log("ssl-client", err) promises.push(new Promise((resolve, reject) => {
}); testSslClientServer.listen(testSslClientPort, function(err){
console.log(' done testSslClientServer')
if (err) {
reject(err)
} else {
resolve()
}
});
}))
testProxyPort += 1; testProxyPort += 1;
testProxyServer = stoppable(httpProxy(http.createServer())) testProxyServer = stoppable(httpProxy(http.createServer()))
@ -109,7 +121,17 @@ describe('HTTP Request Node', function() {
res.setHeader("x-testproxy-header", "foobar") 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 testProxyAuthPort += 1
testProxyServerAuth = stoppable(httpProxy(http.createServer())) testProxyServerAuth = stoppable(httpProxy(http.createServer()))
@ -131,9 +153,19 @@ describe('HTTP Request Node', function() {
res.setHeader("x-testproxy-header", "foobar") 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) { if (err) {
done(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 installer = NR_TEST_UTILS.require("@node-red/registry/lib/installer");
var registry = NR_TEST_UTILS.require("@node-red/registry/lib/index"); var registry = NR_TEST_UTILS.require("@node-red/registry/lib/index");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry/lib/registry"); 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"); const { events, exec, log, hooks } = NR_TEST_UTILS.require("@node-red/util");
describe('nodes/registry/installer', function() { describe('nodes/registry/installer', function() {
@ -66,6 +67,9 @@ describe('nodes/registry/installer', function() {
if (typeRegistry.setModulePendingUpdated.restore) { if (typeRegistry.setModulePendingUpdated.restore) {
typeRegistry.setModulePendingUpdated.restore(); typeRegistry.setModulePendingUpdated.restore();
} }
if (pluginRegistry.removeModule.restore) {
pluginRegistry.removeModule.restore();
}
if (fs.statSync.restore) { if (fs.statSync.restore) {
fs.statSync.restore(); fs.statSync.restore();
} }
@ -502,6 +506,9 @@ describe('nodes/registry/installer', function() {
var removeModule = sinon.stub(typeRegistry,"removeModule").callsFake(function(md) { var removeModule = sinon.stub(typeRegistry,"removeModule").callsFake(function(md) {
return nodeInfo; return nodeInfo;
}); });
let removePluginModule = sinon.stub(pluginRegistry,"removeModule").callsFake(function(md) {
return [];
});
var getModuleInfo = sinon.stub(registry,"getModuleInfo").callsFake(function(md) { var getModuleInfo = sinon.stub(registry,"getModuleInfo").callsFake(function(md) {
return {nodes:[]}; return {nodes:[]};
}); });

View File

@ -115,38 +115,7 @@ test-module-config`)
let pluginList = plugins.getPluginList(); let pluginList = plugins.getPluginList();
JSON.stringify(pluginList).should.eql(JSON.stringify( JSON.stringify(pluginList).should.eql(JSON.stringify(
[ [{"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":[]}]
{
"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": []
}
]
)) ))
}) })
}) })