Merge branch 'dev' into remote_editor_shade_tray_click_listener

pull/5122/head
Nick O'Leary 2025-06-25 15:03:53 +01:00 committed by GitHub
commit e8d4fe8a07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 2425 additions and 576 deletions

View File

@ -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 #### 4.0.9: Maintenance Release
Editor Editor

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -26,7 +26,7 @@
} }
], ],
"dependencies": { "dependencies": {
"acorn": "8.14.1", "acorn": "8.15.0",
"acorn-walk": "8.3.4", "acorn-walk": "8.3.4",
"ajv": "8.17.1", "ajv": "8.17.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
@ -63,9 +63,9 @@
"moment": "2.30.1", "moment": "2.30.1",
"moment-timezone": "0.5.48", "moment-timezone": "0.5.48",
"mqtt": "5.11.0", "mqtt": "5.11.0",
"multer": "1.4.5-lts.2", "multer": "2.0.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-red-admin": "^4.0.2", "node-red-admin": "^4.1.0",
"node-watch": "0.7.4", "node-watch": "0.7.4",
"nopt": "5.0.0", "nopt": "5.0.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
@ -87,7 +87,7 @@
"@node-rs/bcrypt": "1.10.7" "@node-rs/bcrypt": "1.10.7"
}, },
"devDependencies": { "devDependencies": {
"dompurify": "2.5.8", "dompurify": "3.2.5",
"grunt": "1.6.1", "grunt": "1.6.1",
"grunt-chmod": "~1.1.1", "grunt-chmod": "~1.1.1",
"grunt-cli": "~1.5.0", "grunt-cli": "~1.5.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-api", "name": "@node-red/editor-api",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,8 +16,8 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.1.0-beta.0", "@node-red/util": "4.1.0-beta.1",
"@node-red/editor-client": "4.1.0-beta.0", "@node-red/editor-client": "4.1.0-beta.1",
"bcryptjs": "3.0.2", "bcryptjs": "3.0.2",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"clone": "2.1.2", "clone": "2.1.2",
@ -26,7 +26,7 @@
"express": "4.21.2", "express": "4.21.2",
"memorystore": "1.6.7", "memorystore": "1.6.7",
"mime": "3.0.0", "mime": "3.0.0",
"multer": "1.4.5-lts.2", "multer": "2.0.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"passport-http-bearer": "1.0.1", "passport-http-bearer": "1.0.1",

View File

@ -265,7 +265,7 @@
"download": "Download", "download": "Download",
"importUnrecognised": "Imported unrecognised type:", "importUnrecognised": "Imported unrecognised type:",
"importUnrecognised_plural": "Imported unrecognised types:", "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:", "importWithModuleInfoDesc": "These nodes are not currently installed in your palette and are required for the imported flow:",
"importDuplicate": "Imported duplicate node:", "importDuplicate": "Imported duplicate node:",
"importDuplicate_plural": "Imported duplicate nodes:", "importDuplicate_plural": "Imported duplicate nodes:",
@ -626,6 +626,7 @@
"yearsMonthsV": "__y__ years, __count__ month ago", "yearsMonthsV": "__y__ years, __count__ month ago",
"yearsMonthsV_plural": "__y__ years, __count__ months ago" "yearsMonthsV_plural": "__y__ years, __count__ months ago"
}, },
"manageModules": "Manage modules",
"nodeCount": "__label__ node", "nodeCount": "__label__ node",
"nodeCount_plural": "__label__ nodes", "nodeCount_plural": "__label__ nodes",
"pluginCount": "__count__ plugin", "pluginCount": "__count__ plugin",
@ -643,9 +644,12 @@
"update": "update to __version__", "update": "update to __version__",
"updated": "updated", "updated": "updated",
"install": "install", "install": "install",
"installAll": "Install all",
"installed": "installed", "installed": "installed",
"installing": "Module installation in progress: __module__",
"conflict": "conflict", "conflict": "conflict",
"conflictTip": "<p>This module cannot be installed as it includes a<br/>node type that has already been installed</p><p>Conflicts with <code>__module__</code></p>", "conflictTip": "<p>This module cannot be installed as it includes a<br/>node type that has already been installed</p><p>Conflicts with <code>__module__</code></p>",
"majorVersion": "<p>This is a major version update of the node. Check the documentation for details of the update.</p>",
"loading": "Loading catalogues...", "loading": "Loading catalogues...",
"tab-nodes": "Nodes", "tab-nodes": "Nodes",
"tab-install": "Install", "tab-install": "Install",
@ -653,9 +657,12 @@
"sortRelevance": "relevance", "sortRelevance": "relevance",
"sortAZ": "a-z", "sortAZ": "a-z",
"sortRecent": "recent", "sortRecent": "recent",
"successfulInstall": "Successfully installed modules",
"more": "+ __count__ more", "more": "+ __count__ more",
"upload": "Upload module tgz file", "upload": "Upload module tgz file",
"refresh": "Refresh module list", "refresh": "Refresh module list",
"deprecated": "deprecated",
"deprecatedTip": "This module has been deprecated",
"errors": { "errors": {
"catalogLoadFailed": "<p>Failed to load node catalogue.</p><p>Check the browser console for more information</p>", "catalogLoadFailed": "<p>Failed to load node catalogue.</p><p>Check the browser console for more information</p>",
"installFailed": "<p>Failed to install: __module__</p><p>__message__</p><p>Check the log for more information</p>", "installFailed": "<p>Failed to install: __module__</p><p>__message__</p><p>Check the log for more information</p>",
@ -1278,5 +1285,15 @@
"environment": "Environment", "environment": "Environment",
"header": "Global Environment Variables", "header": "Global Environment Variables",
"revert": "Revert" "revert": "Revert"
},
"telemetry": {
"label": "Update Notifications",
"settingsTitle": "Enable Update Notifications",
"settingsDescription": "<p>Node-RED can notify you when there is a new version available. This ensures you keep up to date with the latest features and fixes.</p><p>This requires sending anonymised data back to the Node-RED team. It does not include any details of your flows or users.</p><p>For full information on what information is collected and how it is used, please see the <a href=\"https://nodered.org/docs/telemetry\" target=\"_blank\">documentation</a>.</p>",
"settingsDescription2": "<p>You can change this setting at any time in the User Settings.</p>",
"enableLabel": "Yes, enable notifications",
"disableLabel": "No, do not enable notifications",
"updateAvailable": "Update available",
"updateAvailableDesc": "Node-RED __version__ is now available"
} }
} }

View File

