diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e77657c8..6ab22448f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,77 @@ +#### 4.1.0-beta.1: Beta Release + +Editor + + - Add update notification (#5117) @knolleary + - Add a node annotation if the info property is set (#4955) @knolleary + - Add node suggestion api to editor and apply to typeSearch (#5135) @knolleary + - Node filter support for typedInput's builtin node (#5154) @GogoVega + - Import `got` module only once when sending metrics (#5152) @GogoVega + - Trigger button action of the selected nodes with new Hotkey (#4924) @GogoVega + - Handle deleting of subflow context entries (#5071) @knolleary + - Add the `changed` badge to the config node (#5062) @GogoVega + - Default Palette Search: Sort by Downloads (#5108) @joepavitt + - Show deprecated message if module flagged (#5134) @knolleary + - Add link icon to node docs and warn for major update (#5143) @GogoVega + - Support for a module with nodes and plugins in the palette (#4945) @GogoVega + - Include module list in global-config node when importing/exporting flows (#4599) @knolleary + - Add `Install all` button to the module list feature (#5123) @GogoVega + - Fix node tab filtering (#5119) @knolleary + - Cleanup global Palette Manager variables (#4958) @GogoVega + - Add a new `update available` widget to statusBar (#4948) @knolleary + - Add a queue while installing or removing a module from the Palette Manager (#4937) @GogoVega + - Ignore state of disabled nodes/flows during deployment (#5054) @GogoVega + - Exclude internal properties from node definition (#5144) @GogoVega + - Refresh config node sidebar when changing lock state of a flow (#5072) @knolleary + - Add a border to better distinguish typedInput type/option dropdowns (#5078) @knolleary + - Fix undo of subflow color change not applying to instances (#5012) @GogoVega + - Properly handle scale factor in getLinksAtPoint for firefox (#5087) @knolleary + - Update markdown drop-target appearance (#5059) @knolleary + - Support for disabled flows in Sidebar Config (#5061) @GogoVega + - Support text drag & drop into markdown editor (#5056) @gorenje + - Truncate long messages from the Debug Sidebar (#4944) @GogoVega + - Handle link nodes with show/hide label action (#5106) @knolleary + - Update the Node-RED logo to use the hex variant (#5103) @joepavitt + - Add the vertical marker to the palette hand (#4954) @GogoVega + - Monaco Latest (0.52.0) (#4930) @Steve-Mcl + - Updates monaco to 0.52.0 for action widget sizing fix (#5110) @Steve-Mcl + - Bump Multer to 2.0.1 (#5151) @hardillb + - Upgrade multer to 2.0.0 (#5148) @hardillb + - Update dompurify (#5120) @knolleary + - Colourise the Node-RED logs (#5109) @hardillb + - Only apply colours for non-default log lines (#5129) @knolleary + - feat: import default export if plugin is a transpiled es module (#5137) @dschmidt + - Add an additional git_auth_failed condition (#5145) @sonnyp + - Fix Sass deprecation warnings (#4922) @bonanitech + - chore(editor)!: remove Internet Explorer polyfill (#5070) @Rotzbua + - Remove Internet Explorer CSS hacks (#5142) @bonanitech + +Runtime + + - fix: set label in themeSettings.deployButton despite type attribute (#5053) @matiseni51 + - fix(html): correct buggy html (#4768) @Rotzbua + - Update dev (#4836) @knolleary + - Update dependencies (#5107) @knolleary + - Bump i18next to 24.x and auto-migrate message catalog format (#5088) @knolleary + - chore(editor): update `DOMPurify` flag (#5073) @Rotzbua + - Add .editorconfig to .gitignore (#5060) @gorenje + +Nodes + + - Complete/Status: Fix complete node to not feedback immediately connected nodes (#5114) @dceejay + - Function: Add URL/URLSearchParams to Function sandbox (#5159) @knolleary + - Function: Add support for node: prefixed modules in function node (#5067) @knolleary + - Function: Add globalFunctionTimeout (#4985) @vasuvanka + - Exec: Make encoding handling consistent between stdout and err (#5158) @knolleary + - Split: Let split node send original msg to complete node (#5113) @dceejay + - Split: Rename Split The field (#5130) @dceejay + - MQTT: Ensure generated mqtt clientId uses only valid chars (#5156) @knolleary + - HTTP Request: Fix the capitisation for ALPN settings in http-request (#5105) @hardillb + - HTTP Request: (docs) Recommend HTTPS over HTTP (#5141) @ZJvandeWeg + - HTTP Request: Include URL query params in HTTP Digest (#5166) @hardillb + - Catch: Add code to error object sent by Catch node (#5081) @knolleary + - Debug: Improve debug display of error objects (#5079) @knolleary + #### 4.0.9: Maintenance Release Editor diff --git a/package.json b/package.json index db3b94f22..2a132cd9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -26,7 +26,7 @@ } ], "dependencies": { - "acorn": "8.14.1", + "acorn": "8.15.0", "acorn-walk": "8.3.4", "ajv": "8.17.1", "async-mutex": "0.5.0", @@ -63,9 +63,9 @@ "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-red-admin": "^4.1.0", "node-watch": "0.7.4", "nopt": "5.0.0", "oauth2orize": "1.12.0", @@ -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..a9f91fa3c 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "4.1.0-beta.0", - "@node-red/editor-client": "4.1.0-beta.0", + "@node-red/util": "4.1.0-beta.1", + "@node-red/editor-client": "4.1.0-beta.1", "bcryptjs": "3.0.2", "body-parser": "1.20.3", "clone": "2.1.2", @@ -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 a59dcacf4..1aa92c370 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 @@ -265,7 +265,7 @@ "download": "Download", "importUnrecognised": "Imported unrecognised type:", "importUnrecognised_plural": "Imported unrecognised types:", - "importWithModuleInfo": "Required dependencies missing", + "importWithModuleInfo": "Required modules missing", "importWithModuleInfoDesc": "These nodes are not currently installed in your palette and are required for the imported flow:", "importDuplicate": "Imported duplicate node:", "importDuplicate_plural": "Imported duplicate nodes:", @@ -626,6 +626,7 @@ "yearsMonthsV": "__y__ years, __count__ month ago", "yearsMonthsV_plural": "__y__ years, __count__ months ago" }, + "manageModules": "Manage modules", "nodeCount": "__label__ node", "nodeCount_plural": "__label__ nodes", "pluginCount": "__count__ plugin", @@ -643,9 +644,12 @@ "update": "update to __version__", "updated": "updated", "install": "install", + "installAll": "Install all", "installed": "installed", + "installing": "Module installation in progress: __module__", "conflict": "conflict", "conflictTip": "

This module cannot be installed as it includes a
node type that has already been installed

Conflicts with __module__

", + "majorVersion": "

This is a major version update of the node. Check the documentation for details of the update.

", "loading": "Loading catalogues...", "tab-nodes": "Nodes", "tab-install": "Install", @@ -653,9 +657,12 @@ "sortRelevance": "relevance", "sortAZ": "a-z", "sortRecent": "recent", + "successfulInstall": "Successfully installed modules", "more": "+ __count__ more", "upload": "Upload module tgz file", "refresh": "Refresh module list", + "deprecated": "deprecated", + "deprecatedTip": "This module has been deprecated", "errors": { "catalogLoadFailed": "

Failed to load node catalogue.

Check the browser console for more information

", "installFailed": "

Failed to install: __module__

__message__

Check the log for more information

