Merge branch 'dev' into @feature/issue-5029
@@ -63,7 +63,7 @@
|
||||
"moment": "2.30.1",
|
||||
"moment-timezone": "0.5.48",
|
||||
"mqtt": "5.11.0",
|
||||
"multer": "1.4.5-lts.2",
|
||||
"multer": "2.0.1",
|
||||
"mustache": "4.2.0",
|
||||
"node-red-admin": "^4.0.2",
|
||||
"node-watch": "0.7.4",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"express": "4.21.2",
|
||||
"memorystore": "1.6.7",
|
||||
"mime": "3.0.0",
|
||||
"multer": "1.4.5-lts.2",
|
||||
"multer": "2.0.1",
|
||||
"mustache": "4.2.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"passport-http-bearer": "1.0.1",
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
"download": "Download",
|
||||
"importUnrecognised": "Imported unrecognised type:",
|
||||
"importUnrecognised_plural": "Imported unrecognised types:",
|
||||
"importWithModuleInfo": "Required dependencies missing",
|
||||
"importWithModuleInfo": "Required modules missing",
|
||||
"importWithModuleInfoDesc": "These nodes are not currently installed in your palette and are required for the imported flow:",
|
||||
"importDuplicate": "Imported duplicate node:",
|
||||
"importDuplicate_plural": "Imported duplicate nodes:",
|
||||
@@ -626,6 +626,7 @@
|
||||
"yearsMonthsV": "__y__ years, __count__ month ago",
|
||||
"yearsMonthsV_plural": "__y__ years, __count__ months ago"
|
||||
},
|
||||
"manageModules": "Manage modules",
|
||||
"nodeCount": "__label__ node",
|
||||
"nodeCount_plural": "__label__ nodes",
|
||||
"pluginCount": "__count__ plugin",
|
||||
@@ -643,9 +644,12 @@
|
||||
"update": "update to __version__",
|
||||
"updated": "updated",
|
||||
"install": "install",
|
||||
"installAll": "Install all",
|
||||
"installed": "installed",
|
||||
"installing": "Module installation in progress: __module__",
|
||||
"conflict": "conflict",
|
||||
"conflictTip": "<p>This module cannot be installed as it includes a<br/>node type that has already been installed</p><p>Conflicts with <code>__module__</code></p>",
|
||||
"majorVersion": "<p>This is a major version update of the node. Check the documentation for details of the update.</p>",
|
||||
"loading": "Loading catalogues...",
|
||||
"tab-nodes": "Nodes",
|
||||
"tab-install": "Install",
|
||||
@@ -653,9 +657,12 @@
|
||||
"sortRelevance": "relevance",
|
||||
"sortAZ": "a-z",
|
||||
"sortRecent": "recent",
|
||||
"successfulInstall": "Successfully installed modules",
|
||||
"more": "+ __count__ more",
|
||||
"upload": "Upload module tgz file",
|
||||
"refresh": "Refresh module list",
|
||||
"deprecated": "deprecated",
|
||||
"deprecatedTip": "This module has been deprecated",
|
||||
"errors": {
|
||||
"catalogLoadFailed": "<p>Failed to load node catalogue.</p><p>Check the browser console for more information</p>",
|
||||
"installFailed": "<p>Failed to install: __module__</p><p>__message__</p><p>Check the log for more information</p>",
|
||||
@@ -1278,5 +1285,15 @@
|
||||
"environment": "Environment",
|
||||
"header": "Global Environment Variables",
|
||||
"revert": "Revert"
|
||||
},
|
||||
"telemetry": {
|
||||
"label": "Update Notifications",
|
||||
"settingsTitle": "Enable Update Notifications",
|
||||
"settingsDescription": "<p>Node-RED can notify you when there is a new version available. This ensures you keep up to date with the latest features and fixes.</p><p>This requires sending anonymised data back to the Node-RED team. It does not include any details of your flows or users.</p><p>For full information on what information is collected and how it is used, please see the <a href=\"https://nodered.org/docs/telemetry\" target=\"_blank\">documentation</a>.</p>",
|
||||
"settingsDescription2": "<p>You can change this setting at any time in the editor settings.</p>",
|
||||
"enableLabel": "Yes, enable notifications",
|
||||
"disableLabel": "No, do not enable notifications",
|
||||
"updateAvailable": "Update available",
|
||||
"updateAvailableDesc": "Node-RED __version__ is now available"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,7 +689,7 @@ RED.nodes = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function addNode(n) {
|
||||
function addNode(n, opt) {
|
||||
let newNode
|
||||
if (!n.__isProxy__) {
|
||||
newNode = new Proxy(n, nodeProxyHandler)
|
||||
@@ -728,7 +728,7 @@ RED.nodes = (function() {
|
||||
nodeLinks[n.id] = {in:[],out:[]};
|
||||
}
|
||||
}
|
||||
RED.events.emit('nodes:add',newNode);
|
||||
RED.events.emit('nodes:add',newNode, opt);
|
||||
return newNode
|
||||
}
|
||||
function addLink(l) {
|
||||
@@ -1494,7 +1494,14 @@ RED.nodes = (function() {
|
||||
}
|
||||
/**
|
||||
* Converts the current node selection to an exportable JSON Object
|
||||
**/
|
||||
* @param {Array<Node>} set the node selection to export
|
||||
* @param {Object} options
|
||||
* @param {Record<string, boolean>} [options.exportedIds]
|
||||
* @param {Record<string, boolean>} [options.exportedSubflows]
|
||||
* @param {Record<string, boolean>} [options.exportedConfigNodes]
|
||||
* @param {boolean} [options.includeModuleConfig]
|
||||
* @returns {Array<Node>}
|
||||
*/
|
||||
function createExportableNodeSet(set, {
|
||||
exportedIds,
|
||||
exportedSubflows,
|
||||
@@ -1582,10 +1589,14 @@ RED.nodes = (function() {
|
||||
return nns;
|
||||
}
|
||||
|
||||
// Create the Flow JSON for the current configuration
|
||||
// opts.credentials (whether to include (known) credentials) - default: true
|
||||
// opts.dimensions (whether to include node dimensions) - default: false
|
||||
// opts.includeModuleConfig (whether to include modules) - default: false
|
||||
/**
|
||||
* Converts the current configuration to an exportable JSON Object
|
||||
* @param {object} opts
|
||||
* @param {boolean} [opts.credentials] whether to include (known) credentials. Default `true`.
|
||||
* @param {boolean} [opts.dimensions] whether to include node dimensions. Default `false`.
|
||||
* @param {boolean} [opts.includeModuleConfig] whether to include modules. Default `false`.
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
function createCompleteNodeSet(opts) {
|
||||
var nns = [];
|
||||
var i;
|
||||
@@ -1848,14 +1859,23 @@ RED.nodes = (function() {
|
||||
* - id:copy - import with new id
|
||||
* - id:replace - import over the top of existing
|
||||
* - modules: map of module:version - hints for unknown nodes
|
||||
* - applyNodeDefaults - whether to apply default values to the imported nodes (default: false)
|
||||
*/
|
||||
function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) {
|
||||
const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} }
|
||||
const defOpts = {
|
||||
generateIds: false,
|
||||
addFlow: false,
|
||||
markChanged: false,
|
||||
reimport: false,
|
||||
importMap: {},
|
||||
applyNodeDefaults: false
|
||||
}
|
||||
options = Object.assign({}, defOpts, options)
|
||||
options.importMap = options.importMap || {}
|
||||
const createNewIds = options.generateIds;
|
||||
const reimport = (!createNewIds && !!options.reimport)
|
||||
const createMissingWorkspace = options.addFlow;
|
||||
const applyNodeDefaults = options.applyNodeDefaults;
|
||||
var i;
|
||||
var n;
|
||||
var newNodes;
|
||||
@@ -2000,15 +2020,30 @@ RED.nodes = (function() {
|
||||
// Provide option to install missing modules
|
||||
notificationOptions.buttons = [
|
||||
{
|
||||
text: "Manage dependencies",
|
||||
class:"primary",
|
||||
text: RED._("palette.editor.manageModules"),
|
||||
class: "primary",
|
||||
click: function(e) {
|
||||
unknownNotification.close();
|
||||
|
||||
RED.actions.invoke('core:manage-palette', {
|
||||
view: 'install',
|
||||
filter: '"' + missingModules.join('", "') + '"'
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
text: RED._("palette.editor.installAll"),
|
||||
class: "pull-left",
|
||||
click: function(e) {
|
||||
unknownNotification.close();
|
||||
|
||||
RED.actions.invoke('core:manage-palette', {
|
||||
autoInstall: true,
|
||||
modules: missingModules.reduce((modules, moduleName) => {
|
||||
modules[moduleName] = options.modules[moduleName];
|
||||
return modules;
|
||||
}, {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -2234,6 +2269,13 @@ RED.nodes = (function() {
|
||||
for (d in def.defaults) {
|
||||
if (def.defaults.hasOwnProperty(d)) {
|
||||
configNode[d] = n[d];
|
||||
if (applyNodeDefaults && n[d] === undefined) {
|
||||
// If the node has a default value, but the imported node does not
|
||||
// set it, then set it to the default value
|
||||
if (def.defaults[d].value !== undefined) {
|
||||
configNode[d] = JSON.parse(JSON.stringify(def.defaults[d].value))
|
||||
}
|
||||
}
|
||||
configNode._config[d] = JSON.stringify(n[d]);
|
||||
if (def.defaults[d].type) {
|
||||
configNode._configNodeReferences.add(n[d])
|
||||
@@ -2508,6 +2550,13 @@ RED.nodes = (function() {
|
||||
for (d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
|
||||
node[d] = n[d];
|
||||
if (applyNodeDefaults && n[d] === undefined) {
|
||||
// If the node has a default value, but the imported node does not
|
||||
// set it, then set it to the default value
|
||||
if (node._def.defaults[d].value !== undefined) {
|
||||
node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value))
|
||||
}
|
||||
}
|
||||
node._config[d] = JSON.stringify(n[d]);
|
||||
}
|
||||
}
|
||||
@@ -2761,7 +2810,8 @@ RED.nodes = (function() {
|
||||
workspaces:new_workspaces,
|
||||
subflows:new_subflows,
|
||||
missingWorkspace: missingWorkspace,
|
||||
removedNodes: removedNodes
|
||||
removedNodes: removedNodes,
|
||||
nodeMap: node_map
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3164,21 +3214,33 @@ RED.nodes = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the module list for the given nodes
|
||||
* @param {Array<Node>} nodes the nodes to search in
|
||||
* @returns {Record<string, string>} an object with {[moduleName]: moduleVersion}
|
||||
*/
|
||||
function getModuleListForNodes(nodes) {
|
||||
const modules = {}
|
||||
nodes.forEach(n => {
|
||||
const nodeSet = RED.nodes.registry.getNodeSetForType(n.type)
|
||||
if (nodeSet) {
|
||||
modules[nodeSet.module] = nodeSet.version
|
||||
const typeSet = new Set()
|
||||
nodes.forEach((n) => {
|
||||
if (!typeSet.has(n.type)) {
|
||||
typeSet.add(n.type)
|
||||
const nodeSet = RED.nodes.registry.getNodeSetForType(n.type)
|
||||
if (nodeSet) {
|
||||
modules[nodeSet.module] = nodeSet.version
|
||||
nodeSet.types.forEach((t) => typeSet.add(t))
|
||||
}
|
||||
}
|
||||
})
|
||||
return modules
|
||||
}
|
||||
|
||||
function updateGlobalConfigModuleList(nodes) {
|
||||
const modules = getModuleListForNodes(nodes)
|
||||
delete modules['node-red']
|
||||
const hasModules = (Object.keys(modules).length > 0)
|
||||
let globalConfigNode = nodes.find(n => n.type === 'global-config')
|
||||
let globalConfigNode = nodes.find((n) => n.type === 'global-config')
|
||||
if (!globalConfigNode && hasModules) {
|
||||
globalConfigNode = {
|
||||
id: RED.nodes.id(),
|
||||
@@ -3187,7 +3249,7 @@ RED.nodes = (function() {
|
||||
modules
|
||||
}
|
||||
nodes.push(globalConfigNode)
|
||||
} else if (globalConfigNode) {
|
||||
} else if (hasModules) {
|
||||
globalConfigNode.modules = modules
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +358,10 @@ var RED = (function() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationId === "update-available") {
|
||||
// re-emit as an event to be handled in editor-client/src/js/ui/palette-editor.js
|
||||
RED.events.emit("notification/update-available", msg)
|
||||
}
|
||||
if (msg.text) {
|
||||
msg.default = msg.text;
|
||||
var text = RED._(msg.text,msg);
|
||||
@@ -672,14 +675,48 @@ var RED = (function() {
|
||||
|
||||
setTimeout(function() {
|
||||
loader.end();
|
||||
checkFirstRun(function() {
|
||||
if (showProjectWelcome) {
|
||||
RED.projects.showStartup();
|
||||
}
|
||||
});
|
||||
checkTelemetry(function () {
|
||||
checkFirstRun(function() {
|
||||
if (showProjectWelcome) {
|
||||
RED.projects.showStartup();
|
||||
}
|
||||
});
|
||||
})
|
||||
},100);
|
||||
}
|
||||
|
||||
function checkTelemetry(done) {
|
||||
const telemetrySettings = RED.settings.telemetryEnabled;
|
||||
// Can only get telemetry permission from a user with permission to modify settings
|
||||
if (RED.user.hasPermission("settings.write") && telemetrySettings === undefined) {
|
||||
|
||||
const dialog = RED.popover.dialog({
|
||||
title: RED._("telemetry.settingsTitle"),
|
||||
content: `${RED._("telemetry.settingsDescription")}${RED._("telemetry.settingsDescription2")}`,
|
||||
closeButton: false,
|
||||
buttons: [
|
||||
{
|
||||
text: RED._("telemetry.enableLabel"),
|
||||
click: () => {
|
||||
RED.settings.set("telemetryEnabled", true)
|
||||
dialog.close()
|
||||
done()
|
||||
}
|
||||
},
|
||||
{
|
||||
text: RED._("telemetry.disableLabel"),
|
||||
click: () => {
|
||||
RED.settings.set("telemetryEnabled", false)
|
||||
dialog.close()
|
||||
done()
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
}
|
||||
function checkFirstRun(done) {
|
||||
if (RED.settings.theme("tours") === false) {
|
||||
done();
|
||||
|
||||
@@ -163,13 +163,18 @@ RED.popover = (function() {
|
||||
}
|
||||
|
||||
var timer = null;
|
||||
let isOpen = false
|
||||
var active;
|
||||
var div;
|
||||
var contentDiv;
|
||||
var currentStyle;
|
||||
|
||||
var openPopup = function(instant) {
|
||||
if (isOpen) {
|
||||
return
|
||||
}
|
||||
if (active) {
|
||||
isOpen = true
|
||||
var existingPopover = target.data("red-ui-popover");
|
||||
if (options.tooltip && existingPopover) {
|
||||
active = false;
|
||||
@@ -334,6 +339,7 @@ RED.popover = (function() {
|
||||
|
||||
}
|
||||
var closePopup = function(instant) {
|
||||
isOpen = false
|
||||
$(document).off('mousedown.red-ui-popover');
|
||||
if (!active) {
|
||||
if (div) {
|
||||
@@ -673,6 +679,74 @@ RED.popover = (function() {
|
||||
show:show,
|
||||
hide:hide
|
||||
}
|
||||
},
|
||||
dialog: function(options) {
|
||||
|
||||
const dialogContent = $('<div style="position:relative"></div>');
|
||||
|
||||
if (options.closeButton !== false) {
|
||||
$('<button type="button" class="red-ui-button red-ui-button-small" style="float: right; margin-top: -4px; margin-right: -4px;"><i class="fa fa-times"></i></button>').appendTo(dialogContent).click(function(evt) {
|
||||
evt.preventDefault();
|
||||
close();
|
||||
})
|
||||
}
|
||||
|
||||
const dialogBody = $('<div class="red-ui-dialog-body"></div>').appendTo(dialogContent);
|
||||
if (options.title) {
|
||||
$('<h2>').text(options.title).appendTo(dialogBody);
|
||||
}
|
||||
$('<div>').css("text-align","left").html(options.content).appendTo(dialogBody);
|
||||
|
||||
const stepToolbar = $('<div>',{class:"red-ui-dialog-toolbar"}).appendTo(dialogContent);
|
||||
|
||||
if (options.buttons) {
|
||||
options.buttons.forEach(button => {
|
||||
const btn = $('<button type="button" class="red-ui-button"></button>').text(button.text).appendTo(stepToolbar);
|
||||
if (button.class) {
|
||||
btn.addClass(button.class);
|
||||
}
|
||||
if (button.click) {
|
||||
btn.on('click', function(evt) {
|
||||
evt.preventDefault();
|
||||
button.click();
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
const width = 500;
|
||||
const maxWidth = Math.min($(window).width()-10,Math.max(width || 0, 300));
|
||||
|
||||
let shade = $('<div class="red-ui-shade" style="z-index: 2000"></div>').appendTo(document.body);
|
||||
shade.fadeIn()
|
||||
|
||||
let popover = RED.popover.create({
|
||||
target: $(".red-ui-editor"),
|
||||
width: width || "auto",
|
||||
maxWidth: maxWidth+"px",
|
||||
direction: "inset",
|
||||
class: "red-ui-dialog",
|
||||
trigger: "manual",
|
||||
content: dialogContent
|
||||
}).open()
|
||||
|
||||
function close() {
|
||||
if (shade) {
|
||||
shade.fadeOut(() => {
|
||||
shade.remove()
|
||||
shade = null
|
||||
})
|
||||
}
|
||||
if (popover) {
|
||||
popover.close()
|
||||
popover = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -519,10 +519,25 @@
|
||||
}
|
||||
},
|
||||
expand: function () {
|
||||
var that = this;
|
||||
const that = this;
|
||||
let filter;
|
||||
if (that.options.node) {
|
||||
let nodeFilter = that.options.node.filter;
|
||||
if ((typeof nodeFilter === "string" || typeof nodeFilter === "object") && nodeFilter) {
|
||||
if (!Array.isArray(nodeFilter)) {
|
||||
nodeFilter = [nodeFilter];
|
||||
}
|
||||
filter = function (node) {
|
||||
return nodeFilter.includes(node.type);
|
||||
};
|
||||
} else if (typeof nodeFilter === "function") {
|
||||
filter = nodeFilter;
|
||||
}
|
||||
}
|
||||
RED.tray.hide();
|
||||
RED.view.selectNodes({
|
||||
single: true,
|
||||
filter: filter,
|
||||
selected: [that.value()],
|
||||
onselect: function (selection) {
|
||||
that.value(selection.id);
|
||||
|
||||
@@ -46,10 +46,20 @@ RED.contextMenu = (function () {
|
||||
hasEnabledNode = true;
|
||||
}
|
||||
}
|
||||
if (n.l === undefined || n.l) {
|
||||
hasLabeledNode = true;
|
||||
if (n.l === undefined) {
|
||||
// Check if the node sets showLabel in the defaults
|
||||
// as that determines the default behaviour for the node
|
||||
if (n._def.showLabel !== false) {
|
||||
hasLabeledNode = true;
|
||||
} else {
|
||||
hasUnlabeledNode = true;
|
||||
}
|
||||
} else {
|
||||
hasUnlabeledNode = true;
|
||||
if (n.l) {
|
||||
hasLabeledNode = true;
|
||||
} else {
|
||||
hasUnlabeledNode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,37 +284,85 @@ RED.palette.editor = (function() {
|
||||
|
||||
function _refreshNodeModule(module) {
|
||||
if (!nodeEntries.hasOwnProperty(module)) {
|
||||
nodeEntries[module] = {info:RED.nodes.registry.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)
|
||||
const nodeInfo = RED.nodes.registry.getModule(module);
|
||||
let index = [module];
|
||||
|
||||
nodeEntries[module] = {
|
||||
info: {
|
||||
name: nodeInfo.name,
|
||||
version: nodeInfo.version,
|
||||
local: nodeInfo.local,
|
||||
nodeSet: nodeInfo.sets,
|
||||
},
|
||||
};
|
||||
|
||||
if (nodeInfo.pending_version) {
|
||||
nodeEntries[module].info.pending_version = nodeInfo.pending_version;
|
||||
}
|
||||
|
||||
if (loadedIndex[module] && loadedIndex[module].url) {
|
||||
// Add the link to the node documentation if the catalog contains it
|
||||
nodeEntries[module].info.url = loadedIndex[module].url;
|
||||
}
|
||||
|
||||
for (const set in nodeInfo.sets) {
|
||||
if (nodeInfo.sets.hasOwnProperty(set)) {
|
||||
index.push(set);
|
||||
index = index.concat(nodeInfo.sets[set].types);
|
||||
}
|
||||
}
|
||||
|
||||
nodeEntries[module].index = index.join(",").toLowerCase();
|
||||
nodeList.editableList('addItem', nodeEntries[module]);
|
||||
} else {
|
||||
var moduleInfo = nodeEntries[module].info;
|
||||
var nodeEntry = nodeEntries[module].elements;
|
||||
if (nodeEntry) {
|
||||
if (moduleInfo.plugin) {
|
||||
nodeEntry.enableButton.hide();
|
||||
nodeEntry.removeButton.show();
|
||||
if (nodeEntries[module].info.pluginSet && !nodeEntries[module].info.nodeSet) {
|
||||
// Since plugins are loaded before nodes, check if the module has nodes too
|
||||
const nodeInfo = RED.nodes.registry.getModule(module);
|
||||
|
||||
if (nodeInfo) {
|
||||
let index = [nodeEntries[module].index];
|
||||
|
||||
for (const set in nodeInfo.sets) {
|
||||
if (nodeInfo.sets.hasOwnProperty(set)) {
|
||||
index.push(set);
|
||||
index = index.concat(nodeInfo.sets[set].types)
|
||||
}
|
||||
}
|
||||
|
||||
nodeEntries[module].info.nodeSet = nodeInfo.sets;
|
||||
nodeEntries[module].index = index.join(",").toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
const moduleInfo = nodeEntries[module].info;
|
||||
const nodeEntry = nodeEntries[module].elements;
|
||||
if (nodeEntry) {
|
||||
const setCount = [];
|
||||
|
||||
if (moduleInfo.pluginSet) {
|
||||
let pluginCount = 0;
|
||||
for (let setName in moduleInfo.sets) {
|
||||
if (moduleInfo.sets.hasOwnProperty(setName)) {
|
||||
let set = moduleInfo.sets[setName];
|
||||
if (set.plugins) {
|
||||
for (const setName in moduleInfo.pluginSet) {
|
||||
if (moduleInfo.pluginSet.hasOwnProperty(setName)) {
|
||||
let set = moduleInfo.pluginSet[setName];
|
||||
if (set.plugins && set.plugins.length) {
|
||||
pluginCount += set.plugins.length;
|
||||
} else if (set.plugins && !!RED.plugins.getPlugin(setName)) {
|
||||
// `registerPlugin` in runtime not called but called in editor, add it
|
||||
pluginCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount}));
|
||||
|
||||
} else {
|
||||
setCount.push(RED._('palette.editor.pluginCount', { count: pluginCount }));
|
||||
|
||||
if (!moduleInfo.nodeSet) {
|
||||
// Module only have plugins
|
||||
nodeEntry.enableButton.hide();
|
||||
nodeEntry.removeButton.show();
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleInfo.nodeSet) {
|
||||
var activeTypeCount = 0;
|
||||
var typeCount = 0;
|
||||
var errorCount = 0;
|
||||
@@ -322,10 +370,10 @@ RED.palette.editor = (function() {
|
||||
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];
|
||||
for (const setName in moduleInfo.nodeSet) {
|
||||
if (moduleInfo.nodeSet.hasOwnProperty(setName)) {
|
||||
let inUseCount = 0;
|
||||
const set = moduleInfo.nodeSet[setName];
|
||||
const setElements = nodeEntry.sets[setName]
|
||||
|
||||
if (set.err) {
|
||||
@@ -342,8 +390,8 @@ RED.palette.editor = (function() {
|
||||
activeTypeCount += set.types.length;
|
||||
}
|
||||
typeCount += set.types.length;
|
||||
for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
|
||||
var t = moduleInfo.sets[setName].types[i];
|
||||
for (var i=0;i<moduleInfo.nodeSet[setName].types.length;i++) {
|
||||
var t = moduleInfo.nodeSet[setName].types[i];
|
||||
inUseCount += (typesInUse[t]||0);
|
||||
if (setElements && set.enabled) {
|
||||
var def = RED.nodes.getType(t);
|
||||
@@ -379,8 +427,8 @@ RED.palette.editor = (function() {
|
||||
nodeEntry.errorRow.show();
|
||||
}
|
||||
|
||||
var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
|
||||
nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
|
||||
const nodeCount = (activeTypeCount === typeCount) ? typeCount : activeTypeCount + " / " + typeCount;
|
||||
setCount.push(RED._('palette.editor.nodeCount', { count: typeCount, label: nodeCount }));
|
||||
|
||||
if (nodeEntries[module].totalUseCount > 0) {
|
||||
nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
|
||||
@@ -399,6 +447,7 @@ RED.palette.editor = (function() {
|
||||
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
|
||||
}
|
||||
}
|
||||
nodeEntry.setCount.text(setCount.join(" & ") || RED._("sidebar.info.empty"));
|
||||
}
|
||||
if (moduleInfo.pending_version) {
|
||||
nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
|
||||
@@ -700,6 +749,9 @@ RED.palette.editor = (function() {
|
||||
refreshCatalogues()
|
||||
|
||||
RED.actions.add("core:manage-palette", function(opts) {
|
||||
if (opts && opts.autoInstall && opts.modules) {
|
||||
autoInstallModules(opts.modules);
|
||||
} else {
|
||||
RED.userSettings.show('palette');
|
||||
if (opts) {
|
||||
if (opts.view) {
|
||||
@@ -713,7 +765,8 @@ RED.palette.editor = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
RED.events.on('registry:module-updated', function(ns) {
|
||||
refreshNodeModule(ns.module);
|
||||
@@ -789,19 +842,41 @@ 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)
|
||||
const pluginInfo = RED.plugins.getModule(module);
|
||||
let index = [module];
|
||||
|
||||
nodeEntries[module] = {
|
||||
info: {
|
||||
name: pluginInfo.name,
|
||||
version: pluginInfo.version,
|
||||
local: pluginInfo.local,
|
||||
pluginSet: pluginInfo.sets,
|
||||
}
|
||||
};
|
||||
|
||||
if (pluginInfo.pending_version) {
|
||||
nodeEntries[module].info.pending_version = pluginInfo.pending_version;
|
||||
}
|
||||
|
||||
if (loadedIndex[module] && loadedIndex[module].url) {
|
||||
// Add the link to the plugin documentation if the catalog contains it
|
||||
nodeEntries[module].info.url = loadedIndex[module].url;
|
||||
}
|
||||
|
||||
for (const set in pluginInfo.sets) {
|
||||
if (pluginInfo.sets.hasOwnProperty(set)) {
|
||||
index.push(set);
|
||||
// TODO: not sure plugin has `types` property
|
||||
index = index.concat(pluginInfo.sets[set].types)
|
||||
}
|
||||
}
|
||||
|
||||
nodeEntries[module].index = index.join(",").toLowerCase();
|
||||
nodeList.editableList('addItem', nodeEntries[module]);
|
||||
} else {
|
||||
// Since plugins are loaded before nodes,
|
||||
// `nodeEntries[module]` should be undefined
|
||||
_refreshNodeModule(module);
|
||||
}
|
||||
|
||||
@@ -815,6 +890,14 @@ RED.palette.editor = (function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
RED.events.on("notification/update-available", function (msg) {
|
||||
const updateKnownAbout = updateStatusState.version === msg.version
|
||||
updateStatusState.version = msg.version
|
||||
if (updateStatusWidgetPopover && !updateKnownAbout) {
|
||||
setTimeout(() => { updateStatusWidgetPopover.open(); setTimeout(() => updateStatusWidgetPopover.close(), 20000) }, 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getSettingsPane() {
|
||||
@@ -912,6 +995,12 @@ RED.palette.editor = (function() {
|
||||
var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container);
|
||||
var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
|
||||
$('<span>').text(entry.name).appendTo(titleRow);
|
||||
|
||||
if (entry.url) {
|
||||
// Add the link icon to the node documentation
|
||||
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href', entry.url).appendTo(titleRow);
|
||||
}
|
||||
|
||||
var metaRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-version"><i class="fa fa-tag"></i></div>').appendTo(headerRow);
|
||||
var versionSpan = $('<span>').text(entry.version).appendTo(metaRow);
|
||||
|
||||
@@ -976,12 +1065,28 @@ RED.palette.editor = (function() {
|
||||
}
|
||||
})
|
||||
const populateSetList = function () {
|
||||
var setList = Object.keys(entry.sets)
|
||||
setList.sort(function(A,B) {
|
||||
const setList = [...Object.keys(entry.nodeSet || {}), ...Object.keys(entry.pluginSet || {})];
|
||||
setList.sort(function (A, B) {
|
||||
return A.toLowerCase().localeCompare(B.toLowerCase());
|
||||
});
|
||||
setList.forEach(function(setName) {
|
||||
var set = entry.sets[setName];
|
||||
setList.forEach(function (setName) {
|
||||
const set = (entry.nodeSet && setName in entry.nodeSet) ? entry.nodeSet[setName] : entry.pluginSet[setName];
|
||||
|
||||
if (set.plugins && !set.plugins.length) {
|
||||
// `registerPlugin` in the runtime not called
|
||||
if (!!RED.plugins.getPlugin(setName)) {
|
||||
// Add plugin if registered in editor but not in runtime
|
||||
// Can happen if plugin doesn't have .js file
|
||||
set.plugins.push({ id: setName });
|
||||
} else {
|
||||
// `registerPlugin` in the editor not called - do not add this empty set
|
||||
return;
|
||||
}
|
||||
} else if (set.types && !set.types.length) {
|
||||
// `registerPlugin` in the runtime not called - do not add this empty set
|
||||
return;
|
||||
}
|
||||
|
||||
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 = {};
|
||||
@@ -1198,7 +1303,17 @@ RED.palette.editor = (function() {
|
||||
var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container);
|
||||
var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
|
||||
$('<span>').text(entry.name||entry.id).appendTo(titleRow);
|
||||
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href',entry.url).appendTo(titleRow);
|
||||
if (entry.url) {
|
||||
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href',entry.url).appendTo(titleRow);
|
||||
}
|
||||
if (entry.deprecated) {
|
||||
const deprecatedWarning = $('<span class="red-ui-palette-module-deprecated"></span>').text(RED._('palette.editor.deprecated')).appendTo(titleRow);
|
||||
let message = $('<span>').text(RED._('palette.editor.deprecatedTip'))
|
||||
if (typeof entry.deprecated === 'string') {
|
||||
$('<p>').text(entry.deprecated).appendTo(message)
|
||||
}
|
||||
RED.popover.tooltip(deprecatedWarning, message);
|
||||
}
|
||||
var descRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
|
||||
$('<div>',{class:"red-ui-palette-module-description"}).text(entry.description).appendTo(descRow);
|
||||
var metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
|
||||
@@ -1339,63 +1454,88 @@ RED.palette.editor = (function() {
|
||||
$('<div id="red-ui-palette-module-install-shade" class="red-ui-palette-module-shade hide"><div class="red-ui-palette-module-shade-status"></div><img src="red/images/spin.svg" class="red-ui-palette-spinner"/></div>').appendTo(installTab);
|
||||
}
|
||||
|
||||
function update(entry,version,url,container,done) {
|
||||
function update(entry, version, url, container, done) {
|
||||
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
|
||||
done(new Error('Palette not editable'));
|
||||
return;
|
||||
}
|
||||
var notification = RED.notify(RED._("palette.editor.confirm.update.body",{module:entry.name}),{
|
||||
|
||||
let notification;
|
||||
let msg = RED._("palette.editor.confirm.update.body", { module: entry.name });
|
||||
const buttons = [
|
||||
{
|
||||
text: RED._("common.label.cancel"),
|
||||
click: function () {
|
||||
notification.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: RED._("palette.editor.confirm.button.update"),
|
||||
class: "primary red-ui-palette-module-install-confirm-button-update",
|
||||
click: function () {
|
||||
const spinner = RED.utils.addSpinnerOverlay(container, true);
|
||||
const buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
|
||||
$('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function (evt) {
|
||||
evt.preventDefault();
|
||||
RED.actions.invoke("core:show-event-log");
|
||||
});
|
||||
installNodeModule(entry.name, version, url, function (xhr) {
|
||||
spinner.remove();
|
||||
if (xhr) {
|
||||
if (xhr.responseJSON) {
|
||||
const notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{
|
||||
type: 'error',
|
||||
modal: true,
|
||||
fixed: true,
|
||||
buttons: [
|
||||
{
|
||||
text: RED._("common.label.close"),
|
||||
click: function () {
|
||||
notification.close();
|
||||
}
|
||||
}, {
|
||||
text: RED._("eventLog.view"),
|
||||
click: function () {
|
||||
notification.close();
|
||||
RED.actions.invoke("core:show-event-log");
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
done(xhr);
|
||||
});
|
||||
notification.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const currentVersion = semverre.exec(nodeEntries[entry.name].info.version);
|
||||
const targetVersion = semverre.exec(version);
|
||||
if (currentVersion && targetVersion) {
|
||||
if (targetVersion[1] > currentVersion[1]) {
|
||||
// Updating to Major version
|
||||
msg = msg + RED._("palette.editor.majorVersion");
|
||||
|
||||
if (entry.url) {
|
||||
// Add a button to open the node documentation
|
||||
buttons.splice(1, 0, {
|
||||
text: RED._("palette.editor.confirm.button.review"),
|
||||
class: "primary red-ui-palette-module-install-confirm-button-review",
|
||||
click: function () {
|
||||
window.open(entry.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notification = RED.notify(msg, {
|
||||
modal: true,
|
||||
fixed: true,
|
||||
buttons: [
|
||||
{
|
||||
text: RED._("common.label.cancel"),
|
||||
click: function() {
|
||||
notification.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: RED._("palette.editor.confirm.button.update"),
|
||||
class: "primary red-ui-palette-module-install-confirm-button-update",
|
||||
click: function() {
|
||||
var spinner = RED.utils.addSpinnerOverlay(container, true);
|
||||
var buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
|
||||
$('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
|
||||
evt.preventDefault();
|
||||
RED.actions.invoke("core:show-event-log");
|
||||
});
|
||||
installNodeModule(entry.name,version,url,function(xhr) {
|
||||
spinner.remove();
|
||||
if (xhr) {
|
||||
if (xhr.responseJSON) {
|
||||
var notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{
|
||||
type: 'error',
|
||||
modal: true,
|
||||
fixed: true,
|
||||
buttons: [
|
||||
{
|
||||
text: RED._("common.label.close"),
|
||||
click: function() {
|
||||
notification.close();
|
||||
}
|
||||
},{
|
||||
text: RED._("eventLog.view"),
|
||||
click: function() {
|
||||
notification.close();
|
||||
RED.actions.invoke("core:show-event-log");
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
done(xhr);
|
||||
});
|
||||
notification.close();
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
buttons: buttons
|
||||
});
|
||||
}
|
||||
function remove(entry,container,done) {
|
||||
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
|
||||
@@ -1448,7 +1588,7 @@ RED.palette.editor = (function() {
|
||||
}
|
||||
} else {
|
||||
// dedicated list management for plugins
|
||||
if (entry.plugin) {
|
||||
if (entry.pluginSet) {
|
||||
|
||||
let e = nodeEntries[entry.name];
|
||||
if (e) {
|
||||
@@ -1461,9 +1601,9 @@ RED.palette.editor = (function() {
|
||||
// cleans the editor accordingly of its left-overs.
|
||||
let found_onremove = true;
|
||||
|
||||
let keys = Object.keys(entry.sets);
|
||||
let keys = Object.keys(entry.pluginSet);
|
||||
keys.forEach((key) => {
|
||||
let set = entry.sets[key];
|
||||
let set = entry.pluginSet[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') {
|
||||
@@ -1593,29 +1733,113 @@ RED.palette.editor = (function() {
|
||||
})
|
||||
}
|
||||
|
||||
function autoInstallModules(modules) {
|
||||
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
|
||||
console.error('Palette not editable');
|
||||
return;
|
||||
}
|
||||
|
||||
let notification;
|
||||
const notificationOptions = {
|
||||
fixed: true,
|
||||
buttons: [
|
||||
{
|
||||
text: RED._("common.label.close"),
|
||||
click: function () {
|
||||
notification.close();
|
||||
}
|
||||
}, {
|
||||
text: RED._("eventLog.view"),
|
||||
click: function () {
|
||||
notification.close();
|
||||
RED.actions.invoke("core:show-event-log");
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const moduleArray = Object.entries(modules);
|
||||
const installModule = function (module) {
|
||||
const [moduleName, moduleVersion] = module;
|
||||
|
||||
const spinner = '<div style="text-align: center; height: 100%; padding-top: 20px;"><img src="red/images/spin.svg" style="width: 60px;"/></div>';
|
||||
const msg = "<p>" + RED._("palette.editor.installing", { module: moduleName }) + "</p>" + spinner;
|
||||
|
||||
if (!notification) {
|
||||
notification = RED.notify(msg, notificationOptions);
|
||||
} else {
|
||||
notification.update(msg, notificationOptions);
|
||||
}
|
||||
|
||||
installNodeModule(moduleName, moduleVersion, undefined, function(xhr, textStatus, err) {
|
||||
if (err && xhr.status === 504) {
|
||||
notification.update(RED._("palette.editor.errors.installTimeout"), {
|
||||
modal: true,
|
||||
fixed: true,
|
||||
buttons: notificationOptions.buttons
|
||||
});
|
||||
} else if (xhr) {
|
||||
if (xhr.responseJSON) {
|
||||
notification.update(RED._("palette.editor.errors.installFailed", { module: moduleName, message:xhr.responseJSON.message }), {
|
||||
type: "error",
|
||||
modal: true,
|
||||
fixed: true,
|
||||
buttons: notificationOptions.buttons
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (moduleArray.length) {
|
||||
installModule(moduleArray.shift());
|
||||
} else {
|
||||
notification.update(RED._("palette.editor.successfulInstall"), { ...notificationOptions, type: "success", timeout: 10000 });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (moduleArray.length) {
|
||||
installModule(moduleArray.shift());
|
||||
}
|
||||
}
|
||||
|
||||
const updateStatusWidget = $('<button type="button" class="red-ui-footer-button red-ui-update-status"></button>');
|
||||
let updateStatusWidgetPopover;
|
||||
const updateStatusState = { moduleCount: 0 }
|
||||
let updateAvailable = [];
|
||||
|
||||
function addUpdateInfoToStatusBar() {
|
||||
updateStatusWidget.on("click", function (evt) {
|
||||
RED.actions.invoke("core:manage-palette", {
|
||||
view: "nodes",
|
||||
filter: '"' + updateAvailable.join('", "') + '"'
|
||||
});
|
||||
});
|
||||
|
||||
RED.popover.tooltip(updateStatusWidget, function () {
|
||||
const count = updateAvailable.length || 0;
|
||||
return RED._("palette.editor.updateCount", { count: count });
|
||||
updateStatusWidgetPopover = RED.popover.create({
|
||||
target: updateStatusWidget,
|
||||
trigger: "click",
|
||||
interactive: true,
|
||||
direction: "bottom",
|
||||
content: function () {
|
||||
const count = updateAvailable.length || 0;
|
||||
const content = $('<div style="display: flex; flex-direction: column; gap: 5px;"></div>');
|
||||
if (updateStatusState.version) {
|
||||
$(`<a class='red-ui-button' href="https://github.com/node-red/node-red/releases/tag/${updateStatusState.version}" target="_blank">${RED._("telemetry.updateAvailableDesc", updateStatusState)}</a>`).appendTo(content)
|
||||
}
|
||||
if (count > 0) {
|
||||
$(`<button type="button" class="red-ui-button"><i class="fa fa-cube"></i> ${RED._("palette.editor.updateCount", { count: count })}</button>`).on("click", function (evt) {
|
||||
updateStatusWidgetPopover.close()
|
||||
RED.actions.invoke("core:manage-palette", {
|
||||
view: "nodes",
|
||||
filter: '"' + updateAvailable.join('", "') + '"'
|
||||
});
|
||||
}).appendTo(content)
|
||||
}
|
||||
return content
|
||||
},
|
||||
delay: { show: 750, hide: 250 }
|
||||
});
|
||||
|
||||
RED.statusBar.add({
|
||||
id: "update",
|
||||
id: "red-ui-status-package-update",
|
||||
align: "right",
|
||||
element: updateStatusWidget
|
||||
});
|
||||
|
||||
updateStatus({ count: 0 });
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
let pendingRefreshTimeout
|
||||
@@ -1638,18 +1862,22 @@ RED.palette.editor = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus({ count: updateAvailable.length });
|
||||
updateStatusState.moduleCount = updateAvailable.length;
|
||||
updateStatus();
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function updateStatus(opts) {
|
||||
if (opts.count) {
|
||||
RED.statusBar.show("update");
|
||||
function updateStatus() {
|
||||
if (updateStatusState.moduleCount || updateStatusState.version) {
|
||||
updateStatusWidget.empty();
|
||||
$('<span><i class="fa fa-cube"></i> ' + opts.count + '</span>').appendTo(updateStatusWidget);
|
||||
let count = updateStatusState.moduleCount || 0;
|
||||
if (updateStatusState.version) {
|
||||
count ++
|
||||
}
|
||||
$(`<span><i class="fa fa-cube"></i> ${RED._("telemetry.updateAvailable", { count: count })}</span>`).appendTo(updateStatusWidget);
|
||||
RED.statusBar.show("red-ui-status-package-update");
|
||||
} else {
|
||||
RED.statusBar.hide("update");
|
||||
RED.statusBar.hide("red-ui-status-package-update");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -435,10 +435,15 @@ RED.tourGuide = (function() {
|
||||
|
||||
function listTour() {
|
||||
return [
|
||||
{
|
||||
id: "4_1",
|
||||
label: "4.1",
|
||||
path: "./tours/welcome.js"
|
||||
},
|
||||
{
|
||||
id: "4_0",
|
||||
label: "4.0",
|
||||
path: "./tours/welcome.js"
|
||||
path: "./tours/4.0/welcome.js"
|
||||
},
|
||||
{
|
||||
id: "3_1",
|
||||
|
||||
@@ -14,6 +14,7 @@ RED.typeSearch = (function() {
|
||||
var addCallback;
|
||||
var cancelCallback;
|
||||
var moveCallback;
|
||||
var suggestCallback
|
||||
|
||||
var typesUsed = {};
|
||||
|
||||
@@ -25,6 +26,11 @@ RED.typeSearch = (function() {
|
||||
selected = 0;
|
||||
searchResults.children().removeClass('selected');
|
||||
searchResults.children(":visible:first").addClass('selected');
|
||||
const children = searchResults.children(":visible");
|
||||
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (n) {
|
||||
updateSuggestion(n)
|
||||
}
|
||||
},100);
|
||||
|
||||
}
|
||||
@@ -63,7 +69,7 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
});
|
||||
searchInput.on('keydown',function(evt) {
|
||||
var children = searchResults.children(":visible");
|
||||
const children = searchResults.children(":visible");
|
||||
if (evt.keyCode === 40 && evt.shiftKey) {
|
||||
evt.preventDefault();
|
||||
moveDialog(0,10);
|
||||
@@ -86,9 +92,14 @@ RED.typeSearch = (function() {
|
||||
selected++;
|
||||
}
|
||||
$(children[selected]).addClass('selected');
|
||||
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (n) {
|
||||
updateSuggestion(n)
|
||||
}
|
||||
ensureSelectedIsVisible();
|
||||
evt.preventDefault();
|
||||
} else if (evt.keyCode === 38) {
|
||||
// Up
|
||||
if (selected > 0) {
|
||||
if (selected < children.length) {
|
||||
$(children[selected]).removeClass('selected');
|
||||
@@ -96,6 +107,10 @@ RED.typeSearch = (function() {
|
||||
selected--;
|
||||
}
|
||||
$(children[selected]).addClass('selected');
|
||||
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (n) {
|
||||
updateSuggestion(n)
|
||||
}
|
||||
ensureSelectedIsVisible();
|
||||
evt.preventDefault();
|
||||
} else if ((evt.metaKey || evt.ctrlKey) && evt.keyCode === 13 ) {
|
||||
@@ -103,14 +118,14 @@ RED.typeSearch = (function() {
|
||||
// (ctrl or cmd) and enter
|
||||
var index = Math.max(0,selected);
|
||||
if (index < children.length) {
|
||||
var n = $(children[index]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (!/^_action_:/.test(n.type)) {
|
||||
const n = $(children[index]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (!n.nodes && !/^_action_:/.test(n.type)) {
|
||||
typesUsed[n.type] = Date.now();
|
||||
}
|
||||
if (n.def.outputs === 0) {
|
||||
confirm(n);
|
||||
} else {
|
||||
addCallback(n.type,true);
|
||||
addCallback(n, true);
|
||||
}
|
||||
$("#red-ui-type-search-input").val("").trigger("keyup");
|
||||
setTimeout(function() {
|
||||
@@ -142,7 +157,7 @@ RED.typeSearch = (function() {
|
||||
if (activeFilter === "" ) {
|
||||
return true;
|
||||
}
|
||||
if (data.recent || data.common) {
|
||||
if (data.recent || data.common || data.suggestion) {
|
||||
return false;
|
||||
}
|
||||
return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1);
|
||||
@@ -164,67 +179,116 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
return Ai-Bi;
|
||||
},
|
||||
addItem: function(container,i,object) {
|
||||
var def = object.def;
|
||||
object.index = object.type.toLowerCase();
|
||||
if (object.separator) {
|
||||
addItem: function(container, i, nodeItem) {
|
||||
// nodeItem can take multiple forms
|
||||
// - A node type: {type: "inject", def: RED.nodes.getType("inject"), label: "Inject"}
|
||||
// - A flow suggestion: { suggestion: true, nodes: [] }
|
||||
// - A placeholder suggestion: { suggestionPlaceholder: true, label: 'loading suggestions...' }
|
||||
|
||||
let nodeDef = nodeItem.def;
|
||||
let nodeType = nodeItem.type;
|
||||
if (nodeItem.suggestion && nodeItem.nodes.length > 0) {
|
||||
nodeDef = RED.nodes.getType(nodeItem.nodes[0].type);
|
||||
nodeType = nodeItem.nodes[0].type;
|
||||
}
|
||||
|
||||
nodeItem.index = nodeItem.type?.toLowerCase() || '';
|
||||
if (nodeItem.separator) {
|
||||
container.addClass("red-ui-search-result-separator")
|
||||
}
|
||||
var div = $('<div>',{class:"red-ui-search-result"}).appendTo(container);
|
||||
|
||||
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
|
||||
if (object.type === "junction") {
|
||||
const div = $('<div>',{class:"red-ui-search-result"}).appendTo(container);
|
||||
const nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
|
||||
|
||||
if (nodeItem.suggestionPlaceholder) {
|
||||
nodeDiv.addClass("red-ui-palette-icon-suggestion")
|
||||
const iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
|
||||
$('<i class="spinner" style="margin-top: -1px">').appendTo(iconContainer);
|
||||
} else if (nodeType === "junction") {
|
||||
nodeDiv.addClass("red-ui-palette-icon-junction");
|
||||
} else if (/^_action_:/.test(object.type)) {
|
||||
nodeDiv.addClass("red-ui-palette-icon-junction")
|
||||
} else {
|
||||
var colour = RED.utils.getNodeColor(object.type,def);
|
||||
nodeDiv.css('backgroundColor',colour);
|
||||
nodeDiv.css('backgroundColor', RED.utils.getNodeColor(nodeType, nodeDef));
|
||||
}
|
||||
var icon_url = RED.utils.getNodeIcon(def);
|
||||
|
||||
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
|
||||
RED.utils.createIconElement(icon_url, iconContainer, false);
|
||||
if (nodeDef) {
|
||||
// Add the node icon
|
||||
const icon_url = RED.utils.getNodeIcon(nodeDef);
|
||||
const iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
|
||||
RED.utils.createIconElement(icon_url, iconContainer, false);
|
||||
}
|
||||
|
||||
if (/^subflow:/.test(object.type)) {
|
||||
var sf = RED.nodes.subflow(object.type.substring(8));
|
||||
if (/^subflow:/.test(nodeType)) {
|
||||
var sf = RED.nodes.subflow(nodeType.substring(8));
|
||||
if (sf.in.length > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
|
||||
}
|
||||
if (sf.out.length > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
|
||||
}
|
||||
} else if (!/^_action_:/.test(object.type) && object.type !== "junction") {
|
||||
if (def.inputs > 0) {
|
||||
} else if (nodeDef && nodeType !== "junction") {
|
||||
if (nodeDef.inputs > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
|
||||
}
|
||||
if (def.outputs > 0) {
|
||||
if (nodeDef.outputs > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
|
||||
}
|
||||
}
|
||||
|
||||
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
|
||||
|
||||
var label = object.label;
|
||||
object.index += "|"+label.toLowerCase();
|
||||
var label = nodeItem.label;
|
||||
nodeItem.index += "|"+label.toLowerCase();
|
||||
|
||||
$('<div>',{class:"red-ui-search-result-node-label"}).text(label).appendTo(contentDiv);
|
||||
|
||||
nodeItem.element = container;
|
||||
|
||||
div.on("click", function(evt) {
|
||||
evt.preventDefault();
|
||||
confirm(object);
|
||||
confirm(nodeItem);
|
||||
});
|
||||
div.on('mouseenter', function() {
|
||||
const children = searchResults.children(":visible");
|
||||
if (selected > -1 && selected < children.length) {
|
||||
$(children[selected]).removeClass('selected');
|
||||
}
|
||||
const editableListItem = container.parent()
|
||||
selected = children.index(editableListItem);
|
||||
$(children[selected]).addClass('selected');
|
||||
updateSuggestion(nodeItem);
|
||||
})
|
||||
},
|
||||
scrollOnAdd: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateSuggestion(nodeItem) {
|
||||
if (suggestCallback) {
|
||||
if (!nodeItem) {
|
||||
suggestCallback(null);
|
||||
} else if (nodeItem.nodes) {
|
||||
// This is a multi-node suggestion
|
||||
suggestCallback({
|
||||
nodes: nodeItem.nodes
|
||||
});
|
||||
} else if (nodeItem.type) {
|
||||
// Single node suggestion
|
||||
suggestCallback({
|
||||
nodes: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
type: nodeItem.type
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function confirm(def) {
|
||||
hide();
|
||||
if (!/^_action_:/.test(def.type)) {
|
||||
if (!def.nodes && !/^_action_:/.test(def.type)) {
|
||||
typesUsed[def.type] = Date.now();
|
||||
}
|
||||
addCallback(def.type);
|
||||
addCallback(def);
|
||||
}
|
||||
|
||||
function handleMouseActivity(evt) {
|
||||
@@ -274,6 +338,7 @@ RED.typeSearch = (function() {
|
||||
addCallback = opts.add;
|
||||
cancelCallback = opts.cancel;
|
||||
moveCallback = opts.move;
|
||||
suggestCallback = opts.suggest;
|
||||
RED.events.emit("type-search:open");
|
||||
//shade.show();
|
||||
if ($("#red-ui-main-container").height() - opts.y - 195 < 0) {
|
||||
@@ -294,6 +359,9 @@ RED.typeSearch = (function() {
|
||||
},200);
|
||||
}
|
||||
function hide(fast) {
|
||||
if (suggestCallback) {
|
||||
suggestCallback(null);
|
||||
}
|
||||
if (visible) {
|
||||
visible = false;
|
||||
if (dialog !== null) {
|
||||
@@ -356,11 +424,11 @@ RED.typeSearch = (function() {
|
||||
(!filter.output || def.outputs > 0)
|
||||
}
|
||||
function refreshTypeList(opts) {
|
||||
var i;
|
||||
let i;
|
||||
searchResults.editableList('empty');
|
||||
searchInput.searchBox('value','').focus();
|
||||
selected = -1;
|
||||
var common = [
|
||||
const common = [
|
||||
'inject','debug','function','change','switch','junction'
|
||||
].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); });
|
||||
|
||||
@@ -371,7 +439,7 @@ RED.typeSearch = (function() {
|
||||
// common.push('_action_:core:split-wire-with-link-nodes')
|
||||
// }
|
||||
|
||||
var recentlyUsed = Object.keys(typesUsed);
|
||||
let recentlyUsed = Object.keys(typesUsed);
|
||||
recentlyUsed.sort(function(a,b) {
|
||||
return typesUsed[b]-typesUsed[a];
|
||||
});
|
||||
@@ -379,9 +447,10 @@ RED.typeSearch = (function() {
|
||||
return applyFilter(opts.filter,t,RED.nodes.getType(t)) && common.indexOf(t) === -1;
|
||||
});
|
||||
|
||||
var items = [];
|
||||
const items = [];
|
||||
|
||||
RED.nodes.registry.getNodeTypes().forEach(function(t) {
|
||||
var def = RED.nodes.getType(t);
|
||||
const def = RED.nodes.getType(t);
|
||||
if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
|
||||
items.push({type:t,def: def, label:getTypeLabel(t,def)});
|
||||
}
|
||||
@@ -389,18 +458,46 @@ RED.typeSearch = (function() {
|
||||
items.push({ type: 'junction', def: { inputs:1, outputs: 1, label: 'junction', type: 'junction'}, label: 'junction' })
|
||||
items.sort(sortTypeLabels);
|
||||
|
||||
var commonCount = 0;
|
||||
var item;
|
||||
var index = 0;
|
||||
let index = 0;
|
||||
|
||||
// const suggestionItem = {
|
||||
// suggestionPlaceholder: true,
|
||||
// label: 'loading suggestions...',
|
||||
// separator: true,
|
||||
// i: index++
|
||||
// }
|
||||
// searchResults.editableList('addItem', suggestionItem);
|
||||
// setTimeout(function() {
|
||||
// searchResults.editableList('removeItem', suggestionItem);
|
||||
|
||||
// const suggestedItem = {
|
||||
// suggestion: true,
|
||||
// label: 'Change/Debug Combo',
|
||||
// separator: true,
|
||||
// i: suggestionItem.i,
|
||||
// nodes: [
|
||||
// { id: 'suggestion-1', type: 'change', x: 0, y: 0, wires:[['suggestion-2']] },
|
||||
// { id: 'suggestion-2', type: 'function', outputs: 3, x: 200, y: 0, wires:[['suggestion-3'],['suggestion-4'],['suggestion-6']] },
|
||||
// { id: 'suggestion-3', _g: 'suggestion-group-1', type: 'debug', x: 375, y: -40 },
|
||||
// { id: 'suggestion-4', _g: 'suggestion-group-1', type: 'debug', x: 375, y: 0 },
|
||||
// { id: 'suggestion-5', _g: 'suggestion-group-1', type: 'debug', x: 410, y: 40 },
|
||||
// { id: 'suggestion-6', type: 'junction', wires: [['suggestion-5']], x:325, y:40 }
|
||||
// ]
|
||||
// }
|
||||
// searchResults.editableList('addItem', suggestedItem);
|
||||
// }, 1000)
|
||||
|
||||
for(i=0;i<common.length;i++) {
|
||||
var itemDef = RED.nodes.getType(common[i]);
|
||||
let itemDef
|
||||
if (common[i] === 'junction') {
|
||||
itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'}
|
||||
} else if (/^_action_:/.test(common[i]) ) {
|
||||
itemDef = { inputs:1, outputs: 1, label: common[i], type: common[i]}
|
||||
} else {
|
||||
itemDef = RED.nodes.getType(common[i]);
|
||||
}
|
||||
if (itemDef) {
|
||||
item = {
|
||||
const item = {
|
||||
type: common[i],
|
||||
common: true,
|
||||
def: itemDef,
|
||||
@@ -414,7 +511,7 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
}
|
||||
for(i=0;i<Math.min(5,recentlyUsed.length);i++) {
|
||||
item = {
|
||||
const item = {
|
||||
type:recentlyUsed[i],
|
||||
def: RED.nodes.getType(recentlyUsed[i]),
|
||||
recent: true,
|
||||
|
||||
@@ -144,6 +144,18 @@ RED.userSettings = (function() {
|
||||
{setting:"view-node-show-label",label:"menu.label.showNodeLabelDefault",default: true, toggle:true}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "telemetry.label",
|
||||
options: [
|
||||
{
|
||||
global: true,
|
||||
setting: "telemetryEnabled",
|
||||
label: "telemetry.settingsTitle",
|
||||
description: "telemetry.settingsDescription",
|
||||
toggle: true
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "menu.label.other",
|
||||
options: [
|
||||
@@ -170,13 +182,20 @@ RED.userSettings = (function() {
|
||||
var initialState;
|
||||
if (opt.local) {
|
||||
initialState = localStorage.getItem(opt.setting);
|
||||
} else if (opt.global) {
|
||||
initialState = RED.settings.get(opt.setting);
|
||||
} else {
|
||||
initialState = currentEditorSettings.view[opt.setting];
|
||||
}
|
||||
var row = $('<div class="red-ui-settings-row"></div>').appendTo(pane);
|
||||
var input;
|
||||
if (opt.toggle) {
|
||||
input = $('<label for="user-settings-'+opt.setting+'"><input id="user-settings-'+opt.setting+'" type="checkbox"> '+RED._(opt.label)+'</label>').appendTo(row).find("input");
|
||||
let label = RED._(opt.label)
|
||||
if (opt.description) {
|
||||
label = `<p>${label}</p>${RED._(opt.description)}`;
|
||||
}
|
||||
input = $('<input id="user-settings-'+opt.setting+'" type="checkbox">').appendTo(row)
|
||||
$('<label for="user-settings-'+opt.setting+'">'+label+'</label>').appendTo(row)
|
||||
input.prop('checked',initialState);
|
||||
} else if (opt.options) {
|
||||
$('<label for="user-settings-'+opt.setting+'">'+RED._(opt.label)+'</label>').appendTo(row);
|
||||
@@ -210,6 +229,8 @@ RED.userSettings = (function() {
|
||||
var opt = allSettings[id];
|
||||
if (opt.local) {
|
||||
localStorage.setItem(opt.setting,value);
|
||||
} else if (opt.global) {
|
||||
RED.settings.set(opt.setting, value)
|
||||
} else {
|
||||
var currentEditorSettings = RED.settings.get('editor') || {};
|
||||
currentEditorSettings.view = currentEditorSettings.view || {};
|
||||
@@ -238,7 +259,7 @@ RED.userSettings = (function() {
|
||||
|
||||
addPane({
|
||||
id:'view',
|
||||
title: RED._("menu.label.view.view"),
|
||||
title: RED._("menu.label.settings"),
|
||||
get: createViewPane,
|
||||
close: function() {
|
||||
viewSettings.forEach(function(section) {
|
||||
|
||||
@@ -24,7 +24,7 @@ RED.view.annotations = (function() {
|
||||
refreshAnnotation = !!evt.node[opts.refresh]
|
||||
delete evt.node[opts.refresh]
|
||||
} else if (typeof opts.refresh === "function") {
|
||||
refreshAnnotation = opts.refresh(evnt.node)
|
||||
refreshAnnotation = opts.refresh(evt.node)
|
||||
}
|
||||
if (refreshAnnotation) {
|
||||
refreshAnnotationElement(annotation.id, annotation.node, annotation.element)
|
||||
|
||||
@@ -176,8 +176,8 @@ RED.view.tools = (function() {
|
||||
}
|
||||
nodes.forEach(function(n) {
|
||||
var modified = false;
|
||||
var oldValue = n.l === undefined?true:n.l;
|
||||
var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true;
|
||||
var showLabel = n._def.hasOwnProperty("showLabel") ? n._def.showLabel : true;
|
||||
var oldValue = n.l === undefined ? showLabel : n.l;
|
||||
|
||||
if (labelShown) {
|
||||
if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) {
|
||||
|
||||
@@ -100,6 +100,11 @@ RED.view = (function() {
|
||||
var clipboard = "";
|
||||
let clipboardSource
|
||||
|
||||
let currentSuggestion = null;
|
||||
let suggestedNodes = [];
|
||||
let suggestedLinks = [];
|
||||
let suggestedJunctions = [];
|
||||
|
||||
// Note: these are the permitted status colour aliases. The actual RGB values
|
||||
// are set in the CSS - flow.scss/colors.scss
|
||||
const status_colours = {
|
||||
@@ -548,6 +553,8 @@ RED.view = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
clearSuggestedFlow();
|
||||
|
||||
RED.menu.setDisabled("menu-item-workspace-edit", activeFlowLocked || activeSubflow || event.workspace === 0);
|
||||
RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow);
|
||||
|
||||
@@ -653,7 +660,7 @@ RED.view = (function() {
|
||||
return;
|
||||
}
|
||||
var historyEvent = result.historyEvent;
|
||||
var nn = RED.nodes.add(result.node);
|
||||
var nn = RED.nodes.add(result.node, { source: 'palette' });
|
||||
|
||||
var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
|
||||
if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
|
||||
@@ -1395,6 +1402,20 @@ RED.view = (function() {
|
||||
var lastAddedX;
|
||||
var lastAddedWidth;
|
||||
|
||||
const context = {}
|
||||
|
||||
if (quickAddLink) {
|
||||
context.source = quickAddLink.node.id;
|
||||
context.sourcePort = quickAddLink.port;
|
||||
context.sourcePortType = quickAddLink.portType;
|
||||
if (quickAddLink?.virtualLink) {
|
||||
context.virtualLink = true;
|
||||
}
|
||||
context.flow = RED.nodes.createExportableNodeSet(RED.nodes.getAllFlowNodes(quickAddLink.node))
|
||||
}
|
||||
|
||||
// console.log(context)
|
||||
|
||||
RED.typeSearch.show({
|
||||
x:clientX-mainPos.left-node_width/2 - (ox-point[0]),
|
||||
y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]),
|
||||
@@ -1430,7 +1451,63 @@ RED.view = (function() {
|
||||
keepAdding = false;
|
||||
resetMouseVars();
|
||||
}
|
||||
if (typeof type !== 'string') {
|
||||
if (type.nodes) {
|
||||
|
||||
// Importing a flow definition
|
||||
// console.log('Importing flow definition', type.nodes)
|
||||
const importResult = importNodes(type.nodes, {
|
||||
generateIds: true,
|
||||
touchImport: true,
|
||||
notify: false,
|
||||
// Ensure the node gets all of its defaults applied
|
||||
applyNodeDefaults: true
|
||||
})
|
||||
quickAddActive = false;
|
||||
ghostNode.remove();
|
||||
|
||||
if (quickAddLink) {
|
||||
// Need to attach the link to the suggestion. This is assumed to be the first
|
||||
// node in the array - as that's the one we've focussed on.
|
||||
const targetNode = importResult.nodeMap[type.nodes[0].id]
|
||||
|
||||
const drag_line = quickAddLink;
|
||||
let src = null, dst, src_port;
|
||||
if (drag_line.portType === PORT_TYPE_OUTPUT && (targetNode.inputs > 0 || drag_line.virtualLink) ) {
|
||||
src = drag_line.node;
|
||||
src_port = drag_line.port;
|
||||
dst = targetNode;
|
||||
} else if (drag_line.portType === PORT_TYPE_INPUT && (targetNode.outputs > 0 || drag_line.virtualLink)) {
|
||||
src = targetNode;
|
||||
dst = drag_line.node;
|
||||
src_port = 0;
|
||||
}
|
||||
if (src && dst) {
|
||||
var link = {source: src, sourcePort:src_port, target: dst};
|
||||
RED.nodes.addLink(link);
|
||||
const historyEvent = RED.history.peek()
|
||||
if (historyEvent.t === 'add') {
|
||||
historyEvent.links = historyEvent.links || []
|
||||
historyEvent.links.push(link)
|
||||
} else {
|
||||
// TODO: importNodes *can* generate a multi history event
|
||||
// but we don't currently support that
|
||||
}
|
||||
}
|
||||
if (quickAddLink.el) {
|
||||
quickAddLink.el.remove();
|
||||
}
|
||||
quickAddLink = null;
|
||||
}
|
||||
updateActiveNodes();
|
||||
updateSelection();
|
||||
redraw();
|
||||
|
||||
return
|
||||
} else {
|
||||
type = type.type
|
||||
}
|
||||
}
|
||||
var nn;
|
||||
var historyEvent;
|
||||
if (/^_action_:/.test(type)) {
|
||||
@@ -1479,7 +1556,7 @@ RED.view = (function() {
|
||||
if (nn.type === 'junction') {
|
||||
nn = RED.nodes.addJunction(nn);
|
||||
} else {
|
||||
nn = RED.nodes.add(nn);
|
||||
nn = RED.nodes.add(nn, { source: 'typeSearch' });
|
||||
}
|
||||
if (quickAddLink) {
|
||||
var drag_line = quickAddLink;
|
||||
@@ -1662,6 +1739,22 @@ RED.view = (function() {
|
||||
quickAddActive = false;
|
||||
ghostNode.remove();
|
||||
}
|
||||
},
|
||||
suggest: function (suggestion) {
|
||||
if (suggestion?.nodes?.length > 0) {
|
||||
// Reposition the suggestion relative to the existing ghost node position
|
||||
const deltaX = suggestion.nodes[0].x - point[0]
|
||||
const deltaY = suggestion.nodes[0].y - point[1]
|
||||
suggestion.nodes.forEach(node => {
|
||||
if (Object.hasOwn(node, 'x')) {
|
||||
node.x = node.x - deltaX
|
||||
}
|
||||
if (Object.hasOwn(node, 'y')) {
|
||||
node.y = node.y - deltaY
|
||||
}
|
||||
})
|
||||
}
|
||||
setSuggestedFlow(suggestion);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4576,20 +4669,28 @@ RED.view = (function() {
|
||||
nodeLayer.selectAll(".red-ui-flow-subflow-port-input").remove();
|
||||
nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove();
|
||||
}
|
||||
|
||||
var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(activeNodes,function(d){return d.id});
|
||||
let nodesToDraw = activeNodes;
|
||||
if (suggestedNodes.length > 0) {
|
||||
nodesToDraw = [...activeNodes, ...suggestedNodes]
|
||||
}
|
||||
var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(nodesToDraw,function(d){return d.id});
|
||||
node.exit().each(function(d,i) {
|
||||
RED.hooks.trigger("viewRemoveNode",{node:d,el:this})
|
||||
if (!d.__ghost) {
|
||||
RED.hooks.trigger("viewRemoveNode",{node:d,el:this})
|
||||
}
|
||||
}).remove();
|
||||
|
||||
var nodeEnter = node.enter().insert("svg:g")
|
||||
.attr("class", "red-ui-flow-node red-ui-flow-node-group")
|
||||
.classed("red-ui-flow-subflow", activeSubflow != null);
|
||||
.classed("red-ui-flow-subflow", activeSubflow != null)
|
||||
|
||||
nodeEnter.each(function(d,i) {
|
||||
this.__outputs__ = [];
|
||||
this.__inputs__ = [];
|
||||
var node = d3.select(this);
|
||||
if (d.__ghost) {
|
||||
node.classed("red-ui-flow-node-ghost",true);
|
||||
}
|
||||
var nodeContents = document.createDocumentFragment();
|
||||
var isLink = (d.type === "link in" || d.type === "link out")
|
||||
var hideLabel = d.hasOwnProperty('l')?!d.l : isLink;
|
||||
@@ -4624,19 +4725,21 @@ RED.view = (function() {
|
||||
bgButton.setAttribute("width",16);
|
||||
bgButton.setAttribute("height",node_height-12);
|
||||
bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
|
||||
d3.select(bgButton)
|
||||
.on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
|
||||
.on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
|
||||
.on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
|
||||
.on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
|
||||
var op = 1;
|
||||
if (d._def.button.toggle) {
|
||||
op = d[d._def.button.toggle]?1:0.2;
|
||||
}
|
||||
d3.select(this).attr("fill-opacity",op);
|
||||
}})
|
||||
.on("click",nodeButtonClicked)
|
||||
.on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
|
||||
if (!d.__ghost) {
|
||||
d3.select(bgButton)
|
||||
.on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
|
||||
.on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
|
||||
.on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
|
||||
.on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
|
||||
var op = 1;
|
||||
if (d._def.button.toggle) {
|
||||
op = d[d._def.button.toggle]?1:0.2;
|
||||
}
|
||||
d3.select(this).attr("fill-opacity",op);
|
||||
}})
|
||||
.on("click",nodeButtonClicked)
|
||||
.on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
|
||||
}
|
||||
buttonGroup.appendChild(bgButton);
|
||||
node[0][0].__buttonGroupButton__ = bgButton;
|
||||
|
||||
@@ -4651,13 +4754,15 @@ RED.view = (function() {
|
||||
mainRect.setAttribute("ry", 5);
|
||||
mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
|
||||
node[0][0].__mainRect__ = mainRect;
|
||||
d3.select(mainRect)
|
||||
.on("mouseup",nodeMouseUp)
|
||||
.on("mousedown",nodeMouseDown)
|
||||
.on("touchstart",nodeTouchStart)
|
||||
.on("touchend",nodeTouchEnd)
|
||||
.on("mouseover",nodeMouseOver)
|
||||
.on("mouseout",nodeMouseOut);
|
||||
if (!d.__ghost) {
|
||||
d3.select(mainRect)
|
||||
.on("mouseup",nodeMouseUp)
|
||||
.on("mousedown",nodeMouseDown)
|
||||
.on("touchstart",nodeTouchStart)
|
||||
.on("touchend",nodeTouchEnd)
|
||||
.on("mouseover",nodeMouseOver)
|
||||
.on("mouseout",nodeMouseOut);
|
||||
}
|
||||
nodeContents.appendChild(mainRect);
|
||||
//node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none");
|
||||
//node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none");
|
||||
@@ -4739,7 +4844,10 @@ RED.view = (function() {
|
||||
|
||||
node[0][0].appendChild(nodeContents);
|
||||
|
||||
RED.hooks.trigger("viewAddNode",{node:d,el:this})
|
||||
if (!d.__ghost) {
|
||||
// Do not trigger hooks for ghost nodes
|
||||
RED.hooks.trigger("viewAddNode",{node:d,el:this})
|
||||
}
|
||||
});
|
||||
|
||||
var nodesReordered = false;
|
||||
@@ -4862,13 +4970,15 @@ RED.view = (function() {
|
||||
var inputPorts = thisNode.selectAll(".red-ui-flow-port-input");
|
||||
if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) {
|
||||
inputPorts.each(function(d,i) {
|
||||
RED.hooks.trigger("viewRemovePort",{
|
||||
node:d,
|
||||
el:self,
|
||||
port:d3.select(this)[0][0],
|
||||
portType: "input",
|
||||
portIndex: 0
|
||||
})
|
||||
if (!d.__ghost) {
|
||||
RED.hooks.trigger("viewRemovePort",{
|
||||
node:d,
|
||||
el:self,
|
||||
port:d3.select(this)[0][0],
|
||||
portType: "input",
|
||||
portIndex: 0
|
||||
})
|
||||
}
|
||||
}).remove();
|
||||
} else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) {
|
||||
var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input");
|
||||
@@ -4886,13 +4996,15 @@ RED.view = (function() {
|
||||
inputGroupPorts[0][0].__data__ = this.__data__;
|
||||
inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT;
|
||||
inputGroupPorts[0][0].__portIndex__ = 0;
|
||||
inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
|
||||
.on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
|
||||
.on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
|
||||
.on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
|
||||
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
|
||||
.on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
|
||||
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
|
||||
if (!d.__ghost) {
|
||||
inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
|
||||
.on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
|
||||
.on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
|
||||
.on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
|
||||
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
|
||||
.on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
|
||||
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
|
||||
}
|
||||
}
|
||||
var numOutputs = d.outputs;
|
||||
if (isLink && d.type === "link out") {
|
||||
@@ -4907,13 +5019,15 @@ RED.view = (function() {
|
||||
// Remove extra ports
|
||||
while (this.__outputs__.length > numOutputs) {
|
||||
var port = this.__outputs__.pop();
|
||||
RED.hooks.trigger("viewRemovePort",{
|
||||
node:d,
|
||||
el:this,
|
||||
port:port,
|
||||
portType: "output",
|
||||
portIndex: this.__outputs__.length
|
||||
})
|
||||
if (!d.__ghost) {
|
||||
RED.hooks.trigger("viewRemovePort",{
|
||||
node:d,
|
||||
el:this,
|
||||
port:port,
|
||||
portType: "output",
|
||||
portIndex: this.__outputs__.length
|
||||
})
|
||||
}
|
||||
port.remove();
|
||||
}
|
||||
for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) {
|
||||
@@ -4941,16 +5055,20 @@ RED.view = (function() {
|
||||
portPort.__data__ = this.__data__;
|
||||
portPort.__portType__ = PORT_TYPE_OUTPUT;
|
||||
portPort.__portIndex__ = portIndex;
|
||||
portPort.addEventListener("mousedown", portMouseDownProxy);
|
||||
portPort.addEventListener("touchstart", portTouchStartProxy);
|
||||
portPort.addEventListener("mouseup", portMouseUpProxy);
|
||||
portPort.addEventListener("touchend", portTouchEndProxy);
|
||||
portPort.addEventListener("mouseover", portMouseOverProxy);
|
||||
portPort.addEventListener("mouseout", portMouseOutProxy);
|
||||
if (!d.__ghost) {
|
||||
portPort.addEventListener("mousedown", portMouseDownProxy);
|
||||
portPort.addEventListener("touchstart", portTouchStartProxy);
|
||||
portPort.addEventListener("mouseup", portMouseUpProxy);
|
||||
portPort.addEventListener("touchend", portTouchEndProxy);
|
||||
portPort.addEventListener("mouseover", portMouseOverProxy);
|
||||
portPort.addEventListener("mouseout", portMouseOutProxy);
|
||||
}
|
||||
|
||||
this.appendChild(portGroup);
|
||||
this.__outputs__.push(portGroup);
|
||||
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
|
||||
if (!d.__ghost) {
|
||||
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
|
||||
}
|
||||
} else {
|
||||
portGroup = this.__outputs__[portIndex];
|
||||
}
|
||||
@@ -5067,8 +5185,10 @@ RED.view = (function() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
|
||||
if (!d.__ghost) {
|
||||
// Only trigger redraw hooks for non-ghost nodes
|
||||
RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
|
||||
}
|
||||
});
|
||||
|
||||
if (nodesReordered) {
|
||||
@@ -5077,13 +5197,20 @@ RED.view = (function() {
|
||||
})
|
||||
}
|
||||
|
||||
let junctionsToDraw = activeJunctions;
|
||||
if (suggestedJunctions.length > 0) {
|
||||
junctionsToDraw = [...activeJunctions, ...suggestedJunctions]
|
||||
}
|
||||
var junction = junctionLayer.selectAll(".red-ui-flow-junction").data(
|
||||
activeJunctions,
|
||||
junctionsToDraw,
|
||||
d => d.id
|
||||
)
|
||||
var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction")
|
||||
junctionEnter.each(function(d,i) {
|
||||
var junction = d3.select(this);
|
||||
if (d.__ghost) {
|
||||
junction.classed("red-ui-flow-junction-ghost",true);
|
||||
}
|
||||
var contents = document.createDocumentFragment();
|
||||
// d.added = true;
|
||||
var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect");
|
||||
@@ -5177,8 +5304,12 @@ RED.view = (function() {
|
||||
|
||||
})
|
||||
|
||||
let linksToDraw = activeLinks
|
||||
if (suggestedLinks.length > 0) {
|
||||
linksToDraw = [...activeLinks, ...suggestedLinks]
|
||||
}
|
||||
var link = linkLayer.selectAll(".red-ui-flow-link").data(
|
||||
activeLinks,
|
||||
linksToDraw,
|
||||
function(d) {
|
||||
return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i;
|
||||
}
|
||||
@@ -5189,44 +5320,50 @@ RED.view = (function() {
|
||||
var l = d3.select(this);
|
||||
var pathContents = document.createDocumentFragment();
|
||||
|
||||
if (d.__ghost) {
|
||||
l.classed("red-ui-flow-link-ghost",true);
|
||||
}
|
||||
|
||||
d.added = true;
|
||||
var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path");
|
||||
pathBack.__data__ = d;
|
||||
pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":""));
|
||||
this.__pathBack__ = pathBack;
|
||||
pathContents.appendChild(pathBack);
|
||||
d3.select(pathBack)
|
||||
.on("mousedown",linkMouseDown)
|
||||
.on("touchstart",linkTouchStart)
|
||||
.on("mousemove", function(d) {
|
||||
if (mouse_mode === RED.state.SLICING) {
|
||||
if (!d.__ghost) {
|
||||
d3.select(pathBack)
|
||||
.on("mousedown",linkMouseDown)
|
||||
.on("touchstart",linkTouchStart)
|
||||
.on("mousemove", function(d) {
|
||||
if (mouse_mode === RED.state.SLICING) {
|
||||
|
||||
selectedLinks.add(d)
|
||||
l.classed("red-ui-flow-link-splice",true)
|
||||
redraw()
|
||||
} else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) {
|
||||
if (!l.classed("red-ui-flow-link-splice")) {
|
||||
// Find intersection point
|
||||
var lineLength = pathLine.getTotalLength();
|
||||
var pos;
|
||||
var delta = Infinity;
|
||||
for (var i = 0; i < lineLength; i++) {
|
||||
var linePos = pathLine.getPointAtLength(i);
|
||||
var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
|
||||
var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
|
||||
var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
|
||||
if (posDelta < delta) {
|
||||
pos = linePos
|
||||
delta = posDelta
|
||||
}
|
||||
}
|
||||
d._sliceLocation = pos
|
||||
selectedLinks.add(d)
|
||||
l.classed("red-ui-flow-link-splice",true)
|
||||
redraw()
|
||||
} else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) {
|
||||
if (!l.classed("red-ui-flow-link-splice")) {
|
||||
// Find intersection point
|
||||
var lineLength = pathLine.getTotalLength();
|
||||
var pos;
|
||||
var delta = Infinity;
|
||||
for (var i = 0; i < lineLength; i++) {
|
||||
var linePos = pathLine.getPointAtLength(i);
|
||||
var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
|
||||
var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
|
||||
var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
|
||||
if (posDelta < delta) {
|
||||
pos = linePos
|
||||
delta = posDelta
|
||||
}
|
||||
}
|
||||
d._sliceLocation = pos
|
||||
selectedLinks.add(d)
|
||||
l.classed("red-ui-flow-link-splice",true)
|
||||
redraw()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path");
|
||||
pathOutline.__data__ = d;
|
||||
@@ -5688,16 +5825,21 @@ RED.view = (function() {
|
||||
* - generateIds - whether to automatically generate new ids for all imported nodes
|
||||
* - generateDefaultNames - whether to automatically update any nodes with clashing
|
||||
* default names
|
||||
* - notify - whether to show a notification if the import was successful
|
||||
*/
|
||||
function importNodes(newNodesObj,options) {
|
||||
options = options || {
|
||||
addFlow: false,
|
||||
touchImport: false,
|
||||
generateIds: false,
|
||||
generateDefaultNames: false
|
||||
generateDefaultNames: false,
|
||||
notify: true,
|
||||
applyNodeDefaults: false
|
||||
}
|
||||
var addNewFlow = options.addFlow
|
||||
var touchImport = options.touchImport;
|
||||
const addNewFlow = options.addFlow
|
||||
const touchImport = options.touchImport;
|
||||
const showNotification = options.notify ?? true
|
||||
const applyNodeDefaults = options.applyNodeDefaults ?? false
|
||||
|
||||
if (mouse_mode === RED.state.SELECTING_NODE) {
|
||||
return;
|
||||
@@ -5781,7 +5923,8 @@ RED.view = (function() {
|
||||
addFlow: addNewFlow,
|
||||
importMap: options.importMap,
|
||||
markChanged: true,
|
||||
modules: modules
|
||||
modules: modules,
|
||||
applyNodeDefaults: applyNodeDefaults
|
||||
});
|
||||
if (importResult) {
|
||||
var new_nodes = importResult.nodes;
|
||||
@@ -5792,6 +5935,7 @@ RED.view = (function() {
|
||||
var new_subflows = importResult.subflows;
|
||||
var removedNodes = importResult.removedNodes;
|
||||
var new_default_workspace = importResult.missingWorkspace;
|
||||
const nodeMap = importResult.nodeMap;
|
||||
if (addNewFlow && new_default_workspace) {
|
||||
RED.workspaces.show(new_default_workspace.id);
|
||||
}
|
||||
@@ -5813,16 +5957,18 @@ RED.view = (function() {
|
||||
|
||||
var dx = mouse_position[0];
|
||||
var dy = mouse_position[1];
|
||||
if (movingSet.length() > 0) {
|
||||
var root_node = movingSet.get(0).n;
|
||||
dx = root_node.x;
|
||||
dy = root_node.y;
|
||||
if (!touchImport) {
|
||||
if (movingSet.length() > 0) {
|
||||
const root_node = movingSet.get(0).n;
|
||||
dx = root_node.x;
|
||||
dy = root_node.y;
|
||||
}
|
||||
}
|
||||
|
||||
var minX = 0;
|
||||
var minY = 0;
|
||||
var i;
|
||||
var node,group;
|
||||
var node;
|
||||
var l =movingSet.length();
|
||||
for (i=0;i<l;i++) {
|
||||
node = movingSet.get(i);
|
||||
@@ -5953,42 +6099,48 @@ RED.view = (function() {
|
||||
|
||||
updateActiveNodes();
|
||||
redraw();
|
||||
|
||||
var counts = [];
|
||||
var newNodeCount = 0;
|
||||
var newConfigNodeCount = 0;
|
||||
new_nodes.forEach(function(n) {
|
||||
if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
|
||||
newNodeCount++;
|
||||
} else {
|
||||
newConfigNodeCount++;
|
||||
if (showNotification) {
|
||||
var counts = [];
|
||||
var newNodeCount = 0;
|
||||
var newConfigNodeCount = 0;
|
||||
new_nodes.forEach(function(n) {
|
||||
if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
|
||||
newNodeCount++;
|
||||
} else {
|
||||
newConfigNodeCount++;
|
||||
}
|
||||
})
|
||||
var newGroupCount = new_groups.length;
|
||||
var newJunctionCount = new_junctions.length;
|
||||
if (new_workspaces.length > 0) {
|
||||
counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
|
||||
}
|
||||
if (newNodeCount > 0) {
|
||||
counts.push(RED._("clipboard.node",{count:newNodeCount}));
|
||||
}
|
||||
if (newGroupCount > 0) {
|
||||
counts.push(RED._("clipboard.group",{count:newGroupCount}));
|
||||
}
|
||||
if (newConfigNodeCount > 0) {
|
||||
counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
|
||||
}
|
||||
if (new_subflows.length > 0) {
|
||||
counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
|
||||
}
|
||||
if (removedNodes && removedNodes.length > 0) {
|
||||
counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
|
||||
}
|
||||
if (counts.length > 0) {
|
||||
var countList = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
|
||||
RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+countList,{id:"clipboard"});
|
||||
}
|
||||
})
|
||||
var newGroupCount = new_groups.length;
|
||||
var newJunctionCount = new_junctions.length;
|
||||
if (new_workspaces.length > 0) {
|
||||
counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
|
||||
}
|
||||
if (newNodeCount > 0) {
|
||||
counts.push(RED._("clipboard.node",{count:newNodeCount}));
|
||||
return {
|
||||
nodeMap
|
||||
}
|
||||
if (newGroupCount > 0) {
|
||||
counts.push(RED._("clipboard.group",{count:newGroupCount}));
|
||||
}
|
||||
if (newConfigNodeCount > 0) {
|
||||
counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
|
||||
}
|
||||
if (new_subflows.length > 0) {
|
||||
counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
|
||||
}
|
||||
if (removedNodes && removedNodes.length > 0) {
|
||||
counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
|
||||
}
|
||||
if (counts.length > 0) {
|
||||
var countList = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
|
||||
RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+countList,{id:"clipboard"});
|
||||
}
|
||||
|
||||
}
|
||||
return {
|
||||
nodeMap: {}
|
||||
}
|
||||
} catch(error) {
|
||||
if (error.code === "import_conflict") {
|
||||
@@ -6307,6 +6459,157 @@ RED.view = (function() {
|
||||
node.highlighted = true;
|
||||
RED.view.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a suggested flow to the workspace.
|
||||
*
|
||||
* This appears as a ghost set of nodes.
|
||||
*
|
||||
* {
|
||||
* "nodes": [
|
||||
* {
|
||||
* type: "node-type",
|
||||
* x: 0,
|
||||
* y: 0,
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* If `nodes` is a single node without an id property, it will be generated
|
||||
* using its default properties.
|
||||
*
|
||||
* If `nodes` has multiple, they must all have ids and will be assumed to be 'importable'.
|
||||
* In other words, a piece of valid flow json.
|
||||
*
|
||||
* Limitations:
|
||||
* - does not support groups, subflows or whole tabs
|
||||
* - does not support config nodes
|
||||
*
|
||||
* To clear the current suggestion, pass in `null`.
|
||||
*
|
||||
*
|
||||
* @param {Object} suggestion - The suggestion object
|
||||
*/
|
||||
function setSuggestedFlow (suggestion) {
|
||||
if (!currentSuggestion && !suggestion) {
|
||||
// Avoid unnecessary redraws
|
||||
return
|
||||
}
|
||||
// Clear up any existing suggestion state
|
||||
clearSuggestedFlow()
|
||||
currentSuggestion = suggestion
|
||||
if (suggestion?.nodes?.length > 0) {
|
||||
const nodeMap = {}
|
||||
const links = []
|
||||
suggestion.nodes.forEach(nodeConfig => {
|
||||
if (!nodeConfig.type || nodeConfig.type === 'group' || nodeConfig.type === 'subflow' || nodeConfig.type === 'tab') {
|
||||
// A node type we don't support previewing
|
||||
return
|
||||
}
|
||||
|
||||
let node
|
||||
|
||||
if (nodeConfig.type === 'junction') {
|
||||
node = {
|
||||
_def: {defaults:{}},
|
||||
type: 'junction',
|
||||
z: RED.workspaces.active(),
|
||||
id: RED.nodes.id(),
|
||||
x: nodeConfig.x,
|
||||
y: nodeConfig.y,
|
||||
w: 0, h: 0,
|
||||
outputs: 1,
|
||||
inputs: 1,
|
||||
dirty: true,
|
||||
moved: true
|
||||
}
|
||||
} else {
|
||||
const def = RED.nodes.getType(nodeConfig.type)
|
||||
if (!def || def.category === 'config') {
|
||||
// Unknown node or config node
|
||||
// TODO: unknown node types could happen...
|
||||
return
|
||||
}
|
||||
const result = createNode(nodeConfig.type, nodeConfig.x, nodeConfig.y)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
node = result.node
|
||||
node["_"] = node._def._;
|
||||
|
||||
for (let d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'name') {
|
||||
if (nodeConfig[d] !== undefined) {
|
||||
node[d] = nodeConfig[d]
|
||||
} else if (node._def.defaults[d].value) {
|
||||
node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value))
|
||||
}
|
||||
}
|
||||
}
|
||||
suggestedNodes.push(node)
|
||||
}
|
||||
if (node) {
|
||||
node.id = nodeConfig.id || node.id
|
||||
node.__ghost = true;
|
||||
node.dirty = true;
|
||||
nodeMap[node.id] = node
|
||||
|
||||
if (nodeConfig.wires) {
|
||||
nodeConfig.wires.forEach((wire, i) => {
|
||||
if (wire.length > 0) {
|
||||
wire.forEach(targetId => {
|
||||
links.push({
|
||||
sourceId: nodeConfig.id || node.id,
|
||||
sourcePort: i,
|
||||
targetId: targetId,
|
||||
targetPort: 0,
|
||||
__ghost: true
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
links.forEach(link => {
|
||||
const sourceNode = nodeMap[link.sourceId]
|
||||
const targetNode = nodeMap[link.targetId]
|
||||
if (sourceNode && targetNode) {
|
||||
link.source = sourceNode
|
||||
link.target = targetNode
|
||||
suggestedLinks.push(link)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (ghostNode) {
|
||||
if (suggestedNodes.length > 0) {
|
||||
ghostNode.style('opacity', 0)
|
||||
} else {
|
||||
ghostNode.style('opacity', 1)
|
||||
}
|
||||
}
|
||||
redraw();
|
||||
}
|
||||
|
||||
function clearSuggestedFlow () {
|
||||
currentSuggestion = null
|
||||
suggestedNodes = []
|
||||
suggestedLinks = []
|
||||
}
|
||||
|
||||
function applySuggestedFlow () {
|
||||
if (currentSuggestion && currentSuggestion.nodes) {
|
||||
const nodesToImport = currentSuggestion.nodes
|
||||
setSuggestedFlow(null)
|
||||
return importNodes(nodesToImport, {
|
||||
generateIds: true,
|
||||
touchImport: true,
|
||||
notify: false,
|
||||
// Ensure the node gets all of its defaults applied
|
||||
applyNodeDefaults: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
state:function(state) {
|
||||
@@ -6567,6 +6870,8 @@ RED.view = (function() {
|
||||
width: space_width,
|
||||
height: space_height
|
||||
};
|
||||
}
|
||||
},
|
||||
setSuggestedFlow,
|
||||
applySuggestedFlow
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -208,12 +208,10 @@ body {
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto\9;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
border: 0;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
|
||||
@@ -161,7 +161,15 @@ svg:not(.red-ui-workspace-lasso-active) {
|
||||
fill: var(--red-ui-group-default-label-color);
|
||||
}
|
||||
|
||||
.red-ui-flow-node-ghost {
|
||||
opacity: 0.6;
|
||||
rect.red-ui-flow-node {
|
||||
stroke: var(--red-ui-node-border-placeholder);
|
||||
stroke-dasharray:10,4;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.red-ui-flow-node-unknown {
|
||||
stroke-dasharray:10,4;
|
||||
@@ -401,6 +409,13 @@ g.red-ui-flow-node-selected {
|
||||
g.red-ui-flow-link-selected path.red-ui-flow-link-line {
|
||||
stroke: var(--red-ui-node-selected-color);
|
||||
}
|
||||
|
||||
g.red-ui-flow-link-ghost path.red-ui-flow-link-line {
|
||||
stroke: var(--red-ui-node-border-placeholder);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 10, 4;
|
||||
}
|
||||
|
||||
g.red-ui-flow-link-unknown path.red-ui-flow-link-line {
|
||||
stroke: var(--red-ui-link-unknown-color);
|
||||
stroke-width: 2;
|
||||
|
||||
@@ -216,14 +216,11 @@
|
||||
.uneditable-input:focus {
|
||||
border-color: var(--red-ui-form-input-focus-color);
|
||||
outline: 0;
|
||||
outline: thin dotted \9;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
margin: 4px 0 0;
|
||||
margin-top: 1px \9;
|
||||
*margin-top: 0;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
@@ -285,12 +282,6 @@
|
||||
color: var(--red-ui-form-placeholder-color);
|
||||
}
|
||||
|
||||
input:-ms-input-placeholder,
|
||||
div[contenteditable="true"]:-ms-input-placeholder,
|
||||
textarea:-ms-input-placeholder {
|
||||
color: var(--red-ui-form-placeholder-color);
|
||||
}
|
||||
|
||||
input::-webkit-input-placeholder,
|
||||
div[contenteditable="true"]::-webkit-input-placeholder,
|
||||
textarea::-webkit-input-placeholder {
|
||||
@@ -568,11 +559,7 @@
|
||||
|
||||
input.search-query {
|
||||
padding-right: 14px;
|
||||
padding-right: 4px \9;
|
||||
padding-left: 14px;
|
||||
padding-left: 4px \9;
|
||||
/* IE7-8 doesn't have border-radius, so don't indent the padding */
|
||||
|
||||
margin-bottom: 0;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -26,7 +25,6 @@
|
||||
-webkit-user-select: auto;
|
||||
-khtml-user-select: auto;
|
||||
-moz-user-select: auto;
|
||||
-ms-user-select: auto;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,15 +126,20 @@
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.red-ui-palette-module-deprecated {
|
||||
cursor: pointer;
|
||||
color: var(--red-ui-text-color-error);
|
||||
font-size: 0.7em;
|
||||
border: 1px solid var(--red-ui-text-color-error);
|
||||
border-radius: 30px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.red-ui-palette-module-description {
|
||||
margin-left: 20px;
|
||||
font-size: 0.9em;
|
||||
color: var(--red-ui-secondary-text-color);
|
||||
}
|
||||
.red-ui-palette-module-link {
|
||||
}
|
||||
.red-ui-palette-module-set-button-group {
|
||||
}
|
||||
.red-ui-palette-module-content {
|
||||
display: none;
|
||||
padding: 10px 3px;
|
||||
|
||||
@@ -205,3 +205,39 @@
|
||||
background: var(--red-ui-secondary-background);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
|
||||
.red-ui-popover.red-ui-dialog {
|
||||
z-index: 2003;
|
||||
--red-ui-popover-background: var(--red-ui-secondary-background);
|
||||
--red-ui-popover-border: var(--red-ui-tourGuide-border);
|
||||
--red-ui-popover-color: var(--red-ui-primary-text-color);
|
||||
|
||||
.red-ui-popover-content {
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-top: 0px;
|
||||
line-height: 1.2em;
|
||||
color: var(--red-ui-tourGuide-heading-color);
|
||||
i.fa {
|
||||
font-size: 1.5em
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.red-ui-dialog-toolbar {
|
||||
min-height: 36px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
.red-ui-dialog-body {
|
||||
padding: 20px 40px 10px;
|
||||
a {
|
||||
color: var(--red-ui-text-color-link) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +70,14 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
.red-ui-settings-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items:flex-start;
|
||||
padding: 5px 10px 2px;
|
||||
}
|
||||
.red-ui-settings-row input[type="checkbox"] {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.red-ui-settings-section {
|
||||
position: relative;
|
||||
&:after {
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
231
packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
export default {
|
||||
version: "4.0.0",
|
||||
steps: [
|
||||
{
|
||||
titleIcon: "fa fa-map-o",
|
||||
title: {
|
||||
"en-US": "Welcome to Node-RED 4.0!",
|
||||
"ja": "Node-RED 4.0 へようこそ!",
|
||||
"fr": "Bienvenue dans Node-RED 4.0!"
|
||||
},
|
||||
description: {
|
||||
"en-US": "<p>Let's take a moment to discover the new features in this release.</p>",
|
||||
"ja": "<p>本リリースの新機能を見つけてみましょう。</p>",
|
||||
"fr": "<p>Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Multiplayer Mode",
|
||||
"ja": "複数ユーザ同時利用モード",
|
||||
"fr": "Mode Multi-utilisateur"
|
||||
},
|
||||
image: 'images/nr4-multiplayer-location.png',
|
||||
description: {
|
||||
"en-US": `<p>This release includes the first small steps towards making Node-RED easier
|
||||
to work with when you have multiple people editing flows at the same time.</p>
|
||||
<p>When this feature is enabled, you will now see who else has the editor open and some
|
||||
basic information on where they are in the editor.</p>
|
||||
<p>Check the release post for details on how to enable this feature in your settings file.</p>`,
|
||||
"ja": `<p>本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。</p>
|
||||
<p>本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。</p>
|
||||
<p>設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。</p>`,
|
||||
"fr": `<p>Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser
|
||||
lorsque plusieurs personnes modifient des flux en même temps.</p>
|
||||
<p>Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont
|
||||
ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.</p>
|
||||
<p>Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité
|
||||
dans votre fichier de paramètres.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Better background deploy handling",
|
||||
"ja": "バックグラウンドのデプロイ処理の改善",
|
||||
"fr": "Meilleure gestion du déploiement en arrière-plan"
|
||||
},
|
||||
image: 'images/nr4-background-deploy.png',
|
||||
description: {
|
||||
"en-US": `<p>If another user deploys changes whilst you are editing, we now use a more discrete notification
|
||||
that doesn't stop you continuing your work - especially if they are being very productive and deploying lots
|
||||
of changes.</p>`,
|
||||
"ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`,
|
||||
"fr": `<p>Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez
|
||||
une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Improved flow diffs",
|
||||
"ja": "フローの差分表示の改善",
|
||||
"fr": "Amélioration des différences de flux"
|
||||
},
|
||||
image: 'images/nr4-diff-update.png',
|
||||
description: {
|
||||
"en-US": `<p>When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration
|
||||
changes and those that have only been moved.<p>
|
||||
<p>When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.</p>`,
|
||||
"ja": `<p>フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。<p>
|
||||
<p>これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。</p>`,
|
||||
"fr": `<p>Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les
|
||||
noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.<p>
|
||||
<p>Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les
|
||||
plus importants.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Better Configuration Node UX",
|
||||
"ja": "設定ノードのUXが向上",
|
||||
"fr": "Meilleure expérience utilisateur du noeud de configuration"
|
||||
},
|
||||
image: 'images/nr4-config-select.png',
|
||||
description: {
|
||||
"en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button
|
||||
next to the select box.</p>
|
||||
<p>It's a small change, but should make it easier to work with your config nodes.</p>`,
|
||||
"ja": `<p>設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。</p>
|
||||
<p>微修正ですが設定ノードの操作が容易になります。</p>`,
|
||||
"fr": `<p>L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite
|
||||
mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.</p>
|
||||
<p>C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Timestamp formatting options",
|
||||
"ja": "タイムスタンプの形式の項目",
|
||||
"fr": "Options de formatage de l'horodatage"
|
||||
},
|
||||
image: 'images/nr4-timestamp-formatting.png',
|
||||
description: {
|
||||
"en-US": `<p>Nodes that let you set a timestamp now have options on what format that timestamp should be in.</p>
|
||||
<p>We're keeping it simple to begin with by providing three options:<p>
|
||||
<ul>
|
||||
<li>Milliseconds since epoch - this is existing behaviour of the timestamp option</li>
|
||||
<li>ISO 8601 - a common format used by many systems</li>
|
||||
<li>JavaScript Date Object</li>
|
||||
</ul>`,
|
||||
"ja": `<p>タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。</p>
|
||||
<p>次の3つの項目を追加したことで、簡単に選択できるようになりました:<p>
|
||||
<ul>
|
||||
<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": "フロー/グローバル、環境変数の型の自動補完",
|
||||
"fr": "Saisie automatique des types de flux/global et env"
|
||||
},
|
||||
image: 'images/nr4-auto-complete.png',
|
||||
description: {
|
||||
"en-US": `<p>The <code>flow</code>/<code>global</code> context inputs and the <code>env</code> input
|
||||
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": "サブフローでの設定ノードのカスタマイズ",
|
||||
"fr": "Personnalisation du noeud de configuration dans les sous-flux"
|
||||
},
|
||||
image: 'images/nr4-sf-config.png',
|
||||
description: {
|
||||
"en-US": `<p>Subflows can now be customised to allow each instance to use a different
|
||||
config node of a selected type.</p>
|
||||
<p>For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing
|
||||
of the messages received can be pointed at a different broker.</p>
|
||||
`,
|
||||
"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>
|
||||
`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Remembering palette state",
|
||||
"ja": "パレットの状態を維持",
|
||||
"fr": "Mémorisation de l'état de la palette"
|
||||
},
|
||||
description: {
|
||||
"en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any
|
||||
filter you have applied.</p>`,
|
||||
"ja": `<p>パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。</p>`,
|
||||
"fr": `<p>La palette se souvient désormais des catégories que vous avez masquées entre les rechargements,
|
||||
ainsi que le filtre que vous avez appliqué.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Plugins shown in the Palette Manager",
|
||||
"ja": "パレット管理にプラグインを表示",
|
||||
"fr": "Affichage des Plugins dans le gestionnaire de palettes"
|
||||
},
|
||||
image: 'images/nr4-plugins.png',
|
||||
description: {
|
||||
"en-US": `<p>The palette manager now shows any plugin modules you have installed, such as
|
||||
<code>node-red-debugger</code>. Previously they would only be shown if the plugins include
|
||||
nodes for the palette.</p>`,
|
||||
"ja": `<p>パレットの管理に <code>node-red-debugger</code> の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。</p>`,
|
||||
"fr": `<p>Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
|
||||
tels que <code>node-red-debugger</code>. Auparavant, ils n'étaient affichés que s'ils contenaient
|
||||
des noeuds pour la palette.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Node Updates",
|
||||
"ja": "ノードの更新",
|
||||
"fr": "Mises à jour des noeuds"
|
||||
},
|
||||
// image: "images/",
|
||||
description: {
|
||||
"en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and
|
||||
small enhancements. Check the full changelog in the Help sidebar for a full list.</p>
|
||||
<ul>
|
||||
<li>A fully RFC4180 compliant CSV mode</li>
|
||||
<li>Customisable headers on the WebSocket node</li>
|
||||
<li>Split node now can operate on any message property</li>
|
||||
<li>and lots more...</li>
|
||||
</ul>`,
|
||||
"ja": `<p>コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。</p>
|
||||
<ul>
|
||||
<li>RFC4180に完全に準拠したCSVモード</li>
|
||||
<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>`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
export default {
|
||||
version: "4.0.0",
|
||||
version: "4.1.0",
|
||||
steps: [
|
||||
{
|
||||
titleIcon: "fa fa-map-o",
|
||||
title: {
|
||||
"en-US": "Welcome to Node-RED 4.0!",
|
||||
"ja": "Node-RED 4.0 へようこそ!",
|
||||
"fr": "Bienvenue dans Node-RED 4.0!"
|
||||
"en-US": "Welcome to Node-RED 4.1!",
|
||||
"ja": "Node-RED 4.1 へようこそ!",
|
||||
"fr": "Bienvenue dans Node-RED 4.1!"
|
||||
},
|
||||
description: {
|
||||
"en-US": "<p>Let's take a moment to discover the new features in this release.</p>",
|
||||
@@ -16,216 +16,45 @@ export default {
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Multiplayer Mode",
|
||||
"ja": "複数ユーザ同時利用モード",
|
||||
"fr": "Mode Multi-utilisateur"
|
||||
"en-US": "Something new",
|
||||
},
|
||||
image: 'images/nr4-multiplayer-location.png',
|
||||
// image: 'images/nr4-multiplayer-location.png',
|
||||
description: {
|
||||
"en-US": `<p>This release includes the first small steps towards making Node-RED easier
|
||||
to work with when you have multiple people editing flows at the same time.</p>
|
||||
<p>When this feature is enabled, you will now see who else has the editor open and some
|
||||
basic information on where they are in the editor.</p>
|
||||
<p>Check the release post for details on how to enable this feature in your settings file.</p>`,
|
||||
"ja": `<p>本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。</p>
|
||||
<p>本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。</p>
|
||||
<p>設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。</p>`,
|
||||
"fr": `<p>Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser
|
||||
lorsque plusieurs personnes modifient des flux en même temps.</p>
|
||||
<p>Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont
|
||||
ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.</p>
|
||||
<p>Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité
|
||||
dans votre fichier de paramètres.</p>`
|
||||
"en-US": `<p>Something new</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Better background deploy handling",
|
||||
"ja": "バックグラウンドのデプロイ処理の改善",
|
||||
"fr": "Meilleure gestion du déploiement en arrière-plan"
|
||||
},
|
||||
image: 'images/nr4-background-deploy.png',
|
||||
description: {
|
||||
"en-US": `<p>If another user deploys changes whilst you are editing, we now use a more discrete notification
|
||||
that doesn't stop you continuing your work - especially if they are being very productive and deploying lots
|
||||
of changes.</p>`,
|
||||
"ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`,
|
||||
"fr": `<p>Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez
|
||||
une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Improved flow diffs",
|
||||
"ja": "フローの差分表示の改善",
|
||||
"fr": "Amélioration des différences de flux"
|
||||
},
|
||||
image: 'images/nr4-diff-update.png',
|
||||
description: {
|
||||
"en-US": `<p>When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration
|
||||
changes and those that have only been moved.<p>
|
||||
<p>When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.</p>`,
|
||||
"ja": `<p>フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。<p>
|
||||
<p>これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。</p>`,
|
||||
"fr": `<p>Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les
|
||||
noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.<p>
|
||||
<p>Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les
|
||||
plus importants.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Better Configuration Node UX",
|
||||
"ja": "設定ノードのUXが向上",
|
||||
"fr": "Meilleure expérience utilisateur du noeud de configuration"
|
||||
},
|
||||
image: 'images/nr4-config-select.png',
|
||||
description: {
|
||||
"en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button
|
||||
next to the select box.</p>
|
||||
<p>It's a small change, but should make it easier to work with your config nodes.</p>`,
|
||||
"ja": `<p>設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。</p>
|
||||
<p>微修正ですが設定ノードの操作が容易になります。</p>`,
|
||||
"fr": `<p>L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite
|
||||
mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.</p>
|
||||
<p>C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Timestamp formatting options",
|
||||
"ja": "タイムスタンプの形式の項目",
|
||||
"fr": "Options de formatage de l'horodatage"
|
||||
},
|
||||
image: 'images/nr4-timestamp-formatting.png',
|
||||
description: {
|
||||
"en-US": `<p>Nodes that let you set a timestamp now have options on what format that timestamp should be in.</p>
|
||||
<p>We're keeping it simple to begin with by providing three options:<p>
|
||||
<ul>
|
||||
<li>Milliseconds since epoch - this is existing behaviour of the timestamp option</li>
|
||||
<li>ISO 8601 - a common format used by many systems</li>
|
||||
<li>JavaScript Date Object</li>
|
||||
</ul>`,
|
||||
"ja": `<p>タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。</p>
|
||||
<p>次の3つの項目を追加したことで、簡単に選択できるようになりました:<p>
|
||||
<ul>
|
||||
<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": "フロー/グローバル、環境変数の型の自動補完",
|
||||
"fr": "Saisie automatique des types de flux/global et env"
|
||||
},
|
||||
image: 'images/nr4-auto-complete.png',
|
||||
description: {
|
||||
"en-US": `<p>The <code>flow</code>/<code>global</code> context inputs and the <code>env</code> input
|
||||
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": "サブフローでの設定ノードのカスタマイズ",
|
||||
"fr": "Personnalisation du noeud de configuration dans les sous-flux"
|
||||
},
|
||||
image: 'images/nr4-sf-config.png',
|
||||
description: {
|
||||
"en-US": `<p>Subflows can now be customised to allow each instance to use a different
|
||||
config node of a selected type.</p>
|
||||
<p>For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing
|
||||
of the messages received can be pointed at a different broker.</p>
|
||||
`,
|
||||
"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>
|
||||
`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Remembering palette state",
|
||||
"ja": "パレットの状態を維持",
|
||||
"fr": "Mémorisation de l'état de la palette"
|
||||
},
|
||||
description: {
|
||||
"en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any
|
||||
filter you have applied.</p>`,
|
||||
"ja": `<p>パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。</p>`,
|
||||
"fr": `<p>La palette se souvient désormais des catégories que vous avez masquées entre les rechargements,
|
||||
ainsi que le filtre que vous avez appliqué.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Plugins shown in the Palette Manager",
|
||||
"ja": "パレット管理にプラグインを表示",
|
||||
"fr": "Affichage des Plugins dans le gestionnaire de palettes"
|
||||
},
|
||||
image: 'images/nr4-plugins.png',
|
||||
description: {
|
||||
"en-US": `<p>The palette manager now shows any plugin modules you have installed, such as
|
||||
<code>node-red-debugger</code>. Previously they would only be shown if the plugins include
|
||||
nodes for the palette.</p>`,
|
||||
"ja": `<p>パレットの管理に <code>node-red-debugger</code> の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。</p>`,
|
||||
"fr": `<p>Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
|
||||
tels que <code>node-red-debugger</code>. Auparavant, ils n'étaient affichés que s'ils contenaient
|
||||
des noeuds pour la palette.</p>`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: {
|
||||
"en-US": "Node Updates",
|
||||
"ja": "ノードの更新",
|
||||
"fr": "Mises à jour des noeuds"
|
||||
},
|
||||
// image: "images/",
|
||||
description: {
|
||||
"en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and
|
||||
small enhancements. Check the full changelog in the Help sidebar for a full list.</p>
|
||||
<ul>
|
||||
<li>A fully RFC4180 compliant CSV mode</li>
|
||||
<li>Customisable headers on the WebSocket node</li>
|
||||
<li>Split node now can operate on any message property</li>
|
||||
<li>and lots more...</li>
|
||||
</ul>`,
|
||||
"ja": `<p>コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。</p>
|
||||
<ul>
|
||||
<li>RFC4180に完全に準拠したCSVモード</li>
|
||||
<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>`
|
||||
}
|
||||
}
|
||||
// {
|
||||
// title: {
|
||||
// "en-US": "Node Updates",
|
||||
// "ja": "ノードの更新",
|
||||
// "fr": "Mises à jour des noeuds"
|
||||
// },
|
||||
// // image: "images/",
|
||||
// description: {
|
||||
// "en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and
|
||||
// small enhancements. Check the full changelog in the Help sidebar for a full list.</p>
|
||||
// <ul>
|
||||
// <li>A fully RFC4180 compliant CSV mode</li>
|
||||
// <li>Customisable headers on the WebSocket node</li>
|
||||
// <li>Split node now can operate on any message property</li>
|
||||
// <li>and lots more...</li>
|
||||
// </ul>`,
|
||||
// "ja": `<p>コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。</p>
|
||||
// <ul>
|
||||
// <li>RFC4180に完全に準拠したCSVモード</li>
|
||||
// <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>`
|
||||
// }
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="form-tips">
|
||||
<span data-i18n="[html]unknown.tip"></span>
|
||||
<p id="unknown-module-known">
|
||||
<button id="unknown-manage-dependencies" class="red-ui-button">Manage dependencies</button>
|
||||
<button id="unknown-manage-dependencies" class="red-ui-button"><span data-i18n="unknown.manageModules"></span></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -162,6 +162,8 @@ module.exports = function(RED) {
|
||||
console:console,
|
||||
util:util,
|
||||
Buffer:Buffer,
|
||||
URL: URL,
|
||||
URLSearchParams: URLSearchParams,
|
||||
Date: Date,
|
||||
RED: {
|
||||
util: {
|
||||
@@ -403,6 +405,8 @@ module.exports = function(RED) {
|
||||
if(node.timeout>0){
|
||||
finOpt.timeout = node.timeout;
|
||||
finOpt.breakOnSigint = true;
|
||||
} else if (RED.settings.globalFunctionTimeout > 0){
|
||||
finOpt.timeout = RED.settings.globalFunctionTimeout * 1000
|
||||
}
|
||||
}
|
||||
var promise = Promise.resolve();
|
||||
@@ -419,8 +423,14 @@ module.exports = function(RED) {
|
||||
var opts = {};
|
||||
if (node.timeout>0){
|
||||
opts = node.timeoutOptions;
|
||||
} else if (RED.settings. globalFunctionTimeout > 0){
|
||||
opts.timeout = RED.settings. globalFunctionTimeout * 1000
|
||||
}
|
||||
try {
|
||||
node.script.runInContext(context,opts);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
node.script.runInContext(context,opts);
|
||||
context.results.then(function(results) {
|
||||
sendResults(node,send,msg._msgid,results,false);
|
||||
if (handleNodeDoneCall) {
|
||||
|
||||
@@ -109,7 +109,7 @@ module.exports = function(RED) {
|
||||
child.stderr.on('data', function (data) {
|
||||
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
|
||||
if (isUtf8(data)) { msg.payload = data.toString(); }
|
||||
else { msg.payload = Buffer.from(data); }
|
||||
else { msg.payload = data; }
|
||||
nodeSend([null,RED.util.cloneMessage(msg),null]);
|
||||
}
|
||||
});
|
||||
@@ -146,7 +146,8 @@ module.exports = function(RED) {
|
||||
delete msg.payload;
|
||||
if (stderr) {
|
||||
msg2 = RED.util.cloneMessage(msg);
|
||||
msg2.payload = stderr;
|
||||
msg2.payload = Buffer.from(stderr,"binary");
|
||||
if (isUtf8(msg2.payload)) { msg2.payload = msg2.payload.toString(); }
|
||||
}
|
||||
msg.payload = Buffer.from(stdout,"binary");
|
||||
if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); }
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label>
|
||||
<input id="node-input-url" type="text" placeholder="http://">
|
||||
<input id="node-input-url" type="text" placeholder="https://">
|
||||
</div>
|
||||
|
||||
<div class="form-row node-input-paytoqs-row">
|
||||
|
||||
@@ -586,6 +586,17 @@ in your Node-RED user directory (${RED.settings.userDir}).
|
||||
opts.https.certificate = opts.https.cert;
|
||||
delete opts.https.cert;
|
||||
}
|
||||
// The got library uses a different case for some https properties compared to the
|
||||
// standard node tls options object.
|
||||
if (opts.https.ALPNProtocols) {
|
||||
opts.https.alpnProtocols = opts.https.ALPNProtocols
|
||||
delete opts.https.ALPNProtocols
|
||||
}
|
||||
// The got library doesn't support servername at this time
|
||||
// https://github.com/sindresorhus/got/issues/2320
|
||||
if (opts.https.servername) {
|
||||
delete opts.https.servername
|
||||
}
|
||||
} else {
|
||||
if (msg.hasOwnProperty('rejectUnauthorized')) {
|
||||
opts.https = { rejectUnauthorized: msg.rejectUnauthorized };
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.splitThe"></span></label>
|
||||
<input type="text" id="node-input-property" style="width:70%;">
|
||||
<label for="node-input-property" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitThe"></label>
|
||||
<input type="text" id="node-input-property" style="width:70%;"/>
|
||||
</div>
|
||||
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
|
||||
<div class="form-row">
|
||||
|
||||
@@ -151,10 +151,11 @@ module.exports = function(RED) {
|
||||
if (node.arraySplt === 1) {
|
||||
m = m[0];
|
||||
}
|
||||
RED.util.setMessageProperty(msg,node.property,m);
|
||||
msg.parts.index = i;
|
||||
const newmsg = RED.util.cloneMessage(msg)
|
||||
RED.util.setMessageProperty(newmsg,node.property,m);
|
||||
newmsg.parts.index = i;
|
||||
pos += node.arraySplt;
|
||||
send(RED.util.cloneMessage(msg));
|
||||
send(newmsg);
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
@@ -406,6 +406,7 @@
|
||||
"label": {
|
||||
"unknown": "unknown"
|
||||
},
|
||||
"manageModules": "Manage modules",
|
||||
"tip": "<p>This node is a type unknown to your installation of Node-RED.</p><p><i>If you deploy with the node in this state, it's configuration will be preserved, but the flow will not start until the missing type is installed.</i></p><p>See the Info side bar for more help</p>"
|
||||
},
|
||||
"mqtt": {
|
||||
@@ -1018,7 +1019,7 @@
|
||||
"objectSend": "Send a message for each key/value pair",
|
||||
"strBuff": "<b>String</b> / <b>Buffer</b>",
|
||||
"array": "<b>Array</b>",
|
||||
"splitThe": "Split the",
|
||||
"splitThe": "Split property",
|
||||
"splitUsing": "Split using",
|
||||
"splitLength": "Fixed length of",
|
||||
"stream": "Handle as a stream of messages",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"js-yaml": "4.1.0",
|
||||
"media-typer": "1.1.0",
|
||||
"mqtt": "5.11.0",
|
||||
"multer": "1.4.5-lts.2",
|
||||
"multer": "2.0.1",
|
||||
"mustache": "4.2.0",
|
||||
"node-watch": "0.7.4",
|
||||
"on-headers": "1.0.2",
|
||||
|
||||
@@ -406,6 +406,7 @@ async function loadPlugin(plugin) {
|
||||
}
|
||||
try {
|
||||
var r = require(plugin.file);
|
||||
r = r.__esModule ? r.default : r
|
||||
if (typeof r === "function") {
|
||||
|
||||
var red = registryUtil.createNodeApi(plugin);
|
||||
|
||||
@@ -161,6 +161,8 @@ var api = module.exports = {
|
||||
safeSettings.diagnostics.ui = false; // cannot have UI without endpoint
|
||||
}
|
||||
|
||||
safeSettings.telemetryEnabled = runtime.telemetry.isEnabled()
|
||||
|
||||
safeSettings.runtimeState = {
|
||||
//unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false.
|
||||
enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true,
|
||||
@@ -213,7 +215,19 @@ var api = module.exports = {
|
||||
}
|
||||
var currentSettings = runtime.settings.getUserSettings(username)||{};
|
||||
currentSettings = extend(currentSettings, opts.settings);
|
||||
|
||||
try {
|
||||
if (currentSettings.hasOwnProperty("telemetryEnabled")) {
|
||||
// This is a global setting that is being set by the user. It should
|
||||
// not be stored per-user as it applies to the whole runtime.
|
||||
const telemetryEnabled = currentSettings.telemetryEnabled;
|
||||
delete currentSettings.telemetryEnabled;
|
||||
if (telemetryEnabled) {
|
||||
runtime.telemetry.enable()
|
||||
} else {
|
||||
runtime.telemetry.disable()
|
||||
}
|
||||
}
|
||||
return runtime.settings.setUserSettings(username, currentSettings).then(function() {
|
||||
runtime.log.audit({event: "settings.update",username:username}, opts.req);
|
||||
return;
|
||||
|
||||
@@ -23,6 +23,7 @@ var library = require("./library");
|
||||
var plugins = require("./plugins");
|
||||
var settings = require("./settings");
|
||||
const multiplayer = require("./multiplayer");
|
||||
const telemetry = require("./telemetry");
|
||||
|
||||
var express = require("express");
|
||||
var path = require('path');
|
||||
@@ -135,6 +136,7 @@ function start() {
|
||||
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json")
|
||||
.then(function() { return storage.init(runtime)})
|
||||
.then(function() { return settings.load(storage)})
|
||||
.then(function() { return telemetry.init(runtime)})
|
||||
.then(function() { return library.init(runtime)})
|
||||
.then(function() { return multiplayer.init(runtime)})
|
||||
.then(function() {
|
||||
@@ -337,6 +339,7 @@ var runtime = {
|
||||
library: library,
|
||||
exec: exec,
|
||||
util: util,
|
||||
telemetry: telemetry,
|
||||
get adminApi() { return adminApi },
|
||||
get adminApp() { return adminApp },
|
||||
get nodeApp() { return nodeApp },
|
||||
|
||||
@@ -51,6 +51,8 @@ function runGitCommand(args,cwd,env,emit) {
|
||||
err.code = "git_auth_failed";
|
||||
} else if(/Authentication failed/i.test(stderr)) {
|
||||
err.code = "git_auth_failed";
|
||||
} else if (/The requested URL returned error: 403/i.test(stderr)) {
|
||||
err.code = "git_auth_failed";
|
||||
} else if (/commit your changes or stash/i.test(stderr)) {
|
||||
err.code = "git_local_overwrite";
|
||||
} else if (/CONFLICT/.test(err.stdout)) {
|
||||
|
||||
213
packages/node_modules/@node-red/runtime/lib/telemetry/index.js
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs/promises')
|
||||
const semver = require('semver')
|
||||
const cronosjs = require('cronosjs')
|
||||
|
||||
const METRICS_DIR = path.join(__dirname, 'metrics')
|
||||
const INITIAL_PING_DELAY = 1000 * 60 * 30 // 30 minutes from startup
|
||||
|
||||
/** @type {import("got").Got | undefined} */
|
||||
let got
|
||||
|
||||
let runtime
|
||||
|
||||
let scheduleTask
|
||||
|
||||
async function gather () {
|
||||
let metricFiles = await fs.readdir(METRICS_DIR)
|
||||
metricFiles = metricFiles.filter(name => /^\d+-.*\.js$/.test(name))
|
||||
metricFiles.sort()
|
||||
|
||||
const metrics = {}
|
||||
|
||||
for (let i = 0, l = metricFiles.length; i < l; i++) {
|
||||
const metricModule = require(path.join(METRICS_DIR, metricFiles[i]))
|
||||
let result = metricModule(runtime)
|
||||
if (!!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
|
||||
result = await result
|
||||
}
|
||||
const keys = Object.keys(result)
|
||||
keys.forEach(key => {
|
||||
const keyParts = key.split('.')
|
||||
let p = metrics
|
||||
keyParts.forEach((part, index) => {
|
||||
if (index < keyParts.length - 1) {
|
||||
if (!p[part]) {
|
||||
p[part] = {}
|
||||
}
|
||||
p = p[part]
|
||||
} else {
|
||||
p[part] = result[key]
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
async function report () {
|
||||
if (!isTelemetryEnabled()) {
|
||||
return
|
||||
}
|
||||
// If enabled, gather metrics
|
||||
const metrics = await gather()
|
||||
|
||||
// Post metrics to endpoint - handle any error silently
|
||||
|
||||
if (!got) {
|
||||
got = (await import('got')).got
|
||||
}
|
||||
|
||||
runtime.log.debug('Sending telemetry')
|
||||
const response = await got.post('https://telemetry.nodered.org/ping', {
|
||||
json: metrics,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'User-Agent': `Node-RED/${runtime.settings.version}`
|
||||
}
|
||||
}).json().catch(err => {
|
||||
// swallow errors
|
||||
runtime.log.debug('Failed to send telemetry: ' + err.toString())
|
||||
})
|
||||
// Example response:
|
||||
// { 'node-red': { latest: '4.0.9', next: '4.1.0-beta.1.9' } }
|
||||
runtime.log.debug(`Telemetry response: ${JSON.stringify(response)}`)
|
||||
// Get response from endpoint
|
||||
if (response?.['node-red']) {
|
||||
const currentVersion = metrics.env['node-red']
|
||||
if (semver.valid(currentVersion)) {
|
||||
const latest = response['node-red'].latest
|
||||
const next = response['node-red'].next
|
||||
let updatePayload
|
||||
if (semver.lt(currentVersion, latest)) {
|
||||
// Case one: current < latest
|
||||
runtime.log.info(`A new version of Node-RED is available: ${latest}`)
|
||||
updatePayload = { version: latest }
|
||||
} else if (semver.gt(currentVersion, latest) && semver.lt(currentVersion, next)) {
|
||||
// Case two: current > latest && current < next
|
||||
runtime.log.info(`A new beta version of Node-RED is available: ${next}`)
|
||||
updatePayload = { version: next }
|
||||
}
|
||||
|
||||
if (updatePayload && isUpdateNotificationEnabled()) {
|
||||
runtime.events.emit("runtime-event",{id:"update-available", payload: updatePayload, retain: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTelemetryEnabled () {
|
||||
// If NODE_RED_DISABLE_TELEMETRY was set, or --no-telemetry was specified,
|
||||
// the settings object will have been updated to disable telemetry explicitly
|
||||
|
||||
// If there are no telemetry settings then the user has not had a chance
|
||||
// to opt out yet - so keep it disabled until they do
|
||||
|
||||
let telemetrySettings
|
||||
try {
|
||||
telemetrySettings = runtime.settings.get('telemetry')
|
||||
} catch (err) {
|
||||
// Settings not available
|
||||
}
|
||||
let runtimeTelemetryEnabled
|
||||
try {
|
||||
runtimeTelemetryEnabled = runtime.settings.get('telemetryEnabled')
|
||||
} catch (err) {
|
||||
// Settings not available
|
||||
}
|
||||
|
||||
if (telemetrySettings === undefined && runtimeTelemetryEnabled === undefined) {
|
||||
// No telemetry settings - so keep it disabled
|
||||
return undefined
|
||||
}
|
||||
|
||||
// User has made a choice; defer to that
|
||||
if (runtimeTelemetryEnabled !== undefined) {
|
||||
return runtimeTelemetryEnabled
|
||||
}
|
||||
|
||||
// If there are telemetry settings, use what it says
|
||||
if (telemetrySettings && telemetrySettings.enabled !== undefined) {
|
||||
return telemetrySettings.enabled
|
||||
}
|
||||
|
||||
// At this point, we have no sign the user has consented to telemetry, so
|
||||
// keep disabled - but return undefined as a false-like value to distinguish
|
||||
// it from the explicit disable above
|
||||
return undefined
|
||||
}
|
||||
|
||||
function isUpdateNotificationEnabled () {
|
||||
const telemetrySettings = runtime.settings.get('telemetry') || {}
|
||||
return telemetrySettings.updateNotification !== false
|
||||
}
|
||||
/**
|
||||
* Start the telemetry schedule
|
||||
*/
|
||||
function startTelemetry () {
|
||||
if (scheduleTask) {
|
||||
// Already scheduled - nothing left to do
|
||||
return
|
||||
}
|
||||
|
||||
const pingTime = new Date(Date.now() + INITIAL_PING_DELAY)
|
||||
const pingMinutes = pingTime.getMinutes()
|
||||
const pingHours = pingTime.getHours()
|
||||
const pingSchedule = `${pingMinutes} ${pingHours} * * *`
|
||||
|
||||
runtime.log.debug(`Telemetry enabled. Schedule: ${pingSchedule}`)
|
||||
|
||||
scheduleTask = cronosjs.scheduleTask(pingSchedule, () => {
|
||||
report()
|
||||
})
|
||||
}
|
||||
|
||||
function stopTelemetry () {
|
||||
if (scheduleTask) {
|
||||
runtime.log.debug(`Telemetry disabled`)
|
||||
scheduleTask.stop()
|
||||
scheduleTask = null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: (_runtime) => {
|
||||
runtime = _runtime
|
||||
|
||||
if (isTelemetryEnabled()) {
|
||||
startTelemetry()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Enable telemetry via user opt-in in the editor
|
||||
*/
|
||||
enable: () => {
|
||||
if (runtime.settings.available()) {
|
||||
runtime.settings.set('telemetryEnabled', true)
|
||||
}
|
||||
startTelemetry()
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable telemetry via user opt-in in the editor
|
||||
*/
|
||||
disable: () => {
|
||||
if (runtime.settings.available()) {
|
||||
runtime.settings.set('telemetryEnabled', false)
|
||||
}
|
||||
stopTelemetry()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get telemetry enabled status
|
||||
* @returns {boolean} true if telemetry is enabled, false if disabled, undefined if not set
|
||||
*/
|
||||
isEnabled: isTelemetryEnabled,
|
||||
|
||||
stop: () => {
|
||||
if (scheduleTask) {
|
||||
scheduleTask.stop()
|
||||
scheduleTask = null
|
||||
}
|
||||
}
|
||||
}
|
||||
5
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = (runtime) => {
|
||||
return {
|
||||
instanceId: runtime.settings.get('instanceId')
|
||||
}
|
||||
}
|
||||
9
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
const os = require('os')
|
||||
|
||||
module.exports = (_) => {
|
||||
return {
|
||||
'os.type': os.type(),
|
||||
'os.release': os.release(),
|
||||
'os.arch': os.arch()
|
||||
}
|
||||
}
|
||||
8
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
const process = require('process')
|
||||
|
||||
module.exports = (runtime) => {
|
||||
return {
|
||||
'env.nodejs': process.version.replace(/^v/, ''),
|
||||
'env.node-red': runtime.settings.version
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,11 @@
|
||||
"@node-red/util": "4.1.0-beta.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"clone": "2.1.2",
|
||||
"cronosjs": "1.7.1",
|
||||
"express": "4.21.2",
|
||||
"fs-extra": "11.3.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"rfdc": "^1.3.1"
|
||||
"rfdc": "^1.3.1",
|
||||
"semver": "7.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
8
packages/node_modules/node-red/red.js
vendored
@@ -63,7 +63,8 @@ var knownOpts = {
|
||||
"verbose": Boolean,
|
||||
"safe": Boolean,
|
||||
"version": Boolean,
|
||||
"define": [String, Array]
|
||||
"define": [String, Array],
|
||||
"no-telemetry": Boolean
|
||||
};
|
||||
|
||||
var shortHands = {
|
||||
@@ -98,6 +99,7 @@ if (parsedArgs.help) {
|
||||
console.log(" --safe enable safe mode");
|
||||
console.log(" -D, --define X=Y overwrite value in settings file");
|
||||
console.log(" --version show version information");
|
||||
console.log(" --no-telemetry do not share usage data with the Node-RED project");
|
||||
console.log(" -?, --help show this help");
|
||||
console.log(" admin <command> run an admin command");
|
||||
console.log("");
|
||||
@@ -223,6 +225,10 @@ if (process.env.NODE_RED_ENABLE_TOURS) {
|
||||
settings.editorTheme.tours = !/^false$/i.test(process.env.NODE_RED_ENABLE_TOURS);
|
||||
}
|
||||
|
||||
if (parsedArgs.telemetry === false || process.env.NODE_RED_DISABLE_TELEMETRY) {
|
||||
settings.telemetry = settings.telemetry || {};
|
||||
settings.telemetry.enabled = false;
|
||||
}
|
||||
|
||||
var defaultServerSettings = {
|
||||
"x-powered-by": false
|
||||
|
||||
32
packages/node_modules/node-red/settings.js
vendored
@@ -273,6 +273,7 @@ module.exports = {
|
||||
* Runtime Settings
|
||||
* - lang
|
||||
* - runtimeState
|
||||
* - telemetry
|
||||
* - diagnostics
|
||||
* - logging
|
||||
* - contextStorage
|
||||
@@ -311,6 +312,22 @@ module.exports = {
|
||||
/** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */
|
||||
ui: false,
|
||||
},
|
||||
telemetry: {
|
||||
/**
|
||||
* By default, telemetry is disabled until the user provides consent the first
|
||||
* time they open the editor.
|
||||
*
|
||||
* The following property can be uncommented and set to true/false to enable/disable
|
||||
* telemetry without seeking further consent in the editor.
|
||||
* The user can override this setting via the user settings dialog within the editor
|
||||
*/
|
||||
// enabled: true,
|
||||
/**
|
||||
* If telemetry is enabled, the editor will notify the user if a new version of Node-RED
|
||||
* is available. Set the following property to false to disable this notification.
|
||||
*/
|
||||
// updateNotification: true
|
||||
},
|
||||
/** Configure the logging output */
|
||||
logging: {
|
||||
/** Only console logging is currently supported */
|
||||
@@ -473,6 +490,7 @@ module.exports = {
|
||||
* - fileWorkingDirectory
|
||||
* - functionGlobalContext
|
||||
* - functionExternalModules
|
||||
* - globalFunctionTimeout
|
||||
* - functionTimeout
|
||||
* - nodeMessageBufferMaxLength
|
||||
* - ui (for use with Node-RED Dashboard)
|
||||
@@ -499,7 +517,19 @@ module.exports = {
|
||||
/** Allow the Function node to load additional npm modules directly */
|
||||
functionExternalModules: true,
|
||||
|
||||
/** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */
|
||||
|
||||
/**
|
||||
* The default timeout (in seconds) for all Function nodes.
|
||||
* Individual nodes can set their own timeout value within their configuration.
|
||||
*/
|
||||
globalFunctionTimeout: 0,
|
||||
|
||||
/**
|
||||
* Default timeout, in seconds, for the Function node. 0 means no timeout is applied
|
||||
* This value is applied when the node is first added to the workspace - any changes
|
||||
* must then be made with the individual node configurations.
|
||||
* To set a global timeout value, use `globalFunctionTimeout`
|
||||
*/
|
||||
functionTimeout: 0,
|
||||
|
||||
/** The following property can be used to set predefined values in Global Context.
|
||||
|
||||
@@ -1451,7 +1451,7 @@ describe('function node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('check if default function timeout settings are recognized', function (done) {
|
||||
it('check if function timeout settings are recognized', function (done) {
|
||||
RED.settings.functionTimeout = 0.01;
|
||||
var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
|
||||
helper.load(functionNode, flow, function () {
|
||||
@@ -1479,6 +1479,65 @@ describe('function node', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('check if default function timeout settings are recognized', function (done) {
|
||||
RED.settings.globalFunctionTimeout = 0.01;
|
||||
var flow = [{id: "n1",type: "function",wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
|
||||
helper.load(functionNode, flow, function () {
|
||||
var n1 = helper.getNode("n1");
|
||||
n1.receive({ payload: "foo", topic: "bar" });
|
||||
setTimeout(function () {
|
||||
try {
|
||||
helper.log().called.should.be.true();
|
||||
var logEvents = helper.log().args.filter(function (evt) {
|
||||
return evt[0].type == "function";
|
||||
});
|
||||
logEvents.should.have.length(1);
|
||||
var msg = logEvents[0][0];
|
||||
msg.should.have.property('level', helper.log().ERROR);
|
||||
msg.should.have.property('id', 'n1');
|
||||
msg.should.have.property('type', 'function');
|
||||
should.equal(RED.settings.globalFunctionTimeout, 0.01);
|
||||
should.equal(msg.msg.message, 'Script execution timed out after 10ms');
|
||||
delete RED.settings.globalFunctionTimeout;
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
it('check if functionTimeout has higher precedence over default function timeout setting', function (done) {
|
||||
RED.settings.globalFunctionTimeout = 0.02;
|
||||
RED.settings.functionTimeout = 0.01;
|
||||
var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
|
||||
helper.load(functionNode, flow, function () {
|
||||
var n1 = helper.getNode("n1");
|
||||
n1.receive({ payload: "foo", topic: "bar" });
|
||||
setTimeout(function () {
|
||||
try {
|
||||
helper.log().called.should.be.true();
|
||||
var logEvents = helper.log().args.filter(function (evt) {
|
||||
return evt[0].type == "function";
|
||||
});
|
||||
logEvents.should.have.length(1);
|
||||
var msg = logEvents[0][0];
|
||||
msg.should.have.property('level', helper.log().ERROR);
|
||||
msg.should.have.property('id', 'n1');
|
||||
msg.should.have.property('type', 'function');
|
||||
should.equal(RED.settings.functionTimeout, 0.01);
|
||||
should.equal(RED.settings.globalFunctionTimeout, 0.02);
|
||||
should.equal(msg.msg.message, 'Script execution timed out after 10ms');
|
||||
delete RED.settings.functionTimeout;
|
||||
delete RED.settings.globalFunctionTimeout;
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
describe("finalize function", function() {
|
||||
|
||||
it('should execute', function(done) {
|
||||
|
||||
@@ -57,7 +57,8 @@ describe("runtime-api/settings", function() {
|
||||
getCredentialKeyType: () => "test-key-type"
|
||||
},
|
||||
library: {getLibraries: () => ["lib1"] },
|
||||
storage: {}
|
||||
storage: {},
|
||||
telemetry: { isEnabled: () => true }
|
||||
})
|
||||
return settings.getRuntimeSettings({}).then(result => {
|
||||
result.should.have.property("httpNodeRoot","testHttpNodeRoot");
|
||||
@@ -96,7 +97,8 @@ describe("runtime-api/settings", function() {
|
||||
getCredentialKeyType: () => "test-key-type"
|
||||
},
|
||||
library: {getLibraries: () => { ["lib1"]} },
|
||||
storage: {}
|
||||
storage: {},
|
||||
telemetry: { isEnabled: () => true }
|
||||
})
|
||||
return settings.getRuntimeSettings({
|
||||
user: {
|
||||
@@ -145,7 +147,8 @@ describe("runtime-api/settings", function() {
|
||||
getCredentialsFilename: () => 'test-creds-file',
|
||||
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
|
||||
}
|
||||
}
|
||||
},
|
||||
telemetry: { isEnabled: () => true }
|
||||
})
|
||||
return settings.getRuntimeSettings({
|
||||
user: {
|
||||
@@ -202,7 +205,8 @@ describe("runtime-api/settings", function() {
|
||||
getCredentialsFilename: () => 'test-creds-file',
|
||||
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
|
||||
}
|
||||
}
|
||||
},
|
||||
telemetry: { isEnabled: () => true }
|
||||
})
|
||||
return settings.getRuntimeSettings({
|
||||
user: {
|
||||
@@ -250,7 +254,8 @@ describe("runtime-api/settings", function() {
|
||||
getCredentialsFilename: () => 'test-creds-file',
|
||||
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
|
||||
}
|
||||
}
|
||||
},
|
||||
telemetry: { isEnabled: () => true }
|
||||
})
|
||||
return settings.getRuntimeSettings({
|
||||
user: {
|
||||
@@ -301,7 +306,8 @@ describe("runtime-api/settings", function() {
|
||||
getCredentialsFilename: () => 'test-creds-file',
|
||||
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
|
||||
}
|
||||
}
|
||||
},
|
||||
telemetry: { isEnabled: () => true }
|
||||
})
|
||||
return settings.getRuntimeSettings({
|
||||
user: {
|
||||
|
||||
96
test/unit/@node-red/runtime/lib/telemetry/index_spec.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const should = require("should");
|
||||
const NR_TEST_UTILS = require("nr-test-utils");
|
||||
|
||||
const telemetryApi = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/index");
|
||||
|
||||
describe("telemetry", function() {
|
||||
|
||||
afterEach(function () {
|
||||
telemetryApi.stop()
|
||||
messages = []
|
||||
})
|
||||
|
||||
let messages = []
|
||||
|
||||
function getMockRuntime(settings) {
|
||||
return {
|
||||
settings: {
|
||||
get: key => { return settings[key] },
|
||||
set: (key, value) => { settings[key] = value },
|
||||
available: () => true,
|
||||
},
|
||||
log: {
|
||||
debug: (msg) => { messages.push(msg)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Principles to test:
|
||||
// - No settings at all; disable telemetry
|
||||
// - Runtime settings only; do what it says
|
||||
// - User settings take precedence over runtime settings
|
||||
|
||||
it('Disables telemetry with no settings present', function () {
|
||||
telemetryApi.init(getMockRuntime({}))
|
||||
messages.should.have.length(0)
|
||||
// Returns undefined as we don't know either way
|
||||
;(telemetryApi.isEnabled() === undefined).should.be.true()
|
||||
})
|
||||
it('Runtime settings - enable', function () {
|
||||
// Enabled in runtime settings
|
||||
telemetryApi.init(getMockRuntime({
|
||||
telemetry: { enabled: true }
|
||||
}))
|
||||
telemetryApi.isEnabled().should.be.true()
|
||||
messages.should.have.length(1)
|
||||
;/Telemetry enabled/.test(messages[0]).should.be.true()
|
||||
})
|
||||
it('Runtime settings - disable', function () {
|
||||
telemetryApi.init(getMockRuntime({
|
||||
telemetry: { enabled: false },
|
||||
}))
|
||||
// Returns false, not undefined
|
||||
telemetryApi.isEnabled().should.be.false()
|
||||
messages.should.have.length(0)
|
||||
})
|
||||
|
||||
it('User settings - enable overrides runtime settings', function () {
|
||||
telemetryApi.init(getMockRuntime({
|
||||
telemetry: { enabled: false },
|
||||
telemetryEnabled: true
|
||||
}))
|
||||
telemetryApi.isEnabled().should.be.true()
|
||||
messages.should.have.length(1)
|
||||
;/Telemetry enabled/.test(messages[0]).should.be.true()
|
||||
})
|
||||
|
||||
it('User settings - disable overrides runtime settings', function () {
|
||||
telemetryApi.init(getMockRuntime({
|
||||
telemetry: { enabled: true },
|
||||
telemetryEnabled: false
|
||||
}))
|
||||
telemetryApi.isEnabled().should.be.false()
|
||||
messages.should.have.length(0)
|
||||
})
|
||||
|
||||
it('Can enable/disable telemetry', function () {
|
||||
const settings = {}
|
||||
telemetryApi.init(getMockRuntime(settings))
|
||||
;(telemetryApi.isEnabled() === undefined).should.be.true()
|
||||
|
||||
telemetryApi.enable()
|
||||
|
||||
telemetryApi.isEnabled().should.be.true()
|
||||
messages.should.have.length(1)
|
||||
;/Telemetry enabled/.test(messages[0]).should.be.true()
|
||||
settings.should.have.property('telemetryEnabled', true)
|
||||
|
||||
telemetryApi.disable()
|
||||
|
||||
telemetryApi.isEnabled().should.be.false()
|
||||
messages.should.have.length(2)
|
||||
;/Telemetry disabled/.test(messages[1]).should.be.true()
|
||||
settings.should.have.property('telemetryEnabled', false)
|
||||
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
const should = require("should");
|
||||
const NR_TEST_UTILS = require("nr-test-utils");
|
||||
|
||||
const api = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/metrics/01-core");
|
||||
|
||||
describe("telemetry metrics/01-core", function() {
|
||||
|
||||
it('reports core metrics', function () {
|
||||
const result = api({
|
||||
settings: {
|
||||
get: key => { return {instanceId: "1234"}[key]}
|
||||
}
|
||||
})
|
||||
result.should.have.property("instanceId", "1234")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
const should = require("should");
|
||||
const NR_TEST_UTILS = require("nr-test-utils");
|
||||
|
||||
const api = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/metrics/02-os");
|
||||
|
||||
describe("telemetry metrics/02-os", function() {
|
||||
|
||||
it('reports os metrics', function () {
|
||||
const result = api()
|
||||
result.should.have.property("os.type")
|
||||
result.should.have.property("os.release")
|
||||
result.should.have.property("os.arch")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
const should = require("should");
|
||||
const NR_TEST_UTILS = require("nr-test-utils");
|
||||
|
||||
const api = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/metrics/03-env");
|
||||
|
||||
describe("telemetry metrics/03-env", function() {
|
||||
|
||||
it('reports env metrics', function () {
|
||||
const result = api({
|
||||
settings: {
|
||||
version: '1.2.3'
|
||||
}
|
||||
})
|
||||
result.should.have.property("env.nodejs", process.version.replace(/^v/, ''))
|
||||
result.should.have.property("env.node-red", '1.2.3')
|
||||
})
|
||||
})
|
||||