diff --git a/package.json b/package.json index db3b94f22..df423ad0f 100644 --- a/package.json +++ b/package.json @@ -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", @@ -87,7 +87,7 @@ "@node-rs/bcrypt": "1.10.7" }, "devDependencies": { - "dompurify": "2.5.8", + "dompurify": "3.2.5", "grunt": "1.6.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.5.0", diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index ea199077c..4ed4e3f4a 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -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", diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index a8bf70a19..a479b36e5 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -660,6 +660,8 @@ "more": "+ __count__ more", "upload": "Upload module tgz file", "refresh": "Refresh module list", + "deprecated": "deprecated", + "deprecatedTip": "This module has been deprecated", "errors": { "catalogLoadFailed": "

Failed to load node catalogue.

Check the browser console for more information

", "installFailed": "

Failed to install: __module__

__message__

Check the log for more information

", @@ -1282,5 +1284,15 @@ "environment": "Environment", "header": "Global Environment Variables", "revert": "Revert" + }, + "telemetry": { + "label": "Update Notifications", + "settingsTitle": "Enable Update Notifications", + "settingsDescription": "

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.

This requires sending anonymised data back to the Node-RED team. It does not include any details of your flows or users.

For full information on what information is collected and how it is used, please see the documentation.

", + "settingsDescription2": "

You can change this setting at any time in the editor settings.

", + "enableLabel": "Yes, enable notifications", + "disableLabel": "No, do not enable notifications", + "updateAvailable": "Update available", + "updateAvailableDesc": "Node-RED __version__ is now available" } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 20af0bd11..a4cb79698 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -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) { @@ -1859,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; @@ -2260,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]) @@ -2534,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]); } } @@ -2787,7 +2810,8 @@ RED.nodes = (function() { workspaces:new_workspaces, subflows:new_subflows, missingWorkspace: missingWorkspace, - removedNodes: removedNodes + removedNodes: removedNodes, + nodeMap: node_map } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index f8130a686..99cb8375b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -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(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 381bb9d3a..a294bc484 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -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 = $('
'); + + if (options.closeButton !== false) { + $('').appendTo(dialogContent).click(function(evt) { + evt.preventDefault(); + close(); + }) + } + + const dialogBody = $('
').appendTo(dialogContent); + if (options.title) { + $('

').text(options.title).appendTo(dialogBody); + } + $('
').css("text-align","left").html(options.content).appendTo(dialogBody); + + const stepToolbar = $('
',{class:"red-ui-dialog-toolbar"}).appendTo(dialogContent); + + if (options.buttons) { + options.buttons.forEach(button => { + const btn = $('').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 = $('
').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 + } } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js index 53ebe5c4b..c70757dfa 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -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; + } } } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index 2f60ff03f..91ad65fe2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -284,37 +284,80 @@ 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; + } + + 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 +365,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 +385,8 @@ RED.palette.editor = (function() { activeTypeCount += set.types.length; } typeCount += set.types.length; - for (var i=0;i 0) { nodeEntry.enableButton.text(RED._('palette.editor.inuse')); @@ -399,6 +442,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+' '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow) @@ -793,19 +837,36 @@ 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; + } + + 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); } @@ -819,6 +880,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() { @@ -980,12 +1049,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 = $('
',{class:"red-ui-palette-module-set"}).appendTo(contentRow); var buttonGroup = $('
',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow); var typeSwatches = {}; @@ -1202,7 +1287,17 @@ RED.palette.editor = (function() { var headerRow = $('
',{class:"red-ui-palette-module-header"}).appendTo(container); var titleRow = $('
').appendTo(headerRow); $('').text(entry.name||entry.id).appendTo(titleRow); - $('').attr('href',entry.url).appendTo(titleRow); + if (entry.url) { + $('').attr('href',entry.url).appendTo(titleRow); + } + if (entry.deprecated) { + const deprecatedWarning = $('').text(RED._('palette.editor.deprecated')).appendTo(titleRow); + let message = $('').text(RED._('palette.editor.deprecatedTip')) + if (typeof entry.deprecated === 'string') { + $('

').text(entry.deprecated).appendTo(message) + } + RED.popover.tooltip(deprecatedWarning, message); + } var descRow = $('

').appendTo(headerRow); $('
',{class:"red-ui-palette-module-description"}).text(entry.description).appendTo(descRow); var metaRow = $('
').appendTo(headerRow); @@ -1452,7 +1547,7 @@ RED.palette.editor = (function() { } } else { // dedicated list management for plugins - if (entry.plugin) { + if (entry.pluginSet) { let e = nodeEntries[entry.name]; if (e) { @@ -1465,9 +1560,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'); + 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 = $('
'); + if (updateStatusState.version) { + $(`${RED._("telemetry.updateAvailableDesc", updateStatusState)}`).appendTo(content) + } + if (count > 0) { + $(``).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 @@ -1711,18 +1821,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(); - $(' ' + opts.count + '').appendTo(updateStatusWidget); + let count = updateStatusState.moduleCount || 0; + if (updateStatusState.version) { + count ++ + } + $(` ${RED._("telemetry.updateAvailable", { count: count })}`).appendTo(updateStatusWidget); + RED.statusBar.show("red-ui-status-package-update"); } else { - RED.statusBar.hide("update"); + RED.statusBar.hide("red-ui-status-package-update"); } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js index 5ae66720c..30130d2d1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js @@ -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", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js index f284f2464..089898a95 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js @@ -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 = $('
',{class:"red-ui-search-result"}).appendTo(container); - - var nodeDiv = $('
',{class:"red-ui-search-result-node"}).appendTo(div); - if (object.type === "junction") { + const div = $('
',{class:"red-ui-search-result"}).appendTo(container); + const nodeDiv = $('
',{class:"red-ui-search-result-node"}).appendTo(div); + + if (nodeItem.suggestionPlaceholder) { + nodeDiv.addClass("red-ui-palette-icon-suggestion") + const iconContainer = $('
',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); + $('').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 = $('
',{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 = $('
',{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) { $('
',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); } if (sf.out.length > 0) { $('
',{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) { $('
',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); } - if (def.outputs > 0) { + if (nodeDef.outputs > 0) { $('
',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv); } } var contentDiv = $('
',{class:"red-ui-search-result-description"}).appendTo(div); - var label = object.label; - object.index += "|"+label.toLowerCase(); + var label = nodeItem.label; + nodeItem.index += "|"+label.toLowerCase(); $('
',{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
').appendTo(pane); var input; if (opt.toggle) { - input = $('').appendTo(row).find("input"); + let label = RED._(opt.label) + if (opt.description) { + label = `

${label}

${RED._(opt.description)}`; + } + input = $('').appendTo(row) + $('').appendTo(row) input.prop('checked',initialState); } else if (opt.options) { $('').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) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js index f4cda8d2c..c09be65ab 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js @@ -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) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index eecd309d1..f5e0df05f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -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'))) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index e7820f83a..08ab0ec0a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -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 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 = "
  • "+counts.join("
  • ")+"
"; + RED.notify("

"+RED._("clipboard.nodesImported")+"

"+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 = "
  • "+counts.join("
  • ")+"
"; - RED.notify("

"+RED._("clipboard.nodesImported")+"

"+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 }; })(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/base.scss b/packages/node_modules/@node-red/editor-client/src/sass/base.scss index 63ab6b77f..afbafe049 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/base.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/base.scss @@ -208,12 +208,10 @@ body { } img { - width: auto\9; height: auto; max-width: 100%; vertical-align: middle; border: 0; - -ms-interpolation-mode: bicubic; } blockquote { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss index ad055b97c..944536845 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss @@ -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; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/forms.scss b/packages/node_modules/@node-red/editor-client/src/sass/forms.scss index a281b9265..3fa8bcc65 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/forms.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/forms.scss @@ -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; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss index 6262597a1..486396c59 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss @@ -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; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss index cdbfa406b..8955887dd 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss @@ -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; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss index 3df2b495b..027e783a3 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss @@ -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; + } +} diff --git a/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss b/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss index 5e0c7fa47..7abae094c 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss @@ -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 { diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-auto-complete.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-auto-complete.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-background-deploy.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-background-deploy.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-background-deploy.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-background-deploy.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-config-select.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-config-select.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-diff-update.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-diff-update.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-diff-update.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-diff-update.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer-location.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer-location.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer-location.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer-location.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-plugins.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-plugins.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-sf-config.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-sf-config.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-timestamp-formatting.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-timestamp-formatting.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js new file mode 100644 index 000000000..02a559136 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js @@ -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": "

Let's take a moment to discover the new features in this release.

", + "ja": "

本リリースの新機能を見つけてみましょう。

", + "fr": "

Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.

" + } + }, + { + title: { + "en-US": "Multiplayer Mode", + "ja": "複数ユーザ同時利用モード", + "fr": "Mode Multi-utilisateur" + }, + image: 'images/nr4-multiplayer-location.png', + description: { + "en-US": `

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.

+

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.

+

Check the release post for details on how to enable this feature in your settings file.

`, + "ja": `

本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。

+

本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。

+

設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。

`, + "fr": `

Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser + lorsque plusieurs personnes modifient des flux en même temps.

+

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.

+

Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité + dans votre fichier de paramètres.

` + } + }, + { + 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": `

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.

`, + "ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`, + "fr": `

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.

` + } + }, + { + title: { + "en-US": "Improved flow diffs", + "ja": "フローの差分表示の改善", + "fr": "Amélioration des différences de flux" + }, + image: 'images/nr4-diff-update.png', + description: { + "en-US": `

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.

+

When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.

`, + "ja": `

フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。

+

これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。

`, + "fr": `

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.

+

Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les + plus importants.

` + } + }, + { + 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": `

The Configuration node selection UI has had a small update to have a dedicated 'add' button + next to the select box.

+

It's a small change, but should make it easier to work with your config nodes.

`, + "ja": `

設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。

+

微修正ですが設定ノードの操作が容易になります。

`, + "fr": `

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.

+

C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.

` + } + }, + { + title: { + "en-US": "Timestamp formatting options", + "ja": "タイムスタンプの形式の項目", + "fr": "Options de formatage de l'horodatage" + }, + image: 'images/nr4-timestamp-formatting.png', + description: { + "en-US": `

Nodes that let you set a timestamp now have options on what format that timestamp should be in.

+

We're keeping it simple to begin with by providing three options:

+

    +
  • Milliseconds since epoch - this is existing behaviour of the timestamp option
  • +
  • ISO 8601 - a common format used by many systems
  • +
  • JavaScript Date Object
  • +
`, + "ja": `

タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。

+

次の3つの項目を追加したことで、簡単に選択できるようになりました:

+

    +
  • エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目
  • +
  • ISO 8601 - 多くのシステムで使用されている共通の形式
  • +
  • JavaScript日付オブジェクト
  • +
`, + "fr": `

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.

+

Nous gardons les choses simples en proposant trois options :

+

    +
  • Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage
  • +
  • ISO 8601 : un format commun utilisé par de nombreux systèmes
  • +
  • Objet Date JavaScript
  • +
` + } + }, + { + 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": `

The flow/global context inputs and the env input + now all include auto-complete suggestions based on the live state of your flows.

+ `, + "ja": `

flow/globalコンテキストやenvの入力を、現在のフローの状態をもとに自動補完で提案するようになりました。

+ `, + "fr": `

Les entrées contextuelles flow/global et l'entrée env + incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.

+ `, + } + }, + { + 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": `

Subflows can now be customised to allow each instance to use a different + config node of a selected type.

+

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.

+ `, + "ja": `

サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。

+

例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。

+ `, + "fr": `

Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un + noeud de configuration d'un type sélectionné.

+

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.

+ ` + } + }, + { + title: { + "en-US": "Remembering palette state", + "ja": "パレットの状態を維持", + "fr": "Mémorisation de l'état de la palette" + }, + description: { + "en-US": `

The palette now remembers what categories you have hidden between reloads - as well as any + filter you have applied.

`, + "ja": `

パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。

`, + "fr": `

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é.

` + } + }, + { + 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": `

The palette manager now shows any plugin modules you have installed, such as + node-red-debugger. Previously they would only be shown if the plugins include + nodes for the palette.

`, + "ja": `

パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。

`, + "fr": `

Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés, + tels que node-red-debugger. Auparavant, ils n'étaient affichés que s'ils contenaient + des noeuds pour la palette.

` + } + }, + { + title: { + "en-US": "Node Updates", + "ja": "ノードの更新", + "fr": "Mises à jour des noeuds" + }, + // image: "images/", + description: { + "en-US": `

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.

+
    +
  • A fully RFC4180 compliant CSV mode
  • +
  • Customisable headers on the WebSocket node
  • +
  • Split node now can operate on any message property
  • +
  • and lots more...
  • +
`, + "ja": `

コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。

+
    +
  • RFC4180に完全に準拠したCSVモード
  • +
  • WebSocketノードのカスタマイズ可能なヘッダ
  • +
  • Splitノードは、メッセージプロパティで操作できるようになりました
  • +
  • 他にも沢山あります...
  • +
`, + "fr": `

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 :

+
    +
  • Un mode CSV entièrement conforme à la norme RFC4180
  • +
  • En-têtes personnalisables pour le noeud WebSocket
  • +
  • Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message
  • +
  • Et bien plus encore...
  • +
` + } + } + ] +} diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js index 02a559136..ab09e8455 100644 --- a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js @@ -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": "

Let's take a moment to discover the new features in this release.

", @@ -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": `

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.

-

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.

-

Check the release post for details on how to enable this feature in your settings file.

`, - "ja": `

本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。

-

本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。

-

設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。

`, - "fr": `

Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser - lorsque plusieurs personnes modifient des flux en même temps.

-

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.

-

Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité - dans votre fichier de paramètres.

` + "en-US": `

Something new

` } }, - { - 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": `

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.

`, - "ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`, - "fr": `

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.

` - } - }, - { - title: { - "en-US": "Improved flow diffs", - "ja": "フローの差分表示の改善", - "fr": "Amélioration des différences de flux" - }, - image: 'images/nr4-diff-update.png', - description: { - "en-US": `

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.

-

When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.

`, - "ja": `

フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。

-

これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。

`, - "fr": `

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.

-

Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les - plus importants.

` - } - }, - { - 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": `

The Configuration node selection UI has had a small update to have a dedicated 'add' button - next to the select box.

-

It's a small change, but should make it easier to work with your config nodes.

`, - "ja": `

設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。

-

微修正ですが設定ノードの操作が容易になります。

`, - "fr": `

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.

-

C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.

` - } - }, - { - title: { - "en-US": "Timestamp formatting options", - "ja": "タイムスタンプの形式の項目", - "fr": "Options de formatage de l'horodatage" - }, - image: 'images/nr4-timestamp-formatting.png', - description: { - "en-US": `

Nodes that let you set a timestamp now have options on what format that timestamp should be in.

-

We're keeping it simple to begin with by providing three options:

-

    -
  • Milliseconds since epoch - this is existing behaviour of the timestamp option
  • -
  • ISO 8601 - a common format used by many systems
  • -
  • JavaScript Date Object
  • -
`, - "ja": `

タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。

-

次の3つの項目を追加したことで、簡単に選択できるようになりました:

-

    -
  • エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目
  • -
  • ISO 8601 - 多くのシステムで使用されている共通の形式
  • -
  • JavaScript日付オブジェクト
  • -
`, - "fr": `

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.

-

Nous gardons les choses simples en proposant trois options :

-

    -
  • Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage
  • -
  • ISO 8601 : un format commun utilisé par de nombreux systèmes
  • -
  • Objet Date JavaScript
  • -
` - } - }, - { - 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": `

The flow/global context inputs and the env input - now all include auto-complete suggestions based on the live state of your flows.

- `, - "ja": `

flow/globalコンテキストやenvの入力を、現在のフローの状態をもとに自動補完で提案するようになりました。

- `, - "fr": `

Les entrées contextuelles flow/global et l'entrée env - incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.

- `, - } - }, - { - 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": `

Subflows can now be customised to allow each instance to use a different - config node of a selected type.

-

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.

- `, - "ja": `

サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。

-

例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。

- `, - "fr": `

Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un - noeud de configuration d'un type sélectionné.

-

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.

- ` - } - }, - { - title: { - "en-US": "Remembering palette state", - "ja": "パレットの状態を維持", - "fr": "Mémorisation de l'état de la palette" - }, - description: { - "en-US": `

The palette now remembers what categories you have hidden between reloads - as well as any - filter you have applied.

`, - "ja": `

パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。

`, - "fr": `

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é.

` - } - }, - { - 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": `

The palette manager now shows any plugin modules you have installed, such as - node-red-debugger. Previously they would only be shown if the plugins include - nodes for the palette.

`, - "ja": `

パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。

`, - "fr": `

Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés, - tels que node-red-debugger. Auparavant, ils n'étaient affichés que s'ils contenaient - des noeuds pour la palette.

` - } - }, - { - title: { - "en-US": "Node Updates", - "ja": "ノードの更新", - "fr": "Mises à jour des noeuds" - }, - // image: "images/", - description: { - "en-US": `

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.

-
    -
  • A fully RFC4180 compliant CSV mode
  • -
  • Customisable headers on the WebSocket node
  • -
  • Split node now can operate on any message property
  • -
  • and lots more...
  • -
`, - "ja": `

コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。

-
    -
  • RFC4180に完全に準拠したCSVモード
  • -
  • WebSocketノードのカスタマイズ可能なヘッダ
  • -
  • Splitノードは、メッセージプロパティで操作できるようになりました
  • -
  • 他にも沢山あります...
  • -
`, - "fr": `

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 :

-
    -
  • Un mode CSV entièrement conforme à la norme RFC4180
  • -
  • En-têtes personnalisables pour le noeud WebSocket
  • -
  • Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message
  • -
  • Et bien plus encore...
  • -
` - } - } + // { + // title: { + // "en-US": "Node Updates", + // "ja": "ノードの更新", + // "fr": "Mises à jour des noeuds" + // }, + // // image: "images/", + // description: { + // "en-US": `

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.

+ //
    + //
  • A fully RFC4180 compliant CSV mode
  • + //
  • Customisable headers on the WebSocket node
  • + //
  • Split node now can operate on any message property
  • + //
  • and lots more...
  • + //
`, + // "ja": `

コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。

+ //
    + //
  • RFC4180に完全に準拠したCSVモード
  • + //
  • WebSocketノードのカスタマイズ可能なヘッダ
  • + //
  • Splitノードは、メッセージプロパティで操作できるようになりました
  • + //
  • 他にも沢山あります...
  • + //
`, + // "fr": `

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 :

+ //
    + //
  • Un mode CSV entièrement conforme à la norme RFC4180
  • + //
  • En-têtes personnalisables pour le noeud WebSocket
  • + //
  • Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message
  • + //
  • Et bien plus encore...
  • + //
` + // } + // } ] } diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html index 7cce956bb..9233975a7 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html +++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.html @@ -29,7 +29,7 @@
- +
diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js index 90c4134a4..194d06175 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js +++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js @@ -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 }; diff --git a/packages/node_modules/@node-red/nodes/core/sequence/17-split.html b/packages/node_modules/@node-red/nodes/core/sequence/17-split.html index b754700cd..975dd593e 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.html +++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.html @@ -21,8 +21,8 @@
- - + +
diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index ecc261416..1af06be11 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -1018,7 +1018,7 @@ "objectSend": "Send a message for each key/value pair", "strBuff": "String / Buffer", "array": "Array", - "splitThe": "Split the", + "splitThe": "Split property", "splitUsing": "Split using", "splitLength": "Fixed length of", "stream": "Handle as a stream of messages", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index a513e14da..c9a6f0166 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -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", diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index 27783be7f..eb27d9411 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -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); diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js index 1aa335f1a..634f5dbf3 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js @@ -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; diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 4ac7cfb5b..3251ff2fa 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -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 }, diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js index 96dab3417..983b4ca52 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/git/index.js @@ -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)) { diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/index.js b/packages/node_modules/@node-red/runtime/lib/telemetry/index.js new file mode 100644 index 000000000..8ff3d845a --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/telemetry/index.js @@ -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 + } + } +} \ No newline at end of file diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js new file mode 100644 index 000000000..acac829fb --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js @@ -0,0 +1,5 @@ +module.exports = (runtime) => { + return { + instanceId: runtime.settings.get('instanceId') + } +} diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js new file mode 100644 index 000000000..ae2a31859 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js @@ -0,0 +1,9 @@ +const os = require('os') + +module.exports = (_) => { + return { + 'os.type': os.type(), + 'os.release': os.release(), + 'os.arch': os.arch() + } +} diff --git a/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js new file mode 100644 index 000000000..173adc752 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js @@ -0,0 +1,8 @@ +const process = require('process') + +module.exports = (runtime) => { + return { + 'env.nodejs': process.version.replace(/^v/, ''), + 'env.node-red': runtime.settings.version + } +} diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index e6f1ca0d0..03ef360d2 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -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" } } diff --git a/packages/node_modules/@node-red/util/lib/log.js b/packages/node_modules/@node-red/util/lib/log.js index 14b93d5b5..fa8c0416d 100644 --- a/packages/node_modules/@node-red/util/lib/log.js +++ b/packages/node_modules/@node-red/util/lib/log.js @@ -52,11 +52,11 @@ var levelColours = { 10: 'red', 20: 'red', 30: 'yellow', - 40: 'white', + 40: '', 50: 'cyan', 60: 'gray', - 98: 'white', - 99: 'white' + 98: '', + 99: '' }; var logHandlers = []; @@ -99,7 +99,12 @@ const utilLog = function (msg, level) { d.getMinutes().toString().padStart(2, '0'), d.getSeconds().toString().padStart(2, '0') ].join(':'); - console.log(chalk[levelColours[level] || 'white'](`${d.getDate()} ${months[d.getMonth()]} ${time} - ${msg}`)) + const logLine = `${d.getDate()} ${months[d.getMonth()]} ${time} - ${msg}` + if (levelColours[level]) { + console.log(chalk[levelColours[level]](logLine)) + } else { + console.log(logLine) + } } var consoleLogger = function(msg) { diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 5f3c9da25..d98b69f8f 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -63,7 +63,8 @@ var knownOpts = { "verbose": Boolean, "safe": Boolean, "version": Boolean, - "define": [String, Array] + "define": [String, Array], + "no-telemetry": Boolean }; var shortHands = { "?":["--help"], @@ -97,6 +98,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 run an admin command"); console.log(""); @@ -222,6 +224,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 diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index e8bb01228..5c4ce0e94 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -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 */ diff --git a/test/unit/@node-red/runtime/lib/api/settings_spec.js b/test/unit/@node-red/runtime/lib/api/settings_spec.js index 9b3b94229..0e9e20422 100644 --- a/test/unit/@node-red/runtime/lib/api/settings_spec.js +++ b/test/unit/@node-red/runtime/lib/api/settings_spec.js @@ -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: { diff --git a/test/unit/@node-red/runtime/lib/telemetry/index_spec.js b/test/unit/@node-red/runtime/lib/telemetry/index_spec.js new file mode 100644 index 000000000..7ab1b89d1 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/telemetry/index_spec.js @@ -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) + + }) +}) \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/telemetry/metrics/01-core_spec.js b/test/unit/@node-red/runtime/lib/telemetry/metrics/01-core_spec.js new file mode 100644 index 000000000..d1e012e5a --- /dev/null +++ b/test/unit/@node-red/runtime/lib/telemetry/metrics/01-core_spec.js @@ -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") + }) +}) \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/telemetry/metrics/02-os_spec.js b/test/unit/@node-red/runtime/lib/telemetry/metrics/02-os_spec.js new file mode 100644 index 000000000..77a4b60af --- /dev/null +++ b/test/unit/@node-red/runtime/lib/telemetry/metrics/02-os_spec.js @@ -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") + }) +}) \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/telemetry/metrics/03-env_spec.js b/test/unit/@node-red/runtime/lib/telemetry/metrics/03-env_spec.js new file mode 100644 index 000000000..eff539270 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/telemetry/metrics/03-env_spec.js @@ -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') + }) +}) \ No newline at end of file