", @@ -1278,5 +1285,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 User 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/locales/fr/editor.json b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json index ddc464650..8d12615b7 100644 --- a/packages/node_modules/@node-red/editor-client/locales/fr/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/fr/editor.json @@ -111,6 +111,7 @@ "userSettings": "Paramètres de l'utilisateur", "nodes": "Noeuds", "displayStatus": "Afficher l'état du noeud", + "displayInfoIcon": "Afficher l'icône d'information sur le noeud", "displayConfig": "Noeuds de configuration", "import": "Importer", "importExample": "Importer un exemple de flux", @@ -264,6 +265,8 @@ "download": "Télécharger", "importUnrecognised": "Importation d'un type inconnu :", "importUnrecognised_plural": "Importation de plusieurs types inconnus :", + "importWithModuleInfo": "Modules requis manquants", + "importWithModuleInfoDesc": "Ces noeuds ne sont pas actuellement installés dans votre palette et sont requis pour le flux importé :", "importDuplicate": "Noeud en double importé :", "importDuplicate_plural": "Noeuds en double importés :", "nodesExported": "Noeuds exportés vers le presse-papiers", @@ -623,12 +626,15 @@ "yearsMonthsV": "il y a __y__ ans, __count__ mois", "yearsMonthsV_plural": "il y a __y__ ans, __count__ mois" }, + "manageModules": "Gérer les modules", "nodeCount": "__label__ noeud", "nodeCount_plural": "__label__ noeuds", "pluginCount": "__count__ plugin", "pluginCount_plural": "__count__ plugins", "moduleCount": "__count__ module disponible", "moduleCount_plural": "__count__ modules disponibles", + "updateCount": "__count__ mise à jour disponible", + "updateCount_plural": "__count__ mises à jour disponibles", "inuse": "En cours d'utilisation", "enableall": "Activer tout", "disableall": "Désactiver tout", @@ -638,9 +644,12 @@ "update": "Mettre à jour vers __version__", "updated": "Mis à jour", "install": "Installer", + "installAll": "Installer tout", "installed": "Installé", + "installing": "Installation du module en cours : __module__", "conflict": "Conflit", "conflictTip": "

Ce module ne peut pas être installé car il inclut un
type de noeud qui a déjà été installé

Conflits avec __module__

", + "majorVersion": "

Il s'agit d'une mise à jour majeure du noeud. Consulter la documentation pour plus de détails sur la mise à jour.

", "loading": "Chargement des catalogues...", "tab-nodes": "Noeuds", "tab-install": "Installer", @@ -648,9 +657,12 @@ "sortRelevance": "Pertinence", "sortAZ": "A-Z", "sortRecent": "Récent", + "successfulInstall": "Modules installés avec succès", "more": "+ __count__ en plus", "upload": "Charger le fichier .tgz du module", "refresh": "Actualiser la liste des modules", + "deprecated": "Obsolète", + "deprecatedTip": "Ce module est obsolète", "errors": { "catalogLoadFailed": "

Échec du chargement du catalogue de noeuds.

Vérifier la console du navigateur pour plus d'informations

", "installFailed": "

Échec lors de l'installation : __module__

__message__

Consulter le journal pour plus d'informations

", @@ -1262,6 +1274,16 @@ "header": "Variables d'environnement globales", "revert": "Rétablir" }, + "telemetry": { + "label": "Notifications de mise à jour", + "settingsTitle": "Activer les notifications de mise à jour", + "settingsDescription": "

Node-RED peut vous avertir de la disponibilité d'une nouvelle version. Vous êtes ainsi informé des dernières fonctionnalités et correctifs.

Cela nécessite d'envoyer des données anonymes à l'équipe Node-RED. Elles n'incluent aucun détail sur vos flux ou vos utilisateurs.

Pour plus d'informations sur les informations collectées et leur utilisation, veuillez consulter la documentation.

", + "settingsDescription2": "

Vous pouvez modifier ce paramètre à tout moment dans les paramètres de l'éditeur.

", + "enableLabel": "Oui, activer les notifications", + "disableLabel": "Non, ne pas activer les notifications", + "updateAvailable": "Mise(s) à jour disponible(s)", + "updateAvailableDesc": "Node-RED __version__ est désormais disponible" + }, "action-list": { "toggle-show-tips": "Basculer l'affichage des astuces", "show-about": "Afficher la description de Node-RED", diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index e6c02590b..b65730c52 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -111,6 +111,7 @@ "userSettings": "ユーザ設定", "nodes": "ノード", "displayStatus": "ノードのステータスを表示", + "displayInfoIcon": "ノード情報のアイコンを表示", "displayConfig": "設定ノード", "import": "読み込み", "importExample": "フロー例を読み込み", @@ -264,6 +265,8 @@ "download": "ダウンロード", "importUnrecognised": "認識できない型が読み込まれました:", "importUnrecognised_plural": "認識できない型が読み込まれました:", + "importWithModuleInfo": "必要なモジュールが不足", + "importWithModuleInfoDesc": "以下のノードは現在パレットにインストールされていませんが、読み込んだフローには必要なノードです:", "importDuplicate": "重複したノードを読み込みました:", "importDuplicate_plural": "重複したノードを読み込みました:", "nodesExported": "クリップボードへフローを書き出しました", @@ -623,12 +626,15 @@ "yearsMonthsV": "__y__ 年 __count__ ヵ月前", "yearsMonthsV_plural": "__y__ 年 __count__ ヵ月前" }, + "manageModules": "モジュールを管理", "nodeCount": "__label__ 個のノード", "nodeCount_plural": "__label__ 個のノード", "pluginCount": "__count__ 個のプラグイン", "pluginCount_plural": "__count__ 個のプラグイン", "moduleCount": "__count__ 個のモジュール", "moduleCount_plural": "__count__ 個のモジュール", + "updateCount": "__count__ 個の更新が存在", + "updateCount_plural": "__count__ 個の更新が存在", "inuse": "使用中", "enableall": "全て有効化", "disableall": "全て無効化", @@ -638,9 +644,12 @@ "update": "__version__ へ更新", "updated": "更新済", "install": "ノードを追加", + "installAll": "全てインストール", "installed": "追加しました", + "installing": "モジュールのインストールが進行中: __module__", "conflict": "競合", "conflictTip": "

インストール済みのノードの種別と競合しているため
ノードをインストールできません

競合: __module__

", + "majorVersion": "

これはノードのメジャーバージョンの更新です。更新内容の詳細については、ドキュメントを確認してください。

", "loading": "カタログを読み込み中", "tab-nodes": "現在のノード", "tab-install": "ノードを追加", @@ -648,9 +657,12 @@ "sortRelevance": "関連順", "sortAZ": "辞書順", "sortRecent": "日付順", + "successfulInstall": "モジュールのインストールが成功", "more": "+ さらに __count__ 個", "upload": "モジュールのtgzファイルをアップロード", "refresh": "モジュールリスト更新", + "deprecated": "非推奨", + "deprecatedTip": "本モジュールは非推奨です", "errors": { "catalogLoadFailed": "

ノードのカタログの読み込みに失敗しました。

詳細はブラウザのコンソールを確認してください。

", "installFailed": "

追加処理が失敗しました: __module__

__message__

詳細はログを確認してください。

", @@ -1262,6 +1274,16 @@ "header": "グローバル環境変数", "revert": "破棄" }, + "telemetry": { + "label": "更新の通知", + "settingsTitle": "更新の通知を有効化", + "settingsDescription": "

新バージョンのNode-REDが存在した時に、通知を受けることができます。この機能によって最新機能の提供や修正があることを把握できます。

この通知を受け取るには、匿名化されたデータをNode-REDチームに送る必要があります。このデータには、フローやユーザの詳細は含まれません。

収集される情報と利用方法の詳細については、ドキュメントを参照してください。

", + "settingsDescription2": "

この設定はユーザ設定からいつでも変更できます。