@ -111,6 +111,7 @@
"userSettings": "Paramètres de l'utilisateur", "userSettings": "Paramètres de l'utilisateur",
"nodes": "Noeuds", "nodes": "Noeuds",
"displayStatus": "Afficher l'état du noeud", "displayStatus": "Afficher l'état du noeud",
"displayInfoIcon": "Afficher l'icône d'information sur le noeud",
"displayConfig": "Noeuds de configuration", "displayConfig": "Noeuds de configuration",
"import": "Importer", "import": "Importer",
"importExample": "Importer un exemple de flux", "importExample": "Importer un exemple de flux",
@ -264,6 +265,8 @@
"download": "Télécharger", "download": "Télécharger",
"importUnrecognised": "Importation d'un type inconnu :", "importUnrecognised": "Importation d'un type inconnu :",
"importUnrecognised_plural": "Importation de plusieurs types inconnus :", "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": "Noeud en double importé :",
"importDuplicate_plural": "Noeuds en double importés :", "importDuplicate_plural": "Noeuds en double importés :",
"nodesExported": "Noeuds exportés vers le presse-papiers", "nodesExported": "Noeuds exportés vers le presse-papiers",
@ -623,12 +626,15 @@
"yearsMonthsV": "il y a __y__ ans, __count__ mois", "yearsMonthsV": "il y a __y__ ans, __count__ mois",
"yearsMonthsV_plural": "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": "__label__ noeud",
"nodeCount_plural": "__label__ noeuds", "nodeCount_plural": "__label__ noeuds",
"pluginCount": "__count__ plugin", "pluginCount": "__count__ plugin",
"pluginCount_plural": "__count__ plugins", "pluginCount_plural": "__count__ plugins",
"moduleCount": "__count__ module disponible", "moduleCount": "__count__ module disponible",
"moduleCount_plural": "__count__ modules disponibles", "moduleCount_plural": "__count__ modules disponibles",
"updateCount": "__count__ mise à jour disponible",
"updateCount_plural": "__count__ mises à jour disponibles",
"inuse": "En cours d'utilisation", "inuse": "En cours d'utilisation",
"enableall": "Activer tout", "enableall": "Activer tout",
"disableall": "Désactiver tout", "disableall": "Désactiver tout",
@ -638,9 +644,12 @@
"update": "Mettre à jour vers __version__", "update": "Mettre à jour vers __version__",
"updated": "Mis à jour", "updated": "Mis à jour",
"install": "Installer", "install": "Installer",
"installAll": "Installer tout",
"installed": "Installé", "installed": "Installé",
"installing": "Installation du module en cours : __module__",
"conflict": "Conflit", "conflict": "Conflit",
"conflictTip": "<p>Ce module ne peut pas être installé car il inclut un<br/>type de noeud qui a déjà été installé</p><p>Conflits avec <code>__module__</code></p>", "conflictTip": "<p>Ce module ne peut pas être installé car il inclut un<br/>type de noeud qui a déjà été installé</p><p>Conflits avec <code>__module__</code></p>",
"majorVersion": "<p>Il s'agit d'une mise à jour majeure du noeud. Consulter la documentation pour plus de détails sur la mise à jour.</p>",
"loading": "Chargement des catalogues...", "loading": "Chargement des catalogues...",
"tab-nodes": "Noeuds", "tab-nodes": "Noeuds",
"tab-install": "Installer", "tab-install": "Installer",
@ -648,9 +657,12 @@
"sortRelevance": "Pertinence", "sortRelevance": "Pertinence",
"sortAZ": "A-Z", "sortAZ": "A-Z",
"sortRecent": "Récent", "sortRecent": "Récent",
"successfulInstall": "Modules installés avec succès",
"more": "+ __count__ en plus", "more": "+ __count__ en plus",
"upload": "Charger le fichier .tgz du module", "upload": "Charger le fichier .tgz du module",
"refresh": "Actualiser la liste des modules", "refresh": "Actualiser la liste des modules",
"deprecated": "Obsolète",
"deprecatedTip": "Ce module est obsolète",
"errors": { "errors": {
"catalogLoadFailed": "<p>Échec du chargement du catalogue de noeuds.</p><p>Vérifier la console du navigateur pour plus d'informations</p>", "catalogLoadFailed": "<p>Échec du chargement du catalogue de noeuds.</p><p>Vérifier la console du navigateur pour plus d'informations</p>",
"installFailed": "<p>Échec lors de l'installation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>", "installFailed": "<p>Échec lors de l'installation : __module__</p><p>__message__</p><p>Consulter le journal pour plus d'informations</p>",
@ -1262,6 +1274,16 @@
"header": "Variables d'environnement globales", "header": "Variables d'environnement globales",
"revert": "Rétablir" "revert": "Rétablir"
}, },
"telemetry": {
"label": "Notifications de mise à jour",
"settingsTitle": "Activer les notifications de mise à jour",
"settingsDescription": "<p>Node-RED peut vous avertir de la disponibilité d'une nouvelle version. Vous êtes ainsi informé des dernières fonctionnalités et correctifs.</p><p>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.</p><p>Pour plus d'informations sur les informations collectées et leur utilisation, veuillez consulter la <a href=\"https://nodered.org/docs/telemetry\" target=\"_blank\">documentation</a>.</p>",
"settingsDescription2": "<p>Vous pouvez modifier ce paramètre à tout moment dans les paramètres de l'éditeur.</p>",
"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": { "action-list": {
"toggle-show-tips": "Basculer l'affichage des astuces", "toggle-show-tips": "Basculer l'affichage des astuces",
"show-about": "Afficher la description de Node-RED", "show-about": "Afficher la description de Node-RED",

View File

@ -111,6 +111,7 @@
"userSettings": "ユーザ設定", "userSettings": "ユーザ設定",
"nodes": "ノード", "nodes": "ノード",
"displayStatus": "ノードのステータスを表示", "displayStatus": "ノードのステータスを表示",
"displayInfoIcon": "ノード情報のアイコンを表示",
"displayConfig": "設定ノード", "displayConfig": "設定ノード",
"import": "読み込み", "import": "読み込み",
"importExample": "フロー例を読み込み", "importExample": "フロー例を読み込み",
@ -264,6 +265,8 @@
"download": "ダウンロード", "download": "ダウンロード",
"importUnrecognised": "認識できない型が読み込まれました:", "importUnrecognised": "認識できない型が読み込まれました:",
"importUnrecognised_plural": "認識できない型が読み込まれました:", "importUnrecognised_plural": "認識できない型が読み込まれました:",
"importWithModuleInfo": "必要なモジュールが不足",
"importWithModuleInfoDesc": "以下のノードは現在パレットにインストールされていませんが、読み込んだフローには必要なノードです:",
"importDuplicate": "重複したノードを読み込みました:", "importDuplicate": "重複したノードを読み込みました:",
"importDuplicate_plural": "重複したノードを読み込みました:", "importDuplicate_plural": "重複したノードを読み込みました:",
"nodesExported": "クリップボードへフローを書き出しました", "nodesExported": "クリップボードへフローを書き出しました",
@ -623,12 +626,15 @@
"yearsMonthsV": "__y__ 年 __count__ ヵ月前", "yearsMonthsV": "__y__ 年 __count__ ヵ月前",
"yearsMonthsV_plural": "__y__ 年 __count__ ヵ月前" "yearsMonthsV_plural": "__y__ 年 __count__ ヵ月前"
}, },
"manageModules": "モジュールを管理",
"nodeCount": "__label__ 個のノード", "nodeCount": "__label__ 個のノード",
"nodeCount_plural": "__label__ 個のノード", "nodeCount_plural": "__label__ 個のノード",
"pluginCount": "__count__ 個のプラグイン", "pluginCount": "__count__ 個のプラグイン",
"pluginCount_plural": "__count__ 個のプラグイン", "pluginCount_plural": "__count__ 個のプラグイン",
"moduleCount": "__count__ 個のモジュール", "moduleCount": "__count__ 個のモジュール",
"moduleCount_plural": "__count__ 個のモジュール", "moduleCount_plural": "__count__ 個のモジュール",
"updateCount": "__count__ 個の更新が存在",
"updateCount_plural": "__count__ 個の更新が存在",
"inuse": "使用中", "inuse": "使用中",
"enableall": "全て有効化", "enableall": "全て有効化",
"disableall": "全て無効化", "disableall": "全て無効化",
@ -638,9 +644,12 @@
"update": "__version__ へ更新", "update": "__version__ へ更新",
"updated": "更新済", "updated": "更新済",
"install": "ノードを追加", "install": "ノードを追加",
"installAll": "全てインストール",
"installed": "追加しました", "installed": "追加しました",
"installing": "モジュールのインストールが進行中: __module__",
"conflict": "競合", "conflict": "競合",
"conflictTip": "<p>インストール済みのノードの種別と競合しているため<br/>ノードをインストールできません</p><p>競合: <code>__module__</code></p>", "conflictTip": "<p>インストール済みのノードの種別と競合しているため<br/>ノードをインストールできません</p><p>競合: <code>__module__</code></p>",
"majorVersion": "<p>これはノードのメジャーバージョンの更新です。更新内容の詳細については、ドキュメントを確認してください。</p>",
"loading": "カタログを読み込み中", "loading": "カタログを読み込み中",
"tab-nodes": "現在のノード", "tab-nodes": "現在のノード",
"tab-install": "ノードを追加", "tab-install": "ノードを追加",
@ -648,9 +657,12 @@
"sortRelevance": "関連順", "sortRelevance": "関連順",
"sortAZ": "辞書順", "sortAZ": "辞書順",
"sortRecent": "日付順", "sortRecent": "日付順",
"successfulInstall": "モジュールのインストールが成功",
"more": "+ さらに __count__ 個", "more": "+ さらに __count__ 個",
"upload": "モジュールのtgzファイルをアップロード", "upload": "モジュールのtgzファイルをアップロード",
"refresh": "モジュールリスト更新", "refresh": "モジュールリスト更新",
"deprecated": "非推奨",
"deprecatedTip": "本モジュールは非推奨です",
"errors": { "errors": {
"catalogLoadFailed": "<p>ノードのカタログの読み込みに失敗しました。</p><p>詳細はブラウザのコンソールを確認してください。</p>", "catalogLoadFailed": "<p>ノードのカタログの読み込みに失敗しました。</p><p>詳細はブラウザのコンソールを確認してください。</p>",
"installFailed": "<p>追加処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>", "installFailed": "<p>追加処理が失敗しました: __module__</p><p>__message__</p><p>詳細はログを確認してください。</p>",
@ -1262,6 +1274,16 @@
"header": "グローバル環境変数", "header": "グローバル環境変数",
"revert": "破棄" "revert": "破棄"
}, },
"telemetry": {
"label": "更新の通知",
"settingsTitle": "更新の通知を有効化",
"settingsDescription": "<p>新バージョンのNode-REDが存在した時に、通知を受けることができます。この機能によって最新機能の提供や修正があることを把握できます。</p><p>この通知を受け取るには、匿名化されたデータをNode-REDチームに送る必要があります。このデータには、フローやユーザの詳細は含まれません。</p><p>収集される情報と利用方法の詳細については、<a href=\"https://nodered.org/docs/telemetry\" target=\"_blank\">ドキュメント</a>を参照してください。</p>",
"settingsDescription2": "<p>この設定はユーザ設定からいつでも変更できます。</p>",
"enableLabel": "はい、通知を有効にします",
"disableLabel": "いいえ、通知を有効にしません",
"updateAvailable": "更新を利用可能",
"updateAvailableDesc": "現在、Node-RED __version__ が利用可能"
},
"action-list": { "action-list": {
"toggle-show-tips": "ヒント表示切替", "toggle-show-tips": "ヒント表示切替",
"show-about": "Node-REDの説明を表示", "show-about": "Node-REDの説明を表示",
@ -1302,6 +1324,7 @@
"toggle-show-grid": "グリッド表示切替", "toggle-show-grid": "グリッド表示切替",
"toggle-snap-grid": "ノードの配置補助切替", "toggle-snap-grid": "ノードの配置補助切替",
"toggle-status": "ステータス表示切替", "toggle-status": "ステータス表示切替",
"toggle-node-info-icon": "ノード情報のアイコン表示切替",
"show-selected-node-labels": "選択したノードのラベルを表示", "show-selected-node-labels": "選択したノードのラベルを表示",
"hide-selected-node-labels": "選択したノードのラベルを非表示", "hide-selected-node-labels": "選択したノードのラベルを非表示",
"scroll-view-up": "上スクロール", "scroll-view-up": "上スクロール",
@ -1414,6 +1437,7 @@
"show-global-env": "グローバル環境変数を表示", "show-global-env": "グローバル環境変数を表示",
"lock-flow": "フローを固定", "lock-flow": "フローを固定",
"unlock-flow": "フローの固定を解除", "unlock-flow": "フローの固定を解除",
"show-node-help": "ノードのヘルプを表示" "show-node-help": "ノードのヘルプを表示",
"trigger-selected-nodes-action": "選択したノードのアクションを実行"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-client", "name": "@node-red/editor-client",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -44,6 +44,51 @@ RED.nodes = (function() {
var dirty = false; 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) { function setDirty(d) {
dirty = d; dirty = d;
if (!d) { if (!d) {
@ -231,7 +276,6 @@ RED.nodes = (function() {
def.type = nt; def.type = nt;
nodeDefinitions[nt] = def; nodeDefinitions[nt] = def;
if (def.defaults) { if (def.defaults) {
for (var d in def.defaults) { for (var d in def.defaults) {
if (def.defaults.hasOwnProperty(d)) { if (def.defaults.hasOwnProperty(d)) {
@ -242,6 +286,11 @@ RED.nodes = (function() {
console.warn(err); 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 let newNode
if (!n.__isProxy__) { if (!n.__isProxy__) {
newNode = new Proxy(n, nodeProxyHandler) newNode = new Proxy(n, nodeProxyHandler)
@ -728,7 +777,7 @@ RED.nodes = (function() {
nodeLinks[n.id] = {in:[],out:[]}; nodeLinks[n.id] = {in:[],out:[]};
} }
} }
RED.events.emit('nodes:add',newNode); RED.events.emit('nodes:add',newNode, opt);
return newNode return newNode
} }
function addLink(l) { function addLink(l) {
@ -1494,7 +1543,14 @@ RED.nodes = (function() {
} }
/** /**
* Converts the current node selection to an exportable JSON Object * Converts the current node selection to an exportable JSON Object
**/ * @param {Array<Node>} set the node selection to export
* @param {Object} options
* @param {Record<string, boolean>} [options.exportedIds]
* @param {Record<string, boolean>} [options.exportedSubflows]
* @param {Record<string, boolean>} [options.exportedConfigNodes]
* @param {boolean} [options.includeModuleConfig]
* @returns {Array<Node>}
*/
function createExportableNodeSet(set, { function createExportableNodeSet(set, {
exportedIds, exportedIds,
exportedSubflows, exportedSubflows,
@ -1582,10 +1638,14 @@ RED.nodes = (function() {
return nns; return nns;
} }
// Create the Flow JSON for the current configuration /**
// opts.credentials (whether to include (known) credentials) - default: true * Converts the current configuration to an exportable JSON Object
// opts.dimensions (whether to include node dimensions) - default: false * @param {object} opts
// opts.includeModuleConfig (whether to include modules) - default: false * @param {boolean} [opts.credentials] whether to include (known) credentials. Default `true`.
* @param {boolean} [opts.dimensions] whether to include node dimensions. Default `false`.
* @param {boolean} [opts.includeModuleConfig] whether to include modules. Default `false`.
* @returns {Array<object>}
*/
function createCompleteNodeSet(opts) { function createCompleteNodeSet(opts) {
var nns = []; var nns = [];
var i; var i;
@ -1848,14 +1908,23 @@ RED.nodes = (function() {
* - id:copy - import with new id * - id:copy - import with new id
* - id:replace - import over the top of existing * - id:replace - import over the top of existing
* - modules: map of module:version - hints for unknown nodes * - 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) { 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 = Object.assign({}, defOpts, options)
options.importMap = options.importMap || {} options.importMap = options.importMap || {}
const createNewIds = options.generateIds; const createNewIds = options.generateIds;
const reimport = (!createNewIds && !!options.reimport) const reimport = (!createNewIds && !!options.reimport)
const createMissingWorkspace = options.addFlow; const createMissingWorkspace = options.addFlow;
const applyNodeDefaults = options.applyNodeDefaults;
var i; var i;
var n; var n;
var newNodes; var newNodes;
@ -2000,15 +2069,30 @@ RED.nodes = (function() {
// Provide option to install missing modules // Provide option to install missing modules
notificationOptions.buttons = [ notificationOptions.buttons = [
{ {
text: "Manage dependencies", text: RED._("palette.editor.manageModules"),
class:"primary", class: "primary",
click: function(e) { click: function(e) {
unknownNotification.close(); unknownNotification.close();
RED.actions.invoke('core:manage-palette', { RED.actions.invoke('core:manage-palette', {
view: 'install', view: 'install',
filter: '"' + missingModules.join('", "') + '"' 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) { for (d in def.defaults) {
if (def.defaults.hasOwnProperty(d)) { if (def.defaults.hasOwnProperty(d)) {
configNode[d] = n[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]); configNode._config[d] = JSON.stringify(n[d]);
if (def.defaults[d].type) { if (def.defaults[d].type) {
configNode._configNodeReferences.add(n[d]) configNode._configNodeReferences.add(n[d])
@ -2508,6 +2599,13 @@ RED.nodes = (function() {
for (d in node._def.defaults) { for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
node[d] = n[d]; 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]); node._config[d] = JSON.stringify(n[d]);
} }
} }
@ -2761,7 +2859,8 @@ RED.nodes = (function() {
workspaces:new_workspaces, workspaces:new_workspaces,
subflows:new_subflows, subflows:new_subflows,
missingWorkspace: missingWorkspace, 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<Node>} nodes the nodes to search in
* @returns {Record<string, string>} an object with {[moduleName]: moduleVersion}
*/
function getModuleListForNodes(nodes) { function getModuleListForNodes(nodes) {
const modules = {} const modules = {}
nodes.forEach(n => { const typeSet = new Set()
const nodeSet = RED.nodes.registry.getNodeSetForType(n.type) nodes.forEach((n) => {
if (nodeSet) { if (!typeSet.has(n.type)) {
modules[nodeSet.module] = nodeSet.version 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 return modules
} }
function updateGlobalConfigModuleList(nodes) { function updateGlobalConfigModuleList(nodes) {
const modules = getModuleListForNodes(nodes) const modules = getModuleListForNodes(nodes)
delete modules['node-red'] delete modules['node-red']
const hasModules = (Object.keys(modules).length > 0) 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) { if (!globalConfigNode && hasModules) {
globalConfigNode = { globalConfigNode = {
id: RED.nodes.id(), id: RED.nodes.id(),
@ -3187,7 +3298,7 @@ RED.nodes = (function() {
modules modules
} }
nodes.push(globalConfigNode) nodes.push(globalConfigNode)
} else if (globalConfigNode) { } else if (hasModules) {
globalConfigNode.modules = modules globalConfigNode.modules = modules
} }
} }

View File

@ -358,7 +358,10 @@ var RED = (function() {
}); });
return; 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) { if (msg.text) {
msg.default = msg.text; msg.default = msg.text;
var text = RED._(msg.text,msg); var text = RED._(msg.text,msg);
@ -672,14 +675,48 @@ var RED = (function() {
setTimeout(function() { setTimeout(function() {
loader.end(); loader.end();
checkFirstRun(function() { checkTelemetry(function () {
if (showProjectWelcome) { checkFirstRun(function() {
RED.projects.showStartup(); if (showProjectWelcome) {
} RED.projects.showStartup();
}); }
});
})
},100); },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) { function checkFirstRun(done) {
if (RED.settings.theme("tours") === false) { if (RED.settings.theme("tours") === false) {
done(); done();

View File

@ -163,13 +163,18 @@ RED.popover = (function() {
} }
var timer = null; var timer = null;
let isOpen = false
var active; var active;
var div; var div;
var contentDiv; var contentDiv;
var currentStyle; var currentStyle;
var openPopup = function(instant) { var openPopup = function(instant) {
if (isOpen) {
return
}
if (active) { if (active) {
isOpen = true
var existingPopover = target.data("red-ui-popover"); var existingPopover = target.data("red-ui-popover");
if (options.tooltip && existingPopover) { if (options.tooltip && existingPopover) {
active = false; active = false;
@ -334,6 +339,7 @@ RED.popover = (function() {
} }
var closePopup = function(instant) { var closePopup = function(instant) {
isOpen = false
$(document).off('mousedown.red-ui-popover'); $(document).off('mousedown.red-ui-popover');
if (!active) { if (!active) {
if (div) { if (div) {
@ -673,6 +679,74 @@ RED.popover = (function() {
show:show, show:show,
hide:hide hide:hide
} }
},
dialog: function(options) {
const dialogContent = $('<div style="position:relative"></div>');
if (options.closeButton !== false) {
$('<button type="button" class="red-ui-button red-ui-button-small" style="float: right; margin-top: -4px; margin-right: -4px;"><i class="fa fa-times"></i></button>').appendTo(dialogContent).click(function(evt) {
evt.preventDefault();
close();
})
}
const dialogBody = $('<div class="red-ui-dialog-body"></div>').appendTo(dialogContent);
if (options.title) {
$('<h2>').text(options.title).appendTo(dialogBody);
}
$('<div>').css("text-align","left").html(options.content).appendTo(dialogBody);
const stepToolbar = $('<div>',{class:"red-ui-dialog-toolbar"}).appendTo(dialogContent);
if (options.buttons) {
options.buttons.forEach(button => {
const btn = $('<button type="button" class="red-ui-button"></button>').text(button.text).appendTo(stepToolbar);
if (button.class) {
btn.addClass(button.class);
}
if (button.click) {
btn.on('click', function(evt) {
evt.preventDefault();
button.click();
})
}
})
}
const width = 500;
const maxWidth = Math.min($(window).width()-10,Math.max(width || 0, 300));
let shade = $('<div class="red-ui-shade" style="z-index: 2000"></div>').appendTo(document.body);
shade.fadeIn()
let popover = RED.popover.create({
target: $(".red-ui-editor"),
width: width || "auto",
maxWidth: maxWidth+"px",
direction: "inset",
class: "red-ui-dialog",
trigger: "manual",
content: dialogContent
}).open()
function close() {
if (shade) {
shade.fadeOut(() => {
shade.remove()
shade = null
})
}
if (popover) {
popover.close()
popover = null
}
}
return {
close
}
} }
} }

View File

@ -519,10 +519,25 @@
} }
}, },
expand: function () { 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.tray.hide();
RED.view.selectNodes({ RED.view.selectNodes({
single: true, single: true,
filter: filter,
selected: [that.value()], selected: [that.value()],
onselect: function (selection) { onselect: function (selection) {
that.value(selection.id); that.value(selection.id);

View File

@ -46,10 +46,20 @@ RED.contextMenu = (function () {
hasEnabledNode = true; hasEnabledNode = true;
} }
} }
if (n.l === undefined || n.l) { if (n.l === undefined) {
hasLabeledNode = true; // 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 { } else {
hasUnlabeledNode = true; if (n.l) {
hasLabeledNode = true;
} else {
hasUnlabeledNode = true;
}
} }
} }
} }

View File

@ -15,11 +15,14 @@
**/ **/
RED.eventLog = (function() { RED.eventLog = (function() {
var template = '<script type="text/x-red" data-template-name="_eventLog"><div class="form-row node-text-editor-row"><div style="height: 100%;min-height: 150px;" class="node-text-editor" id="red-ui-event-log-editor"></div></div></script>'; const template = '<script type="text/x-red" data-template-name="_eventLog"><div class="form-row node-text-editor-row"><div style="height: 100%;min-height: 150px;" class="node-text-editor" id="red-ui-event-log-editor"></div></div></script>';
let eventLogEditor;
let backlog = [];
let shown = false;
const activeLogs = new Set()
var eventLogEditor;
var backlog = [];
var shown = false;
function appendLogLine(line) { function appendLogLine(line) {
backlog.push(line); backlog.push(line);
@ -38,6 +41,18 @@ RED.eventLog = (function() {
init: function() { init: function() {
$(template).appendTo("#red-ui-editor-node-configs"); $(template).appendTo("#red-ui-editor-node-configs");
RED.actions.add("core:show-event-log",RED.eventLog.show); RED.actions.add("core:show-event-log",RED.eventLog.show);
const statusWidget = $('<button type="button" class="red-ui-footer-button red-ui-event-log-status" style="line-height: normal"><img style="width: 80%" src="red/images/spin.svg"/></div></button>');
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() { show: function() {
if (shown) { if (shown) {
@ -98,6 +113,12 @@ RED.eventLog = (function() {
}, },
log: function(id,payload) { log: function(id,payload) {
var ts = (new Date(payload.ts)).toISOString()+" "; var ts = (new Date(payload.ts)).toISOString()+" ";
if (!payload.end) {
activeLogs.add(id)
} else {
activeLogs.delete(id);
}
if (payload.type) { if (payload.type) {
ts += "["+payload.type+"] " ts += "["+payload.type+"] "
} }
@ -111,6 +132,11 @@ RED.eventLog = (function() {
appendLogLine(ts+line); 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) { startEvent: function(name) {
backlog.push(""); backlog.push("");

View File

@ -284,37 +284,85 @@ RED.palette.editor = (function() {
function _refreshNodeModule(module) { function _refreshNodeModule(module) {
if (!nodeEntries.hasOwnProperty(module)) { if (!nodeEntries.hasOwnProperty(module)) {
nodeEntries[module] = {info:RED.nodes.registry.getModule(module)}; const nodeInfo = RED.nodes.registry.getModule(module);
var index = [module]; let index = [module];
for (var s in nodeEntries[module].info.sets) {
if (nodeEntries[module].info.sets.hasOwnProperty(s)) { nodeEntries[module] = {
index.push(s); info: {
index = index.concat(nodeEntries[module].info.sets[s].types) 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(); nodeEntries[module].index = index.join(",").toLowerCase();
nodeList.editableList('addItem', nodeEntries[module]); nodeList.editableList('addItem', nodeEntries[module]);
} else { } else {
var moduleInfo = nodeEntries[module].info; if (nodeEntries[module].info.pluginSet && !nodeEntries[module].info.nodeSet) {
var nodeEntry = nodeEntries[module].elements; // Since plugins are loaded before nodes, check if the module has nodes too
if (nodeEntry) { const nodeInfo = RED.nodes.registry.getModule(module);
if (moduleInfo.plugin) {
nodeEntry.enableButton.hide();
nodeEntry.removeButton.show();
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; let pluginCount = 0;
for (let setName in moduleInfo.sets) { for (const setName in moduleInfo.pluginSet) {
if (moduleInfo.sets.hasOwnProperty(setName)) { if (moduleInfo.pluginSet.hasOwnProperty(setName)) {
let set = moduleInfo.sets[setName]; let set = moduleInfo.pluginSet[setName];
if (set.plugins) { if (set.plugins && set.plugins.length) {
pluginCount += 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})); setCount.push(RED._('palette.editor.pluginCount', { count: pluginCount }));
} else { if (!moduleInfo.nodeSet) {
// Module only have plugins
nodeEntry.enableButton.hide();
nodeEntry.removeButton.show();
}
}
if (moduleInfo.nodeSet) {
var activeTypeCount = 0; var activeTypeCount = 0;
var typeCount = 0; var typeCount = 0;
var errorCount = 0; var errorCount = 0;
@ -322,10 +370,10 @@ RED.palette.editor = (function() {
nodeEntries[module].totalUseCount = 0; nodeEntries[module].totalUseCount = 0;
nodeEntries[module].setUseCount = {}; nodeEntries[module].setUseCount = {};
for (var setName in moduleInfo.sets) { for (const setName in moduleInfo.nodeSet) {
if (moduleInfo.sets.hasOwnProperty(setName)) { if (moduleInfo.nodeSet.hasOwnProperty(setName)) {
var inUseCount = 0; let inUseCount = 0;
const set = moduleInfo.sets[setName]; const set = moduleInfo.nodeSet[setName];
const setElements = nodeEntry.sets[setName] const setElements = nodeEntry.sets[setName]
if (set.err) { if (set.err) {
@ -342,8 +390,8 @@ RED.palette.editor = (function() {
activeTypeCount += set.types.length; activeTypeCount += set.types.length;
} }
typeCount += set.types.length; typeCount += set.types.length;
for (var i=0;i<moduleInfo.sets[setName].types.length;i++) { for (var i=0;i<moduleInfo.nodeSet[setName].types.length;i++) {
var t = moduleInfo.sets[setName].types[i]; var t = moduleInfo.nodeSet[setName].types[i];
inUseCount += (typesInUse[t]||0); inUseCount += (typesInUse[t]||0);
if (setElements && set.enabled) { if (setElements && set.enabled) {
var def = RED.nodes.getType(t); var def = RED.nodes.getType(t);
@ -379,8 +427,8 @@ RED.palette.editor = (function() {
nodeEntry.errorRow.show(); nodeEntry.errorRow.show();
} }
var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount; const nodeCount = (activeTypeCount === typeCount) ? typeCount : activeTypeCount + " / " + typeCount;
nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount})); setCount.push(RED._('palette.editor.nodeCount', { count: typeCount, label: nodeCount }));
if (nodeEntries[module].totalUseCount > 0) { if (nodeEntries[module].totalUseCount > 0) {
nodeEntry.enableButton.text(RED._('palette.editor.inuse')); nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
@ -399,6 +447,7 @@ RED.palette.editor = (function() {
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0)); nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
} }
} }
nodeEntry.setCount.text(setCount.join(" & ") || RED._("sidebar.info.empty"));
} }
if (moduleInfo.pending_version) { if (moduleInfo.pending_version) {
nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow) nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
@ -700,6 +749,9 @@ RED.palette.editor = (function() {
refreshCatalogues() refreshCatalogues()
RED.actions.add("core:manage-palette", function(opts) { RED.actions.add("core:manage-palette", function(opts) {
if (opts && opts.autoInstall && opts.modules) {
autoInstallModules(opts.modules);
} else {
RED.userSettings.show('palette'); RED.userSettings.show('palette');
if (opts) { if (opts) {
if (opts.view) { if (opts.view) {
@ -713,9 +765,15 @@ RED.palette.editor = (function() {
} }
} }
} }
}); }
});
RED.events.on('registry:module-updated', function(ns) { 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); refreshNodeModule(ns.module);
refreshUpdateStatus(); refreshUpdateStatus();
}); });
@ -789,19 +847,41 @@ RED.palette.editor = (function() {
}) })
RED.events.on("registry:plugin-module-added", function(module) { RED.events.on("registry:plugin-module-added", function(module) {
if (!nodeEntries.hasOwnProperty(module)) { if (!nodeEntries.hasOwnProperty(module)) {
nodeEntries[module] = {info:RED.plugins.getModule(module)}; const pluginInfo = RED.plugins.getModule(module);
var index = [module]; let index = [module];
for (var s in nodeEntries[module].info.sets) {
if (nodeEntries[module].info.sets.hasOwnProperty(s)) { nodeEntries[module] = {
index.push(s); info: {
index = index.concat(nodeEntries[module].info.sets[s].types) 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(); nodeEntries[module].index = index.join(",").toLowerCase();
nodeList.editableList('addItem', nodeEntries[module]); nodeList.editableList('addItem', nodeEntries[module]);
} else { } else {
// Since plugins are loaded before nodes,
// `nodeEntries[module]` should be undefined
_refreshNodeModule(module); _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() { function getSettingsPane() {
@ -912,6 +1000,12 @@ RED.palette.editor = (function() {
var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container); var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container);
var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow); var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
$('<span>').text(entry.name).appendTo(titleRow); $('<span>').text(entry.name).appendTo(titleRow);
if (entry.url) {
// Add the link icon to the node documentation
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href', entry.url).appendTo(titleRow);
}
var metaRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-version"><i class="fa fa-tag"></i></div>').appendTo(headerRow); var metaRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-version"><i class="fa fa-tag"></i></div>').appendTo(headerRow);
var versionSpan = $('<span>').text(entry.version).appendTo(metaRow); var versionSpan = $('<span>').text(entry.version).appendTo(metaRow);
@ -976,12 +1070,28 @@ RED.palette.editor = (function() {
} }
}) })
const populateSetList = function () { const populateSetList = function () {
var setList = Object.keys(entry.sets) const setList = [...Object.keys(entry.nodeSet || {}), ...Object.keys(entry.pluginSet || {})];
setList.sort(function(A,B) { setList.sort(function (A, B) {
return A.toLowerCase().localeCompare(B.toLowerCase()); return A.toLowerCase().localeCompare(B.toLowerCase());
}); });
setList.forEach(function(setName) { setList.forEach(function (setName) {
var set = entry.sets[setName]; const set = (entry.nodeSet && setName in entry.nodeSet) ? entry.nodeSet[setName] : entry.pluginSet[setName];
if (set.plugins && !set.plugins.length) {
// `registerPlugin` in the runtime not called
if (!!RED.plugins.getPlugin(setName)) {
// Add plugin if registered in editor but not in runtime
// Can happen if plugin doesn't have .js file
set.plugins.push({ id: setName });
} else {
// `registerPlugin` in the editor not called - do not add this empty set
return;
}
} else if (set.types && !set.types.length) {
// `registerPlugin` in the runtime not called - do not add this empty set
return;
}
var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow); var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow); var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
var typeSwatches = {}; var typeSwatches = {};
@ -1198,7 +1308,17 @@ RED.palette.editor = (function() {
var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container); var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container);
var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow); var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
$('<span>').text(entry.name||entry.id).appendTo(titleRow); $('<span>').text(entry.name||entry.id).appendTo(titleRow);
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href',entry.url).appendTo(titleRow); if (entry.url) {
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href',entry.url).appendTo(titleRow);
}
if (entry.deprecated) {
const deprecatedWarning = $('<span class="red-ui-palette-module-deprecated"></span>').text(RED._('palette.editor.deprecated')).appendTo(titleRow);
let message = $('<span>').text(RED._('palette.editor.deprecatedTip'))
if (typeof entry.deprecated === 'string') {
$('<p>').text(entry.deprecated).appendTo(message)
}
RED.popover.tooltip(deprecatedWarning, message);
}
var descRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow); var descRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
$('<div>',{class:"red-ui-palette-module-description"}).text(entry.description).appendTo(descRow); $('<div>',{class:"red-ui-palette-module-description"}).text(entry.description).appendTo(descRow);
var metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow); var metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
@ -1339,63 +1459,88 @@ RED.palette.editor = (function() {
$('<div id="red-ui-palette-module-install-shade" class="red-ui-palette-module-shade hide"><div class="red-ui-palette-module-shade-status"></div><img src="red/images/spin.svg" class="red-ui-palette-spinner"/></div>').appendTo(installTab); $('<div id="red-ui-palette-module-install-shade" class="red-ui-palette-module-shade hide"><div class="red-ui-palette-module-shade-status"></div><img src="red/images/spin.svg" class="red-ui-palette-spinner"/></div>').appendTo(installTab);
} }
function update(entry,version,url,container,done) { function update(entry, version, url, container, done) {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
done(new Error('Palette not editable')); done(new Error('Palette not editable'));
return; return;
} }
var notification = RED.notify(RED._("palette.editor.confirm.update.body",{module:entry.name}),{
let notification;
let msg = RED._("palette.editor.confirm.update.body", { module: entry.name });
const buttons = [
{
text: RED._("common.label.cancel"),
click: function () {
notification.close();
}
},
{
text: RED._("palette.editor.confirm.button.update"),
class: "primary red-ui-palette-module-install-confirm-button-update",
click: function () {
const spinner = RED.utils.addSpinnerOverlay(container, true);
const buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
$('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function (evt) {
evt.preventDefault();
RED.actions.invoke("core:show-event-log");
});
installNodeModule(entry.name, version, url, function (xhr) {
spinner.remove();
if (xhr) {
if (xhr.responseJSON) {
const notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{
type: 'error',
modal: true,
fixed: true,
buttons: [
{
text: RED._("common.label.close"),
click: function () {
notification.close();
}
}, {
text: RED._("eventLog.view"),
click: function () {
notification.close();
RED.actions.invoke("core:show-event-log");
}
}
]
});
}
}
done(xhr);
});
notification.close();
}
}
];
const currentVersion = semverre.exec(nodeEntries[entry.name].info.version);
const targetVersion = semverre.exec(version);
if (currentVersion && targetVersion) {
if (targetVersion[1] > currentVersion[1]) {
// Updating to Major version
msg = msg + RED._("palette.editor.majorVersion");
if (entry.url) {
// Add a button to open the node documentation
buttons.splice(1, 0, {
text: RED._("palette.editor.confirm.button.review"),
class: "primary red-ui-palette-module-install-confirm-button-review",
click: function () {
window.open(entry.url);
}
});
}
}
}
notification = RED.notify(msg, {
modal: true, modal: true,
fixed: true, fixed: true,
buttons: [ buttons: buttons
{ });
text: RED._("common.label.cancel"),
click: function() {
notification.close();
}
},
{
text: RED._("palette.editor.confirm.button.update"),
class: "primary red-ui-palette-module-install-confirm-button-update",
click: function() {
var spinner = RED.utils.addSpinnerOverlay(container, true);
var buttonRow = $('<div style="position: relative;bottom: calc(50% + 17px); padding-right: 10px;text-align: right;"></div>').appendTo(spinner);
$('<button class="red-ui-button"></button>').text(RED._("eventLog.view")).appendTo(buttonRow).on("click", function(evt) {
evt.preventDefault();
RED.actions.invoke("core:show-event-log");
});
installNodeModule(entry.name,version,url,function(xhr) {
spinner.remove();
if (xhr) {
if (xhr.responseJSON) {
var notification = RED.notify(RED._('palette.editor.errors.updateFailed',{module: entry.name,message:xhr.responseJSON.message}),{
type: 'error',
modal: true,
fixed: true,
buttons: [
{
text: RED._("common.label.close"),
click: function() {
notification.close();
}
},{
text: RED._("eventLog.view"),
click: function() {
notification.close();
RED.actions.invoke("core:show-event-log");
}
}
]
});
}
}
done(xhr);
});
notification.close();
}
}
]
})
} }
function remove(entry,container,done) { function remove(entry,container,done) {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
@ -1448,7 +1593,7 @@ RED.palette.editor = (function() {
} }
} else { } else {
// dedicated list management for plugins // dedicated list management for plugins
if (entry.plugin) { if (entry.pluginSet) {
let e = nodeEntries[entry.name]; let e = nodeEntries[entry.name];
if (e) { if (e) {
@ -1461,9 +1606,9 @@ RED.palette.editor = (function() {
// cleans the editor accordingly of its left-overs. // cleans the editor accordingly of its left-overs.
let found_onremove = true; let found_onremove = true;
let keys = Object.keys(entry.sets); let keys = Object.keys(entry.pluginSet);
keys.forEach((key) => { keys.forEach((key) => {
let set = entry.sets[key]; let set = entry.pluginSet[key];
for (let i=0; i<set.plugins?.length; i++) { for (let i=0; i<set.plugins?.length; i++) {
let plgn = RED.plugins.getPlugin(set.plugins[i].id); let plgn = RED.plugins.getPlugin(set.plugins[i].id);
if (plgn && plgn.onremove && typeof plgn.onremove === 'function') { if (plgn && plgn.onremove && typeof plgn.onremove === 'function') {
@ -1593,29 +1738,113 @@ RED.palette.editor = (function() {
}) })
} }
function autoInstallModules(modules) {
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
console.error('Palette not editable');
return;
}
let notification;
const notificationOptions = {
fixed: true,
buttons: [
{
text: RED._("common.label.close"),
click: function () {
notification.close();
}
}, {
text: RED._("eventLog.view"),
click: function () {
notification.close();
RED.actions.invoke("core:show-event-log");
}
}
]
};
const moduleArray = Object.entries(modules);
const installModule = function (module) {
const [moduleName, moduleVersion] = module;
const spinner = '<div style="text-align: center; height: 100%; padding-top: 20px;"><img src="red/images/spin.svg" style="width: 60px;"/></div>';
const msg = "<p>" + RED._("palette.editor.installing", { module: moduleName }) + "</p>" + spinner;
if (!notification) {
notification = RED.notify(msg, notificationOptions);
} else {
notification.update(msg, notificationOptions);
}
installNodeModule(moduleName, moduleVersion, undefined, function(xhr, textStatus, err) {
if (err && xhr.status === 504) {
notification.update(RED._("palette.editor.errors.installTimeout"), {
modal: true,
fixed: true,
buttons: notificationOptions.buttons
});
} else if (xhr) {
if (xhr.responseJSON) {
notification.update(RED._("palette.editor.errors.installFailed", { module: moduleName, message:xhr.responseJSON.message }), {
type: "error",
modal: true,
fixed: true,
buttons: notificationOptions.buttons
});
}
} else {
if (moduleArray.length) {
installModule(moduleArray.shift());
} else {
notification.update(RED._("palette.editor.successfulInstall"), { ...notificationOptions, type: "success", timeout: 10000 });
}
}
});
};
if (moduleArray.length) {
installModule(moduleArray.shift());
}
}
const updateStatusWidget = $('<button type="button" class="red-ui-footer-button red-ui-update-status"></button>'); const updateStatusWidget = $('<button type="button" class="red-ui-footer-button red-ui-update-status"></button>');
let updateStatusWidgetPopover;
const updateStatusState = { moduleCount: 0 }
let updateAvailable = []; let updateAvailable = [];
function addUpdateInfoToStatusBar() { function addUpdateInfoToStatusBar() {
updateStatusWidget.on("click", function (evt) { updateStatusWidgetPopover = RED.popover.create({
RED.actions.invoke("core:manage-palette", { target: updateStatusWidget,
view: "nodes", trigger: "click",
filter: '"' + updateAvailable.join('", "') + '"' interactive: true,
}); direction: "bottom",
}); content: function () {
const count = updateAvailable.length || 0;
RED.popover.tooltip(updateStatusWidget, function () { const content = $('<div style="display: flex; flex-direction: column; gap: 5px;"></div>');
const count = updateAvailable.length || 0; if (updateStatusState.version) {
return RED._("palette.editor.updateCount", { count: count }); $(`<a class='red-ui-button' href="https://github.com/node-red/node-red/releases/tag/${updateStatusState.version}" target="_blank">${RED._("telemetry.updateAvailableDesc", updateStatusState)}</a>`).appendTo(content)
}
if (count > 0) {
$(`<button type="button" class="red-ui-button"><i class="fa fa-cube"></i> ${RED._("palette.editor.updateCount", { count: count })}</button>`).on("click", function (evt) {
updateStatusWidgetPopover.close()
RED.actions.invoke("core:manage-palette", {
view: "nodes",
filter: '"' + updateAvailable.join('", "') + '"'
});
}).appendTo(content)
}
return content
},
delay: { show: 750, hide: 250 }
}); });
RED.statusBar.add({ RED.statusBar.add({
id: "update", id: "red-ui-status-package-update",
align: "right", align: "right",
element: updateStatusWidget element: updateStatusWidget
}); });
updateStatus({ count: 0 }); updateStatus();
} }
let pendingRefreshTimeout let pendingRefreshTimeout
@ -1638,18 +1867,22 @@ RED.palette.editor = (function() {
} }
} }
} }
updateStatusState.moduleCount = updateAvailable.length;
updateStatus({ count: updateAvailable.length }); updateStatus();
}, 200) }, 200)
} }
function updateStatus(opts) { function updateStatus() {
if (opts.count) { if (updateStatusState.moduleCount || updateStatusState.version) {
RED.statusBar.show("update");
updateStatusWidget.empty(); updateStatusWidget.empty();
$('<span><i class="fa fa-cube"></i> ' + opts.count + '</span>').appendTo(updateStatusWidget); let count = updateStatusState.moduleCount || 0;
if (updateStatusState.version) {
count ++
}
$(`<span><i class="fa fa-cube"></i> ${RED._("telemetry.updateAvailable", { count: count })}</span>`).appendTo(updateStatusWidget);
RED.statusBar.show("red-ui-status-package-update");
} else { } else {
RED.statusBar.hide("update"); RED.statusBar.hide("red-ui-status-package-update");
} }
} }

View File

@ -80,7 +80,7 @@ RED.palette = (function() {
getNodeCount: function (visibleOnly) { getNodeCount: function (visibleOnly) {
const nodes = catDiv.find(".red-ui-palette-node") const nodes = catDiv.find(".red-ui-palette-node")
if (visibleOnly) { if (visibleOnly) {
return nodes.filter(function() { return $(this).css('display') !== 'none'}).length return nodes.filter(function() { return $(this).attr("data-filter") !== "true"}).length
} else { } else {
return nodes.length return nodes.length
} }
@ -572,8 +572,10 @@ RED.palette = (function() {
var currentLabel = $(el).attr("data-palette-label"); var currentLabel = $(el).attr("data-palette-label");
var type = $(el).attr("data-palette-type"); var type = $(el).attr("data-palette-type");
if (val === "" || re.test(type) || re.test(currentLabel)) { if (val === "" || re.test(type) || re.test(currentLabel)) {
$(el).attr("data-filter", null)
$(this).show(); $(this).show();
} else { } else {
$(el).attr("data-filter", "true")
$(this).hide(); $(this).hide();
} }
}); });

View File

@ -435,10 +435,15 @@ RED.tourGuide = (function() {
function listTour() { function listTour() {
return [ return [
{
id: "4_1",
label: "4.1",
path: "./tours/welcome.js"
},
{ {
id: "4_0", id: "4_0",
label: "4.0", label: "4.0",
path: "./tours/welcome.js" path: "./tours/4.0/welcome.js"
}, },
{ {
id: "3_1", id: "3_1",

View File

@ -14,6 +14,7 @@ RED.typeSearch = (function() {
var addCallback; var addCallback;
var cancelCallback; var cancelCallback;
var moveCallback; var moveCallback;
var suggestCallback
var typesUsed = {}; var typesUsed = {};
@ -25,6 +26,11 @@ RED.typeSearch = (function() {
selected = 0; selected = 0;
searchResults.children().removeClass('selected'); searchResults.children().removeClass('selected');
searchResults.children(":visible:first").addClass('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); },100);
} }
@ -63,7 +69,7 @@ RED.typeSearch = (function() {
} }
}); });
searchInput.on('keydown',function(evt) { searchInput.on('keydown',function(evt) {
var children = searchResults.children(":visible"); const children = searchResults.children(":visible");
if (evt.keyCode === 40 && evt.shiftKey) { if (evt.keyCode === 40 && evt.shiftKey) {
evt.preventDefault(); evt.preventDefault();
moveDialog(0,10); moveDialog(0,10);
@ -86,9 +92,14 @@ RED.typeSearch = (function() {
selected++; selected++;
} }
$(children[selected]).addClass('selected'); $(children[selected]).addClass('selected');
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
if (n) {
updateSuggestion(n)
}
ensureSelectedIsVisible(); ensureSelectedIsVisible();
evt.preventDefault(); evt.preventDefault();
} else if (evt.keyCode === 38) { } else if (evt.keyCode === 38) {
// Up
if (selected > 0) { if (selected > 0) {
if (selected < children.length) { if (selected < children.length) {
$(children[selected]).removeClass('selected'); $(children[selected]).removeClass('selected');
@ -96,6 +107,10 @@ RED.typeSearch = (function() {
selected--; selected--;
} }
$(children[selected]).addClass('selected'); $(children[selected]).addClass('selected');
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
if (n) {
updateSuggestion(n)
}
ensureSelectedIsVisible(); ensureSelectedIsVisible();
evt.preventDefault(); evt.preventDefault();
} else if ((evt.metaKey || evt.ctrlKey) && evt.keyCode === 13 ) { } else if ((evt.metaKey || evt.ctrlKey) && evt.keyCode === 13 ) {
@ -103,14 +118,14 @@ RED.typeSearch = (function() {
// (ctrl or cmd) and enter // (ctrl or cmd) and enter
var index = Math.max(0,selected); var index = Math.max(0,selected);
if (index < children.length) { if (index < children.length) {
var n = $(children[index]).find(".red-ui-editableList-item-content").data('data'); const n = $(children[index]).find(".red-ui-editableList-item-content").data('data');
if (!/^_action_:/.test(n.type)) { if (!n.nodes && !/^_action_:/.test(n.type)) {
typesUsed[n.type] = Date.now(); typesUsed[n.type] = Date.now();
} }
if (n.def.outputs === 0) { if (n.def.outputs === 0) {
confirm(n); confirm(n);
} else { } else {
addCallback(n.type,true); addCallback(n, true);
} }
$("#red-ui-type-search-input").val("").trigger("keyup"); $("#red-ui-type-search-input").val("").trigger("keyup");
setTimeout(function() { setTimeout(function() {
@ -142,7 +157,7 @@ RED.typeSearch = (function() {
if (activeFilter === "" ) { if (activeFilter === "" ) {
return true; return true;
} }
if (data.recent || data.common) { if (data.recent || data.common || data.suggestion) {
return false; return false;
} }
return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1); return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1);
@ -164,67 +179,116 @@ RED.typeSearch = (function() {
} }
return Ai-Bi; return Ai-Bi;
}, },
addItem: function(container,i,object) { addItem: function(container, i, nodeItem) {
var def = object.def; // nodeItem can take multiple forms
object.index = object.type.toLowerCase(); // - A node type: {type: "inject", def: RED.nodes.getType("inject"), label: "Inject"}
if (object.separator) { // - 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") container.addClass("red-ui-search-result-separator")
} }
var div = $('<div>',{class:"red-ui-search-result"}).appendTo(container); const div = $('<div>',{class:"red-ui-search-result"}).appendTo(container);
const nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div); if (nodeItem.suggestionPlaceholder) {
if (object.type === "junction") { nodeDiv.addClass("red-ui-palette-icon-suggestion")
const iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
$('<i class="spinner" style="margin-top: -1px">').appendTo(iconContainer);
} else if (nodeType === "junction") {
nodeDiv.addClass("red-ui-palette-icon-junction"); nodeDiv.addClass("red-ui-palette-icon-junction");
} else if (/^_action_:/.test(object.type)) {
nodeDiv.addClass("red-ui-palette-icon-junction")
} else { } else {
var colour = RED.utils.getNodeColor(object.type,def); nodeDiv.css('backgroundColor', RED.utils.getNodeColor(nodeType, nodeDef));
nodeDiv.css('backgroundColor',colour);
} }
var icon_url = RED.utils.getNodeIcon(def);
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); if (nodeDef) {
RED.utils.createIconElement(icon_url, iconContainer, false); // Add the node icon
const icon_url = RED.utils.getNodeIcon(nodeDef);
const iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
RED.utils.createIconElement(icon_url, iconContainer, false);
}
if (/^subflow:/.test(object.type)) { if (/^subflow:/.test(nodeType)) {
var sf = RED.nodes.subflow(object.type.substring(8)); var sf = RED.nodes.subflow(nodeType.substring(8));
if (sf.in.length > 0) { if (sf.in.length > 0) {
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); $('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
} }
if (sf.out.length > 0) { if (sf.out.length > 0) {
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv); $('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
} }
} else if (!/^_action_:/.test(object.type) && object.type !== "junction") { } else if (nodeDef && nodeType !== "junction") {
if (def.inputs > 0) { if (nodeDef.inputs > 0) {
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv); $('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
} }
if (def.outputs > 0) { if (nodeDef.outputs > 0) {
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv); $('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
} }
} }
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div); var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
var label = object.label; var label = nodeItem.label;
object.index += "|"+label.toLowerCase(); nodeItem.index += "|"+label.toLowerCase();
$('<div>',{class:"red-ui-search-result-node-label"}).text(label).appendTo(contentDiv); $('<div>',{class:"red-ui-search-result-node-label"}).text(label).appendTo(contentDiv);
nodeItem.element = container;
div.on("click", function(evt) { div.on("click", function(evt) {
evt.preventDefault(); 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 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) { function confirm(def) {
hide(); hide();
if (!/^_action_:/.test(def.type)) { if (!def.nodes && !/^_action_:/.test(def.type)) {
typesUsed[def.type] = Date.now(); typesUsed[def.type] = Date.now();
} }
addCallback(def.type); addCallback(def);
} }
function handleMouseActivity(evt) { function handleMouseActivity(evt) {
@ -274,6 +338,7 @@ RED.typeSearch = (function() {
addCallback = opts.add; addCallback = opts.add;
cancelCallback = opts.cancel; cancelCallback = opts.cancel;
moveCallback = opts.move; moveCallback = opts.move;
suggestCallback = opts.suggest;
RED.events.emit("type-search:open"); RED.events.emit("type-search:open");
//shade.show(); //shade.show();
if ($("#red-ui-main-container").height() - opts.y - 195 < 0) { if ($("#red-ui-main-container").height() - opts.y - 195 < 0) {
@ -294,6 +359,9 @@ RED.typeSearch = (function() {
},200); },200);
} }
function hide(fast) { function hide(fast) {
if (suggestCallback) {
suggestCallback(null);
}
if (visible) { if (visible) {
visible = false; visible = false;
if (dialog !== null) { if (dialog !== null) {
@ -356,11 +424,11 @@ RED.typeSearch = (function() {
(!filter.output || def.outputs > 0) (!filter.output || def.outputs > 0)
} }
function refreshTypeList(opts) { function refreshTypeList(opts) {
var i; let i;
searchResults.editableList('empty'); searchResults.editableList('empty');
searchInput.searchBox('value','').focus(); searchInput.searchBox('value','').focus();
selected = -1; selected = -1;
var common = [ const common = [
'inject','debug','function','change','switch','junction' 'inject','debug','function','change','switch','junction'
].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); }); ].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') // common.push('_action_:core:split-wire-with-link-nodes')
// } // }
var recentlyUsed = Object.keys(typesUsed); let recentlyUsed = Object.keys(typesUsed);
recentlyUsed.sort(function(a,b) { recentlyUsed.sort(function(a,b) {
return typesUsed[b]-typesUsed[a]; 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; return applyFilter(opts.filter,t,RED.nodes.getType(t)) && common.indexOf(t) === -1;
}); });
var items = []; const items = [];
RED.nodes.registry.getNodeTypes().forEach(function(t) { 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') { if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
items.push({type:t,def: def, label:getTypeLabel(t,def)}); 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.push({ type: 'junction', def: { inputs:1, outputs: 1, label: 'junction', type: 'junction'}, label: 'junction' })
items.sort(sortTypeLabels); items.sort(sortTypeLabels);
var commonCount = 0; let index = 0;
var item;
var index = 0; // const suggestionItem = {
// suggestionPlaceholder: true,
// label: 'loading suggestions...',
// separator: true,
// i: index++
// }
// searchResults.editableList('addItem', suggestionItem);
// setTimeout(function() {
// searchResults.editableList('removeItem', suggestionItem);
// const suggestedItem = {
// suggestion: true,
// label: 'Change/Debug Combo',
// separator: true,
// i: suggestionItem.i,
// nodes: [
// { id: 'suggestion-1', type: 'change', x: 0, y: 0, wires:[['suggestion-2']] },
// { id: 'suggestion-2', type: 'function', outputs: 3, x: 200, y: 0, wires:[['suggestion-3'],['suggestion-4'],['suggestion-6']] },
// { id: 'suggestion-3', _g: 'suggestion-group-1', type: 'debug', x: 375, y: -40 },
// { id: 'suggestion-4', _g: 'suggestion-group-1', type: 'debug', x: 375, y: 0 },
// { id: 'suggestion-5', _g: 'suggestion-group-1', type: 'debug', x: 410, y: 40 },
// { id: 'suggestion-6', type: 'junction', wires: [['suggestion-5']], x:325, y:40 }
// ]
// }
// searchResults.editableList('addItem', suggestedItem);
// }, 1000)
for(i=0;i<common.length;i++) { for(i=0;i<common.length;i++) {
var itemDef = RED.nodes.getType(common[i]); let itemDef
if (common[i] === 'junction') { if (common[i] === 'junction') {
itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'} itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'}
} else if (/^_action_:/.test(common[i]) ) { } else if (/^_action_:/.test(common[i]) ) {
itemDef = { inputs:1, outputs: 1, label: common[i], type: common[i]} itemDef = { inputs:1, outputs: 1, label: common[i], type: common[i]}
} else {
itemDef = RED.nodes.getType(common[i]);
} }
if (itemDef) { if (itemDef) {
item = { const item = {
type: common[i], type: common[i],
common: true, common: true,
def: itemDef, def: itemDef,
@ -414,7 +511,7 @@ RED.typeSearch = (function() {
} }
} }
for(i=0;i<Math.min(5,recentlyUsed.length);i++) { for(i=0;i<Math.min(5,recentlyUsed.length);i++) {
item = { const item = {
type:recentlyUsed[i], type:recentlyUsed[i],
def: RED.nodes.getType(recentlyUsed[i]), def: RED.nodes.getType(recentlyUsed[i]),
recent: true, recent: true,

View File

@ -144,6 +144,18 @@ RED.userSettings = (function() {
{setting:"view-node-show-label",label:"menu.label.showNodeLabelDefault",default: true, toggle:true} {setting:"view-node-show-label",label:"menu.label.showNodeLabelDefault",default: true, toggle:true}
] ]
}, },
{
title: "telemetry.label",
options: [
{
global: true,
setting: "telemetryEnabled",
label: "telemetry.settingsTitle",
description: "telemetry.settingsDescription",
toggle: true
},
]
},
{ {
title: "menu.label.other", title: "menu.label.other",
options: [ options: [
@ -170,13 +182,20 @@ RED.userSettings = (function() {
var initialState; var initialState;
if (opt.local) { if (opt.local) {
initialState = localStorage.getItem(opt.setting); initialState = localStorage.getItem(opt.setting);
} else if (opt.global) {
initialState = RED.settings.get(opt.setting);
} else { } else {
initialState = currentEditorSettings.view[opt.setting]; initialState = currentEditorSettings.view[opt.setting];
} }
var row = $('<div class="red-ui-settings-row"></div>').appendTo(pane); var row = $('<div class="red-ui-settings-row"></div>').appendTo(pane);
var input; var input;
if (opt.toggle) { if (opt.toggle) {
input = $('<label for="user-settings-'+opt.setting+'"><input id="user-settings-'+opt.setting+'" type="checkbox"> '+RED._(opt.label)+'</label>').appendTo(row).find("input"); let label = RED._(opt.label)
if (opt.description) {
label = `<p>${label}</p>${RED._(opt.description)}`;
}
input = $('<input id="user-settings-'+opt.setting+'" type="checkbox">').appendTo(row)
$('<label for="user-settings-'+opt.setting+'">'+label+'</label>').appendTo(row)
input.prop('checked',initialState); input.prop('checked',initialState);
} else if (opt.options) { } else if (opt.options) {
$('<label for="user-settings-'+opt.setting+'">'+RED._(opt.label)+'</label>').appendTo(row); $('<label for="user-settings-'+opt.setting+'">'+RED._(opt.label)+'</label>').appendTo(row);
@ -210,6 +229,8 @@ RED.userSettings = (function() {
var opt = allSettings[id]; var opt = allSettings[id];
if (opt.local) { if (opt.local) {
localStorage.setItem(opt.setting,value); localStorage.setItem(opt.setting,value);
} else if (opt.global) {
RED.settings.set(opt.setting, value)
} else { } else {
var currentEditorSettings = RED.settings.get('editor') || {}; var currentEditorSettings = RED.settings.get('editor') || {};
currentEditorSettings.view = currentEditorSettings.view || {}; currentEditorSettings.view = currentEditorSettings.view || {};
@ -238,7 +259,7 @@ RED.userSettings = (function() {
addPane({ addPane({
id:'view', id:'view',
title: RED._("menu.label.view.view"), title: RED._("menu.label.settings"),
get: createViewPane, get: createViewPane,
close: function() { close: function() {
viewSettings.forEach(function(section) { viewSettings.forEach(function(section) {

View File

@ -24,7 +24,7 @@ RED.view.annotations = (function() {
refreshAnnotation = !!evt.node[opts.refresh] refreshAnnotation = !!evt.node[opts.refresh]
delete evt.node[opts.refresh] delete evt.node[opts.refresh]
} else if (typeof opts.refresh === "function") { } else if (typeof opts.refresh === "function") {
refreshAnnotation = opts.refresh(evnt.node) refreshAnnotation = opts.refresh(evt.node)
} }
if (refreshAnnotation) { if (refreshAnnotation) {
refreshAnnotationElement(annotation.id, annotation.node, annotation.element) refreshAnnotationElement(annotation.id, annotation.node, annotation.element)

View File

@ -176,8 +176,8 @@ RED.view.tools = (function() {
} }
nodes.forEach(function(n) { nodes.forEach(function(n) {
var modified = false; 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 (labelShown) {
if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) { if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) {

View File

@ -100,6 +100,11 @@ RED.view = (function() {
var clipboard = ""; var clipboard = "";
let clipboardSource let clipboardSource
let currentSuggestion = null;
let suggestedNodes = [];
let suggestedLinks = [];
let suggestedJunctions = [];
// Note: these are the permitted status colour aliases. The actual RGB values // Note: these are the permitted status colour aliases. The actual RGB values
// are set in the CSS - flow.scss/colors.scss // are set in the CSS - flow.scss/colors.scss
const status_colours = { 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-edit", activeFlowLocked || activeSubflow || event.workspace === 0);
RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow); RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow);
@ -653,7 +660,7 @@ RED.view = (function() {
return; return;
} }
var historyEvent = result.historyEvent; 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"); 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")) { 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 lastAddedX;
var lastAddedWidth; 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({ RED.typeSearch.show({
x:clientX-mainPos.left-node_width/2 - (ox-point[0]), x:clientX-mainPos.left-node_width/2 - (ox-point[0]),
y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]), y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]),
@ -1430,7 +1451,63 @@ RED.view = (function() {
keepAdding = false; keepAdding = false;
resetMouseVars(); 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 nn;
var historyEvent; var historyEvent;
if (/^_action_:/.test(type)) { if (/^_action_:/.test(type)) {
@ -1479,7 +1556,7 @@ RED.view = (function() {
if (nn.type === 'junction') { if (nn.type === 'junction') {
nn = RED.nodes.addJunction(nn); nn = RED.nodes.addJunction(nn);
} else { } else {
nn = RED.nodes.add(nn); nn = RED.nodes.add(nn, { source: 'typeSearch' });
} }
if (quickAddLink) { if (quickAddLink) {
var drag_line = quickAddLink; var drag_line = quickAddLink;
@ -1662,6 +1739,22 @@ RED.view = (function() {
quickAddActive = false; quickAddActive = false;
ghostNode.remove(); 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-input").remove();
nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove(); nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove();
} }
let nodesToDraw = activeNodes;
var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(activeNodes,function(d){return d.id}); 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) { 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(); }).remove();
var nodeEnter = node.enter().insert("svg:g") var nodeEnter = node.enter().insert("svg:g")
.attr("class", "red-ui-flow-node red-ui-flow-node-group") .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) { nodeEnter.each(function(d,i) {
this.__outputs__ = []; this.__outputs__ = [];
this.__inputs__ = []; this.__inputs__ = [];
var node = d3.select(this); var node = d3.select(this);
if (d.__ghost) {
node.classed("red-ui-flow-node-ghost",true);
}
var nodeContents = document.createDocumentFragment(); var nodeContents = document.createDocumentFragment();
var isLink = (d.type === "link in" || d.type === "link out") var isLink = (d.type === "link in" || d.type === "link out")
var hideLabel = d.hasOwnProperty('l')?!d.l : isLink; var hideLabel = d.hasOwnProperty('l')?!d.l : isLink;
@ -4624,19 +4725,21 @@ RED.view = (function() {
bgButton.setAttribute("width",16); bgButton.setAttribute("width",16);
bgButton.setAttribute("height",node_height-12); bgButton.setAttribute("height",node_height-12);
bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def)); bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
d3.select(bgButton) if (!d.__ghost) {
.on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}}) d3.select(bgButton)
.on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}}) .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
.on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}}) .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
.on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) { .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
var op = 1; .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
if (d._def.button.toggle) { var op = 1;
op = d[d._def.button.toggle]?1:0.2; if (d._def.button.toggle) {
} op = d[d._def.button.toggle]?1:0.2;
d3.select(this).attr("fill-opacity",op); }
}}) d3.select(this).attr("fill-opacity",op);
.on("click",nodeButtonClicked) }})
.on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();}) .on("click",nodeButtonClicked)
.on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
}
buttonGroup.appendChild(bgButton); buttonGroup.appendChild(bgButton);
node[0][0].__buttonGroupButton__ = bgButton; node[0][0].__buttonGroupButton__ = bgButton;
@ -4651,13 +4754,15 @@ RED.view = (function() {
mainRect.setAttribute("ry", 5); mainRect.setAttribute("ry", 5);
mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def)); mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
node[0][0].__mainRect__ = mainRect; node[0][0].__mainRect__ = mainRect;
d3.select(mainRect) if (!d.__ghost) {
.on("mouseup",nodeMouseUp) d3.select(mainRect)
.on("mousedown",nodeMouseDown) .on("mouseup",nodeMouseUp)
.on("touchstart",nodeTouchStart) .on("mousedown",nodeMouseDown)
.on("touchend",nodeTouchEnd) .on("touchstart",nodeTouchStart)
.on("mouseover",nodeMouseOver) .on("touchend",nodeTouchEnd)
.on("mouseout",nodeMouseOut); .on("mouseover",nodeMouseOver)
.on("mouseout",nodeMouseOut);
}
nodeContents.appendChild(mainRect); 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-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"); //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); 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; var nodesReordered = false;
@ -4862,13 +4970,15 @@ RED.view = (function() {
var inputPorts = thisNode.selectAll(".red-ui-flow-port-input"); var inputPorts = thisNode.selectAll(".red-ui-flow-port-input");
if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) { if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) {
inputPorts.each(function(d,i) { inputPorts.each(function(d,i) {
RED.hooks.trigger("viewRemovePort",{ if (!d.__ghost) {
node:d, RED.hooks.trigger("viewRemovePort",{
el:self, node:d,
port:d3.select(this)[0][0], el:self,
portType: "input", port:d3.select(this)[0][0],
portIndex: 0 portType: "input",
}) portIndex: 0
})
}
}).remove(); }).remove();
} else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) { } 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"); 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].__data__ = this.__data__;
inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT; inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT;
inputGroupPorts[0][0].__portIndex__ = 0; inputGroupPorts[0][0].__portIndex__ = 0;
inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);}) if (!d.__ghost) {
.on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();}) inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
.on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} ) .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
.on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} ) .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
.on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
.on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 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; var numOutputs = d.outputs;
if (isLink && d.type === "link out") { if (isLink && d.type === "link out") {
@ -4907,13 +5019,15 @@ RED.view = (function() {
// Remove extra ports // Remove extra ports
while (this.__outputs__.length > numOutputs) { while (this.__outputs__.length > numOutputs) {
var port = this.__outputs__.pop(); var port = this.__outputs__.pop();
RED.hooks.trigger("viewRemovePort",{ if (!d.__ghost) {
node:d, RED.hooks.trigger("viewRemovePort",{
el:this, node:d,
port:port, el:this,
portType: "output", port:port,
portIndex: this.__outputs__.length portType: "output",
}) portIndex: this.__outputs__.length
})
}
port.remove(); port.remove();
} }
for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) { for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) {
@ -4941,16 +5055,20 @@ RED.view = (function() {
portPort.__data__ = this.__data__; portPort.__data__ = this.__data__;
portPort.__portType__ = PORT_TYPE_OUTPUT; portPort.__portType__ = PORT_TYPE_OUTPUT;
portPort.__portIndex__ = portIndex; portPort.__portIndex__ = portIndex;
portPort.addEventListener("mousedown", portMouseDownProxy); if (!d.__ghost) {
portPort.addEventListener("touchstart", portTouchStartProxy); portPort.addEventListener("mousedown", portMouseDownProxy);
portPort.addEventListener("mouseup", portMouseUpProxy); portPort.addEventListener("touchstart", portTouchStartProxy);
portPort.addEventListener("touchend", portTouchEndProxy); portPort.addEventListener("mouseup", portMouseUpProxy);
portPort.addEventListener("mouseover", portMouseOverProxy); portPort.addEventListener("touchend", portTouchEndProxy);
portPort.addEventListener("mouseout", portMouseOutProxy); portPort.addEventListener("mouseover", portMouseOverProxy);
portPort.addEventListener("mouseout", portMouseOutProxy);
}
this.appendChild(portGroup); this.appendChild(portGroup);
this.__outputs__.push(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 { } else {
portGroup = this.__outputs__[portIndex]; portGroup = this.__outputs__[portIndex];
} }
@ -5067,8 +5185,10 @@ RED.view = (function() {
} }
} }
} }
if (!d.__ghost) {
RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) // Only trigger redraw hooks for non-ghost nodes
RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
}
}); });
if (nodesReordered) { 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( var junction = junctionLayer.selectAll(".red-ui-flow-junction").data(
activeJunctions, junctionsToDraw,
d => d.id d => d.id
) )
var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction") var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction")
junctionEnter.each(function(d,i) { junctionEnter.each(function(d,i) {
var junction = d3.select(this); var junction = d3.select(this);
if (d.__ghost) {
junction.classed("red-ui-flow-junction-ghost",true);
}
var contents = document.createDocumentFragment(); var contents = document.createDocumentFragment();
// d.added = true; // d.added = true;
var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect"); 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( var link = linkLayer.selectAll(".red-ui-flow-link").data(
activeLinks, linksToDraw,
function(d) { function(d) {
return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i; 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 l = d3.select(this);
var pathContents = document.createDocumentFragment(); var pathContents = document.createDocumentFragment();
if (d.__ghost) {
l.classed("red-ui-flow-link-ghost",true);
}
d.added = true; d.added = true;
var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path"); var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path");
pathBack.__data__ = d; pathBack.__data__ = d;
pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":"")); pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":""));
this.__pathBack__ = pathBack; this.__pathBack__ = pathBack;
pathContents.appendChild(pathBack); pathContents.appendChild(pathBack);
d3.select(pathBack) if (!d.__ghost) {
.on("mousedown",linkMouseDown) d3.select(pathBack)
.on("touchstart",linkTouchStart) .on("mousedown",linkMouseDown)
.on("mousemove", function(d) { .on("touchstart",linkTouchStart)
if (mouse_mode === RED.state.SLICING) { .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) selectedLinks.add(d)
l.classed("red-ui-flow-link-splice",true) l.classed("red-ui-flow-link-splice",true)
redraw() 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"); var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path");
pathOutline.__data__ = d; pathOutline.__data__ = d;
@ -5688,16 +5825,21 @@ RED.view = (function() {
* - generateIds - whether to automatically generate new ids for all imported nodes * - generateIds - whether to automatically generate new ids for all imported nodes
* - generateDefaultNames - whether to automatically update any nodes with clashing * - generateDefaultNames - whether to automatically update any nodes with clashing
* default names * default names
* - notify - whether to show a notification if the import was successful
*/ */
function importNodes(newNodesObj,options) { function importNodes(newNodesObj,options) {
options = options || { options = options || {
addFlow: false, addFlow: false,
touchImport: false, touchImport: false,
generateIds: false, generateIds: false,
generateDefaultNames: false generateDefaultNames: false,
notify: true,
applyNodeDefaults: false
} }
var addNewFlow = options.addFlow const addNewFlow = options.addFlow
var touchImport = options.touchImport; const touchImport = options.touchImport;
const showNotification = options.notify ?? true
const applyNodeDefaults = options.applyNodeDefaults ?? false
if (mouse_mode === RED.state.SELECTING_NODE) { if (mouse_mode === RED.state.SELECTING_NODE) {
return; return;
@ -5781,7 +5923,8 @@ RED.view = (function() {
addFlow: addNewFlow, addFlow: addNewFlow,
importMap: options.importMap, importMap: options.importMap,
markChanged: true, markChanged: true,
modules: modules modules: modules,
applyNodeDefaults: applyNodeDefaults
}); });
if (importResult) { if (importResult) {
var new_nodes = importResult.nodes; var new_nodes = importResult.nodes;
@ -5792,6 +5935,7 @@ RED.view = (function() {
var new_subflows = importResult.subflows; var new_subflows = importResult.subflows;
var removedNodes = importResult.removedNodes; var removedNodes = importResult.removedNodes;
var new_default_workspace = importResult.missingWorkspace; var new_default_workspace = importResult.missingWorkspace;
const nodeMap = importResult.nodeMap;
if (addNewFlow && new_default_workspace) { if (addNewFlow && new_default_workspace) {
RED.workspaces.show(new_default_workspace.id); RED.workspaces.show(new_default_workspace.id);
} }
@ -5813,16 +5957,18 @@ RED.view = (function() {
var dx = mouse_position[0]; var dx = mouse_position[0];
var dy = mouse_position[1]; var dy = mouse_position[1];
if (movingSet.length() > 0) { if (!touchImport) {
var root_node = movingSet.get(0).n; if (movingSet.length() > 0) {
dx = root_node.x; const root_node = movingSet.get(0).n;
dy = root_node.y; dx = root_node.x;
dy = root_node.y;
}
} }
var minX = 0; var minX = 0;
var minY = 0; var minY = 0;
var i; var i;
var node,group; var node;
var l =movingSet.length(); var l =movingSet.length();
for (i=0;i<l;i++) { for (i=0;i<l;i++) {
node = movingSet.get(i); node = movingSet.get(i);
@ -5953,42 +6099,48 @@ RED.view = (function() {
updateActiveNodes(); updateActiveNodes();
redraw(); redraw();
if (showNotification) {
var counts = []; var counts = [];
var newNodeCount = 0; var newNodeCount = 0;
var newConfigNodeCount = 0; var newConfigNodeCount = 0;
new_nodes.forEach(function(n) { new_nodes.forEach(function(n) {
if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) { if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
newNodeCount++; newNodeCount++;
} else { } else {
newConfigNodeCount++; newConfigNodeCount++;
}
})
var newGroupCount = new_groups.length;
var newJunctionCount = new_junctions.length;
if (new_workspaces.length > 0) {
counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
}
if (newNodeCount > 0) {
counts.push(RED._("clipboard.node",{count:newNodeCount}));
}
if (newGroupCount > 0) {
counts.push(RED._("clipboard.group",{count:newGroupCount}));
}
if (newConfigNodeCount > 0) {
counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
}
if (new_subflows.length > 0) {
counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
}
if (removedNodes && removedNodes.length > 0) {
counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
}
if (counts.length > 0) {
var countList = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+countList,{id:"clipboard"});
} }
})
var newGroupCount = new_groups.length;
var newJunctionCount = new_junctions.length;
if (new_workspaces.length > 0) {
counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
} }
if (newNodeCount > 0) { return {
counts.push(RED._("clipboard.node",{count:newNodeCount})); nodeMap
} }
if (newGroupCount > 0) { }
counts.push(RED._("clipboard.group",{count:newGroupCount})); return {
} nodeMap: {}
if (newConfigNodeCount > 0) {
counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
}
if (new_subflows.length > 0) {
counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
}
if (removedNodes && removedNodes.length > 0) {
counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
}
if (counts.length > 0) {
var countList = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+countList,{id:"clipboard"});
}
} }
} catch(error) { } catch(error) {
if (error.code === "import_conflict") { if (error.code === "import_conflict") {
@ -6307,6 +6459,157 @@ RED.view = (function() {
node.highlighted = true; node.highlighted = true;
RED.view.redraw(); 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 { return {
init: init, init: init,
state:function(state) { state:function(state) {
@ -6567,6 +6870,8 @@ RED.view = (function() {
width: space_width, width: space_width,
height: space_height height: space_height
}; };
} },
setSuggestedFlow,
applySuggestedFlow
}; };
})(); })();

View File

@ -208,12 +208,10 @@ body {
} }
img { img {
width: auto\9;
height: auto; height: auto;
max-width: 100%; max-width: 100%;
vertical-align: middle; vertical-align: middle;
border: 0; border: 0;
-ms-interpolation-mode: bicubic;
} }
blockquote { blockquote {

View File

@ -161,7 +161,15 @@ svg:not(.red-ui-workspace-lasso-active) {
fill: var(--red-ui-group-default-label-color); 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 { .red-ui-flow-node-unknown {
stroke-dasharray:10,4; 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 { g.red-ui-flow-link-selected path.red-ui-flow-link-line {
stroke: var(--red-ui-node-selected-color); 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 { g.red-ui-flow-link-unknown path.red-ui-flow-link-line {
stroke: var(--red-ui-link-unknown-color); stroke: var(--red-ui-link-unknown-color);
stroke-width: 2; stroke-width: 2;

View File

@ -216,14 +216,11 @@
.uneditable-input:focus { .uneditable-input:focus {
border-color: var(--red-ui-form-input-focus-color); border-color: var(--red-ui-form-input-focus-color);
outline: 0; outline: 0;
outline: thin dotted \9;
} }
input[type="radio"], input[type="radio"],
input[type="checkbox"] { input[type="checkbox"] {
margin: 4px 0 0; margin: 4px 0 0;
margin-top: 1px \9;
*margin-top: 0;
line-height: normal; line-height: normal;
} }
@ -285,12 +282,6 @@
color: var(--red-ui-form-placeholder-color); 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, input::-webkit-input-placeholder,
div[contenteditable="true"]::-webkit-input-placeholder, div[contenteditable="true"]::-webkit-input-placeholder,
textarea::-webkit-input-placeholder { textarea::-webkit-input-placeholder {
@ -568,11 +559,7 @@
input.search-query { input.search-query {
padding-right: 14px; padding-right: 14px;
padding-right: 4px \9;
padding-left: 14px; padding-left: 14px;
padding-left: 4px \9;
/* IE7-8 doesn't have border-radius, so don't indent the padding */
margin-bottom: 0; margin-bottom: 0;
border-radius: 15px; border-radius: 15px;
} }

View File

@ -18,7 +18,6 @@
-webkit-user-select: none; -webkit-user-select: none;
-khtml-user-select: none; -khtml-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none;
user-select: none; user-select: none;
} }
@ -26,7 +25,6 @@
-webkit-user-select: auto; -webkit-user-select: auto;
-khtml-user-select: auto; -khtml-user-select: auto;
-moz-user-select: auto; -moz-user-select: auto;
-ms-user-select: auto;
user-select: auto; user-select: auto;
} }

View File

@ -126,15 +126,20 @@
margin-left: 5px; 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 { .red-ui-palette-module-description {
margin-left: 20px; margin-left: 20px;
font-size: 0.9em; font-size: 0.9em;
color: var(--red-ui-secondary-text-color); color: var(--red-ui-secondary-text-color);
} }
.red-ui-palette-module-link {
}
.red-ui-palette-module-set-button-group {
}
.red-ui-palette-module-content { .red-ui-palette-module-content {
display: none; display: none;
padding: 10px 3px; padding: 10px 3px;

View File

@ -205,3 +205,39 @@
background: var(--red-ui-secondary-background); background: var(--red-ui-secondary-background);
z-index: 2000; 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;
}
}

View File

@ -70,8 +70,14 @@
overflow-y: auto; overflow-y: auto;
} }
.red-ui-settings-row { .red-ui-settings-row {
display: flex;
gap: 10px;
align-items:flex-start;
padding: 5px 10px 2px; padding: 5px 10px 2px;
} }
.red-ui-settings-row input[type="checkbox"] {
margin-top: 8px;
}
.red-ui-settings-section { .red-ui-settings-section {
position: relative; position: relative;
&:after { &:after {

View File

@ -0,0 +1,231 @@
export default {
version: "4.0.0",
steps: [
{
titleIcon: "fa fa-map-o",
title: {
"en-US": "Welcome to Node-RED 4.0!",
"ja": "Node-RED 4.0 へようこそ!",
"fr": "Bienvenue dans Node-RED 4.0!"
},
description: {
"en-US": "<p>Let's take a moment to discover the new features in this release.</p>",
"ja": "<p>本リリースの新機能を見つけてみましょう。</p>",
"fr": "<p>Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.</p>"
}
},
{
title: {
"en-US": "Multiplayer Mode",
"ja": "複数ユーザ同時利用モード",
"fr": "Mode Multi-utilisateur"
},
image: '4.0/images/nr4-multiplayer-location.png',
description: {
"en-US": `<p>This release includes the first small steps towards making Node-RED easier
to work with when you have multiple people editing flows at the same time.</p>
<p>When this feature is enabled, you will now see who else has the editor open and some
basic information on where they are in the editor.</p>
<p>Check the release post for details on how to enable this feature in your settings file.</p>`,
"ja": `<p>本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。</p>
<p>本機能を有効にすると誰がエディタを開いているかその人がエディタ上のどこにいるかの基本的な情報が表示されます</p>
<p>設定ファイルで本機能を有効化する方法の詳細はリリースの投稿を確認してください</p>`,
"fr": `<p>Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser
lorsque plusieurs personnes modifient des flux en même temps.</p>
<p>Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si dautres utilisateurs ont
ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.</p>
<p>Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité
dans votre fichier de paramètres.</p>`
}
},
{
title: {
"en-US": "Better background deploy handling",
"ja": "バックグラウンドのデプロイ処理の改善",
"fr": "Meilleure gestion du déploiement en arrière-plan"
},
image: '4.0/images/nr4-background-deploy.png',
description: {
"en-US": `<p>If another user deploys changes whilst you are editing, we now use a more discrete notification
that doesn't stop you continuing your work - especially if they are being very productive and deploying lots
of changes.</p>`,
"ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`,
"fr": `<p>Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez
une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.</p>`
}
},
{
title: {
"en-US": "Improved flow diffs",
"ja": "フローの差分表示の改善",
"fr": "Amélioration des différences de flux"
},
image: '4.0/images/nr4-diff-update.png',
description: {
"en-US": `<p>When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration
changes and those that have only been moved.<p>
<p>When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.</p>`,
"ja": `<p>フローの変更内容を表示する時に、Node-REDは設定が変更されたードと、移動されただけのードを区別するようになりました。<p>
<p>これによって多くの変更内容を確認する際に重要な項目に焦点を当てることができます</p>`,
"fr": `<p>Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les
noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.<p>
<p>Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les
plus importants.</p>`
}
},
{
title: {
"en-US": "Better Configuration Node UX",
"ja": "設定ードのUXが向上",
"fr": "Meilleure expérience utilisateur du noeud de configuration"
},
image: '4.0/images/nr4-config-select.png',
description: {
"en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button
next to the select box.</p>
<p>It's a small change, but should make it easier to work with your config nodes.</p>`,
"ja": `<p>設定ードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。</p>
<p>微修正ですが設定ノードの操作が容易になります</p>`,
"fr": `<p>L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite
mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.</p>
<p>C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.</p>`
}
},
{
title: {
"en-US": "Timestamp formatting options",
"ja": "タイムスタンプの形式の項目",
"fr": "Options de formatage de l'horodatage"
},
image: '4.0/images/nr4-timestamp-formatting.png',
description: {
"en-US": `<p>Nodes that let you set a timestamp now have options on what format that timestamp should be in.</p>
<p>We're keeping it simple to begin with by providing three options:<p>
<ul>
<li>Milliseconds since epoch - this is existing behaviour of the timestamp option</li>
<li>ISO 8601 - a common format used by many systems</li>
<li>JavaScript Date Object</li>
</ul>`,
"ja": `<p>タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。</p>
<p>次の3つの項目を追加したことで簡単に選択できるようになりました:<p>
<ul>
<li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li>
<li>ISO 8601 - 多くのシステムで使用されている共通の形式</li>
<li>JavaScript日付オブジェクト</li>
</ul>`,
"fr": `<p>Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.</p>
<p>Nous gardons les choses simples en proposant trois options :<p>
<ul>
<li>Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage</li>
<li>ISO 8601 : un format commun utilisé par de nombreux systèmes</li>
<li>Objet Date JavaScript</li>
</ul>`
}
},
{
title: {
"en-US": "Auto-complete of flow/global and env types",
"ja": "フロー/グローバル、環境変数の型の自動補完",
"fr": "Saisie automatique des types de flux/global et env"
},
image: '4.0/images/nr4-auto-complete.png',
description: {
"en-US": `<p>The <code>flow</code>/<code>global</code> context inputs and the <code>env</code> input
now all include auto-complete suggestions based on the live state of your flows.</p>
`,
"ja": `<p><code>flow</code>/<code>global</code>コンテキストや<code>env</code>の入力を、現在のフローの状態をもとに自動補完で提案するようになりました。</p>
`,
"fr": `<p>Les entrées contextuelles <code>flow</code>/<code>global</code> et l'entrée <code>env</code>
incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.</p>
`,
}
},
{
title: {
"en-US": "Config node customisation in Subflows",
"ja": "サブフローでの設定ノードのカスタマイズ",
"fr": "Personnalisation du noeud de configuration dans les sous-flux"
},
image: '4.0/images/nr4-sf-config.png',
description: {
"en-US": `<p>Subflows can now be customised to allow each instance to use a different
config node of a selected type.</p>
<p>For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing
of the messages received can be pointed at a different broker.</p>
`,
"ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p>
<p>例えばMQTTブローカへ接続しメッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です</p>
`,
"fr": `<p>Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un
noeud de configuration d'un type sélectionné.</p>
<p>Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement
des messages reçus peut être pointée vers un autre courtier.</p>
`
}
},
{
title: {
"en-US": "Remembering palette state",
"ja": "パレットの状態を維持",
"fr": "Mémorisation de l'état de la palette"
},
description: {
"en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any
filter you have applied.</p>`,
"ja": `<p>パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。</p>`,
"fr": `<p>La palette se souvient désormais des catégories que vous avez masquées entre les rechargements,
ainsi que le filtre que vous avez appliqué.</p>`
}
},
{
title: {
"en-US": "Plugins shown in the Palette Manager",
"ja": "パレット管理にプラグインを表示",
"fr": "Affichage des Plugins dans le gestionnaire de palettes"
},
image: '4.0/images/nr4-plugins.png',
description: {
"en-US": `<p>The palette manager now shows any plugin modules you have installed, such as
<code>node-red-debugger</code>. Previously they would only be shown if the plugins include
nodes for the palette.</p>`,
"ja": `<p>パレットの管理に <code>node-red-debugger</code> の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。</p>`,
"fr": `<p>Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
tels que <code>node-red-debugger</code>. Auparavant, ils n'étaient affichés que s'ils contenaient
des noeuds pour la palette.</p>`
}
},
{
title: {
"en-US": "Node Updates",
"ja": "ノードの更新",
"fr": "Mises à jour des noeuds"
},
// image: "images/",
description: {
"en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and
small enhancements. Check the full changelog in the Help sidebar for a full list.</p>
<ul>
<li>A fully RFC4180 compliant CSV mode</li>
<li>Customisable headers on the WebSocket node</li>
<li>Split node now can operate on any message property</li>
<li>and lots more...</li>
</ul>`,
"ja": `<p>コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。</p>
<ul>
<li>RFC4180に完全に準拠したCSVモード</li>
<li>WebSocketードのカスタマイズ可能なヘッダ</li>
<li>Splitードはメッセージプロパティで操作できるようになりました</li>
<li>他にも沢山あります...</li>
</ul>`,
"fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour.
Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :</p>
<ul>
<li>Un mode CSV entièrement conforme à la norme RFC4180</li>
<li>En-têtes personnalisables pour le noeud WebSocket</li>
<li>Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message</li>
<li>Et bien plus encore...</li>
</ul>`
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,12 +1,12 @@
export default { export default {
version: "4.0.0", version: "4.1.0",
steps: [ steps: [
{ {
titleIcon: "fa fa-map-o", titleIcon: "fa fa-map-o",
title: { title: {
"en-US": "Welcome to Node-RED 4.0!", "en-US": "Welcome to Node-RED 4.1!",
"ja": "Node-RED 4.0 へようこそ!", "ja": "Node-RED 4.1 へようこそ!",
"fr": "Bienvenue dans Node-RED 4.0!" "fr": "Bienvenue dans Node-RED 4.1!"
}, },
description: { description: {
"en-US": "<p>Let's take a moment to discover the new features in this release.</p>", "en-US": "<p>Let's take a moment to discover the new features in this release.</p>",
@ -16,184 +16,79 @@ export default {
}, },
{ {
title: { title: {
"en-US": "Multiplayer Mode", "en-US": "Update notifications",
"ja": "複数ユーザ同時利用モード", "ja": "更新の通知",
"fr": "Mode Multi-utilisateur" "fr": "Notifications de mise à jour"
}, },
image: 'images/nr4-multiplayer-location.png', image: 'images/update-notification.png',
description: { description: {
"en-US": `<p>This release includes the first small steps towards making Node-RED easier "en-US": `<p>Stay up to date with notifications when there is a new Node-RED version available, or updates to the nodes you have installed</p>`,
to work with when you have multiple people editing flows at the same time.</p> "ja": `<p>新バージョンのNode-REDの提供や、インストールしたードの更新があった時に、通知を受け取ることができます。</p>`,
<p>When this feature is enabled, you will now see who else has the editor open and some "fr": `<p>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</p>`
basic information on where they are in the editor.</p>
<p>Check the release post for details on how to enable this feature in your settings file.</p>`,
"ja": `<p>本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。</p>
<p>本機能を有効にすると誰がエディタを開いているかその人がエディタ上のどこにいるかの基本的な情報が表示されます</p>
<p>設定ファイルで本機能を有効化する方法の詳細はリリースの投稿を確認してください</p>`,
"fr": `<p>Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser
lorsque plusieurs personnes modifient des flux en même temps.</p>
<p>Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si dautres utilisateurs ont
ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.</p>
<p>Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité
dans votre fichier de paramètres.</p>`
} }
}, },
{ {
title: { title: {
"en-US": "Better background deploy handling", "en-US": "Flow documentation",
"ja": "バックグラウンドのデプロイ処理の改善", "ja": "フローのドキュメント",
"fr": "Meilleure gestion du déploiement en arrière-plan" "fr": "Documentation des flux"
}, },
image: 'images/nr4-background-deploy.png', image: 'images/node-docs.png',
description: { description: {
"en-US": `<p>If another user deploys changes whilst you are editing, we now use a more discrete notification "en-US": `<p>Quickly see which nodes have additional documentation with the new documentation icon.</p>
that doesn't stop you continuing your work - especially if they are being very productive and deploying lots <p>Clicking on the icon opens up the Description tab of the node edit dialog.</p>`,
of changes.</p>`, "ja": `<p>ドキュメントアイコンによって、どのノードにドキュメントが追加されているかをすぐに確認できます。</p>
"ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`, <p>アイコンをクリックするとノード編集ダイアログの説明タブが開きます</p>`,
"fr": `<p>Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez "fr": `<p>Voyez rapidement quels noeuds ont une documentation supplémentaire avec la nouvelle icône de documentation.</p>
une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.</p>` <p>Cliquer sur l'icône ouvre l'onglet Description de la boîte de dialogue d'édition du noeud.</p>`
} }
}, },
{ {
title: { title: {
"en-US": "Improved flow diffs", "en-US": "Palette Manager Improvements",
"ja": "フローの差分表示の改善", "ja": "パレットの管理の改善",
"fr": "Amélioration des différences de flux" "fr": "Améliorations du Gestionnaire de Palettes"
}, },
image: 'images/nr4-diff-update.png',
description: { description: {
"en-US": `<p>When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration "en-US": `<p>There are lots of improvements to the palette manager:</p>
changes and those that have only been moved.<p>
<p>When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.</p>`,
"ja": `<p>フローの変更内容を表示する時に、Node-REDは設定が変更されたードと、移動されただけのードを区別するようになりました。<p>
<p>これによって多くの変更内容を確認する際に重要な項目に焦点を当てることができます</p>`,
"fr": `<p>Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les
noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.<p>
<p>Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les
plus importants.</p>`
}
},
{
title: {
"en-US": "Better Configuration Node UX",
"ja": "設定ードのUXが向上",
"fr": "Meilleure expérience utilisateur du noeud de configuration"
},
image: 'images/nr4-config-select.png',
description: {
"en-US": `<p>The Configuration node selection UI has had a small update to have a dedicated 'add' button
next to the select box.</p>
<p>It's a small change, but should make it easier to work with your config nodes.</p>`,
"ja": `<p>設定ードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。</p>
<p>微修正ですが設定ノードの操作が容易になります</p>`,
"fr": `<p>L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite
mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.</p>
<p>C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.</p>`
}
},
{
title: {
"en-US": "Timestamp formatting options",
"ja": "タイムスタンプの形式の項目",
"fr": "Options de formatage de l'horodatage"
},
image: 'images/nr4-timestamp-formatting.png',
description: {
"en-US": `<p>Nodes that let you set a timestamp now have options on what format that timestamp should be in.</p>
<p>We're keeping it simple to begin with by providing three options:<p>
<ul> <ul>
<li>Milliseconds since epoch - this is existing behaviour of the timestamp option</li> <li>Search results are sorted by downloads to help you find the most popular nodes</li>
<li>ISO 8601 - a common format used by many systems</li> <li>See which nodes have been deprecated by their author and are no longer recommended for use</li>
<li>JavaScript Date Object</li> <li>Links to node documentation for the nodes you already have installed</li>
</ul>`, </ul>`,
"ja": `<p>タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。</p> "ja": `<p>パレットの管理に多くの改善が加えられました:</p>
<p>次の3つの項目を追加したことで簡単に選択できるようになりました:<p>
<ul> <ul>
<li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li> <li>検索結果はダウンロード数順で並べられ最も人気のあるノードを見つけやすくなりました</li>
<li>ISO 8601 - 多くのシステムで使用されている共通の形式</li> <li>作者によって非推奨とされ利用が推奨されなくなったノードかを確認できるようになりました</li>
<li>JavaScript日付オブジェクト</li> <li>既にインストールされているノードにノードのドキュメントへのリンクが追加されました</li>
</ul>`, </ul>`,
"fr": `<p>Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.</p> "fr": `<p>Le Gestionnaire de Palettes a bénéficié de nombreuses améliorations :</p>
<p>Nous gardons les choses simples en proposant trois options :<p>
<ul> <ul>
<li>Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage</li> <li>Les résultats de recherche sont triés par téléchargement pour vous aider à trouver les noeuds les plus populaires.</li>
<li>ISO 8601 : un format commun utilisé par de nombreux systèmes</li> <li>Indique les noeuds obsolètes par leur auteur et dont l'utilisation n'est plus recommandée.</li>
<li>Objet Date JavaScript</li> <li>Liens vers la documentation des noeuds déjà installés.</li>
</ul>` </ul>`
} }
}, },
{ {
title: { title: {
"en-US": "Auto-complete of flow/global and env types", "en-US": "Installing missing modules",
"ja": "フロー/グローバル、環境変数の型の自動補完", "ja": "不足モジュールのインストール",
"fr": "Saisie automatique des types de flux/global et env" "fr": "Installation des modules manquants"
}, },
image: 'images/nr4-auto-complete.png', image: 'images/missing-modules.png',
description: { description: {
"en-US": `<p>The <code>flow</code>/<code>global</code> context inputs and the <code>env</code> input "en-US": `<p>Flows exported from Node-RED 4.1 now include information on what additional modules need to be installed.</p>
now all include auto-complete suggestions based on the live state of your flows.</p> <p>When importing a flow with this information, the editor will let you know what is missing and help to get them installed.</p>
`, `,
"ja": `<p><code>flow</code>/<code>global</code>コンテキストや<code>env</code>の入力を、現在のフローの状態をもとに自動補完で提案するようになりました。</p> "ja": `<p>Node-RED 4.1から書き出したフローには、インストールが必要な追加モジュールの情報が含まれる様になりました。</p>
<p>この情報を含むフローを読み込むとエディタは不足しているモジュールを通知しインストールを支援します</p>
`, `,
"fr": `<p>Les entrées contextuelles <code>flow</code>/<code>global</code> et l'entrée <code>env</code> "fr": `<p>Les flux exportés depuis Node-RED 4.1 incluent désormais des informations sur les modules supplémentaires à installer.</p>
incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.</p> <p>Lors de l'importation d'un flux contenant ces informations, l'éditeur vous indiquera les modules manquants et vous aidera à les installer.</p>
`,
}
},
{
title: {
"en-US": "Config node customisation in Subflows",
"ja": "サブフローでの設定ノードのカスタマイズ",
"fr": "Personnalisation du noeud de configuration dans les sous-flux"
},
image: 'images/nr4-sf-config.png',
description: {
"en-US": `<p>Subflows can now be customised to allow each instance to use a different
config node of a selected type.</p>
<p>For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing
of the messages received can be pointed at a different broker.</p>
`,
"ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p>
<p>例えばMQTTブローカへ接続しメッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です</p>
`,
"fr": `<p>Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un
noeud de configuration d'un type sélectionné.</p>
<p>Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement
des messages reçus peut être pointée vers un autre courtier.</p>
` `
} }
}, },
{
title: {
"en-US": "Remembering palette state",
"ja": "パレットの状態を維持",
"fr": "Mémorisation de l'état de la palette"
},
description: {
"en-US": `<p>The palette now remembers what categories you have hidden between reloads - as well as any
filter you have applied.</p>`,
"ja": `<p>パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。</p>`,
"fr": `<p>La palette se souvient désormais des catégories que vous avez masquées entre les rechargements,
ainsi que le filtre que vous avez appliqué.</p>`
}
},
{
title: {
"en-US": "Plugins shown in the Palette Manager",
"ja": "パレット管理にプラグインを表示",
"fr": "Affichage des Plugins dans le gestionnaire de palettes"
},
image: 'images/nr4-plugins.png',
description: {
"en-US": `<p>The palette manager now shows any plugin modules you have installed, such as
<code>node-red-debugger</code>. Previously they would only be shown if the plugins include
nodes for the palette.</p>`,
"ja": `<p>パレットの管理に <code>node-red-debugger</code> の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。</p>`,
"fr": `<p>Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
tels que <code>node-red-debugger</code>. Auparavant, ils n'étaient affichés que s'ils contenaient
des noeuds pour la palette.</p>`
}
},
{ {
title: { title: {
"en-US": "Node Updates", "en-US": "Node Updates",
@ -205,26 +100,26 @@ export default {
"en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and "en-US": `<p>The core nodes have received lots of minor fixes, documentation updates and
small enhancements. Check the full changelog in the Help sidebar for a full list.</p> small enhancements. Check the full changelog in the Help sidebar for a full list.</p>
<ul> <ul>
<li>A fully RFC4180 compliant CSV mode</li> <li>Support for <code>node:</code> prefixed modules in the Function node</li>
<li>Customisable headers on the WebSocket node</li> <li>The ability to set a global timeout for Function nodes via the runtime settings</li>
<li>Split node now can operate on any message property</li> <li>Better display of error objects in the Debug sidebar</li>
<li>and lots more...</li> <li>and lots more...</li>
</ul>`, </ul>`,
"ja": `<p>コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。</p> "ja": `<p>コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。</p>
<ul> <ul>
<li>RFC4180に完全に準拠したCSVモード</li> <li>Functionードで<code>node:</code></li>
<li>WebSocketードのカスタマイズ可能なヘッダ</li> <li>ランタイム設定からFunctionードのグローバルタイムアウトを設定可能</li>
<li>Splitードはメッセージプロパティで操作できるようになりました</li> <li>デバッグサイドバーでのエラーオブジェクトの表示を改善</li>
<li>他にも沢山あります...</li> <li>その他多数...</li>
</ul>`, </ul>`,
"fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour. "fr": `<p>Les noeuds principaux ont bénéficié de nombreux correctifs mineurs, de mises à jour de documentation et d'améliorations mineures.
Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :</p> Consultez le journal complet des modifications dans la barre latérale d'aide pour une liste complète.</p>
<ul> <ul>
<li>Un mode CSV entièrement conforme à la norme RFC4180</li> <li>Prise en charge des modules préfixés <code>node:</code> dans le noeud Fonction.</li>
<li>En-têtes personnalisables pour le noeud WebSocket</li> <li>Possibilité de définir un délai d'expiration global pour les noeuds Fonction via les paramètres d'exécution.</li>
<li>Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message</li> <li>Meilleur affichage des objets d'erreur dans la barre latérale de débogage.</li>
<li>Et bien plus encore...</li> <li>Et bien plus encore...</li>
</ul>` </ul>`
} }
} }
] ]

View File

@ -20,7 +20,16 @@ module.exports = function(RED) {
function CompleteNode(n) { function CompleteNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
var node = this; 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) { this.on("input",function(msg, send, done) {
send(msg); send(msg);
done(); done();

View File

@ -20,7 +20,16 @@ module.exports = function(RED) {
function StatusNode(n) { function StatusNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
var node = this; 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) { this.on("input", function(msg, send, done) {
send(msg); send(msg);
done(); done();

View File

@ -3,7 +3,7 @@
<div class="form-tips"> <div class="form-tips">
<span data-i18n="[html]unknown.tip"></span> <span data-i18n="[html]unknown.tip"></span>
<p id="unknown-module-known"> <p id="unknown-module-known">
<button id="unknown-manage-dependencies" class="red-ui-button">Manage dependencies</button> <button id="unknown-manage-dependencies" class="red-ui-button"><span data-i18n="unknown.manageModules"></span></button>
</p> </p>
</div> </div>
</div> </div>

View File

@ -162,6 +162,8 @@ module.exports = function(RED) {
console:console, console:console,
util:util, util:util,
Buffer:Buffer, Buffer:Buffer,
URL: URL,
URLSearchParams: URLSearchParams,
Date: Date, Date: Date,
RED: { RED: {
util: { util: {
@ -403,6 +405,8 @@ module.exports = function(RED) {
if(node.timeout>0){ if(node.timeout>0){
finOpt.timeout = node.timeout; finOpt.timeout = node.timeout;
finOpt.breakOnSigint = true; finOpt.breakOnSigint = true;
} else if (RED.settings.globalFunctionTimeout > 0){
finOpt.timeout = RED.settings.globalFunctionTimeout * 1000
} }
} }
var promise = Promise.resolve(); var promise = Promise.resolve();
@ -419,8 +423,14 @@ module.exports = function(RED) {
var opts = {}; var opts = {};
if (node.timeout>0){ if (node.timeout>0){
opts = node.timeoutOptions; 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) { context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false); sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) { if (handleNodeDoneCall) {

View File

@ -109,7 +109,7 @@ module.exports = function(RED) {
child.stderr.on('data', function (data) { child.stderr.on('data', function (data) {
if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) { if (node.activeProcesses.hasOwnProperty(child.pid) && node.activeProcesses[child.pid] !== null) {
if (isUtf8(data)) { msg.payload = data.toString(); } if (isUtf8(data)) { msg.payload = data.toString(); }
else { msg.payload = Buffer.from(data); } else { msg.payload = data; }
nodeSend([null,RED.util.cloneMessage(msg),null]); nodeSend([null,RED.util.cloneMessage(msg),null]);
} }
}); });
@ -146,7 +146,8 @@ module.exports = function(RED) {
delete msg.payload; delete msg.payload;
if (stderr) { if (stderr) {
msg2 = RED.util.cloneMessage(msg); 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"); msg.payload = Buffer.from(stdout,"binary");
if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); } if (isUtf8(msg.payload)) { msg.payload = msg.payload.toString(); }

View File

@ -675,7 +675,7 @@ module.exports = function(RED) {
node.options.password = node.password; node.options.password = node.password;
node.options.keepalive = node.keepalive; node.options.keepalive = node.keepalive;
node.options.clean = node.cleansession; 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; node.options.reconnectPeriod = RED.settings.mqttReconnectTime||5000;
delete node.options.protocolId; //V4+ default delete node.options.protocolId; //V4+ default
delete node.options.protocolVersion; //V4 default delete node.options.protocolVersion; //V4 default

View File

@ -29,7 +29,7 @@
<div class="form-row"> <div class="form-row">
<label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label> <label for="node-input-url"><i class="fa fa-globe"></i> <span data-i18n="httpin.label.url"></span></label>
<input id="node-input-url" type="text" placeholder="http://"> <input id="node-input-url" type="text" placeholder="https://">
</div> </div>
<div class="form-row node-input-paytoqs-row"> <div class="form-row node-input-paytoqs-row">

View File

@ -431,7 +431,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
normalisedHeaders[k.toLowerCase()] = response.headers[k] normalisedHeaders[k.toLowerCase()] = response.headers[k]
}) })
if (normalisedHeaders['www-authenticate']) { 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; options.headers.Authorization = authHeader;
} }
// response.request.options.merge(options) // response.request.options.merge(options)
@ -586,9 +586,31 @@ in your Node-RED user directory (${RED.settings.userDir}).
opts.https.certificate = opts.https.cert; opts.https.certificate = opts.https.cert;
delete 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 { } else {
if (msg.hasOwnProperty('rejectUnauthorized')) { 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"))
}
} }
} }

View File

@ -21,8 +21,8 @@
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name"> <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.splitThe"></span></label> <label for="node-input-property" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitThe"></label>
<input type="text" id="node-input-property" style="width:70%;"> <input type="text" id="node-input-property" style="width:70%;"/>
</div> </div>
<div class="form-row"><span data-i18n="[html]split.strBuff"></span></div> <div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
<div class="form-row"> <div class="form-row">

View File

@ -151,10 +151,11 @@ module.exports = function(RED) {
if (node.arraySplt === 1) { if (node.arraySplt === 1) {
m = m[0]; m = m[0];
} }
RED.util.setMessageProperty(msg,node.property,m); const newmsg = RED.util.cloneMessage(msg)
msg.parts.index = i; RED.util.setMessageProperty(newmsg,node.property,m);
newmsg.parts.index = i;
pos += node.arraySplt; pos += node.arraySplt;
send(RED.util.cloneMessage(msg)); send(newmsg);
} }
done(); done();
} }

View File

@ -81,7 +81,7 @@
<p>Wenn <code>msg.payload</code> ein Objekt ist, setzt der Node automatisch den Inhaltstyp der Anforderung <p>Wenn <code>msg.payload</code> ein Objekt ist, setzt der Node automatisch den Inhaltstyp der Anforderung
auf <code>application/json</code> und kodiert den Hauptteil als solchen.</p> auf <code>application/json</code> und kodiert den Hauptteil als solchen.</p>
<p>Um die Anforderung als Formulardaten zu kodieren, sollte <code>msg.headers["content-type"]</code> auf <p>Um die Anforderung als Formulardaten zu kodieren, sollte <code>msg.headers["content-type"]</code> auf
<code>application/x-wwww-form-urlencoded</code> gesetzt werden.</p> <code>application/x-www-form-urlencoded</code> gesetzt werden.</p>
<h4><b>Datei-Upload</b></h4> <h4><b>Datei-Upload</b></h4>
<p>Um einen Datei-Upload umzusetzen, sollte <code>msg.headers["content-type"]</code> auf <code>multipart/form-data</code> <p>Um einen Datei-Upload umzusetzen, sollte <code>msg.headers["content-type"]</code> auf <code>multipart/form-data</code>
gesetzt werden und das an den Node zu sendende <code>msg.payload</code> muss ein Objekt mit folgender Struktur sein:</p> gesetzt werden und das an den Node zu sendende <code>msg.payload</code> muss ein Objekt mit folgender Struktur sein:</p>

View File

@ -406,6 +406,7 @@
"label": { "label": {
"unknown": "unknown" "unknown": "unknown"
}, },
"manageModules": "Manage modules",
"tip": "<p>This node is a type unknown to your installation of Node-RED.</p><p><i>If you deploy with the node in this state, it's configuration will be preserved, but the flow will not start until the missing type is installed.</i></p><p>See the Info side bar for more help</p>" "tip": "<p>This node is a type unknown to your installation of Node-RED.</p><p><i>If you deploy with the node in this state, it's configuration will be preserved, but the flow will not start until the missing type is installed.</i></p><p>See the Info side bar for more help</p>"
}, },
"mqtt": { "mqtt": {
@ -562,7 +563,8 @@
"timeout-isnan": "Timeout value is not a valid number, ignoring", "timeout-isnan": "Timeout value is not a valid number, ignoring",
"timeout-isnegative": "Timeout value is negative, ignoring", "timeout-isnegative": "Timeout value is negative, ignoring",
"invalid-payload": "Invalid payload", "invalid-payload": "Invalid payload",
"invalid-url": "Invalid url" "invalid-url": "Invalid url",
"rejectunauthorized-invalid": "msg.rejectUnauthorized should be a boolean"
}, },
"status": { "status": {
"requesting": "requesting" "requesting": "requesting"
@ -1017,7 +1019,7 @@
"objectSend": "Send a message for each key/value pair", "objectSend": "Send a message for each key/value pair",
"strBuff": "<b>String</b> / <b>Buffer</b>", "strBuff": "<b>String</b> / <b>Buffer</b>",
"array": "<b>Array</b>", "array": "<b>Array</b>",
"splitThe": "Split the", "splitThe": "Split property",
"splitUsing": "Split using", "splitUsing": "Split using",
"splitLength": "Fixed length of", "splitLength": "Fixed length of",
"stream": "Handle as a stream of messages", "stream": "Handle as a stream of messages",

View File

@ -406,6 +406,7 @@
"label": { "label": {
"unknown": "inconnu" "unknown": "inconnu"
}, },
"manageModules": "Gérer les modules",
"tip": "<p>Ce noeud est un type inconnu de votre installation Node-RED.</p><p><i>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é.</i></p><p>Consulter la barre latérale d'informations pour plus d'aide</p>" "tip": "<p>Ce noeud est un type inconnu de votre installation Node-RED.</p><p><i>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é.</i></p><p>Consulter la barre latérale d'informations pour plus d'aide</p>"
}, },
"mqtt": { "mqtt": {
@ -1017,7 +1018,7 @@
"objectSend": "Envoie un message pour chaque paire clé/valeur", "objectSend": "Envoie un message pour chaque paire clé/valeur",
"strBuff": "<b>Chaîne</b> / <b>Tampon</b>", "strBuff": "<b>Chaîne</b> / <b>Tampon</b>",
"array": "<b>Tableau</b>", "array": "<b>Tableau</b>",
"splitThe": "Diviser le", "splitThe": "Diviser la propriété",
"splitUsing": "Diviser en utilisant", "splitUsing": "Diviser en utilisant",
"splitLength": "Longueur fixe de", "splitLength": "Longueur fixe de",
"stream": "Gérer comme un flux de messages", "stream": "Gérer comme un flux de messages",

View File

@ -406,6 +406,7 @@
"label": { "label": {
"unknown": "unknown" "unknown": "unknown"
}, },
"manageModules": "モジュールを管理",
"tip": "<p>現在のNode-RED環境では、本ードの型が不明です。</p><p><i>現在の状態で本ノードをデプロイすると設定は保存されますが、不明なノードがインストールされるまでフローは実行されません。</i></p><p>詳細はノードの「情報」を参照してください。</p>" "tip": "<p>現在のNode-RED環境では、本ードの型が不明です。</p><p><i>現在の状態で本ノードをデプロイすると設定は保存されますが、不明なノードがインストールされるまでフローは実行されません。</i></p><p>詳細はノードの「情報」を参照してください。</p>"
}, },
"mqtt": { "mqtt": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/nodes", "name": "@node-red/nodes",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -15,7 +15,7 @@
} }
], ],
"dependencies": { "dependencies": {
"acorn": "8.14.1", "acorn": "8.15.0",
"acorn-walk": "8.3.4", "acorn-walk": "8.3.4",
"ajv": "8.17.1", "ajv": "8.17.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
@ -36,7 +36,7 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"media-typer": "1.1.0", "media-typer": "1.1.0",
"mqtt": "5.11.0", "mqtt": "5.11.0",
"multer": "1.4.5-lts.2", "multer": "2.0.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-watch": "0.7.4", "node-watch": "0.7.4",
"on-headers": "1.0.2", "on-headers": "1.0.2",

View File

@ -406,6 +406,7 @@ async function loadPlugin(plugin) {
} }
try { try {
var r = require(plugin.file); var r = require(plugin.file);
r = r.__esModule ? r.default : r
if (typeof r === "function") { if (typeof r === "function") {
var red = registryUtil.createNodeApi(plugin); var red = registryUtil.createNodeApi(plugin);

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/registry", "name": "@node-red/registry",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,7 +16,7 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "4.1.0-beta.0", "@node-red/util": "4.1.0-beta.1",
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "11.3.0", "fs-extra": "11.3.0",
"semver": "7.7.1", "semver": "7.7.1",

View File

@ -161,6 +161,8 @@ var api = module.exports = {
safeSettings.diagnostics.ui = false; // cannot have UI without endpoint safeSettings.diagnostics.ui = false; // cannot have UI without endpoint
} }
safeSettings.telemetryEnabled = runtime.telemetry.isEnabled()
safeSettings.runtimeState = { safeSettings.runtimeState = {
//unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false. //unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false.
enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true, enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true,
@ -213,7 +215,19 @@ var api = module.exports = {
} }
var currentSettings = runtime.settings.getUserSettings(username)||{}; var currentSettings = runtime.settings.getUserSettings(username)||{};
currentSettings = extend(currentSettings, opts.settings); currentSettings = extend(currentSettings, opts.settings);
try { 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() { return runtime.settings.setUserSettings(username, currentSettings).then(function() {
runtime.log.audit({event: "settings.update",username:username}, opts.req); runtime.log.audit({event: "settings.update",username:username}, opts.req);
return; return;

View File

@ -23,6 +23,7 @@ var library = require("./library");
var plugins = require("./plugins"); var plugins = require("./plugins");
var settings = require("./settings"); var settings = require("./settings");
const multiplayer = require("./multiplayer"); const multiplayer = require("./multiplayer");
const telemetry = require("./telemetry");
var express = require("express"); var express = require("express");
var path = require('path'); var path = require('path');
@ -135,6 +136,7 @@ function start() {
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json") return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json")
.then(function() { return storage.init(runtime)}) .then(function() { return storage.init(runtime)})
.then(function() { return settings.load(storage)}) .then(function() { return settings.load(storage)})
.then(function() { return telemetry.init(runtime)})
.then(function() { return library.init(runtime)}) .then(function() { return library.init(runtime)})
.then(function() { return multiplayer.init(runtime)}) .then(function() { return multiplayer.init(runtime)})
.then(function() { .then(function() {
@ -235,8 +237,12 @@ function start() {
} }
} }
return redNodes.loadContextsPlugin().then(function () { return redNodes.loadContextsPlugin().then(function () {
redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {});
started = true; started = true;
redNodes.loadFlows().then(() => {
if (started) {
redNodes.startFlows()
}
}).catch(function(err) {});
}); });
}); });
}); });
@ -337,6 +343,7 @@ var runtime = {
library: library, library: library,
exec: exec, exec: exec,
util: util, util: util,
telemetry: telemetry,
get adminApi() { return adminApi }, get adminApi() { return adminApi },
get adminApp() { return adminApp }, get adminApp() { return adminApp },
get nodeApp() { return nodeApp }, get nodeApp() { return nodeApp },

View File

@ -135,7 +135,7 @@ function getLibraryEntry(type,path) {
throw err; throw err;
}); });
} else { } else {
throw err; throw new Error(`Library Entry not found ${path}`, { cause: err});
} }
}); });
} }

View File

@ -51,6 +51,8 @@ function runGitCommand(args,cwd,env,emit) {
err.code = "git_auth_failed"; err.code = "git_auth_failed";
} else if(/Authentication failed/i.test(stderr)) { } else if(/Authentication failed/i.test(stderr)) {
err.code = "git_auth_failed"; 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)) { } else if (/commit your changes or stash/i.test(stderr)) {
err.code = "git_local_overwrite"; err.code = "git_local_overwrite";
} else if (/CONFLICT/.test(err.stdout)) { } else if (/CONFLICT/.test(err.stdout)) {

View File

@ -0,0 +1,213 @@
const path = require('path')
const fs = require('fs/promises')
const semver = require('semver')
const cronosjs = require('cronosjs')
const METRICS_DIR = path.join(__dirname, 'metrics')
const INITIAL_PING_DELAY = 1000 * 60 * 30 // 30 minutes from startup
/** @type {import("got").Got | undefined} */
let got
let runtime
let scheduleTask
async function gather () {
let metricFiles = await fs.readdir(METRICS_DIR)
metricFiles = metricFiles.filter(name => /^\d+-.*\.js$/.test(name))
metricFiles.sort()
const metrics = {}
for (let i = 0, l = metricFiles.length; i < l; i++) {
const metricModule = require(path.join(METRICS_DIR, metricFiles[i]))
let result = metricModule(runtime)
if (!!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result = await result
}
const keys = Object.keys(result)
keys.forEach(key => {
const keyParts = key.split('.')
let p = metrics
keyParts.forEach((part, index) => {
if (index < keyParts.length - 1) {
if (!p[part]) {
p[part] = {}
}
p = p[part]
} else {
p[part] = result[key]
}
})
})
}
return metrics
}
async function report () {
if (!isTelemetryEnabled()) {
return
}
// If enabled, gather metrics
const metrics = await gather()
// Post metrics to endpoint - handle any error silently
if (!got) {
got = (await import('got')).got
}
runtime.log.debug('Sending telemetry')
const response = await got.post('https://telemetry.nodered.org/ping', {
json: metrics,
responseType: 'json',
headers: {
'User-Agent': `Node-RED/${runtime.settings.version}`
}
}).json().catch(err => {
// swallow errors
runtime.log.debug('Failed to send telemetry: ' + err.toString())
})
// Example response:
// { 'node-red': { latest: '4.0.9', next: '4.1.0-beta.1.9' } }
runtime.log.debug(`Telemetry response: ${JSON.stringify(response)}`)
// Get response from endpoint
if (response?.['node-red']) {
const currentVersion = metrics.env['node-red']
if (semver.valid(currentVersion)) {
const latest = response['node-red'].latest
const next = response['node-red'].next
let updatePayload
if (semver.lt(currentVersion, latest)) {
// Case one: current < latest
runtime.log.info(`A new version of Node-RED is available: ${latest}`)
updatePayload = { version: latest }
} else if (semver.gt(currentVersion, latest) && semver.lt(currentVersion, next)) {
// Case two: current > latest && current < next
runtime.log.info(`A new beta version of Node-RED is available: ${next}`)
updatePayload = { version: next }
}
if (updatePayload && isUpdateNotificationEnabled()) {
runtime.events.emit("runtime-event",{id:"update-available", payload: updatePayload, retain: true});
}
}
}
}
function isTelemetryEnabled () {
// If NODE_RED_DISABLE_TELEMETRY was set, or --no-telemetry was specified,
// the settings object will have been updated to disable telemetry explicitly
// If there are no telemetry settings then the user has not had a chance
// to opt out yet - so keep it disabled until they do
let telemetrySettings
try {
telemetrySettings = runtime.settings.get('telemetry')
} catch (err) {
// Settings not available
}
let runtimeTelemetryEnabled
try {
runtimeTelemetryEnabled = runtime.settings.get('telemetryEnabled')
} catch (err) {
// Settings not available
}
if (telemetrySettings === undefined && runtimeTelemetryEnabled === undefined) {
// No telemetry settings - so keep it disabled
return undefined
}
// User has made a choice; defer to that
if (runtimeTelemetryEnabled !== undefined) {
return runtimeTelemetryEnabled
}
// If there are telemetry settings, use what it says
if (telemetrySettings && telemetrySettings.enabled !== undefined) {
return telemetrySettings.enabled
}
// At this point, we have no sign the user has consented to telemetry, so
// keep disabled - but return undefined as a false-like value to distinguish
// it from the explicit disable above
return undefined
}
function isUpdateNotificationEnabled () {
const telemetrySettings = runtime.settings.get('telemetry') || {}
return telemetrySettings.updateNotification !== false
}
/**
* Start the telemetry schedule
*/
function startTelemetry () {
if (scheduleTask) {
// Already scheduled - nothing left to do
return
}
const pingTime = new Date(Date.now() + INITIAL_PING_DELAY)
const pingMinutes = pingTime.getMinutes()
const pingHours = pingTime.getHours()
const pingSchedule = `${pingMinutes} ${pingHours} * * *`
runtime.log.debug(`Telemetry enabled. Schedule: ${pingSchedule}`)
scheduleTask = cronosjs.scheduleTask(pingSchedule, () => {
report()
})
}
function stopTelemetry () {
if (scheduleTask) {
runtime.log.debug(`Telemetry disabled`)
scheduleTask.stop()
scheduleTask = null
}
}
module.exports = {
init: (_runtime) => {
runtime = _runtime
if (isTelemetryEnabled()) {
startTelemetry()
}
},
/**
* Enable telemetry via user opt-in in the editor
*/
enable: () => {
if (runtime.settings.available()) {
runtime.settings.set('telemetryEnabled', true)
}
startTelemetry()
},
/**
* Disable telemetry via user opt-in in the editor
*/
disable: () => {
if (runtime.settings.available()) {
runtime.settings.set('telemetryEnabled', false)
}
stopTelemetry()
},
/**
* Get telemetry enabled status
* @returns {boolean} true if telemetry is enabled, false if disabled, undefined if not set
*/
isEnabled: isTelemetryEnabled,
stop: () => {
if (scheduleTask) {
scheduleTask.stop()
scheduleTask = null
}
}
}

View File

@ -0,0 +1,5 @@
module.exports = (runtime) => {
return {
instanceId: runtime.settings.get('instanceId')
}
}

View File

@ -0,0 +1,9 @@
const os = require('os')
module.exports = (_) => {
return {
'os.type': os.type(),
'os.release': os.release(),
'os.arch': os.arch()
}
}

View File

@ -0,0 +1,8 @@
const process = require('process')
module.exports = (runtime) => {
return {
'env.nodejs': process.version.replace(/^v/, ''),
'env.node-red': runtime.settings.version
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/runtime", "name": "@node-red/runtime",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,13 +16,15 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/registry": "4.1.0-beta.0", "@node-red/registry": "4.1.0-beta.1",
"@node-red/util": "4.1.0-beta.0", "@node-red/util": "4.1.0-beta.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"clone": "2.1.2", "clone": "2.1.2",
"cronosjs": "1.7.1",
"express": "4.21.2", "express": "4.21.2",
"fs-extra": "11.3.0", "fs-extra": "11.3.0",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"rfdc": "^1.3.1" "rfdc": "^1.3.1",
"semver": "7.7.1"
} }
} }

View File

@ -78,7 +78,7 @@ module.exports = {
stdout: stdout, stdout: stdout,
stderr: stderr 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) { if (code === 0) {
resolve(result) resolve(result)

View File

@ -52,11 +52,11 @@ var levelColours = {
10: 'red', 10: 'red',
20: 'red', 20: 'red',
30: 'yellow', 30: 'yellow',
40: 'white', 40: '',
50: 'cyan', 50: 'cyan',
60: 'gray', 60: 'gray',
98: 'white', 98: '',
99: 'white' 99: ''
}; };
var logHandlers = []; var logHandlers = [];
@ -99,7 +99,12 @@ const utilLog = function (msg, level) {
d.getMinutes().toString().padStart(2, '0'), d.getMinutes().toString().padStart(2, '0'),
d.getSeconds().toString().padStart(2, '0') d.getSeconds().toString().padStart(2, '0')
].join(':'); ].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) { var consoleLogger = function(msg) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/util", "name": "@node-red/util",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "4.1.0-beta.0", "version": "4.1.0-beta.1",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "https://nodered.org", "homepage": "https://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -31,16 +31,16 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"@node-red/editor-api": "4.1.0-beta.0", "@node-red/editor-api": "4.1.0-beta.1",
"@node-red/runtime": "4.1.0-beta.0", "@node-red/runtime": "4.1.0-beta.1",
"@node-red/util": "4.1.0-beta.0", "@node-red/util": "4.1.0-beta.1",
"@node-red/nodes": "4.1.0-beta.0", "@node-red/nodes": "4.1.0-beta.1",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "3.0.2", "bcryptjs": "3.0.2",
"cors": "2.8.5", "cors": "2.8.5",
"express": "4.21.2", "express": "4.21.2",
"fs-extra": "11.3.0", "fs-extra": "11.3.0",
"node-red-admin": "^4.0.2", "node-red-admin": "^4.1.0",
"nopt": "5.0.0", "nopt": "5.0.0",
"semver": "7.7.1" "semver": "7.7.1"
}, },

View File

@ -63,7 +63,8 @@ var knownOpts = {
"verbose": Boolean, "verbose": Boolean,
"safe": Boolean, "safe": Boolean,
"version": Boolean, "version": Boolean,
"define": [String, Array] "define": [String, Array],
"no-telemetry": Boolean
}; };
var shortHands = { var shortHands = {
"?":["--help"], "?":["--help"],
@ -97,6 +98,7 @@ if (parsedArgs.help) {
console.log(" --safe enable safe mode"); console.log(" --safe enable safe mode");
console.log(" -D, --define X=Y overwrite value in settings file"); console.log(" -D, --define X=Y overwrite value in settings file");
console.log(" --version show version information"); 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(" -?, --help show this help");
console.log(" admin <command> run an admin command"); console.log(" admin <command> run an admin command");
console.log(""); 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); 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 = { var defaultServerSettings = {
"x-powered-by": false "x-powered-by": false

View File

@ -273,6 +273,7 @@ module.exports = {
* Runtime Settings * Runtime Settings
* - lang * - lang
* - runtimeState * - runtimeState
* - telemetry
* - diagnostics * - diagnostics
* - logging * - logging
* - contextStorage * - 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 */ /** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */
ui: false, 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 */ /** Configure the logging output */
logging: { logging: {
/** Only console logging is currently supported */ /** Only console logging is currently supported */
@ -473,6 +490,7 @@ module.exports = {
* - fileWorkingDirectory * - fileWorkingDirectory
* - functionGlobalContext * - functionGlobalContext
* - functionExternalModules * - functionExternalModules
* - globalFunctionTimeout
* - functionTimeout * - functionTimeout
* - nodeMessageBufferMaxLength * - nodeMessageBufferMaxLength
* - ui (for use with Node-RED Dashboard) * - ui (for use with Node-RED Dashboard)
@ -499,7 +517,19 @@ module.exports = {
/** Allow the Function node to load additional npm modules directly */ /** Allow the Function node to load additional npm modules directly */
functionExternalModules: true, 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, functionTimeout: 0,
/** The following property can be used to set predefined values in Global Context. /** The following property can be used to set predefined values in Global Context.

View File

@ -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);
});
});
});

View File

@ -25,7 +25,7 @@ describe('status Node', function() {
}); });
it('should output a message when called', function(done) { 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"} ]; {id:"n2", type:"helper"} ];
helper.load(catchNode, flow, function() { helper.load(catchNode, flow, function() {
var n1 = helper.getNode("n1"); var n1 = helper.getNode("n1");

View File

@ -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; RED.settings.functionTimeout = 0.01;
var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}]; var flow = [{id: "n1",type: "function",timeout: RED.settings.functionTimeout,wires: [["n2"]],func: "while(1==1){};\nreturn msg;"}];
helper.load(functionNode, flow, function () { 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() { describe("finalize function", function() {
it('should execute', function(done) { it('should execute', function(done) {

View File

@ -58,7 +58,7 @@ describe('MQTT Nodes', function () {
mqttBroker.should.have.property('options'); mqttBroker.should.have.property('options');
mqttBroker.options.should.have.property('clean', true); mqttBroker.options.should.have.property('clean', true);
mqttBroker.options.should.have.property('clientId'); 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('keepalive').type("number");
mqttBroker.options.should.have.property('reconnectPeriod').type("number"); mqttBroker.options.should.have.property('reconnectPeriod').type("number");
//as this is not a v5 connection, ensure v5 properties are not present //as this is not a v5 connection, ensure v5 properties are not present

View File

@ -57,7 +57,8 @@ describe("runtime-api/settings", function() {
getCredentialKeyType: () => "test-key-type" getCredentialKeyType: () => "test-key-type"
}, },
library: {getLibraries: () => ["lib1"] }, library: {getLibraries: () => ["lib1"] },
storage: {} storage: {},
telemetry: { isEnabled: () => true }
}) })
return settings.getRuntimeSettings({}).then(result => { return settings.getRuntimeSettings({}).then(result => {
result.should.have.property("httpNodeRoot","testHttpNodeRoot"); result.should.have.property("httpNodeRoot","testHttpNodeRoot");
@ -96,7 +97,8 @@ describe("runtime-api/settings", function() {
getCredentialKeyType: () => "test-key-type" getCredentialKeyType: () => "test-key-type"
}, },
library: {getLibraries: () => { ["lib1"]} }, library: {getLibraries: () => { ["lib1"]} },
storage: {} storage: {},
telemetry: { isEnabled: () => true }
}) })
return settings.getRuntimeSettings({ return settings.getRuntimeSettings({
user: { user: {
@ -145,7 +147,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file', getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}} getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
} }
} },
telemetry: { isEnabled: () => true }
}) })
return settings.getRuntimeSettings({ return settings.getRuntimeSettings({
user: { user: {
@ -202,7 +205,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file', getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}} getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
} }
} },
telemetry: { isEnabled: () => true }
}) })
return settings.getRuntimeSettings({ return settings.getRuntimeSettings({
user: { user: {
@ -250,7 +254,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file', getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}} getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
} }
} },
telemetry: { isEnabled: () => true }
}) })
return settings.getRuntimeSettings({ return settings.getRuntimeSettings({
user: { user: {
@ -301,7 +306,8 @@ describe("runtime-api/settings", function() {
getCredentialsFilename: () => 'test-creds-file', getCredentialsFilename: () => 'test-creds-file',
getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}} getGlobalGitUser: () => {return {name:'foo',email:'foo@example.com'}}
} }
} },
telemetry: { isEnabled: () => true }
}) })
return settings.getRuntimeSettings({ return settings.getRuntimeSettings({
user: { user: {

View File

@ -0,0 +1,96 @@
const should = require("should");
const NR_TEST_UTILS = require("nr-test-utils");
const telemetryApi = NR_TEST_UTILS.require("@node-red/runtime/lib/telemetry/index");
describe("telemetry", function() {
afterEach(function () {
telemetryApi.stop()
messages = []
})
let messages = []
function getMockRuntime(settings) {
return {
settings: {
get: key => { return settings[key] },
set: (key, value) => { settings[key] = value },
available: () => true,
},
log: {
debug: (msg) => { messages.push(msg)}
}
}
}
// Principles to test:
// - No settings at all; disable telemetry
// - Runtime settings only; do what it says
// - User settings take precedence over runtime settings
it('Disables telemetry with no settings present', function () {
telemetryApi.init(getMockRuntime({}))
messages.should.have.length(0)
// Returns undefined as we don't know either way
;(telemetryApi.isEnabled() === undefined).should.be.true()
})
it('Runtime settings - enable', function () {
// Enabled in runtime settings
telemetryApi.init(getMockRuntime({
telemetry: { enabled: true }
}))
telemetryApi.isEnabled().should.be.true()
messages.should.have.length(1)
;/Telemetry enabled/.test(messages[0]).should.be.true()
})
it('Runtime settings - disable', function () {
telemetryApi.init(getMockRuntime({
telemetry: { enabled: false },
}))
// Returns false, not undefined
telemetryApi.isEnabled().should.be.false()
messages.should.have.length(0)
})
it('User settings - enable overrides runtime settings', function () {
telemetryApi.init(getMockRuntime({
telemetry: { enabled: false },
telemetryEnabled: true
}))
telemetryApi.isEnabled().should.be.true()
messages.should.have.length(1)
;/Telemetry enabled/.test(messages[0]).should.be.true()
})
it('User settings - disable overrides runtime settings', function () {
telemetryApi.init(getMockRuntime({
telemetry: { enabled: true },
telemetryEnabled: false
}))
telemetryApi.isEnabled().should.be.false()
messages.should.have.length(0)
})
it('Can enable/disable telemetry', function () {
const settings = {}
telemetryApi.init(getMockRuntime(settings))
;(telemetryApi.isEnabled() === undefined).should.be.true()
telemetryApi.enable()
telemetryApi.isEnabled().should.be.true()
messages.should.have.length(1)
;/Telemetry enabled/.test(messages[0]).should.be.true()
settings.should.have.property('telemetryEnabled', true)
telemetryApi.disable()
telemetryApi.isEnabled().should.be.false()
messages.should.have.length(2)
;/Telemetry disabled/.test(messages[1]).should.be.true()
settings.should.have.property('telemetryEnabled', false)
})
})

View File

@ -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")
})
})

View File

@ -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")
})
})

View File

@ -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')
})
})