", + "enableLabel": "はい、通知を有効にします", + "disableLabel": "いいえ、通知を有効にしません", + "updateAvailable": "更新を利用可能", + "updateAvailableDesc": "現在、Node-RED __version__ が利用可能" + }, "action-list": { "toggle-show-tips": "ヒント表示切替", "show-about": "Node-REDの説明を表示", @@ -1302,6 +1324,7 @@ "toggle-show-grid": "グリッド表示切替", "toggle-snap-grid": "ノードの配置補助切替", "toggle-status": "ステータス表示切替", + "toggle-node-info-icon": "ノード情報のアイコン表示切替", "show-selected-node-labels": "選択したノードのラベルを表示", "hide-selected-node-labels": "選択したノードのラベルを非表示", "scroll-view-up": "上スクロール", @@ -1414,6 +1437,7 @@ "show-global-env": "グローバル環境変数を表示", "lock-flow": "フローを固定", "unlock-flow": "フローの固定を解除", - "show-node-help": "ノードのヘルプを表示" + "show-node-help": "ノードのヘルプを表示", + "trigger-selected-nodes-action": "選択したノードのアクションを実行" } } diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index c50b279a1..9cb219576 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "license": "Apache-2.0", "repository": { "type": "git", 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 131d40d18..b66afc4fe 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 @@ -44,6 +44,51 @@ RED.nodes = (function() { var dirty = false; + const internalProperties = [ + "changed", + "dirty", + "id", + "inputLabels", + "moved", + "outputLabels", + "selected", + "type", + "users", + "valid", + "validationErrors", + "wires", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "_", + "_config", + "_def", + "_orig" + ]; + function setDirty(d) { dirty = d; if (!d) { @@ -231,7 +276,6 @@ RED.nodes = (function() { def.type = nt; nodeDefinitions[nt] = def; - if (def.defaults) { for (var d in def.defaults) { if (def.defaults.hasOwnProperty(d)) { @@ -242,6 +286,11 @@ RED.nodes = (function() { console.warn(err); } } + + if (internalProperties.includes(d)) { + console.warn(`registerType: ${nt}: the property "${d}" is internal and cannot be used.`); + delete def.defaults[d]; + } } } } @@ -689,7 +738,7 @@ RED.nodes = (function() { } } - function addNode(n) { + function addNode(n, opt) { let newNode if (!n.__isProxy__) { newNode = new Proxy(n, nodeProxyHandler) @@ -728,7 +777,7 @@ RED.nodes = (function() { nodeLinks[n.id] = {in:[],out:[]}; } } - RED.events.emit('nodes:add',newNode); + RED.events.emit('nodes:add',newNode, opt); return newNode } function addLink(l) { @@ -1494,7 +1543,14 @@ RED.nodes = (function() { } /** * Converts the current node selection to an exportable JSON Object - **/ + * @param {Array} set the node selection to export + * @param {Object} options + * @param {Record} [options.exportedIds] + * @param {Record} [options.exportedSubflows] + * @param {Record} [options.exportedConfigNodes] + * @param {boolean} [options.includeModuleConfig] + * @returns {Array} + */ function createExportableNodeSet(set, { exportedIds, exportedSubflows, @@ -1582,10 +1638,14 @@ RED.nodes = (function() { return nns; } - // Create the Flow JSON for the current configuration - // opts.credentials (whether to include (known) credentials) - default: true - // opts.dimensions (whether to include node dimensions) - default: false - // opts.includeModuleConfig (whether to include modules) - default: false + /** + * Converts the current configuration to an exportable JSON Object + * @param {object} opts + * @param {boolean} [opts.credentials] whether to include (known) credentials. Default `true`. + * @param {boolean} [opts.dimensions] whether to include node dimensions. Default `false`. + * @param {boolean} [opts.includeModuleConfig] whether to include modules. Default `false`. + * @returns {Array} + */ function createCompleteNodeSet(opts) { var nns = []; var i; @@ -1848,14 +1908,23 @@ RED.nodes = (function() { * - id:copy - import with new id * - id:replace - import over the top of existing * - modules: map of module:version - hints for unknown nodes + * - applyNodeDefaults - whether to apply default values to the imported nodes (default: false) */ function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) { - const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} } + const defOpts = { + generateIds: false, + addFlow: false, + markChanged: false, + reimport: false, + importMap: {}, + applyNodeDefaults: false + } options = Object.assign({}, defOpts, options) options.importMap = options.importMap || {} const createNewIds = options.generateIds; const reimport = (!createNewIds && !!options.reimport) const createMissingWorkspace = options.addFlow; + const applyNodeDefaults = options.applyNodeDefaults; var i; var n; var newNodes; @@ -2000,15 +2069,30 @@ RED.nodes = (function() { // Provide option to install missing modules notificationOptions.buttons = [ { - text: "Manage dependencies", - class:"primary", + text: RED._("palette.editor.manageModules"), + class: "primary", click: function(e) { unknownNotification.close(); RED.actions.invoke('core:manage-palette', { view: 'install', filter: '"' + missingModules.join('", "') + '"' - }) + }); + } + }, + { + text: RED._("palette.editor.installAll"), + class: "pull-left", + click: function(e) { + unknownNotification.close(); + + RED.actions.invoke('core:manage-palette', { + autoInstall: true, + modules: missingModules.reduce((modules, moduleName) => { + modules[moduleName] = options.modules[moduleName]; + return modules; + }, {}), + }); } } ] @@ -2234,6 +2318,13 @@ RED.nodes = (function() { for (d in def.defaults) { if (def.defaults.hasOwnProperty(d)) { configNode[d] = n[d]; + if (applyNodeDefaults && n[d] === undefined) { + // If the node has a default value, but the imported node does not + // set it, then set it to the default value + if (def.defaults[d].value !== undefined) { + configNode[d] = JSON.parse(JSON.stringify(def.defaults[d].value)) + } + } configNode._config[d] = JSON.stringify(n[d]); if (def.defaults[d].type) { configNode._configNodeReferences.add(n[d]) @@ -2508,6 +2599,13 @@ RED.nodes = (function() { for (d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { node[d] = n[d]; + if (applyNodeDefaults && n[d] === undefined) { + // If the node has a default value, but the imported node does not + // set it, then set it to the default value + if (node._def.defaults[d].value !== undefined) { + node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value)) + } + } node._config[d] = JSON.stringify(n[d]); } } @@ -2761,7 +2859,8 @@ RED.nodes = (function() { workspaces:new_workspaces, subflows:new_subflows, missingWorkspace: missingWorkspace, - removedNodes: removedNodes + removedNodes: removedNodes, + nodeMap: node_map } } @@ -3164,21 +3263,33 @@ RED.nodes = (function() { } } } + + /** + * Gets the module list for the given nodes + * @param {Array} nodes the nodes to search in + * @returns {Record} an object with {[moduleName]: moduleVersion} + */ function getModuleListForNodes(nodes) { const modules = {} - nodes.forEach(n => { - const nodeSet = RED.nodes.registry.getNodeSetForType(n.type) - if (nodeSet) { - modules[nodeSet.module] = nodeSet.version + const typeSet = new Set() + nodes.forEach((n) => { + if (!typeSet.has(n.type)) { + typeSet.add(n.type) + const nodeSet = RED.nodes.registry.getNodeSetForType(n.type) + if (nodeSet) { + modules[nodeSet.module] = nodeSet.version + nodeSet.types.forEach((t) => typeSet.add(t)) + } } }) return modules } + function updateGlobalConfigModuleList(nodes) { const modules = getModuleListForNodes(nodes) delete modules['node-red'] const hasModules = (Object.keys(modules).length > 0) - let globalConfigNode = nodes.find(n => n.type === 'global-config') + let globalConfigNode = nodes.find((n) => n.type === 'global-config') if (!globalConfigNode && hasModules) { globalConfigNode = { id: RED.nodes.id(), @@ -3187,7 +3298,7 @@ RED.nodes = (function() { modules } nodes.push(globalConfigNode) - } else if (globalConfigNode) { + } else if (hasModules) { globalConfigNode.modules = modules } } 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/common/typedInput.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js index 525cc3d1c..9a1bc030c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js @@ -519,10 +519,25 @@ } }, expand: function () { - var that = this; + const that = this; + let filter; + if (that.options.node) { + let nodeFilter = that.options.node.filter; + if ((typeof nodeFilter === "string" || typeof nodeFilter === "object") && nodeFilter) { + if (!Array.isArray(nodeFilter)) { + nodeFilter = [nodeFilter]; + } + filter = function (node) { + return nodeFilter.includes(node.type); + }; + } else if (typeof nodeFilter === "function") { + filter = nodeFilter; + } + } RED.tray.hide(); RED.view.selectNodes({ single: true, + filter: filter, selected: [that.value()], onselect: function (selection) { that.value(selection.id); 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/event-log.js b/packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js index 54ed0a27b..a641740f6 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js @@ -15,11 +15,14 @@ **/ RED.eventLog = (function() { - var template = ''; + const template = ''; + + let eventLogEditor; + let backlog = []; + let shown = false; + + const activeLogs = new Set() - var eventLogEditor; - var backlog = []; - var shown = false; function appendLogLine(line) { backlog.push(line); @@ -38,6 +41,18 @@ RED.eventLog = (function() { init: function() { $(template).appendTo("#red-ui-editor-node-configs"); RED.actions.add("core:show-event-log",RED.eventLog.show); + + const statusWidget = $('
'); + statusWidget.on("click", function(evt) { + RED.actions.invoke("core:show-event-log"); + }) + RED.statusBar.add({ + id: "red-ui-event-log-status", + align: "right", + element: statusWidget + }); + // RED.statusBar.hide("red-ui-event-log-status"); + }, show: function() { if (shown) { @@ -98,6 +113,12 @@ RED.eventLog = (function() { }, log: function(id,payload) { var ts = (new Date(payload.ts)).toISOString()+" "; + if (!payload.end) { + activeLogs.add(id) + } else { + activeLogs.delete(id); + } + if (payload.type) { ts += "["+payload.type+"] " } @@ -111,6 +132,11 @@ RED.eventLog = (function() { appendLogLine(ts+line); }) } + if (activeLogs.size > 0) { + RED.statusBar.show("red-ui-event-log-status"); + } else { + RED.statusBar.hide("red-ui-event-log-status"); + } }, startEvent: function(name) { backlog.push(""); 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 e73a3a9b1..5cc1faddc 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,85 @@ RED.palette.editor = (function() { function _refreshNodeModule(module) { if (!nodeEntries.hasOwnProperty(module)) { - nodeEntries[module] = {info:RED.nodes.registry.getModule(module)}; - var index = [module]; - for (var s in nodeEntries[module].info.sets) { - if (nodeEntries[module].info.sets.hasOwnProperty(s)) { - index.push(s); - index = index.concat(nodeEntries[module].info.sets[s].types) + const nodeInfo = RED.nodes.registry.getModule(module); + let index = [module]; + + nodeEntries[module] = { + info: { + name: nodeInfo.name, + version: nodeInfo.version, + local: nodeInfo.local, + nodeSet: nodeInfo.sets, + }, + }; + + if (nodeInfo.pending_version) { + nodeEntries[module].info.pending_version = nodeInfo.pending_version; + } + + if (loadedIndex[module] && loadedIndex[module].url) { + // Add the link to the node documentation if the catalog contains it + nodeEntries[module].info.url = loadedIndex[module].url; + } + + for (const set in nodeInfo.sets) { + if (nodeInfo.sets.hasOwnProperty(set)) { + index.push(set); + index = index.concat(nodeInfo.sets[set].types); } } + nodeEntries[module].index = index.join(",").toLowerCase(); nodeList.editableList('addItem', nodeEntries[module]); } else { - var moduleInfo = nodeEntries[module].info; - var nodeEntry = nodeEntries[module].elements; - if (nodeEntry) { - if (moduleInfo.plugin) { - nodeEntry.enableButton.hide(); - nodeEntry.removeButton.show(); + if (nodeEntries[module].info.pluginSet && !nodeEntries[module].info.nodeSet) { + // Since plugins are loaded before nodes, check if the module has nodes too + const nodeInfo = RED.nodes.registry.getModule(module); + + if (nodeInfo) { + let index = [nodeEntries[module].index]; + + for (const set in nodeInfo.sets) { + if (nodeInfo.sets.hasOwnProperty(set)) { + index.push(set); + index = index.concat(nodeInfo.sets[set].types) + } + } + + nodeEntries[module].info.nodeSet = nodeInfo.sets; + nodeEntries[module].index = index.join(",").toLowerCase(); + } + } + const moduleInfo = nodeEntries[module].info; + const nodeEntry = nodeEntries[module].elements; + if (nodeEntry) { + const setCount = []; + + if (moduleInfo.pluginSet) { let pluginCount = 0; - for (let setName in moduleInfo.sets) { - if (moduleInfo.sets.hasOwnProperty(setName)) { - let set = moduleInfo.sets[setName]; - if (set.plugins) { + for (const setName in moduleInfo.pluginSet) { + if (moduleInfo.pluginSet.hasOwnProperty(setName)) { + let set = moduleInfo.pluginSet[setName]; + if (set.plugins && set.plugins.length) { pluginCount += set.plugins.length; + } else if (set.plugins && !!RED.plugins.getPlugin(setName)) { + // `registerPlugin` in runtime not called but called in editor, add it + pluginCount++; } } } - - nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount})); - } else { + setCount.push(RED._('palette.editor.pluginCount', { count: pluginCount })); + + if (!moduleInfo.nodeSet) { + // Module only have plugins + nodeEntry.enableButton.hide(); + nodeEntry.removeButton.show(); + } + } + + if (moduleInfo.nodeSet) { var activeTypeCount = 0; var typeCount = 0; var errorCount = 0; @@ -322,10 +370,10 @@ RED.palette.editor = (function() { nodeEntries[module].totalUseCount = 0; nodeEntries[module].setUseCount = {}; - for (var setName in moduleInfo.sets) { - if (moduleInfo.sets.hasOwnProperty(setName)) { - var inUseCount = 0; - const set = moduleInfo.sets[setName]; + for (const setName in moduleInfo.nodeSet) { + if (moduleInfo.nodeSet.hasOwnProperty(setName)) { + let inUseCount = 0; + const set = moduleInfo.nodeSet[setName]; const setElements = nodeEntry.sets[setName] if (set.err) { @@ -342,8 +390,8 @@ RED.palette.editor = (function() { activeTypeCount += set.types.length; } typeCount += set.types.length; - for (var i=0;i 0) { nodeEntry.enableButton.text(RED._('palette.editor.inuse')); @@ -399,6 +447,7 @@ RED.palette.editor = (function() { nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0)); } } + nodeEntry.setCount.text(setCount.join(" & ") || RED._("sidebar.info.empty")); } if (moduleInfo.pending_version) { nodeEntry.versionSpan.html(moduleInfo.version+' '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow) @@ -700,6 +749,9 @@ RED.palette.editor = (function() { refreshCatalogues() RED.actions.add("core:manage-palette", function(opts) { + if (opts && opts.autoInstall && opts.modules) { + autoInstallModules(opts.modules); + } else { RED.userSettings.show('palette'); if (opts) { if (opts.view) { @@ -713,9 +765,15 @@ RED.palette.editor = (function() { } } } - }); + } + }); RED.events.on('registry:module-updated', function(ns) { + if (nodeEntries[ns.module]) { + // Set the node/plugin as updated + nodeEntries[ns.module].info.pending_version = ns.version; + } + refreshNodeModule(ns.module); refreshUpdateStatus(); }); @@ -789,19 +847,41 @@ RED.palette.editor = (function() { }) RED.events.on("registry:plugin-module-added", function(module) { - if (!nodeEntries.hasOwnProperty(module)) { - nodeEntries[module] = {info:RED.plugins.getModule(module)}; - var index = [module]; - for (var s in nodeEntries[module].info.sets) { - if (nodeEntries[module].info.sets.hasOwnProperty(s)) { - index.push(s); - index = index.concat(nodeEntries[module].info.sets[s].types) + const pluginInfo = RED.plugins.getModule(module); + let index = [module]; + + nodeEntries[module] = { + info: { + name: pluginInfo.name, + version: pluginInfo.version, + local: pluginInfo.local, + pluginSet: pluginInfo.sets, + } + }; + + if (pluginInfo.pending_version) { + nodeEntries[module].info.pending_version = pluginInfo.pending_version; + } + + if (loadedIndex[module] && loadedIndex[module].url) { + // Add the link to the plugin documentation if the catalog contains it + nodeEntries[module].info.url = loadedIndex[module].url; + } + + for (const set in pluginInfo.sets) { + if (pluginInfo.sets.hasOwnProperty(set)) { + index.push(set); + // TODO: not sure plugin has `types` property + index = index.concat(pluginInfo.sets[set].types) } } + nodeEntries[module].index = index.join(",").toLowerCase(); nodeList.editableList('addItem', nodeEntries[module]); } else { + // Since plugins are loaded before nodes, + // `nodeEntries[module]` should be undefined _refreshNodeModule(module); } @@ -815,6 +895,14 @@ RED.palette.editor = (function() { } } }); + + RED.events.on("notification/update-available", function (msg) { + const updateKnownAbout = updateStatusState.version === msg.version + updateStatusState.version = msg.version + if (updateStatusWidgetPopover && !updateKnownAbout) { + setTimeout(() => { updateStatusWidgetPopover.open(); setTimeout(() => updateStatusWidgetPopover.close(), 20000) }, 1000) + } + }) } function getSettingsPane() { @@ -912,6 +1000,12 @@ RED.palette.editor = (function() { var headerRow = $('
',{class:"red-ui-palette-module-header"}).appendTo(container); var titleRow = $('
').appendTo(headerRow); $('').text(entry.name).appendTo(titleRow); + + if (entry.url) { + // Add the link icon to the node documentation + $('').attr('href', entry.url).appendTo(titleRow); + } + var metaRow = $('
').appendTo(headerRow); var versionSpan = $('').text(entry.version).appendTo(metaRow); @@ -976,12 +1070,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 = {}; @@ -1198,7 +1308,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); @@ -1339,63 +1459,88 @@ RED.palette.editor = (function() { $('
').appendTo(installTab); } - function update(entry,version,url,container,done) { + function update(entry, version, url, container, done) { if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { done(new Error('Palette not editable')); return; } - var notification = RED.notify(RED._("palette.editor.confirm.update.body",{module:entry.name}),{ + + let notification; + let msg = RED._("palette.editor.confirm.update.body", { module: entry.name }); + const buttons = [ + { + text: RED._("common.label.cancel"), + click: function () { + notification.close(); + } + }, + { + text: RED._("palette.editor.confirm.button.update"), + class: "primary red-ui-palette-module-install-confirm-button-update", + click: function () { + const spinner = RED.utils.addSpinnerOverlay(container, true); + const buttonRow = $('
').appendTo(spinner); + $('').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function (evt) { + evt.preventDefault(); + RED.actions.invoke("core:show-event-log"); + }); + installNodeModule(entry.name, version, url, function (xhr) { + spinner.remove(); + if (xhr) { + if (xhr.responseJSON) { + const notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{ + type: 'error', + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.close"), + click: function () { + notification.close(); + } + }, { + text: RED._("eventLog.view"), + click: function () { + notification.close(); + RED.actions.invoke("core:show-event-log"); + } + } + ] + }); + } + } + done(xhr); + }); + notification.close(); + } + } + ]; + + const currentVersion = semverre.exec(nodeEntries[entry.name].info.version); + const targetVersion = semverre.exec(version); + if (currentVersion && targetVersion) { + if (targetVersion[1] > currentVersion[1]) { + // Updating to Major version + msg = msg + RED._("palette.editor.majorVersion"); + + if (entry.url) { + // Add a button to open the node documentation + buttons.splice(1, 0, { + text: RED._("palette.editor.confirm.button.review"), + class: "primary red-ui-palette-module-install-confirm-button-review", + click: function () { + window.open(entry.url); + } + }); + } + } + } + + notification = RED.notify(msg, { modal: true, fixed: true, - buttons: [ - { - text: RED._("common.label.cancel"), - click: function() { - notification.close(); - } - }, - { - text: RED._("palette.editor.confirm.button.update"), - class: "primary red-ui-palette-module-install-confirm-button-update", - click: function() { - var spinner = RED.utils.addSpinnerOverlay(container, true); - var buttonRow = $('
').appendTo(spinner); - $('').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) { - evt.preventDefault(); - RED.actions.invoke("core:show-event-log"); - }); - installNodeModule(entry.name,version,url,function(xhr) { - spinner.remove(); - if (xhr) { - if (xhr.responseJSON) { - var notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{ - type: 'error', - modal: true, - fixed: true, - buttons: [ - { - text: RED._("common.label.close"), - click: function() { - notification.close(); - } - },{ - text: RED._("eventLog.view"), - click: function() { - notification.close(); - RED.actions.invoke("core:show-event-log"); - } - } - ] - }); - } - } - done(xhr); - }); - notification.close(); - } - } - ] - }) + buttons: buttons + }); } function remove(entry,container,done) { if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { @@ -1448,7 +1593,7 @@ RED.palette.editor = (function() { } } else { // dedicated list management for plugins - if (entry.plugin) { + if (entry.pluginSet) { let e = nodeEntries[entry.name]; if (e) { @@ -1461,9 +1606,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" + spinner; + + if (!notification) { + notification = RED.notify(msg, notificationOptions); + } else { + notification.update(msg, notificationOptions); + } + + installNodeModule(moduleName, moduleVersion, undefined, function(xhr, textStatus, err) { + if (err && xhr.status === 504) { + notification.update(RED._("palette.editor.errors.installTimeout"), { + modal: true, + fixed: true, + buttons: notificationOptions.buttons + }); + } else if (xhr) { + if (xhr.responseJSON) { + notification.update(RED._("palette.editor.errors.installFailed", { module: moduleName, message:xhr.responseJSON.message }), { + type: "error", + modal: true, + fixed: true, + buttons: notificationOptions.buttons + }); + } + } else { + if (moduleArray.length) { + installModule(moduleArray.shift()); + } else { + notification.update(RED._("palette.editor.successfulInstall"), { ...notificationOptions, type: "success", timeout: 10000 }); + } + } + }); + }; + + if (moduleArray.length) { + installModule(moduleArray.shift()); + } + } + const updateStatusWidget = $(''); + 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 @@ -1638,18 +1867,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/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index 5c9c9e5d2..89337e7c9 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -80,7 +80,7 @@ RED.palette = (function() { getNodeCount: function (visibleOnly) { const nodes = catDiv.find(".red-ui-palette-node") if (visibleOnly) { - return nodes.filter(function() { return $(this).css('display') !== 'none'}).length + return nodes.filter(function() { return $(this).attr("data-filter") !== "true"}).length } else { return nodes.length } @@ -572,8 +572,10 @@ RED.palette = (function() { var currentLabel = $(el).attr("data-palette-label"); var type = $(el).attr("data-palette-type"); if (val === "" || re.test(type) || re.test(currentLabel)) { + $(el).attr("data-filter", null) $(this).show(); } else { + $(el).attr("data-filter", "true") $(this).hide(); } }); 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..a55763189 --- /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: '4.0/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: '4.0/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: '4.0/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: '4.0/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: '4.0/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: '4.0/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: '4.0/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: '4.0/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/images/missing-modules.png b/packages/node_modules/@node-red/editor-client/src/tours/images/missing-modules.png new file mode 100644 index 000000000..f96144395 Binary files /dev/null and b/packages/node_modules/@node-red/editor-client/src/tours/images/missing-modules.png differ diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/node-docs.png b/packages/node_modules/@node-red/editor-client/src/tours/images/node-docs.png new file mode 100644 index 000000000..e0f285a1f Binary files /dev/null and b/packages/node_modules/@node-red/editor-client/src/tours/images/node-docs.png differ diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/update-notification.png b/packages/node_modules/@node-red/editor-client/src/tours/images/update-notification.png new file mode 100644 index 000000000..4e4b610e7 Binary files /dev/null and b/packages/node_modules/@node-red/editor-client/src/tours/images/update-notification.png differ 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..8041db469 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,184 +16,79 @@ export default { }, { title: { - "en-US": "Multiplayer Mode", - "ja": "複数ユーザ同時利用モード", - "fr": "Mode Multi-utilisateur" + "en-US": "Update notifications", + "ja": "更新の通知", + "fr": "Notifications de mise à jour" }, - image: 'images/nr4-multiplayer-location.png', + image: 'images/update-notification.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": `

Stay up to date with notifications when there is a new Node-RED version available, or updates to the nodes you have installed

`, + "ja": `

新バージョンのNode-REDの提供や、インストールしたノードの更新があった時に、通知を受け取ることができます。

`, + "fr": `

Désormais vous recevrez une notification lorsqu'une nouvelle version de Node-RED ou une nouvelle version relative à un des noeuds que vous avez installés est disponible

` } }, { title: { - "en-US": "Better background deploy handling", - "ja": "バックグラウンドのデプロイ処理の改善", - "fr": "Meilleure gestion du déploiement en arrière-plan" + "en-US": "Flow documentation", + "ja": "フローのドキュメント", + "fr": "Documentation des flux" }, - image: 'images/nr4-background-deploy.png', + image: 'images/node-docs.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.

` + "en-US": `

Quickly see which nodes have additional documentation with the new documentation icon.

+

Clicking on the icon opens up the Description tab of the node edit dialog.

`, + "ja": `

ドキュメントアイコンによって、どのノードにドキュメントが追加されているかをすぐに確認できます。

+

アイコンをクリックすると、ノード編集ダイアログの説明タブが開きます。

`, + "fr": `

Voyez rapidement quels noeuds ont une documentation supplémentaire avec la nouvelle icône de documentation.

+

Cliquer sur l'icône ouvre l'onglet Description de la boîte de dialogue d'édition du noeud.

` } }, { title: { - "en-US": "Improved flow diffs", - "ja": "フローの差分表示の改善", - "fr": "Amélioration des différences de flux" + "en-US": "Palette Manager Improvements", + "ja": "パレットの管理の改善", + "fr": "Améliorations du Gestionnaire de Palettes" }, - 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:

+ "en-US": `

There are lots of improvements to the palette manager:

    -
  • Milliseconds since epoch - this is existing behaviour of the timestamp option
  • -
  • ISO 8601 - a common format used by many systems
  • -
  • JavaScript Date Object
  • +
  • Search results are sorted by downloads to help you find the most popular nodes
  • +
  • See which nodes have been deprecated by their author and are no longer recommended for use
  • +
  • Links to node documentation for the nodes you already have installed
`, - "ja": `

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

-

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

+ "ja": `

パレットの管理に多くの改善が加えられました:

    -
  • エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目
  • -
  • 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 :

+ "fr": `

Le Gestionnaire de Palettes a bénéficié de nombreuses améliorations :

    -
  • 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
  • +
  • Les résultats de recherche sont triés par téléchargement pour vous aider à trouver les noeuds les plus populaires.
  • +
  • Indique les noeuds obsolètes par leur auteur et dont l'utilisation n'est plus recommandée.
  • +
  • Liens vers la documentation des noeuds déjà installés.
` } }, { title: { - "en-US": "Auto-complete of flow/global and env types", - "ja": "フロー/グローバル、環境変数の型の自動補完", - "fr": "Saisie automatique des types de flux/global et env" + "en-US": "Installing missing modules", + "ja": "不足モジュールのインストール", + "fr": "Installation des modules manquants" }, - image: 'images/nr4-auto-complete.png', + image: 'images/missing-modules.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.

+ "en-US": `

Flows exported from Node-RED 4.1 now include information on what additional modules need to be installed.

+

When importing a flow with this information, the editor will let you know what is missing and help to get them installed.

`, - "ja": `

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

+ "ja": `

Node-RED 4.1から書き出したフローには、インストールが必要な追加モジュールの情報が含まれる様になりました。

+

この情報を含むフローを読み込むと、エディタは不足しているモジュールを通知し、インストールを支援します。

`, - "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.

+ "fr": `

Les flux exportés depuis Node-RED 4.1 incluent désormais des informations sur les modules supplémentaires à installer.

+

Lors de l'importation d'un flux contenant ces informations, l'éditeur vous indiquera les modules manquants et vous aidera à les installer.

` } }, - { - 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", @@ -205,26 +100,26 @@ export default { "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
  • +
  • Support for node: prefixed modules in the Function node
  • +
  • The ability to set a global timeout for Function nodes via the runtime settings
  • +
  • Better display of error objects in the Debug sidebar
  • and lots more...
`, "ja": `

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

    -
  • RFC4180に完全に準拠したCSVモード
  • -
  • WebSocketノードのカスタマイズ可能なヘッダ
  • -
  • Splitノードは、メッセージプロパティで操作できるようになりました
  • -
  • 他にも沢山あります...
  • +
  • Functionノードでnode:のプレフィックスモジュールをサポート
  • +
  • ランタイム設定からFunctionノードのグローバルタイムアウトを設定可能
  • +
  • デバッグサイドバーでのエラーオブジェクトの表示を改善
  • +
  • その他、多数...
`, - "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...
  • -
` + "fr": `

Les noeuds principaux ont bénéficié de nombreux correctifs mineurs, de mises à jour de documentation et d'améliorations mineures. + Consultez le journal complet des modifications dans la barre latérale d'aide pour une liste complète.

+
    +
  • Prise en charge des modules préfixés node: dans le noeud Fonction.
  • +
  • Possibilité de définir un délai d'expiration global pour les noeuds Fonction via les paramètres d'exécution.
  • +
  • Meilleur affichage des objets d'erreur dans la barre latérale de débogage.
  • +
  • Et bien plus encore...
  • +
` } } ] diff --git a/packages/node_modules/@node-red/nodes/core/common/24-complete.js b/packages/node_modules/@node-red/nodes/core/common/24-complete.js index ea665a265..1ba43a423 100644 --- a/packages/node_modules/@node-red/nodes/core/common/24-complete.js +++ b/packages/node_modules/@node-red/nodes/core/common/24-complete.js @@ -20,7 +20,16 @@ module.exports = function(RED) { function CompleteNode(n) { RED.nodes.createNode(this,n); var node = this; - this.scope = n.scope; + this.scope = n.scope || []; + + // auto-filter out any directly connected nodes to avoid simple loopback + const w = this.wires.flat(); + for (let i=0; i < this.scope.length; i++) { + if (w.includes(this.scope[i])) { + this.scope.splice(i, 1); + } + } + this.on("input",function(msg, send, done) { send(msg); done(); diff --git a/packages/node_modules/@node-red/nodes/core/common/25-status.js b/packages/node_modules/@node-red/nodes/core/common/25-status.js index fc6ccbe29..8c56e2030 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-status.js +++ b/packages/node_modules/@node-red/nodes/core/common/25-status.js @@ -20,7 +20,16 @@ module.exports = function(RED) { function StatusNode(n) { RED.nodes.createNode(this,n); var node = this; - this.scope = n.scope; + this.scope = n.scope || []; + + // auto-filter out any directly connected nodes to avoid simple loopback + const w = this.wires.flat(); + for (let i=0; i < this.scope.length; i++) { + if (w.includes(this.scope[i])) { + this.scope.splice(i, 1); + } + } + this.on("input", function(msg, send, done) { send(msg); done(); diff --git a/packages/node_modules/@node-red/nodes/core/common/98-unknown.html b/packages/node_modules/@node-red/nodes/core/common/98-unknown.html index 282ad3415..64c9e8e4e 100644 --- a/packages/node_modules/@node-red/nodes/core/common/98-unknown.html +++ b/packages/node_modules/@node-red/nodes/core/common/98-unknown.html @@ -3,7 +3,7 @@

- +

diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index 71f6e15d6..c0def92b3 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -162,6 +162,8 @@ module.exports = function(RED) { console:console, util:util, Buffer:Buffer, + URL: URL, + URLSearchParams: URLSearchParams, Date: Date, RED: { util: { @@ -403,6 +405,8 @@ module.exports = function(RED) { if(node.timeout>0){ finOpt.timeout = node.timeout; finOpt.breakOnSigint = true; + } else if (RED.settings.globalFunctionTimeout > 0){ + finOpt.timeout = RED.settings.globalFunctionTimeout * 1000 } } var promise = Promise.resolve(); @@ -419,8 +423,14 @@ module.exports = function(RED) { var opts = {}; if (node.timeout>0){ opts = node.timeoutOptions; + } else if (RED.settings. globalFunctionTimeout > 0){ + opts.timeout = RED.settings. globalFunctionTimeout * 1000 + } + try { + node.script.runInContext(context,opts); + } catch (err) { + return done(err); } - node.script.runInContext(context,opts); context.results.then(function(results) { sendResults(node,send,msg._msgid,results,false); if (handleNodeDoneCall) { diff --git a/packages/node_modules/@node-red/nodes/core/function/90-exec.js b/packages/node_modules/@node-red/nodes/core/function/90-exec.js index 70aec8d2b..23d94059e 100644 --- a/packages/node_modules/@node-red/nodes/core/function/90-exec.js +++ b/packages/node_modules/@node-red/nodes/core/function/90-exec.js @@ -109,7 +109,7 @@ module.exports = function(RED) { child.stderr.on('data', function (data) { if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) { if (isUtf8(data)) { msg.payload = data.toString(); } - else { msg.payload = Buffer.from(data); } + else { msg.payload = data; } nodeSend([null,RED.util.cloneMessage(msg),null]); } }); @@ -146,7 +146,8 @@ module.exports = function(RED) { delete msg.payload; if (stderr) { msg2 = RED.util.cloneMessage(msg); - msg2.payload = stderr; + msg2.payload = Buffer.from(stderr,"binary"); + if (isUtf8(msg2.payload)) { msg2.payload = msg2.payload.toString(); } } msg.payload = Buffer.from(stdout,"binary"); if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); } diff --git a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js index afa0066f4..451035a74 100644 --- a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js +++ b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js @@ -675,7 +675,7 @@ module.exports = function(RED) { node.options.password = node.password; node.options.keepalive = node.keepalive; node.options.clean = node.cleansession; - node.options.clientId = node.clientid || 'nodered_' + RED.util.generateId(); + node.options.clientId = node.clientid || 'nodered' + RED.util.generateId(); node.options.reconnectPeriod = RED.settings.mqttReconnectTime||5000; delete node.options.protocolId; //V4+ default delete node.options.protocolVersion; //V4 default 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..0a3ac2560 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 @@ -431,7 +431,7 @@ in your Node-RED user directory (${RED.settings.userDir}). normalisedHeaders[k.toLowerCase()] = response.headers[k] }) if (normalisedHeaders['www-authenticate']) { - let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname, normalisedHeaders['www-authenticate']) + let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname + requestUrl.search, normalisedHeaders['www-authenticate']) options.headers.Authorization = authHeader; } // response.request.options.merge(options) @@ -586,9 +586,31 @@ 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 }; + if (typeof msg.rejectUnauthorized === 'boolean') { + opts.https = { rejectUnauthorized: msg.rejectUnauthorized } + } else if (typeof msg.rejectUnauthorized === 'string') { + if (msg.rejectUnauthorized.toLowerCase() === 'true' || msg.rejectUnauthorized.toLowerCase() === 'false') { + opts.https = { rejectUnauthorized: (msg.rejectUnauthorized.toLowerCase() === 'true') } + } else { + node.warn(RED._("httpin.errors.rejectunauthorized-invalid")) + } + } else { + node.warn(RED._("httpin.errors.rejectunauthorized-invalid")) + } + } } 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/core/sequence/17-split.js b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js index 46ecb2636..5fe6b3c4e 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js +++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js @@ -151,10 +151,11 @@ module.exports = function(RED) { if (node.arraySplt === 1) { m = m[0]; } - RED.util.setMessageProperty(msg,node.property,m); - msg.parts.index = i; + const newmsg = RED.util.cloneMessage(msg) + RED.util.setMessageProperty(newmsg,node.property,m); + newmsg.parts.index = i; pos += node.arraySplt; - send(RED.util.cloneMessage(msg)); + send(newmsg); } done(); } diff --git a/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html b/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html index bb02eede0..72718d33f 100644 --- a/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html +++ b/packages/node_modules/@node-red/nodes/locales/de/network/21-httprequest.html @@ -81,7 +81,7 @@

Wenn msg.payload ein Objekt ist, setzt der Node automatisch den Inhaltstyp der Anforderung auf application/json und kodiert den Hauptteil als solchen.

Um die Anforderung als Formulardaten zu kodieren, sollte msg.headers["content-type"] auf - application/x-wwww-form-urlencoded gesetzt werden.

+ application/x-www-form-urlencoded gesetzt werden.

Datei-Upload

Um einen Datei-Upload umzusetzen, sollte msg.headers["content-type"] auf multipart/form-data gesetzt werden und das an den Node zu sendende msg.payload muss ein Objekt mit folgender Struktur sein:

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 d26f0f56b..6d33e78aa 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 @@ -406,6 +406,7 @@ "label": { "unknown": "unknown" }, + "manageModules": "Manage modules", "tip": "

This node is a type unknown to your installation of Node-RED.

If you deploy with the node in this state, it's configuration will be preserved, but the flow will not start until the missing type is installed.

See the Info side bar for more help

" }, "mqtt": { @@ -562,7 +563,8 @@ "timeout-isnan": "Timeout value is not a valid number, ignoring", "timeout-isnegative": "Timeout value is negative, ignoring", "invalid-payload": "Invalid payload", - "invalid-url": "Invalid url" + "invalid-url": "Invalid url", + "rejectunauthorized-invalid": "msg.rejectUnauthorized should be a boolean" }, "status": { "requesting": "requesting" @@ -1017,7 +1019,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/locales/fr/messages.json b/packages/node_modules/@node-red/nodes/locales/fr/messages.json index 238763e3b..e4c2d49ab 100644 --- a/packages/node_modules/@node-red/nodes/locales/fr/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/fr/messages.json @@ -406,6 +406,7 @@ "label": { "unknown": "inconnu" }, + "manageModules": "Gérer les modules", "tip": "

Ce noeud est un type inconnu de votre installation Node-RED.

Si vous déployez avec le noeud dans cet état, sa configuration sera préservée, mais le flux ne démarrera pas avant que le type manquant soit installé.

Consulter la barre latérale d'informations pour plus d'aide

" }, "mqtt": { @@ -1017,7 +1018,7 @@ "objectSend": "Envoie un message pour chaque paire clé/valeur", "strBuff": "Chaîne / Tampon", "array": "Tableau", - "splitThe": "Diviser le", + "splitThe": "Diviser la propriété", "splitUsing": "Diviser en utilisant", "splitLength": "Longueur fixe de", "stream": "Gérer comme un flux de messages", diff --git a/packages/node_modules/@node-red/nodes/locales/ja/messages.json b/packages/node_modules/@node-red/nodes/locales/ja/messages.json index 1693f879e..118d6af2c 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ja/messages.json @@ -406,6 +406,7 @@ "label": { "unknown": "unknown" }, + "manageModules": "モジュールを管理", "tip": "

現在のNode-RED環境では、本ノードの型が不明です。

現在の状態で本ノードをデプロイすると設定は保存されますが、不明なノードがインストールされるまでフローは実行されません。

詳細はノードの「情報」を参照してください。

" }, "mqtt": { diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index a513e14da..c6b385979 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "license": "Apache-2.0", "repository": { "type": "git", @@ -15,7 +15,7 @@ } ], "dependencies": { - "acorn": "8.14.1", + "acorn": "8.15.0", "acorn-walk": "8.3.4", "ajv": "8.17.1", "body-parser": "1.20.3", @@ -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/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 1e886a159..0014d853a 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,7 +16,7 @@ } ], "dependencies": { - "@node-red/util": "4.1.0-beta.0", + "@node-red/util": "4.1.0-beta.1", "clone": "2.1.2", "fs-extra": "11.3.0", "semver": "7.7.1", 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..1ac43032b 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() { @@ -235,8 +237,12 @@ function start() { } } return redNodes.loadContextsPlugin().then(function () { - redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {}); started = true; + redNodes.loadFlows().then(() => { + if (started) { + redNodes.startFlows() + } + }).catch(function(err) {}); }); }); }); @@ -337,6 +343,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/library.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js index fbcb44e2f..d8d770677 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/library.js @@ -135,7 +135,7 @@ function getLibraryEntry(type,path) { throw err; }); } else { - throw err; + throw new Error(`Library Entry not found ${path}`, { cause: err}); } }); } 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..c2a0a91ef 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,13 +16,15 @@ } ], "dependencies": { - "@node-red/registry": "4.1.0-beta.0", - "@node-red/util": "4.1.0-beta.0", + "@node-red/registry": "4.1.0-beta.1", + "@node-red/util": "4.1.0-beta.1", "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/exec.js b/packages/node_modules/@node-red/util/lib/exec.js index c7197ef65..15b81aa89 100644 --- a/packages/node_modules/@node-red/util/lib/exec.js +++ b/packages/node_modules/@node-red/util/lib/exec.js @@ -78,7 +78,7 @@ module.exports = { stdout: stdout, stderr: stderr } - emit && events.emit("event-log", {id:invocationId,payload:{ts: Date.now(),data:"rc="+code}}); + emit && events.emit("event-log", {id:invocationId,payload:{ts: Date.now(), data:"rc="+code, end: true}}); if (code === 0) { resolve(result) 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/util/package.json b/packages/node_modules/@node-red/util/package.json index dffc817b0..316d0696f 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index f36273986..d189fb5e3 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "description": "Low-code programming for event-driven applications", "homepage": "https://nodered.org", "license": "Apache-2.0", @@ -31,16 +31,16 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "4.1.0-beta.0", - "@node-red/runtime": "4.1.0-beta.0", - "@node-red/util": "4.1.0-beta.0", - "@node-red/nodes": "4.1.0-beta.0", + "@node-red/editor-api": "4.1.0-beta.1", + "@node-red/runtime": "4.1.0-beta.1", + "@node-red/util": "4.1.0-beta.1", + "@node-red/nodes": "4.1.0-beta.1", "basic-auth": "2.0.1", "bcryptjs": "3.0.2", "cors": "2.8.5", "express": "4.21.2", "fs-extra": "11.3.0", - "node-red-admin": "^4.0.2", + "node-red-admin": "^4.1.0", "nopt": "5.0.0", "semver": "7.7.1" }, 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..269cac160 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 */ @@ -473,6 +490,7 @@ module.exports = { * - fileWorkingDirectory * - functionGlobalContext * - functionExternalModules + * - globalFunctionTimeout * - functionTimeout * - nodeMessageBufferMaxLength * - ui (for use with Node-RED Dashboard) @@ -499,7 +517,19 @@ module.exports = { /** Allow the Function node to load additional npm modules directly */ functionExternalModules: true, - /** Default timeout, in seconds, for the Function node. 0 means no timeout is applied */ + + /** + * The default timeout (in seconds) for all Function nodes. + * Individual nodes can set their own timeout value within their configuration. + */ + globalFunctionTimeout: 0, + + /** + * Default timeout, in seconds, for the Function node. 0 means no timeout is applied + * This value is applied when the node is first added to the workspace - any changes + * must then be made with the individual node configurations. + * To set a global timeout value, use `globalFunctionTimeout` + */ functionTimeout: 0, /** The following property can be used to set predefined values in Global Context. diff --git a/test/nodes/core/common/24-complete_spec.js b/test/nodes/core/common/24-complete_spec.js new file mode 100644 index 000000000..15f27b43b --- /dev/null +++ b/test/nodes/core/common/24-complete_spec.js @@ -0,0 +1,39 @@ + +var should = require("should"); +var catchNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js"); +var helper = require("node-red-node-test-helper"); + +describe('complete Node', function() { + + afterEach(function() { + helper.unload(); + }); + + it('should output a message when called', function(done) { + var flow = [ { id:"n1", type:"complete", name:"status", wires:[["n2"]], scope:[] }, + {id:"n2", type:"helper"} ]; + helper.load(catchNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.should.have.property('name', 'status'); + n2.on("input", function(msg) { + msg.text.should.equal("Oh dear"); + msg.should.have.property('source'); + msg.source.should.have.property('id',"12345"); + msg.source.should.have.property('type',"testnode"); + msg.source.should.have.property('name',"fred"); + done(); + }); + var mst = { + text: "Oh dear", + source: { + id: "12345", + type: "testnode", + name: "fred" + } + } + n1.emit("input", mst); + }); + }); + +}); diff --git a/test/nodes/core/common/25-status_spec.js b/test/nodes/core/common/25-status_spec.js index 41b0a79c8..9457d4372 100644 --- a/test/nodes/core/common/25-status_spec.js +++ b/test/nodes/core/common/25-status_spec.js @@ -25,7 +25,7 @@ describe('status Node', function() { }); it('should output a message when called', function(done) { - var flow = [ { id:"n1", type:"status", name:"status", wires:[["n2"]] }, + var flow = [ { id:"n1", type:"status", name:"status", wires:[["n2"]], scope:[] }, {id:"n2", type:"helper"} ]; helper.load(catchNode, flow, function() { var n1 = helper.getNode("n1"); diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index 56c4ec976..6a04547f4 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -1451,7 +1451,7 @@ describe('function node', function() { }); }); - it('check if default function timeout settings are recognized', function (done) { + it('check if function timeout settings are recognized', function (done) { RED.settings.functionTimeout = 0.01; var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; helper.load(functionNode, flow, function () { @@ -1479,6 +1479,65 @@ describe('function node', function() { }); }); + it('check if default function timeout settings are recognized', function (done) { + RED.settings.globalFunctionTimeout = 0.01; + var flow = [{id: "n1",type: "function",wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; + helper.load(functionNode, flow, function () { + var n1 = helper.getNode("n1"); + n1.receive({ payload: "foo", topic: "bar" }); + setTimeout(function () { + try { + helper.log().called.should.be.true(); + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "function"; + }); + logEvents.should.have.length(1); + var msg = logEvents[0][0]; + msg.should.have.property('level', helper.log().ERROR); + msg.should.have.property('id', 'n1'); + msg.should.have.property('type', 'function'); + should.equal(RED.settings.globalFunctionTimeout, 0.01); + should.equal(msg.msg.message, 'Script execution timed out after 10ms'); + delete RED.settings.globalFunctionTimeout; + done(); + } catch (err) { + done(err); + } + }, 500); + }); + }); + + it('check if functionTimeout has higher precedence over default function timeout setting', function (done) { + RED.settings.globalFunctionTimeout = 0.02; + RED.settings.functionTimeout = 0.01; + var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; + helper.load(functionNode, flow, function () { + var n1 = helper.getNode("n1"); + n1.receive({ payload: "foo", topic: "bar" }); + setTimeout(function () { + try { + helper.log().called.should.be.true(); + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "function"; + }); + logEvents.should.have.length(1); + var msg = logEvents[0][0]; + msg.should.have.property('level', helper.log().ERROR); + msg.should.have.property('id', 'n1'); + msg.should.have.property('type', 'function'); + should.equal(RED.settings.functionTimeout, 0.01); + should.equal(RED.settings.globalFunctionTimeout, 0.02); + should.equal(msg.msg.message, 'Script execution timed out after 10ms'); + delete RED.settings.functionTimeout; + delete RED.settings.globalFunctionTimeout; + done(); + } catch (err) { + done(err); + } + }, 500); + }); + }); + describe("finalize function", function() { it('should execute', function(done) { diff --git a/test/nodes/core/network/21-mqtt_spec.js b/test/nodes/core/network/21-mqtt_spec.js index 16c38d2e5..0075831c9 100644 --- a/test/nodes/core/network/21-mqtt_spec.js +++ b/test/nodes/core/network/21-mqtt_spec.js @@ -58,7 +58,7 @@ describe('MQTT Nodes', function () { mqttBroker.should.have.property('options'); mqttBroker.options.should.have.property('clean', true); mqttBroker.options.should.have.property('clientId'); - mqttBroker.options.clientId.should.containEql('nodered_'); + mqttBroker.options.clientId.should.containEql('nodered'); mqttBroker.options.should.have.property('keepalive').type("number"); mqttBroker.options.should.have.property('reconnectPeriod').type("number"); //as this is not a v5 connection, ensure v5 properties are not present @@ -894,4 +894,4 @@ function nextTopic(topic) { return (base_topic + topic + String(topicNo)); } -//#endregion HELPERS \ No newline at end of file +//#endregion HELPERS 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