Merge pull request #3383 from node-red/dev

Merge dev to master for 2.2.0
This commit is contained in:
Nick O'Leary 2022-01-27 12:50:45 +00:00 committed by GitHub
commit 3a69af9034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 2486 additions and 3444 deletions

File diff suppressed because it is too large Load Diff

View File

@ -38,12 +38,9 @@ If you want to raise a pull-request with a new feature, or a refactoring
of existing code, it may well get rejected if you haven't discussed it on
the [forum](https://discourse.nodered.org) first.
All contributors need to sign the JS Foundation's Contributor License Agreement.
It is an online process and quick to do. You can read the details of the agreement
here: https://cla.js.foundation/node-red/node-red.
If you raise a pull-request without having signed the CLA, you will be prompted
to do so automatically.
All contributors need to sign the OpenJS Foundation's Contributor License Agreement.
It is an online process and quick to do. If you raise a pull-request without
having signed the CLA, you will be prompted to do so automatically.
### Code Branches

View File

@ -56,13 +56,13 @@ This project adheres to the [Contributor Covenant 1.4](http://contributor-covena
## Authors
Node-RED is a project of the [OpenJS Foundation](https://openjsf.org).
Node-RED is a project of the [OpenJS Foundation](http://openjsf.org).
It was created by [IBM Emerging Technology](https://www.ibm.com/blogs/emerging-technology/).
* Nick O'Leary [@knolleary](http://twitter.com/knolleary)
* Dave Conway-Jones [@ceejay](http://twitter.com/ceejay)
It is maintained by:
* Nick O'Leary [@knolleary](http://twitter.com/knolleary)
* Dave Conway-Jones [@ceejay](http://twitter.com/ceejay)
* And many others...
## Copyright and license

View File

@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "2.1.6",
"version": "2.2.0",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@ -28,7 +28,7 @@
"dependencies": {
"acorn": "8.7.0",
"acorn-walk": "8.2.0",
"ajv": "8.8.2",
"ajv": "8.9.0",
"async-mutex": "0.3.2",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
@ -50,7 +50,7 @@
"hash-sum": "2.0.0",
"hpagent": "0.1.2",
"https-proxy-agent": "5.0.0",
"i18next": "21.6.6",
"i18next": "21.6.10",
"iconv-lite": "0.6.3",
"is-utf8": "0.2.1",
"js-yaml": "3.14.1",
@ -59,10 +59,10 @@
"lodash.clonedeep": "^4.5.0",
"media-typer": "1.1.0",
"memorystore": "1.6.6",
"mime": "2.5.2",
"mime": "3.0.0",
"moment-timezone": "0.5.34",
"mqtt": "4.3.4",
"multer": "1.4.3",
"multer": "1.4.4",
"mustache": "4.2.0",
"node-red-admin": "^2.2.1",
"nopt": "5.0.0",
@ -75,9 +75,9 @@
"semver": "7.3.5",
"tar": "6.1.11",
"tough-cookie": "4.0.0",
"uglify-js": "3.14.5",
"uglify-js": "3.15.0",
"uuid": "8.3.2",
"ws": "7.5.1",
"ws": "7.5.6",
"xml2js": "0.4.23"
},
"optionalDependencies": {
@ -104,16 +104,16 @@
"grunt-sass": "~3.1.0",
"grunt-simple-mocha": "~0.4.1",
"grunt-simple-nyc": "^3.0.1",
"i18next-http-backend": "1.3.1",
"i18next-http-backend": "1.3.2",
"jquery-i18next": "1.2.1",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "4.0.10",
"marked": "4.0.12",
"minami": "1.2.3",
"mocha": "9.1.3",
"mocha": "9.2.0",
"node-red-node-test-helper": "^0.2.7",
"nodemon": "2.0.15",
"proxy": "^1.0.2",
"sass": "1.48.0",
"sass": "1.49.0",
"should": "13.2.3",
"sinon": "11.1.2",
"stoppable": "^1.1.0",

View File

@ -1,4 +1,4 @@
Copyright JS Foundation and other contributors, http://js.foundation
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004

View File

@ -122,6 +122,7 @@ module.exports = {
}
if (req.body.active) {
opts.clearContext = req.body.hasOwnProperty('clearContext')?req.body.clearContext:true
runtimeAPI.projects.setActiveProject(opts).then(function() {
listProjects(req,res);
}).catch(function(err) {

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "2.1.6",
"version": "2.2.0",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,8 +16,8 @@
}
],
"dependencies": {
"@node-red/util": "2.1.6",
"@node-red/editor-client": "2.1.6",
"@node-red/util": "2.2.0",
"@node-red/editor-client": "2.2.0",
"bcryptjs": "2.4.3",
"body-parser": "1.19.1",
"clone": "2.1.2",
@ -25,14 +25,14 @@
"express-session": "1.17.2",
"express": "4.17.2",
"memorystore": "1.6.6",
"mime": "2.5.2",
"multer": "1.4.3",
"mime": "3.0.0",
"multer": "1.4.4",
"mustache": "4.2.0",
"oauth2orize": "1.11.1",
"passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2",
"passport": "0.5.2",
"ws": "7.5.1"
"ws": "7.5.6"
},
"optionalDependencies": {
"bcrypt": "5.0.1"

View File

@ -1,4 +1,4 @@
Copyright JS Foundation and other contributors, http://js.foundation
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004

View File

@ -75,6 +75,8 @@
"view": {
"view": "View",
"grid": "Grid",
"storeZoom": "Restore zoom level on load",
"storePosition": "Restore scroll position on load",
"showGrid": "Show grid",
"snapGrid": "Snap to grid",
"gridSize": "Grid size",
@ -894,6 +896,8 @@
"addTitle": "add an item"
},
"search": {
"history": "Search history",
"clear": "clear all",
"empty": "No matches found",
"addNode": "add a node..."
},
@ -1091,7 +1095,8 @@
"not-git": "Not a git repository",
"no-resource": "Repository not found",
"cant-get-ssh-key-path": "Error! Can't get selected SSH key path.",
"unexpected_error": "unexpected_error"
"unexpected_error": "unexpected_error",
"clearContext": "Clear context when switching projects"
},
"delete": {
"confirm": "Are you sure you want to delete this project?"

View File

@ -75,6 +75,8 @@
"view": {
"view": "表示",
"grid": "グリッド",
"storeZoom": "読み込み時に拡大/縮小のレベルを復元",
"storePosition": "読み込み時にスクロール位置を復元",
"showGrid": "グリッドを表示",
"snapGrid": "ノードの配置を補助",
"gridSize": "グリッドの大きさ",
@ -894,6 +896,8 @@
"addTitle": "要素を追加"
},
"search": {
"history": "検索履歴",
"clear": "全て削除",
"empty": "一致したものが見つかりませんでした",
"addNode": "ノードを追加..."
},
@ -1091,7 +1095,8 @@
"not-git": "Gitリポジトリではありません",
"no-resource": "リポジトリが見つかりません",
"cant-get-ssh-key-path": "エラー! 選択したSSHキーのパスを取得できません。",
"unexpected_error": "予期しないエラー"
"unexpected_error": "予期しないエラー",
"clearContext": "プロジェクトを切り替る際にコンテキストを初期化"
},
"delete": {
"confirm": "プロジェクトを削除しても良いですか?"
@ -1151,5 +1156,137 @@
"ru": "ロシア語",
"zh-CN": "中国語(簡体)",
"zh-TW": "中国語(繁体)"
},
"action-list": {
"toggle-show-tips": "ヒント表示切替",
"show-about": "Node-REDの説明を表示",
"show-welcome-tour": "ウェルカムツアー表示",
"show-next-tab": "次のタブを表示",
"show-previous-tab": "前のタブを表示",
"add-flow": "フローを追加",
"add-flow-to-right": "フローを右に追加",
"edit-flow": "フローを編集",
"remove-flow": "フローを削除",
"enable-flow": "フローを有効化",
"disable-flow": "フローを無効化",
"hide-flow": "フローを隠す",
"hide-other-flows": "他のフローを非表示",
"hide-all-flows": "全てのフローを非表示",
"show-all-flows": "全てのフローを表示",
"show-last-hidden-flow": "最後に非表示にしたフローを表示",
"list-hidden-flows": "非表示フローを表示",
"list-flows": "フロー一覧",
"list-subflows": "サブフロー一覧",
"go-to-previous-location": "前の位置に移動",
"go-to-next-location": "次の位置に移動",
"copy-selection-to-internal-clipboard": "選択をクリップボードにコピー",
"cut-selection-to-internal-clipboard": "選択をクリップボードに切り取り",
"paste-from-internal-clipboard": "クリップボードから貼り付け",
"detach-selected-nodes": "選択ノードを接続から外す",
"delete-selection": "選択を削除",
"delete-selection-and-reconnect": "選択を削除し再接続",
"edit-selected-node": "選択したノードを編集",
"go-to-selection": "選択に移動",
"undo": "変更操作を戻す",
"redo": "変更操作をやり直し",
"select-all-nodes": "全てのノードを選択",
"select-none": "ノードを選択",
"enable-selected-nodes": "選択ノードを有効化",
"disable-selected-nodes": "選択ノードを無効化",
"toggle-show-grid": "グリッド表示切替",
"toggle-snap-grid": "ノードの配置補助切替",
"toggle-status": "ステータス表示切替",
"show-selected-node-labels": "選択したノードのラベルを表示",
"hide-selected-node-labels": "選択したノードのラベルを非表示",
"scroll-view-up": "上スクロール",
"scroll-view-right": "右スクロール",
"scroll-view-down": "下スクロール",
"scroll-view-left": "左スクロール",
"step-view-up": "一単位上スクロール",
"step-view-right": "一単位右スクロール",
"step-view-down": "一単位下スクロール",
"step-view-left": "一単位左スクロール",
"move-selection-up": "選択を上移動",
"move-selection-right": "選択を右移動",
"move-selection-down": "選択を下移動",
"move-selection-left": "選択を左移動",
"move-selection-forwards": "選択を前面に移動",
"move-selection-backwards": "選択を背面に移動",
"move-selection-to-front": "選択を最前面に移動",
"move-selection-to-back": "選択を最背面に移動",
"step-selection-up": "選択を一単位上移動",
"step-selection-right": "選択を一単位右移動",
"step-selection-down": "選択を一単位下移動",
"step-selection-left": "選択を一単位左移動",
"select-connected-nodes": "接続されたノードを選択",
"select-downstream-nodes": "後方に接続されたノードを選択",
"select-upstream-nodes": "前方に接続されたノードを選択",
"go-to-next-node": "次のノードに移動",
"go-to-previous-node": "前のノードに移動",
"go-to-next-sibling": "次の兄弟ノードに移動",
"go-to-previous-sibling": "前の兄弟ノードに移動",
"go-to-nearest-node-on-left": "最も近い左側ノードに移動",
"go-to-nearest-node-on-right": "最も近い右側ノードに移動",
"go-to-nearest-node-above": "最も近い上側ノードに移動",
"go-to-nearest-node-below": "最も近い下側ノードに移動",
"align-selection-to-grid": "選択を整列",
"align-selection-to-left": "選択を左揃え",
"align-selection-to-right": "選択を右揃え",
"align-selection-to-top": "選択を上揃え",
"align-selection-to-bottom": "選択を下揃え",
"align-selection-to-middle": "選択を上下中央揃え",
"align-selection-to-center": "選択を左右中央揃え",
"distribute-selection-horizontally": "選択を左右に整列",
"distribute-selection-vertically": "選択を上下に整列",
"wire-series-of-nodes": "ノードを一続きに接続",
"wire-node-to-multiple": "ノードを複数に接続",
"show-user-settings": "ユーザ設定を表示",
"show-help": "ヘルプを表示",
"toggle-palette": "パレットの表示切替",
"show-event-log": "イベントログを表示",
"manage-palette": "パレットの管理",
"toggle-sidebar": "サイドバーの表示切替",
"show-info-tab": "ノード情報タブの表示",
"show-help-tab": "ノードヘルプタブの表示",
"show-config-tab": "設定ノードタブの表示",
"select-all-config-nodes": "全ての設定ノードを選択",
"delete-config-selection": "選択した設定ノードを削除",
"show-context-tab": "コンテキストデータタブを表示",
"create-subflow": "サブフローを作成",
"convert-to-subflow": "選択をサブフローに変換",
"group-selection": "選択をグループ化",
"ungroup-selection": "選択をグループ解除",
"merge-selection-to-group": "選択をグループにマージ",
"remove-selection-from-group": "選択をグループから削除",
"copy-group-style": "グループのスタイルをコピー",
"paste-group-style": "グループのスタイルを貼り付け",
"show-export-dialog": "書き出しダイアログを表示",
"show-import-dialog": "読み込みダイアログを表示",
"show-library-export-dialog": "ライブラリ書き出しダイアログを表示",
"show-library-import-dialog": "ライブラリ読み込みダイアログを表示",
"show-examples-import-dialog": "サンプル読み込みダイアログを表示",
"search": "検索",
"show-action-list": "アクション一覧を表示",
"confirm-edit-tray": "編集を完了",
"cancel-edit-tray": "編集をキャンセル",
"show-remote-diff": "リモートとの変更差分を表示",
"deploy-flows": "フローをデプロイ",
"restart-flows": "フローを再起動",
"set-deploy-type-to-full": "デプロイを「全て」に設定",
"set-deploy-type-to-modified-flows": "デプロイを「変更したフロー」に設定",
"set-deploy-type-to-modified-nodes": "デプロイを「変更したノード」に設定",
"show-debug-tab": "デバッグタブを表示",
"clear-debug-messages": "デバッグメッセージをクリア",
"clear-filtered-debug-messages": "フィルタしたデバッグメッセージをクリア",
"activate-selected-debug-nodes": "選択したデバッグノードを有効化",
"activate-all-debug-nodes": "全てのデバッグノードを有効化",
"activate-all-flow-debug-nodes": "フロー内の全デバッグノードを有効化",
"deactivate-selected-debug-nodes": "選択したデバッグノードを無効化",
"deactivate-all-debug-nodes": "全てのデバッグノードを無効化",
"deactivate-all-flow-debug-nodes": "フロー内の全デバッグノードを無効化",
"zoom-in": "ズームイン",
"zoom-out": "ズームアウト",
"zoom-reset": "ズームリセット",
"toggle-navigator": "ナビゲータ表示切替"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/editor-client",
"version": "2.1.6",
"version": "2.2.0",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@ -684,6 +684,13 @@ RED.history = (function() {
peek: function() {
return undoHistory[undoHistory.length-1];
},
replace: function(ev) {
if (undoHistory.length === 0) {
RED.history.push(ev);
} else {
undoHistory[undoHistory.length-1] = ev;
}
},
clear: function() {
undoHistory = [];
redoHistory = [];

View File

@ -38,7 +38,9 @@
},
"red-ui-workspace": {
"backspace": "core:delete-selection",
"ctrl-backspace": "core:delete-selection-and-reconnect",
"delete": "core:delete-selection",
"ctrl-delete": "core:delete-selection-and-reconnect",
"enter": "core:edit-selected-node",
"ctrl-enter": "core:go-to-selection",
"ctrl-c": "core:copy-selection-to-internal-clipboard",

View File

@ -15,6 +15,9 @@
**/
RED.nodes = (function() {
var PORT_TYPE_INPUT = 1;
var PORT_TYPE_OUTPUT = 0;
var node_defs = {};
var linkTabMap = {};
@ -2458,6 +2461,144 @@ RED.nodes = (function() {
return helpContent;
}
function getNodeIslands(nodes) {
var selectedNodes = new Set(nodes);
// Maps node => island index
var nodeToIslandIndex = new Map();
// Maps island index => [nodes in island]
var islandIndexToNodes = new Map();
var internalLinks = new Set();
nodes.forEach((node, index) => {
nodeToIslandIndex.set(node,index);
islandIndexToNodes.set(index, [node]);
var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT);
var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
inboundLinks.forEach(l => {
if (selectedNodes.has(l.source)) {
internalLinks.add(l)
}
})
outboundLinks.forEach(l => {
if (selectedNodes.has(l.target)) {
internalLinks.add(l)
}
})
})
internalLinks.forEach(l => {
let source = l.source;
let target = l.target;
if (nodeToIslandIndex.get(source) !== nodeToIslandIndex.get(target)) {
let sourceIsland = nodeToIslandIndex.get(source);
let islandToMove = nodeToIslandIndex.get(target);
let nodesToMove = islandIndexToNodes.get(islandToMove);
nodesToMove.forEach(n => {
nodeToIslandIndex.set(n,sourceIsland);
islandIndexToNodes.get(sourceIsland).push(n);
})
islandIndexToNodes.delete(islandToMove);
}
})
const result = [];
islandIndexToNodes.forEach((nodes,index) => {
result.push(nodes);
})
return result;
}
function detachNodes(nodes) {
let allSelectedNodes = [];
nodes.forEach(node => {
if (node.type === 'group') {
let groupNodes = RED.group.getNodes(node,true,true);
allSelectedNodes = allSelectedNodes.concat(groupNodes);
} else {
allSelectedNodes.push(node);
}
})
if (allSelectedNodes.length > 0 ) {
const nodeIslands = RED.nodes.getNodeIslands(allSelectedNodes);
let removedLinks = [];
let newLinks = [];
let createdLinkIds = new Set();
nodeIslands.forEach(nodes => {
let selectedNodes = new Set(nodes);
let allInboundLinks = [];
let allOutboundLinks = [];
// Identify links that enter or exit this island of nodes
nodes.forEach(node => {
var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT);
var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
inboundLinks.forEach(l => {
if (!selectedNodes.has(l.source)) {
allInboundLinks.push(l)
}
})
outboundLinks.forEach(l => {
if (!selectedNodes.has(l.target)) {
allOutboundLinks.push(l)
}
})
});
// Identify the links to restore
allInboundLinks.forEach(inLink => {
// For Each inbound link,
// - get source node.
// - trace through to all outbound links
let sourceNode = inLink.source;
let targetNodes = new Set();
let visited = new Set();
let stack = [inLink.target];
while (stack.length > 0) {
let node = stack.pop(stack);
visited.add(node)
let links = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT);
links.forEach(l => {
if (visited.has(l.target)) {
return
}
visited.add(l.target);
if (selectedNodes.has(l.target)) {
// internal link
stack.push(l.target)
} else {
targetNodes.add(l.target)
}
})
}
targetNodes.forEach(target => {
let linkId = `${sourceNode.id}[${inLink.sourcePort}] -> ${target.id}`
if (!createdLinkIds.has(linkId)) {
createdLinkIds.add(linkId);
let link = {
source: sourceNode,
sourcePort: inLink.sourcePort,
target: target
}
let existingLinks = RED.nodes.filterLinks(link)
if (existingLinks.length === 0) {
newLinks.push(link);
}
}
})
})
// 2. delete all those links
allInboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)})
allOutboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)})
})
newLinks.forEach(l => RED.nodes.addLink(l));
return {
newLinks,
removedLinks
}
}
}
return {
init: function() {
RED.events.on("registry:node-type-added",function(type) {
@ -2539,7 +2680,7 @@ RED.nodes = (function() {
add: addNode,
remove: removeNode,
clear: clear,
detachNodes: detachNodes,
moveNodesForwards: moveNodesForwards,
moveNodesBackwards: moveNodesBackwards,
moveNodesToFront: moveNodesToFront,
@ -2551,7 +2692,20 @@ RED.nodes = (function() {
addLink: addLink,
removeLink: removeLink,
getNodeLinks: function(id, portType) {
if (typeof id !== 'string') {
id = id.id;
}
if (nodeLinks[id]) {
if (portType === 1) {
// Return cloned arrays so they can be safely modified by caller
return [].concat(nodeLinks[id].in)
} else {
return [].concat(nodeLinks[id].out)
}
}
return [];
},
addWorkspace: addWorkspace,
removeWorkspace: removeWorkspace,
getWorkspaceOrder: function() { return workspacesOrder },
@ -2625,6 +2779,7 @@ RED.nodes = (function() {
getAllFlowNodes: getAllFlowNodes,
getAllUpstreamNodes: getAllUpstreamNodes,
getAllDownstreamNodes: getAllDownstreamNodes,
getNodeIslands: getNodeIslands,
createExportableNodeSet: createExportableNodeSet,
createCompleteNodeSet: createCompleteNodeSet,
updateConfigNodeUsers: updateConfigNodeUsers,

View File

@ -556,8 +556,7 @@ var RED = (function() {
$(".red-ui-header-toolbar").show();
RED.sidebar.show(":first");
RED.sidebar.show(":first", true);
setTimeout(function() {
loader.end();

View File

@ -160,18 +160,19 @@ RED.actionList = (function() {
createDialog();
}
dialog.slideDown(300);
searchInput.searchBox('value',v)
searchInput.searchBox('value',v);
searchResults.editableList('empty');
results = [];
var actions = RED.actions.list();
actions.sort(function(A,B) {
return A.id.localeCompare(B.id);
var Akey = A.label;
var Bkey = B.label;
return Akey.localeCompare(Bkey);
});
actions.forEach(function(action) {
action.label = action.id.replace(/:/,": ").replace(/-/g," ").replace(/(^| )./g,function() { return arguments[0].toUpperCase()});
action._label = action.label.toLowerCase();
searchResults.editableList('addItem',action)
})
searchResults.editableList('addItem',action);
});
RED.events.emit("actionList:open");
visible = true;
}

View File

@ -1,33 +1,39 @@
RED.actions = (function() {
var actions = {
}
};
function addAction(name,handler) {
function addAction(name,handler,options) {
if (typeof handler !== 'function') {
throw new Error("Action handler not a function");
}
if (actions[name]) {
throw new Error("Cannot override existing action");
}
actions[name] = handler;
actions[name] = {
handler: handler,
options: options,
};
}
function removeAction(name) {
delete actions[name];
}
function getAction(name) {
return actions[name];
return actions[name].handler;
}
function invokeAction() {
var args = Array.prototype.slice.call(arguments);
var name = args.shift();
if (actions.hasOwnProperty(name)) {
actions[name].apply(null, args);
var handler = actions[name].handler;
handler.apply(null, args);
}
}
function listActions() {
var result = [];
var missing = [];
Object.keys(actions).forEach(function(action) {
var def = actions[action];
var shortcut = RED.keyboard.getShortcut(action);
var isUser = false;
if (shortcut) {
@ -35,13 +41,38 @@ RED.actions = (function() {
} else {
isUser = !!RED.keyboard.getUserShortcut(action);
}
if (!def.label) {
var name = action;
var options = def.options;
var key = options ? options.label : undefined;
if (!key) {
key = "action-list." +name.replace(/^.*:/,"");
}
var label = RED._(key);
if (label === key) {
// no translation. convert `name` to description
label = name.replace(/(^.+:([a-z]))|(-([a-z]))/g, function() {
if (arguments[5] === 0) {
return arguments[2].toUpperCase();
} else {
return " "+arguments[4].toUpperCase();
}
});
missing.push(key);
}
def.label = label;
}
//console.log("; missing:", missing);
result.push({
id:action,
scope:shortcut?shortcut.scope:undefined,
key:shortcut?shortcut.key:undefined,
user:isUser
})
})
user:isUser,
label: def.label,
options: def.options,
});
});
return result;
}
return {

View File

@ -557,23 +557,19 @@ RED.tabs = (function() {
}
}
li.one("transitionend", function(evt) {
li.remove();
if (tabs[id].pinned) {
pinnedTabsCount--;
}
if (options.onremove) {
options.onremove(tabs[id]);
}
delete tabs[id];
updateTabWidths();
if (collapsibleMenu) {
collapsibleMenu.remove();
collapsibleMenu = null;
}
})
li.addClass("hide-tab");
li.width(0);
li.remove();
if (tabs[id].pinned) {
pinnedTabsCount--;
}
if (options.onremove) {
options.onremove(tabs[id]);
}
delete tabs[id];
updateTabWidths();
if (collapsibleMenu) {
collapsibleMenu.remove();
collapsibleMenu = null;
}
}
function findPreviousVisibleTab(li) {

View File

@ -333,6 +333,16 @@ RED.deploy = (function() {
var unknownNodes = [];
var invalidNodes = [];
RED.nodes.eachConfig(function(node) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
if (unknownNodes.indexOf(node.name) == -1) {
unknownNodes.push(node.name);
}
}
});
RED.nodes.eachNode(function(node) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));

View File

@ -35,9 +35,10 @@
editState.changed = true;
}
if (!node._def.defaults || !node._def.defaults.hasOwnProperty("icon")) {
var icon = $("#red-ui-editor-node-icon").val()||""
var icon = $("#red-ui-editor-node-icon").val()||"";
if (!this.isDefaultIcon) {
if (icon !== node.icon) {
if ((icon !== node.icon) &&
(icon !== "")) {
editState.changes.icon = node.icon;
node.icon = icon;
editState.changed = true;
@ -101,14 +102,14 @@
if (showLabel) {
// Default to show label
if (node.l !== false) {
editState.changes.l = node.l
editState.changes.l = node.l;
editState.changed = true;
}
node.l = false;
} else {
// Node has showLabel:false (eg link nodes)
if (node.hasOwnProperty('l') && node.l) {
editState.changes.l = node.l
editState.changes.l = node.l;
editState.changed = true;
}
delete node.l;
@ -118,20 +119,20 @@
if (showLabel) {
// Default to show label
if (node.hasOwnProperty('l') && !node.l) {
editState.changes.l = node.l
editState.changes.l = node.l;
editState.changed = true;
}
delete node.l;
} else {
if (!node.l) {
editState.changes.l = node.l
editState.changes.l = node.l;
editState.changed = true;
}
node.l = true;
}
}
}
}
};
});
function buildAppearanceForm(container,node) {
@ -164,10 +165,10 @@
var categories = RED.palette.getCategories();
categories.sort(function(A,B) {
return A.label.localeCompare(B.label);
})
});
categories.forEach(function(cat) {
categorySelector.append($("<option/>").val(cat.id).text(cat.label));
})
});
categorySelector.append($("<option/>").attr('disabled',true).text("---"));
categorySelector.append($("<option/>").val("_custom_").text(RED._("palette.addCategory")));
@ -180,7 +181,7 @@
$("#subflow-appearance-input-category").width(250);
$("#subflow-appearance-input-custom-category").hide();
}
})
});
$("#subflow-appearance-input-category").val(node.category||"subflows");
var userCount = 0;
@ -204,7 +205,7 @@
$("#node-input-show-label").toggleButton({
enabledLabel: RED._("editor.show"),
disabledLabel: RED._("editor.hide")
})
});
if (!node.hasOwnProperty("l")) {
// Show label unless def.showLabel set to false
@ -230,7 +231,7 @@
"#E9967A", "#F3B567", "#FDD0A2",
"#FDF0C2", "#FFAAAA", "#FFCC66",
"#FFF0F0", "#FFFFFF"
]
];
RED.editor.colorPicker.create({
id: "red-ui-editor-node-color",
@ -245,9 +246,9 @@
nodeDiv.css('backgroundColor',colour);
var borderColor = RED.utils.getDarkerColor(colour);
if (borderColor !== colour) {
nodeDiv.css('border-color',borderColor)
nodeDiv.css('border-color',borderColor);
}
})
});
}
@ -264,7 +265,7 @@
nodeDiv.css('backgroundColor',colour);
var borderColor = RED.utils.getDarkerColor(colour);
if (borderColor !== colour) {
nodeDiv.css('border-color',borderColor)
nodeDiv.css('border-color',borderColor);
}
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
@ -292,7 +293,7 @@
RED.popover.tooltip(iconButton, function() {
return $("#red-ui-editor-node-icon").val() || RED._("editor.default");
})
});
$('<input type="hidden" id="red-ui-editor-node-icon">').val(node.icon).appendTo(iconRow);
}
@ -417,11 +418,11 @@
});
rows.sort(function(A,B) {
return A.i-B.i;
})
});
rows.forEach(function(r,i) {
r.r.find("label").text((i+1)+".");
r.r.appendTo(outputsDiv);
})
});
if (rows.length === 0) {
buildLabelRow("output",i,"").appendTo(outputsDiv);
} else {
@ -467,7 +468,7 @@
clear.on("click", function(evt) {
evt.preventDefault();
input.val("");
})
});
}
return result;
}
@ -501,6 +502,12 @@
}
var v = $(this).val();
hasNonBlankLabel = hasNonBlankLabel || v!== "";
// mark changed output port labels as dirty
if (node.type === "subflow" && (!node.outputLabels || node.outputLabels[index] !== v)) {
node.out[index].dirty = true;
}
newValue[index] = v;
});
@ -509,6 +516,12 @@
changes.outputLabels = node.outputLabels;
node.outputLabels = newValue;
changed = true;
// trigger redraw of dirty port labels
if (node.type === "subflow") {
RED.view.redraw();
}
}
return changed;
}

View File

@ -590,12 +590,14 @@ RED.group = (function() {
markDirty(group);
}
function getNodes(group,recursive) {
function getNodes(group,recursive,excludeGroup) {
var nodes = [];
group.nodes.forEach(function(n) {
nodes.push(n);
if (n.type !== 'group' || !excludeGroup) {
nodes.push(n);
}
if (recursive && n.type === 'group') {
nodes = nodes.concat(getNodes(n,recursive))
nodes = nodes.concat(getNodes(n,recursive,excludeGroup))
}
})
return nodes;

View File

@ -49,15 +49,15 @@ RED.keyboard = (function() {
"]": 221,
"{": 219,// <- QWERTY specific
"}": 221 // <- QWERTY specific
}
};
var metaKeyCodes = {
16: true,
17: true,
18: true,
91: true,
93: true
}
var actionToKeyMap = {}
};
var actionToKeyMap = {};
var defaultKeyMap = {};
// FF generates some different keycodes because reasons.
@ -65,7 +65,7 @@ RED.keyboard = (function() {
59:186,
61:187,
173:189
}
};
function migrateOldKeymap() {
// pre-0.18
@ -80,7 +80,7 @@ RED.keyboard = (function() {
}
function getUserKey(action) {
return RED.settings.get('editor.keymap',{})[action]
return RED.settings.get('editor.keymap',{})[action];
}
function mergeKeymaps(defaultKeymap, themeKeymap) {
@ -105,7 +105,7 @@ RED.keyboard = (function() {
scope:scope,
key:key,
user:false
})
});
}
}
}
@ -115,13 +115,13 @@ RED.keyboard = (function() {
if (themeKeymap.hasOwnProperty(action)) {
if (!themeKeymap[action].key) {
// No key for this action - default is no keybinding
delete mergedKeymap[action]
delete mergedKeymap[action];
} else {
mergedKeymap[action] = [{
scope: themeKeymap[action].scope || "*",
key: themeKeymap[action].key,
user: false
}]
}];
if (mergedKeymap[action][0].scope === "workspace") {
mergedKeymap[action][0].scope = "red-ui-workspace";
}
@ -179,7 +179,7 @@ RED.keyboard = (function() {
close: function() {
RED.menu.refreshShortcuts();
}
})
});
}
function revertToDefault(action) {
@ -327,7 +327,7 @@ RED.keyboard = (function() {
scope:scope,
key:key,
user:false
}
};
}
if (!ondown) {
var userAction = getUserKey(cbdown);
@ -350,7 +350,7 @@ RED.keyboard = (function() {
}
}
} else {
keys.push([key,mod])
keys.push([key,mod]);
}
var slot = handlers;
for (i=0;i<keys.length;i++) {
@ -373,7 +373,7 @@ RED.keyboard = (function() {
//slot[key] = {scope: scope, ondown:cbdown};
}
slot.handlers = slot.handlers || [];
slot.handlers.push({scope:scope,ondown:cbdown})
slot.handlers.push({scope:scope,ondown:cbdown});
slot.scope = scope;
slot.ondown = cbdown;
}
@ -390,12 +390,12 @@ RED.keyboard = (function() {
if (parsedKey) {
keys.push(parsedKey);
} else {
console.log("Unrecognised key specifier:",key)
console.log("Unrecognised key specifier:",key);
return;
}
}
} else {
keys.push([key,mod])
keys.push([key,mod]);
}
var slot = handlers;
for (i=0;i<keys.length;i++) {
@ -417,7 +417,7 @@ RED.keyboard = (function() {
}
if (typeof slot.ondown === "string") {
if (typeof modifiers === 'boolean' && modifiers) {
actionToKeyMap[slot.ondown] = {user: modifiers}
actionToKeyMap[slot.ondown] = {user: modifiers};
} else {
delete actionToKeyMap[slot.ondown];
}
@ -433,11 +433,11 @@ RED.keyboard = (function() {
function formatKey(key,plain) {
var formattedKey = isMac?key.replace(/ctrl-?/,"&#8984;"):key;
formattedKey = isMac?formattedKey.replace(/alt-?/,"&#8997;"):key;
formattedKey = formattedKey.replace(/shift-?/,"&#8679;")
formattedKey = formattedKey.replace(/left/,"&#x2190;")
formattedKey = formattedKey.replace(/up/,"&#x2191;")
formattedKey = formattedKey.replace(/right/,"&#x2192;")
formattedKey = formattedKey.replace(/down/,"&#x2193;")
formattedKey = formattedKey.replace(/shift-?/,"&#8679;");
formattedKey = formattedKey.replace(/left/,"&#x2190;");
formattedKey = formattedKey.replace(/up/,"&#x2191;");
formattedKey = formattedKey.replace(/right/,"&#x2192;");
formattedKey = formattedKey.replace(/down/,"&#x2193;");
if (plain) {
return formattedKey;
}
@ -461,7 +461,6 @@ RED.keyboard = (function() {
var container = $(this);
var object = container.data('data');
if (!container.hasClass('keyboard-shortcut-entry-expanded')) {
endEditShortcut();
@ -485,7 +484,7 @@ RED.keyboard = (function() {
}
$(this).toggleClass("input-error",!valid);
okButton.attr("disabled",!valid);
})
});
var scopeSelect = $('<select><option value="*" data-i18n="keyboard.global"></option><option value="red-ui-workspace" data-i18n="keyboard.workspace"></option></select>').appendTo(scope);
scopeSelect.i18n();
@ -495,7 +494,7 @@ RED.keyboard = (function() {
scopeSelect.val(object.scope||'*');
scopeSelect.on("change", function() {
keyInput.trigger("change");
})
});
var div = $('<div class="keyboard-shortcut-edit button-group-vertical"></div>').appendTo(scope);
var okButton = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-check"></i></button>').appendTo(div);
@ -521,10 +520,13 @@ RED.keyboard = (function() {
id:object.id,
scope:shortcut?shortcut.scope:undefined,
key:shortcut?shortcut.key:undefined,
user:shortcut?shortcut.user:undefined
}
user:shortcut?shortcut.user:undefined,
label: object.label,
options: object.options,
};
buildShortcutRow(container,obj);
})
});
keyInput.trigger("focus");
}
@ -559,7 +561,7 @@ RED.keyboard = (function() {
delete object.scope;
} else {
keyDiv.parent().removeClass("keyboard-shortcut-entry-unassigned");
keyDiv.append(RED.keyboard.formatKey(key))
keyDiv.append(RED.keyboard.formatKey(key));
$("<span>").text(scope).appendTo(scopeDiv);
object.key = key;
object.scope = scope;
@ -572,7 +574,7 @@ RED.keyboard = (function() {
userKeymap[object.id] = {
scope:shortcut.scope,
key:shortcut.key
}
};
RED.settings.set('editor.keymap',userKeymap);
}
}
@ -588,13 +590,7 @@ RED.keyboard = (function() {
var item = $('<div class="keyboard-shortcut-entry">').appendTo(container);
container.data('data',object);
var text = object.id.replace(/(^.+:([a-z]))|(-([a-z]))/g,function() {
if (arguments[5] === 0) {
return arguments[2].toUpperCase();
} else {
return " "+arguments[4].toUpperCase();
}
});
var text = object.label;
var label = $('<div>').addClass("keyboard-shortcut-entry-text").text(text).appendTo(item);
var user = $('<i class="fa fa-user"></i>').prependTo(label);
@ -635,8 +631,9 @@ RED.keyboard = (function() {
} else {
filterValue = filterValue.replace(/\s/g,"");
shortcutList.editableList('filter', function(data) {
return data.id.toLowerCase().replace(/^.*:/,"").replace("-","").indexOf(filterValue) > -1;
})
var label = data.label.toLowerCase();
return label.indexOf(filterValue) > -1;
});
}
}
});
@ -657,9 +654,9 @@ RED.keyboard = (function() {
});
var shortcuts = RED.actions.list();
shortcuts.sort(function(A,B) {
var Aid = A.id.replace(/^.*:/,"").replace(/[ -]/g,"").toLowerCase();
var Bid = B.id.replace(/^.*:/,"").replace(/[ -]/g,"").toLowerCase();
return Aid.localeCompare(Bid);
var Akey = A.label;
var Bkey = B.label;
return Akey.localeCompare(Bkey);
});
knownShortcuts = new Set();
shortcuts.forEach(function(s) {

View File

@ -1212,6 +1212,9 @@ RED.projects = (function() {
}
}).appendTo(row);
row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-open"></div>').hide().appendTo(container);
$('<span style="display: flex; align-items: center;"><input style="padding:0; margin: 0 5px 0 0" checked type="checkbox" id="red-ui-projects-dialog-screen-clear-context"> <label for="red-ui-projects-dialog-screen-clear-context" style="padding:0; margin: 0"> <span data-i18n="projects.create.clearContext"></span></label></span>').appendTo(row).i18n();
row = $('<div class="form-row red-ui-projects-dialog-screen-create-row red-ui-projects-dialog-screen-create-row-empty red-ui-projects-dialog-screen-create-row-clone"></div>').appendTo(container);
$('<label for="red-ui-projects-dialog-screen-create-project-name">'+RED._("projects.create.project-name")+'</label>').appendTo(row);
@ -1501,7 +1504,8 @@ RED.projects = (function() {
};
}
} else if (projectType === 'open') {
return switchProject(selectedProject.name,function(err,data) {
var clearContext = $("#red-ui-projects-dialog-screen-clear-context").prop("checked")
return switchProject(selectedProject.name, clearContext, function(err,data) {
if (err) {
if (err.code !== 'credentials_load_failed') {
console.log(RED._("projects.create.unexpected_error"),err)
@ -1595,7 +1599,7 @@ RED.projects = (function() {
}
}
function switchProject(name,done) {
function switchProject(name,clearContext,done) {
RED.deploy.setDeployInflight(true);
RED.projects.settings.switchProject(name);
sendRequest({
@ -1614,7 +1618,7 @@ RED.projects = (function() {
'*': done
},
}
},{active:true}).then(function() {
},{active:true, clearContext:clearContext}).then(function() {
dialog.dialog( "close" );
RED.events.emit("project:change", {name:name});
}).always(function() {
@ -1687,7 +1691,7 @@ RED.projects = (function() {
dialogHeight = 590 - (750 - winHeight);
}
$(".red-ui-projects-dialog-box").height(dialogHeight);
$(".red-ui-projects-dialog-project-list-inner-container").height(Math.max(500,dialogHeight) - 180);
$(".red-ui-projects-dialog-project-list-inner-container").height(Math.max(500,dialogHeight) - 210);
dialog.dialog('option','title',screen.title||"");
dialog.dialog("open");
}

View File

@ -22,6 +22,7 @@ RED.search = (function() {
var selected = -1;
var visible = false;
var searchHistory = [];
var index = {};
var currentResults = [];
var previousActiveElement;
@ -52,10 +53,22 @@ RED.search = (function() {
}
l = l||n.label||n.name||n.id||"";
var properties = ['id','type','name','label','info'];
if (n._def && n._def.defaults) {
properties = properties.concat(Object.keys(n._def.defaults));
const node_def = n && n._def;
if (node_def) {
if (node_def.defaults) {
properties = properties.concat(Object.keys(node_def.defaults));
}
if (n.type !== "group" && node_def.paletteLabel && node_def.paletteLabel !== node_def.type) {
try {
const label = ("" + (typeof node_def.paletteLabel === "function" ? node_def.paletteLabel.call(node_def) : node_def.paletteLabel)).toLowerCase();
if(label && label !== (""+node_def.type).toLowerCase()) {
indexProperty(n, l, label);
}
} catch(err) {
console.warn(`error indexing ${l}`, err);
}
}
}
for (var i=0;i<properties.length;i++) {
if (n.hasOwnProperty(properties[i])) {
@ -205,6 +218,20 @@ RED.search = (function() {
}
}
function populateSearchHistory() {
if (searchHistory.length > 0) {
searchResults.editableList('addItem',{
historyHeader: true
});
searchHistory.forEach(function(entry) {
searchResults.editableList('addItem',{
history: true,
value: entry
});
})
}
}
function createDialog() {
dialog = $("<div>",{id:"red-ui-search",class:"red-ui-search"}).appendTo("#red-ui-main-container");
var searchDiv = $("<div>",{class:"red-ui-search-container"}).appendTo(dialog);
@ -213,7 +240,12 @@ RED.search = (function() {
change: function() {
searchResults.editableList('empty');
selected = -1;
currentResults = search($(this).val());
var value = $(this).val();
if (value === "") {
populateSearchHistory();
return;
}
currentResults = search(value);
if (currentResults.length > 0) {
for (i=0;i<Math.min(currentResults.length,25);i++) {
searchResults.editableList('addItem',currentResults[i])
@ -285,7 +317,12 @@ RED.search = (function() {
})
}
}
} else {
} if ($(children[selected]).hasClass("red-ui-search-history")) {
var object = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
if (object) {
searchInput.searchBox('value',object.value)
}
} else if (!$(children[selected]).hasClass("red-ui-search-historyHeader")) {
if (currentResults.length > 0) {
reveal(currentResults[Math.max(0,selected)].node);
}
@ -301,7 +338,32 @@ RED.search = (function() {
addItem: function(container,i,object) {
var node = object.node;
var div;
if (object.more) {
if (object.historyHeader) {
container.parent().addClass("red-ui-search-historyHeader")
$('<div>',{class:"red-ui-search-empty"}).text(RED._("search.history")).appendTo(container);
$('<button type="button" class="red-ui-button red-ui-button-small"></button>').text(RED._("search.clear")).appendTo(container).on("click", function(evt) {
evt.preventDefault();
searchHistory = [];
searchResults.editableList('empty');
});
} else if (object.history) {
container.parent().addClass("red-ui-search-history")
div = $('<a>',{href:'#',class:"red-ui-search-result"}).appendTo(container);
div.text(object.value);
div.on("click", function(evt) {
evt.preventDefault();
searchInput.searchBox('value',object.value)
searchInput.focus();
})
$('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-remove"></i></button>').appendTo(container).on("click", function(evt) {
evt.preventDefault();
var index = searchHistory.indexOf(object.value);
searchHistory.splice(index,1);
searchResults.editableList('removeItem', object);
});
} else if (object.more) {
container.parent().addClass("red-ui-search-more")
div = $('<a>',{href:'#',class:"red-ui-search-result red-ui-search-empty"}).appendTo(container);
div.text(RED._("palette.editor.more",{count:object.more.results.length-object.more.start}));
@ -356,6 +418,12 @@ RED.search = (function() {
}
function reveal(node) {
var searchVal = searchInput.val();
var existingIndex = searchHistory.indexOf(searchVal);
if (existingIndex > -1) {
searchHistory.splice(existingIndex,1);
}
searchHistory.unshift(searchInput.val());
hide();
RED.view.reveal(node.id);
}
@ -374,9 +442,14 @@ RED.search = (function() {
if (dialog === null) {
createDialog();
} else {
searchResults.editableList('empty');
}
dialog.slideDown(300);
searchInput.searchBox('value',v)
if (!v || v === "") {
populateSearchHistory();
}
RED.events.emit("search:open");
visible = true;
}

View File

@ -19,6 +19,15 @@ RED.sidebar = (function() {
var sidebar_tabs;
var knownTabs = {};
// We store the current sidebar tab id in localStorage as 'last-sidebar-tab'
// This is restored when the editor is reloaded.
// We use sidebar_tabs.onchange to update localStorage. However that will
// also get triggered when the first tab gets added to the tabs - typically
// the 'info' tab. So we use the following variable to store the retrieved
// value from localStorage before we start adding the actual tabs
var lastSessionSelectedTab = null;
function addTab(title,content,closeable,visible) {
var options;
if (typeof title === "string") {
@ -194,16 +203,16 @@ RED.sidebar = (function() {
RED.events.emit("sidebar:resize");
}
function showSidebar(id) {
function showSidebar(id, skipShowSidebar) {
if (id === ":first") {
id = RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])[0]
id = lastSessionSelectedTab || RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])[0]
}
if (id) {
if (!containsTab(id) && knownTabs[id]) {
sidebar_tabs.addTab(knownTabs[id]);
}
sidebar_tabs.activateTab(id);
if (!RED.menu.isSelected("menu-item-sidebar")) {
if (!skipShowSidebar && !RED.menu.isSelected("menu-item-sidebar")) {
RED.menu.setSelected("menu-item-sidebar",true);
}
}
@ -227,6 +236,7 @@ RED.sidebar = (function() {
if (tab.toolbar) {
$(tab.toolbar).show();
}
RED.settings.setLocal("last-sidebar-tab", tab.id)
},
onremove: function(tab) {
$(tab.wrapper).hide();
@ -255,7 +265,9 @@ RED.sidebar = (function() {
}
});
RED.popover.tooltip($("#red-ui-sidebar-separator").find(".red-ui-sidebar-control-right"),RED._("keyboard.toggleSidebar"),"core:toggle-sidebar");
showSidebar();
lastSessionSelectedTab = RED.settings.getLocal("last-sidebar-tab")
RED.sidebar.info.init();
RED.sidebar.help.init();
RED.sidebar.config.init();

View File

@ -27,5 +27,7 @@ RED.state = {
PANNING: 10,
SELECTING_NODE: 11,
GROUP_DRAGGING: 12,
GROUP_RESIZE: 13
GROUP_RESIZE: 13,
DETACHED_DRAGGING: 14,
SLICING: 15
}

View File

@ -64,15 +64,17 @@ RED.sidebar.help = (function() {
style: "compact",
delay: 100,
change: function() {
var val = $(this).val().toLowerCase();
if (val) {
const searchFor = $(this).val().toLowerCase();
if (searchFor) {
showTOC();
var c = treeList.treeList('filter',function(item) {
treeList.treeList('filter',function(item) {
if (item.depth === 0) {
return true;
}
return (item.nodeType && item.nodeType.indexOf(val) > -1) ||
(item.subflowLabel && item.subflowLabel.indexOf(val) > -1)
let found = item.nodeType && item.nodeType.toLowerCase().indexOf(searchFor) > -1;
found = found || item.subflowLabel && item.subflowLabel.toLowerCase().indexOf(searchFor) > -1;
found = found || item.palleteLabel && item.palleteLabel.toLowerCase().indexOf(searchFor) > -1;
return found;
},true)
} else {
treeList.treeList('filter',null);
@ -224,17 +226,21 @@ RED.sidebar.help = (function() {
moduleNames.forEach(function(moduleName) {
var module = modules[moduleName];
var nodeTypes = [];
var setNames = Object.keys(module.sets);
const module = modules[moduleName];
const nodeTypes = [];
const moduleSets = module.sets;
const setNames = Object.keys(moduleSets);
setNames.forEach(function(setName) {
module.sets[setName].types.forEach(function(nodeType) {
const moduleSet = moduleSets[setName];
moduleSet.types.forEach(function(nodeType) {
if ($("script[data-help-name='"+nodeType+"']").length) {
const n = {_def:RED.nodes.getType(nodeType),type:nodeType}
n.name = getNodePaletteLabel(n);
nodeTypes.push({
id: "node-type:"+nodeType,
nodeType: nodeType,
element:getNodeLabel({_def:RED.nodes.getType(nodeType),type:nodeType})
palleteLabel: n.name,
element: getNodeLabel(n)
})
}
})
@ -254,18 +260,21 @@ RED.sidebar.help = (function() {
treeList.treeList("data",helpData);
}
function getNodeLabel(n) {
var div = $('<div>',{class:"red-ui-node-list-item"});
var icon = RED.utils.createNodeIcon(n).appendTo(div);
var label = n.name;
function getNodePaletteLabel(n) {
let label = n.name;
if (!label && n._def && n._def.paletteLabel) {
try {
label = (typeof n._def.paletteLabel === "function" ? n._def.paletteLabel.call(n._def) : n._def.paletteLabel)||"";
} catch (err) {
}
}
label = label || n.type;
$('<div>',{class:"red-ui-node-label"}).text(n.name||n.type).appendTo(icon);
return label || n.type;
}
function getNodeLabel(n) {
const div = $('<div>',{class:"red-ui-node-list-item"});
const icon = RED.utils.createNodeIcon(n).appendTo(div);
$('<div>',{class:"red-ui-node-label"}).text(getNodePaletteLabel(n)).appendTo(icon);
return div;
}

View File

@ -256,6 +256,10 @@ RED.tourGuide = (function() {
}
$('<div>').css("text-align","left").html(getLocaleText(step.description)).appendTo(stepDescription);
if (step.image) {
$(`<img src="red/tours/${step.image}" />`).appendTo(stepDescription)
}
var stepToolbar = $('<div>',{class:"red-ui-tourGuide-toolbar"}).appendTo(stepContent);
// var breadcrumbs = $('<div>',{class:"red-ui-tourGuide-breadcrumbs"}).appendTo(stepToolbar);

View File

@ -121,6 +121,13 @@ RED.userSettings = (function() {
// {setting:"theme", label:"Theme",options:function(done){ done([{val:'',text:'default'}].concat(RED.settings.theme("themes"))) }},
// ]
// },
{
title: "menu.label.view.view",
options: [
{setting:"view-store-zoom",label:"menu.label.view.storeZoom", default: false, toggle:true, onchange: function(val) { if (!val) { RED.settings.removeLocal("zoom-level")}}},
{setting:"view-store-position",label:"menu.label.view.storePosition", default: false, toggle:true, onchange: function(val) { if (!val) { RED.settings.removeLocal("scroll-positions")}}},
]
},
{
title: "menu.label.view.grid",
options: [

View File

@ -725,6 +725,90 @@ RED.view.tools = (function() {
}
}
function wireSeriesOfNodes() {
var selection = RED.view.selection();
if (selection.nodes) {
if (selection.nodes.length > 1) {
var i = 0;
var newLinks = [];
while (i < selection.nodes.length - 1) {
var nodeA = selection.nodes[i];
var nodeB = selection.nodes[i+1];
if (nodeA.outputs > 0 && nodeB.inputs > 0) {
var existingLinks = RED.nodes.filterLinks({
source: nodeA,
target: nodeB,
sourcePort: 0
})
if (existingLinks.length === 0) {
var newLink = {
source: nodeA,
target: nodeB,
sourcePort: 0
}
RED.nodes.addLink(newLink);
newLinks.push(newLink);
}
}
i++;
}
if (newLinks.length > 0) {
RED.history.push({
t: 'add',
links: newLinks,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
function wireNodeToMultiple() {
var selection = RED.view.selection();
if (selection.nodes) {
if (selection.nodes.length > 1) {
var sourceNode = selection.nodes[0];
if (sourceNode.outputs === 0) {
return;
}
var i = 1;
var newLinks = [];
while (i < selection.nodes.length) {
var targetNode = selection.nodes[i];
if (targetNode.inputs > 0) {
var existingLinks = RED.nodes.filterLinks({
source: sourceNode,
target: targetNode,
sourcePort: Math.min(sourceNode.outputs-1,i-1)
})
if (existingLinks.length === 0) {
var newLink = {
source: sourceNode,
target: targetNode,
sourcePort: Math.min(sourceNode.outputs-1,i-1)
}
RED.nodes.addLink(newLink);
newLinks.push(newLink);
}
}
i++;
}
if (newLinks.length > 0) {
RED.history.push({
t: 'add',
links: newLinks,
dirty: RED.nodes.dirty()
})
RED.nodes.dirty(true);
RED.view.redraw(true);
}
}
}
}
return {
init: function() {
RED.actions.add("core:show-selected-node-labels", function() { setSelectedNodeLabelState(true); })
@ -783,7 +867,8 @@ RED.view.tools = (function() {
RED.actions.add("core:distribute-selection-horizontally", function() { distributeSelection('h') })
RED.actions.add("core:distribute-selection-vertically", function() { distributeSelection('v') })
RED.actions.add("core:wire-series-of-nodes", function() { wireSeriesOfNodes() })
RED.actions.add("core:wire-node-to-multiple", function() { wireNodeToMultiple() })
// RED.actions.add("core:add-node", function() { addNode() })
},

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,13 @@
stroke-dasharray: 10 5;
}
.nr-ui-view-slice {
stroke-width: 1px;
stroke: $view-lasso-stroke;
fill: none;
stroke-dasharray: 10 5;
}
.node_label_italic, // deprecated: use red-ui-flow-node-label-italic
.red-ui-flow-node-label-italic {
font-style: italic;

View File

@ -204,6 +204,28 @@
font-style: italic;
color: $form-placeholder-color;
}
.red-ui-search-history {
button {
display: none;
position: absolute;
top: 8px;
right: 7px;
}
&:hover button {
display: inline;
}
}
.red-ui-search-historyHeader {
button {
position: absolute;
top: 10px;
right: 7px;
}
}
.red-ui-search-history-result {
}
.red-ui-search-result-action {
color: $primary-text-color;

View File

@ -78,6 +78,12 @@
}
.red-ui-tourGuide-popover-description {
padding: 10px 20px 5px;
img {
max-height: 150px;
border: 1px solid var(--red-ui-tourGuide-border);
border-radius: 2px;
}
}
.red-ui-tourGuide-popover-full {
.red-ui-tourGuide-popover-description {

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,12 +1,12 @@
export default {
version: "2.1.0",
version: "2.2.0",
steps: [
{
titleIcon: "fa fa-map-o",
title: {
"en-US": "Welcome to Node-RED 2.1!",
"ja": "Node-RED 2.1へようこそ!"
},
"en-US": "Welcome to Node-RED 2.2!",
"ja": "Node-RED 2.2へようこそ!"
},
description: {
"en-US": "Let's take a moment to discover the new features in this release.",
"ja": "本リリースの新機能を見つけてみましょう。"
@ -14,215 +14,142 @@ export default {
},
{
title: {
"en-US": "A new Tour Guide",
"ja": "新しいツアーガイド"
"en-US": "Search history",
"ja": "検索履歴"
},
description: {
"en-US": "<p>First, as you've already found, we now have this tour of new features. We'll only show the tour the first time you open the editor for each new version of Node-RED.</p>" +
"<p>You can choose not to see this tour in the future by disabling it under the View tab of User Settings.</p>",
"ja": "<p>最初に、既に見つけている様に、新機能の本ツアーがあります。本ツアーは、新バージョンのNode-REDフローエディタを初めて開いた時のみ表示されます。</p>" +
"<p>ユーザ設定の表示タブの中で、この機能を無効化することで、本ツアーを表示しないようにすることもできます。</p>"
}
},
{
title: {
"en-US": "New Edit menu",
"ja": "新しい編集メニュー"
"en-US": "<p>The Search dialog now keeps a history of your searches, making it easier to go back to a previous search.</p>",
"ja": "<p>検索ダイアログが検索履歴を保持するようになりました。これによって、過去の検索に戻りやすくなりました。</p>"
},
prepare() {
$("#red-ui-header-button-sidemenu").trigger("click");
$("#menu-item-edit-menu").parent().addClass("open");
},
complete() {
$("#menu-item-edit-menu").parent().removeClass("open");
},
element: "#menu-item-edit-menu-submenu",
interactive: false,
direction: "left",
description: {
"en-US": "<p>The main menu has been updated with a new 'Edit' section. This includes all of the familar options, like cut/paste and undo/redo.</p>" +
"<p>The menu now displays keyboard shortcuts for the options.</p>",
"ja": "<p>メインメニューに「編集」セクションが追加されました。本セクションには、切り取り/貼り付けや、変更操作を戻す/やり直しの様な使い慣れたオプションが含まれています。</p>" +
"<p>本メニューには、オプションのためのキーボードショートカットも表示されるようになりました。</p>"
}
},
{
title: {
"en-US": "Arranging nodes",
"ja": "ノードの配置"
},
prepare() {
$("#red-ui-header-button-sidemenu").trigger("click");
$("#menu-item-arrange-menu").parent().addClass("open");
},
complete() {
$("#menu-item-arrange-menu").parent().removeClass("open");
},
element: "#menu-item-arrange-menu-submenu",
interactive: false,
direction: "left",
description: {
"en-US": "<p>The new 'Arrange' section of the menu provides new options to help arrange your nodes. You can align them to a common edge, spread them out evenly or change their order.</p>",
"ja": "<p>メニューの新しい「配置」セクションには、ノードの配置を助ける新しいオプションが提供されています。ノードの端を揃えたり、均等に配置したり、表示順序を変更したりできます。</p>"
}
},
{
title: {
"en-US": "Hiding tabs",
"ja": "タブの非表示"
},
element: "#red-ui-workspace-tabs > li.active",
description: {
"en-US": '<p>Tabs can now be hidden by clicking their <i class="fa fa-eye-slash"></i> icon.</p><p>The Info Sidebar will still list all of your tabs, and tell you which ones are currently hidden.',
"ja": '<p><i class="fa fa-eye-slash"></i> アイコンをクリックすることで、タブを非表示にできます。</p><p>情報サイドバーには、全てのタブが一覧表示されており、現在非表示になっているタブを確認できます。'
},
interactive: false,
prepare() {
$("#red-ui-workspace-tabs > li.active .red-ui-tab-close").css("display","block");
},
complete() {
$("#red-ui-workspace-tabs > li.active .red-ui-tab-close").css("display","");
}
},
{
title: {
"en-US": "Tab menu",
"ja": "タブメニュー"
},
element: "#red-ui-workspace-tabs-menu",
description: {
"en-US": "<p>The new tab menu also provides lots of new options for your tabs.</p>",
"ja": "<p>新しいタブメニューには、タブに関する沢山の新しいオプションが提供されています。</p>"
},
interactive: false,
direction: "left",
prepare() {
$("#red-ui-workspace > .red-ui-tabs > .red-ui-tabs-menu a").trigger("click");
},
complete() {
$(document).trigger("click");
}
},
{
title: {
"en-US": "Flow and Group level environment variables",
"ja": "フローとグループの環境変数"
},
element: "#red-ui-workspace-tabs > li.active",
interactive: false,
description: {
"en-US": "<p>Flows and Groups can now have their own environment variables that can be referenced by nodes inside them.</p>",
"ja": "<p>フローとグループには、内部のノードから参照できる環境変数を設定できるようになりました。</p>"
}
},
{
element: "#red-ui-search .red-ui-searchBox-form",
prepare(done) {
RED.editor.editFlow(RED.nodes.workspace(RED.workspaces.active()),"editor-tab-envProperties");
setTimeout(done,700);
RED.search.show();
setTimeout(done,400);
},
complete() {
RED.search.hide();
},
element: "#red-ui-tab-editor-tab-envProperties-link-button",
description: {
"en-US": "<p>Their edit dialogs have a new Environment Variables section.</p>",
"ja": "<p>編集ダイアログに環境変数セクションが追加されました。</p>"
}
},
{
element: ".node-input-env-container-row",
direction: "left",
title: {
"en-US": "Remembering Zoom & Position",
"ja": "拡大/縮小のレベルや位置を記憶"
},
description: {
"en-US": '<p>The environment variables are listed in this table and new ones can be added by clicking the <i class="fa fa-plus"></i> button.</p>',
"ja": '<p>この表に環境変数が一覧表示されており、<i class="fa fa-plus"></i>ボタンをクリックすることで新しい変数を追加できます。</p>'
"en-US": "<p>The editor has new options to restore the zoom level and scroll position when reloading the editor.</p>",
"ja": "<p>エディタを再読み込みした時に、拡大/縮小のレベルやスクロール位置を復元するための新しいオプションを利用できます。</p>"
},
element: function() { return $("#user-settings-view-store-position").parent()},
prepare(done) {
RED.actions.invoke("core:show-user-settings")
setTimeout(done,400);
},
complete(done) {
$("#node-dialog-cancel").trigger("click");
setTimeout(done,500);
$("#node-dialog-ok").trigger("click");
setTimeout(done,400);
},
},
{
title: {
"en-US": "New wiring actions",
"ja": "新しいワイヤー操作"
},
// image: "images/",
description: {
"en-US": `<p>A pair of new actions have been added to help with wiring nodes together:</p>
<ul>
<li><b><code>Wire Series Of Nodes</code></b> - adds a wire (if necessary) between each pair of nodes in the order they were selected.</li>
<li><b><code>Wire Node To Multiple</code></b> - wires the first node selected to all of the other selected nodes.</li>
</ul>
<p>Actions can be accessed from the Action List in the main menu.</p>`,
"ja": `<p>ード接続を支援する2つの新しい操作が追加されました:</p>
<ul>
<li><b><code>Wire Series Of Nodes</code></b> - ()</li>
<li><b><code>Wire Node To Multiple</code></b> - </li>
</ul>
<p>メインメニュー内の動作一覧からこれらの操作を利用できます</p>`
},
},
{
title: {
"en-US": "Deleting nodes and reconnecting wires",
"ja": "ノードの削除とワイヤーの再接続"
},
image: "images/delete-repair.gif",
description: {
"en-US": `<p>It is now possible to delete a selection of nodes and automatically repair the wiring behind them.</p>
<p>This is really useful if you want to remove a node from the middle of the flow.</p>
<p>Hold the Ctrl (or Cmd) key when you press Delete and the nodes will be gone and the wires repaired.</p>
`,
"ja": `<p>選択したノードを削除した後、その背後にあるワイヤーを自動的に修復できるようになりました。</p>
<p>これはフローの中からノードを削除する時にとても便利に使えます</p>
<p>Ctrl (またはCmd)キーを押しながらDeleteキーを押すとノードがなくなりワイヤーが修復されます</p>
`
}
},
{
title: {
"en-US": "Link Call node added",
"ja": "Link Callードを追加"
"en-US": "Detaching nodes from a flow",
"ja": "フローからノードの切り離し"
},
prepare(done) {
this.paletteWasClosed = $("#red-ui-main-container").hasClass("red-ui-palette-closed");
RED.actions.invoke("core:toggle-palette",true)
$('[data-palette-type="link call"]')[0].scrollIntoView({block:"center"})
setTimeout(done,100);
},
element: '[data-palette-type="link call"]',
direction: "right",
image: "images/detach-repair.gif",
description: {
"en-US": "<p>The <code>Link Call</code> node lets you call another flow that begins with a <code>Link In</code> node and get the result back when the message reaches a <code>Link Out</code> node.</p>",
"ja": "<p><code>Link Call</code>ノードを用いることで、<code>Link In</code>ノードから始まるフローを呼び出し、<code>Link Out</code>ノードに到達した時に、結果を取得できます。</p>"
"en-US": `<p>If you want to remove a node from a flow without deleting it,
you can use the <b><code>Detach Selected Nodes</code></b> action.</p>
<p>The nodes will be removed from their flow, the wiring repaired behind them, and then attached to the mouse
so you can drop them wherever you want in the workspace.</p>
<p>There isn't a default keyboard shortcut assigned for this new action, but
you can add your own via the Keyboard pane of the main Settings dialog.</p>`,
"ja": `<p>ノードを削除することなく、フローからノードを除きたい場合は、<b><code>Detach Selected Nodes</code></b>操作を利用できます。</p>
<p>フローからノードが除かれた後背後のワイヤーが修復されノードはマウスポインタにつながりますそのためワークスペースの好きな所にノードを配置できます</p>
<p>この新しい操作に対してデフォルトのキーボードショートカットは登録されていませんがメイン設定ダイアログのキーボード設定から追加できます</p>`
}
},
{
title: {
"en-US": "MQTT nodes support dynamic connections",
"ja": "MQTTードが動的接続をサポート"
"en-US": "More wiring tricks",
"ja": "その他のワイヤー操作"
},
prepare(done) {
$('[data-palette-type="mqtt out"]')[0].scrollIntoView({block:"center"})
setTimeout(done,100);
},
element: '[data-palette-type="mqtt out"]',
direction: "right",
image: "images/slice.gif",
description: {
"en-US": '<p>The <code>MQTT</code> nodes now support creating their connections and subscriptions dynamically.</p>',
"ja": '<p><code>MQTT</code>ノードは、動的な接続や購読ができるようになりました。</p>'
},
},
{
title: {
"en-US": "File nodes renamed",
"ja": "ファイルノードの名前変更"
},
prepare(done) {
$('[data-palette-type="file"]')[0].scrollIntoView({block:"center"});
setTimeout(done,100);
},
complete() {
if (this.paletteWasClosed) {
RED.actions.invoke("core:toggle-palette",false)
}
},
element: '[data-palette-type="file"]',
direction: "right",
description: {
"en-US": "<p>The file nodes have been renamed to make it clearer which node does what.</p>",
"ja": "<p>fileードの名前が変更され、どのードが何を行うかが明確になりました。</p>"
"en-US": `<p>A couple more wiring tricks to share.</p>
<p>You can now select multiple wires by holding the Ctrl (or Cmd) key
when clicking on a wire. This makes it easier to delete multiple wires in one go.</p>
<p>If you hold the Ctrl (or Cmd) key, then click and drag with the right-hand mouse button,
you can slice through wires to remove them.</p>`,
"ja": `<p>その他のいくつかのワイヤー操作</p>
<p>Ctrl (またはCmd)キーを押しながらワイヤーをクリックすることで複数のワイヤーを選択できるようになりましたこれによって複数のワイヤーを一度に削除することが簡単になりました</p>
<p>Ctrl (またはCmd)キーを押しながらマウスの右ボタンを用いてドラッグするとワイヤーを切って削除できます</p>`
}
},
{
title: {
"en-US": "Deep copy option on Change node",
"ja": "Changeードのディープコピーオプション"
},
prepare(done) {
var def = RED.nodes.getType('change');
RED.editor.edit({id:"test",type:"change",rules:[{t:"set",p:"payload",pt:"msg", tot:"msg",to:"anotherProperty"}],_def:def, _:def._});
setTimeout(done,700);
},
complete(done) {
$("#node-dialog-cancel").trigger("click");
setTimeout(done,500);
},
element: function() {
return $(".node-input-rule-property-deepCopy").next();
"en-US": "Subflow Output Labels",
"ja": "サブフローの出力ラベル"
},
image: "images/subflow-labels.png",
description: {
"en-US": "<p>The Set rule has a new option to create a deep copy of the value. This ensures a complete copy is made, rather than using a reference.</p>",
"ja": "<p>値を代入に、値のディープコピーを作成するオプションが追加されました。これによって参照ではなく、完全なコピーが作成されます。</p>"
}
"en-US": "<p>If a subflow has labels set for its outputs, they now get shown on the ports within the subflow template view.</p>",
"ja": "<p>サブフローの出力にラベルが設定されている場合、サブフローテンプレート画面内のポートにラベルが表示されるようになりました。</p>"
},
},
{
title: {
"en-US": "And that's not all...",
"ja": "これが全てではありません..."
"en-US": "Node Updates",
"ja": "ノードの更新"
},
// image: "images/",
description: {
"en-US": "<p>There are many more smaller changes, including:</p><ul><li>Auto-complete suggestions in the <code>msg</code> TypedInput.</li><li>Support for <code>msg.resetTimeout</code> in the <code>Join</code> node.</li><li>Pushing messages to the front of the queue in the <code>Delay</code> node's rate limiting mode.</li><li>An optional second output on the <code>Delay</code> node for rate limited messages.</li></ul>",
"ja": "<p>以下の様な小さな変更が沢山あります:</p><ul><li><code>msg</code> TypedInputの自動補完提案</li><li><code>Join</code>ノードで<code>msg.resetTimeout</code>のサポート</li><li><code>Delay</code>ノードの流量制御モードにおいて先頭メッセージをキューに追加</li><li><code>Delay</code>ードで流量制限されたメッセージ向けの任意の2つ目の出力</li></ul>"
"en-US": `<ul>
<li>The JSON node will now handle parsing Buffer payloads</li>
<li>The TCP Client nodes support TLS connections</li>
<li>The WebSocket node allows you to specify a sub-protocol when connecting</li>
</ul>`,
"ja": `<ul>
<li>JSONードがバッファ形式のペイロードを解析できるようになりました</li>
<li>TCPクライアントードがTLS接続をサポートしました</li>
<li>WebSocketードで接続時にサブプロトコルを指定できるようになりました</li>
</ul>`
}
}
]

View File

@ -31,12 +31,12 @@ interface NodeStatus {
}
declare class node {
/**
* Send 1 or more messages asynchronously
* @param {object | object[]} msg The msg object
* @param {Boolean} [clone=true] Flag to indicate the `msg` should be cloned. Default = `true`
* @see node-red documentation [writing-functions: sending messages asynchronously](https://nodered.org/docs/user-guide/writing-functions#sending-messages-asynchronously)
*/
/**
* Send 1 or more messages asynchronously
* @param {object | object[]} msg The msg object
* @param {Boolean} [clone=true] Flag to indicate the `msg` should be cloned. Default = `true`
* @see node-red documentation [writing-functions: sending messages asynchronously](https://nodered.org/docs/user-guide/writing-functions#sending-messages-asynchronously)
*/
static send(msg:object|object[], clone?:Boolean): void;
/** Inform runtime this instance has completed its operation */
static done();
@ -57,11 +57,13 @@ declare class node {
*/
static status(status:string|boolean|number);
/** the id of this node */
public readonly id:string;
public static readonly id:string;
/** the name of this node */
public readonly name:string;
public static readonly name:string;
/** the path identifier for this node */
public static readonly path:string;
/** the number of outputs of this node */
public readonly outputCount:number;
public static readonly outputCount:number;
}
declare class context {
/**
@ -93,27 +95,27 @@ declare class context {
/**
* Set one or multiple values in context (synchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
*/
static set(name: string | string[], value?: any | any[]);
/**
* Set one or multiple values in context (asynchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param callback - (optional) Callback function (`(err) => {}`)
*/
static set(name: string | string[], value?: any | any[], callback?: Function);
/**
* Set one or multiple values in context (synchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param store - (optional) Name of context store
*/
static set(name: string | string[], value?: any | any[], store?: string);
/**
* Set one or multiple values in context (asynchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param store - (optional) Name of context store
* @param callback - (optional) Callback function (`(err) => {}`)
*/
@ -158,27 +160,27 @@ declare class flow {
/**
* Set one or multiple values in context (synchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
*/
static set(name: string | string[], value?: any | any[]);
/**
* Set one or multiple values in context (asynchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param callback - (optional) Callback function (`(err) => {}`)
*/
static set(name: string | string[], value?: any | any[], callback?: Function);
/**
* Set one or multiple values in context (synchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param store - (optional) Name of context store
*/
static set(name: string | string[], value?: any | any[], store?: string);
/**
* Set one or multiple values in context (asynchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param store - (optional) Name of context store
* @param callback - (optional) Callback function (`(err) => {}`)
*/
@ -225,32 +227,32 @@ declare class global {
/**
* Set one or multiple values in context (synchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
*/
static set(name: string | string[], value?: any | any[]);
/**
* Set one or multiple values in context (asynchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param callback - (optional) Callback function (`(err) => {}`)
*/
static set(name: string | string[], value?: any | any[], callback?: Function);
/**
* Set one or multiple values in context (synchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param store - (optional) Name of context store
*/
static set(name: string | string[], value?: any | any[], store?: string);
/**
* Set one or multiple values in context (asynchronous).
* @param name - Name (or array of names) to set in context
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param value - The value (or array of values) to store in context. If the value(s) are null/undefined, the context item(s) will be removed.
* @param store - (optional) Name of context store
* @param callback - (optional) Callback function (`(err) => {}`)
*/
static set(name: string | string[], value?: any | any[], store?: string, callback?: Function);
/** Get an array of the keys in the context store */
static keys(): Array<string>;
/** Get an array of the keys in the context store */

View File

@ -1,4 +1,4 @@
Copyright JS Foundation and other contributors, http://js.foundation
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004

View File

@ -75,16 +75,12 @@ module.exports = function(RED) {
node.repeaterSetup = function () {
if (this.repeat && !isNaN(this.repeat) && this.repeat > 0) {
this.repeat = this.repeat * 1000;
if (RED.settings.verbose) {
this.log(RED._("inject.repeat", this));
}
this.debug(RED._("inject.repeat", this));
this.interval_id = setInterval(function() {
node.emit("input", {});
}, this.repeat);
} else if (this.crontab) {
if (RED.settings.verbose) {
this.log(RED._("inject.crontab", this));
}
this.debug(RED._("inject.crontab", this));
this.cronjob = scheduleTask(this.crontab,() => { node.emit("input", {})});
}
};
@ -148,10 +144,8 @@ module.exports = function(RED) {
}
if (this.interval_id != null) {
clearInterval(this.interval_id);
if (RED.settings.verbose) { this.log(RED._("inject.stopped")); }
} else if (this.cronjob != null) {
this.cronjob.stop();
if (RED.settings.verbose) { this.log(RED._("inject.stopped")); }
delete this.cronjob;
}
};

View File

@ -112,6 +112,7 @@ module.exports = function(RED) {
"var node = {"+
"id:__node__.id,"+
"name:__node__.name,"+
"path:__node__.path,"+
"outputCount:__node__.outputCount,"+
"log:__node__.log,"+
"error:__node__.error,"+
@ -163,6 +164,7 @@ module.exports = function(RED) {
__node__: {
id: node.id,
name: node.name,
path: node._path,
outputCount: node.outputs,
log: function() {
node.log.apply(node, arguments);
@ -344,6 +346,7 @@ module.exports = function(RED) {
var node = {
id:__node__.id,
name:__node__.name,
path:__node__.path,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,
@ -366,6 +369,7 @@ module.exports = function(RED) {
var node = {
id:__node__.id,
name:__node__.name,
path:__node__.path,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,

View File

@ -86,7 +86,7 @@ module.exports = function(RED) {
});
var cmd = arg.shift();
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(cmd+" ["+arg+"]"); }
node.debug(cmd+" ["+arg+"]");
child = spawn(cmd,arg,node.spawnOpt);
node.status({fill:"blue",shape:"dot",text:"pid:"+child.pid});
var unknownCommand = (child.pid === undefined);
@ -136,7 +136,7 @@ module.exports = function(RED) {
}
else {
/* istanbul ignore else */
if (RED.settings.verbose) { node.log(arg); }
node.debug(arg);
child = exec(arg, node.execOpt, function (error, stdout, stderr) {
var msg2, msg3;
delete msg.payload;
@ -155,7 +155,7 @@ module.exports = function(RED) {
if (error.signal) { msg3.payload.signal = error.signal; }
if (error.code === null) { node.status({fill:"red",shape:"dot",text:"killed"}); }
else { node.status({fill:"red",shape:"dot",text:"error:"+error.code}); }
if (RED.settings.verbose) { node.log('error:' + error); }
node.debug('error:' + error);
}
else if (node.oldrc === "false") {
msg3 = RED.util.cloneMessage(msg);

View File

@ -288,7 +288,7 @@ module.exports = function(RED) {
//TODO: delete msg.responseTopic - to prevent it being resent?
}
}
topicOK = topicOK && !/[\+#]/.test(msg.topic);
topicOK = topicOK && !/[\+#\b\f\n\r\t\v\0]/.test(msg.topic);
if (topicOK) {
node.brokerConn.publish(msg, done); // send the message
@ -730,12 +730,18 @@ module.exports = function(RED) {
node.client.on("reconnect", function() {
setStatusConnecting(node, true);
});
//TODO: what to do with this event? Anything? Necessary?
//Broker Disconnect - V5 event
node.client.on("disconnect", function(packet) {
//Emitted after receiving disconnect packet from broker. MQTT 5.0 feature.
var rc = packet && packet.properties && packet.properties.reasonString;
var rc = packet && packet.properties && packet.reasonCode;
//TODO: If keeping this event, do we use these? log these?
const rc = (packet && packet.properties && packet.reasonCode) || packet.reasonCode;
const rs = packet && packet.properties && packet.properties.reasonString || "";
const details = {
broker: (node.clientid?node.clientid+"@":"")+node.brokerurl,
reasonCode: rc,
reasonString: rs
}
node.log(RED._("mqtt.state.broker-disconnected", details));
setStatusDisconnected(node, true);
});
// Register disconnect handlers
node.client.on('close', function () {

View File

@ -264,7 +264,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
if (opts.headers.hasOwnProperty('cookie')) {
var cookies = cookie.parse(opts.headers.cookie, {decode:String});
for (var name in cookies) {
opts.cookieJar.setCookie(cookie.serialize(name, cookies[name], {encode:String}), url, {ignoreError: true});
opts.cookieJar.setCookieSync(cookie.serialize(name, cookies[name], {encode:String}), url, {ignoreError: true});
}
delete opts.headers.cookie;
}
@ -277,13 +277,13 @@ in your Node-RED user directory (${RED.settings.userDir}).
} else if (typeof msg.cookies[name] === 'object') {
if(msg.cookies[name].encode === false){
// If the encode option is false, the value is not encoded.
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name].value, {encode: String}), url, {ignoreError: true});
opts.cookieJar.setCookieSync(cookie.serialize(name, msg.cookies[name].value, {encode: String}), url, {ignoreError: true});
} else {
// The value is encoded by encodeURIComponent().
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name].value), url, {ignoreError: true});
opts.cookieJar.setCookieSync(cookie.serialize(name, msg.cookies[name].value), url, {ignoreError: true});
}
} else {
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name]), url, {ignoreError: true});
opts.cookieJar.setCookieSync(cookie.serialize(name, msg.cookies[name]), url, {ignoreError: true});
}
}
}

View File

@ -177,7 +177,8 @@
path: {value:"",required:true,validate:RED.validators.regex(/^((?!\/debug\/ws).)*$/)},
tls: {type:"tls-config",required: false},
wholemsg: {value:"false"},
hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) }
hb: {value: "", validate: RED.validators.number(/*blank allowed*/true) },
subprotocol: {value:"",required: false}
},
inputs:0,
outputs:0,
@ -265,7 +266,10 @@
<label for="node-config-input-tls" data-i18n="httpin.tls-config"></label>
<input type="text" id="node-config-input-tls">
</div>
<div class="form-row">
<label for="node-config-input-subprotocol"><i class="fa fa-tag"></i> <span data-i18n="websocket.label.subprotocol"></span></label>
<input type="text" id="node-config-input-subprotocol">
</div>
<div class="form-row">
<label for="node-config-input-wholemsg" data-i18n="websocket.sendrec"></label>
<select type="text" id="node-config-input-wholemsg" style="width: 70%;">

View File

@ -46,6 +46,12 @@ module.exports = function(RED) {
// Store local copies of the node configuration (as defined in the .html)
node.path = n.path;
if (typeof n.subprotocol === "string") {
// Split the string on comma and trim each result
node.subprotocol = n.subprotocol.split(",").map(v => v.trim())
} else {
node.subprotocol = [];
}
node.wholemsg = (n.wholemsg === "true");
node._inputNodes = []; // collection of nodes that want to receive events
@ -92,7 +98,7 @@ module.exports = function(RED) {
tlsNode.addTLSOptions(options);
}
}
var socket = new ws(node.path,options);
var socket = new ws(node.path,node.subprotocol,options);
socket.setMaxListeners(0);
node.server = socket; // keep for closing
handleConnection(socket);

View File

@ -23,9 +23,17 @@
</select>
<span data-i18n="tcpin.label.port"></span> <input type="text" id="node-input-port" style="width:65px">
</div>
<div class="form-row hidden" id="node-input-host-row" style="padding-left: 110px;">
<div class="form-row hidden" id="node-input-host-row" style="padding-left:110px;">
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" placeholder="localhost" style="width: 60%;">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width:auto; vertical-align:top;">
<label for="node-input-usetls" style="width:auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width:auto; margin-left:20px; margin-right:10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row">
<label><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.output"></span></label>
@ -42,7 +50,7 @@
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:110px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
</div>
<div class="form-row">
@ -58,17 +66,18 @@
<script type="text/javascript">
RED.nodes.registerType('tcp in',{
category: 'network',
color:"Silver",
color: "Silver",
defaults: {
name: {value:""},
server: {value:"server",required:true},
host: {value:"",validate:function(v) { return (this.server == "server")||v.length > 0;} },
port: {value:"",required:true,validate:RED.validators.number()},
server: {value:"server", required:true},
host: {value:"", validate:function(v) { return (this.server == "server")||v.length > 0;} },
port: {value:"", required:true, validate:RED.validators.number()},
datamode:{value:"stream"},
datatype:{value:"buffer"},
newline:{value:""},
topic: {value:""},
base64: {/*deprecated*/ value:false,required:true}
base64: {/*deprecated*/ value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
},
inputs:0,
outputs:1,
@ -77,7 +86,7 @@
return this.name || "tcp:"+(this.host?this.host+":":"")+this.port;
},
labelStyle: function() {
return this.name?"node_label_italic":"";
return this.name ? "node_label_italic" : "";
},
oneditprepare: function() {
var updateOptions = function() {
@ -103,6 +112,27 @@
$("#node-input-server").change(updateOptions);
$("#node-input-datatype").change(updateOptions);
$("#node-input-datamode").change(updateOptions);
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>
@ -123,6 +153,15 @@
<span data-i18n="tcpin.label.host"></span> <input type="text" id="node-input-host" style="width: 60%;">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-usetls" style="width: auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row hidden" id="node-input-end-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-end" style="display: inline-block; width: auto; vertical-align: top;">
@ -144,14 +183,15 @@
<script type="text/javascript">
RED.nodes.registerType('tcp out',{
category: 'network',
color:"Silver",
color: "Silver",
defaults: {
name: {value:""},
host: {value:"",validate:function(v) { return (this.beserver != "client")||v.length > 0;} },
port: {value:"",validate:function(v) { return (this.beserver == "reply")||RED.validators.number()(v); } },
beserver: {value:"client",required:true},
base64: {value:false,required:true},
end: {value:false,required:true},
name: {value:""}
beserver: {value:"client", required:true},
base64: {value:false, required:true},
end: {value:false, required:true},
tls: {type:"tls-config", value:'', required:false}
},
inputs:1,
outputs:0,
@ -170,18 +210,42 @@
$("#node-input-port-row").hide();
$("#node-input-host-row").hide();
$("#node-input-end-row").hide();
$("#node-input-tls-enable").hide();
} else if (sockettype == "client"){
$("#node-input-port-row").show();
$("#node-input-host-row").show();
$("#node-input-end-row").show();
$("#node-input-tls-enable").show();
} else {
$("#node-input-port-row").show();
$("#node-input-host-row").hide();
$("#node-input-end-row").show();
$("#node-input-tls-enable").show();
}
};
updateOptions();
$("#node-input-beserver").change(updateOptions);
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>
@ -194,15 +258,23 @@
<span data-i18n="tcpin.label.port"></span>
<input type="text" id="node-input-port" style="width:60px">
</div>
<div class="form-row" id="node-input-tls-enable">
<label> </label>
<input type="checkbox" id="node-input-usetls" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-usetls" style="width: auto" data-i18n="httpin.use-tls"></label>
<div id="node-row-tls" class="hide">
<label style="width: auto; margin-left: 20px; margin-right: 10px;" for="node-input-tls"><span data-i18n="httpin.tls-config"></span></label><input type="text" style="width: 300px" id="node-input-tls">
</div>
</div>
<div class="form-row">
<label for="node-input-out"><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.return"></span></label>
<label for="node-input-ret"><i class="fa fa-sign-out"></i> <span data-i18n="tcpin.label.return"></span></label>
<select type="text" id="node-input-ret" style="width:54%;">
<option value="buffer" data-i18n="tcpin.output.buffer"></option>
<option value="string" data-i18n="tcpin.output.string"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-out"> </label>
<label for="node-input-out"><i class="fa fa-sign-out fa-rotate-90"></i> <span data-i18n="tcpin.label.close"></span></label>
<select type="text" id="node-input-out" style="width:54%;">
<option value="time" data-i18n="tcpin.return.timeout"></option>
<option value="char" data-i18n="tcpin.return.character"></option>
@ -213,6 +285,9 @@
<input type="text" id="node-input-splitc" style="width:50px;">
<span id="node-units"></span>
</div>
<div id="node-row-newline" class="form-row hidden" style="padding-left:162px;">
<span data-i18n="tcpin.label.delimited"></span> <input type="text" id="node-input-newline" style="width:110px;" data-i18n="[placeholder]tcpin.label.optional">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
@ -222,14 +297,16 @@
<script type="text/javascript">
RED.nodes.registerType('tcp request',{
category: 'network',
color:"Silver",
color: "Silver",
defaults: {
name: {value:""},
server: {value:""},
port: {value:"",validate:RED.validators.regex(/^(\d*|)$/)},
out: {value:"time",required:true},
port: {value:"", validate:RED.validators.regex(/^(\d*|)$/)},
out: {value:"time", required:true},
ret: {value:"buffer"},
splitc: {value:"0",required:true},
name: {value:""}
splitc: {value:"0", required:true},
newline: {value:""},
tls: {type:"tls-config", value:'', required:false}
},
inputs:1,
outputs:1,
@ -246,6 +323,14 @@
$("#node-input-ret").val("buffer");
this.ret = "buffer";
}
$("#node-input-ret").on("change", function() {
if ($("#node-input-ret").val() === "string" && $("#node-input-out").val() === "sit") { $("#node-row-newline").show(); }
else { $("#node-row-newline").hide(); }
});
$("#node-input-out").on("change", function() {
if ($("#node-input-ret").val() === "string" && $("#node-input-out").val() === "sit") { $("#node-row-newline").show(); }
else { $("#node-row-newline").hide(); }
});
$("#node-input-out").on('focus', function () { previous = this.value; }).on("change", function() {
$("#node-input-splitc").show();
if (previous === null) { previous = $("#node-input-out").val(); }
@ -272,6 +357,27 @@
$("#node-input-splitc").hide();
}
});
function updateTLSOptions() {
if ($("#node-input-usetls").is(':checked')) {
$("#node-row-tls").show();
} else {
$("#node-row-tls").hide();
}
}
if (this.tls) {
$('#node-input-usetls').prop('checked', true);
} else {
$('#node-input-usetls').prop('checked', false);
}
updateTLSOptions();
$("#node-input-usetls").on("click",function() {
updateTLSOptions();
});
},
oneditsave: function() {
if (!$("#node-input-usetls").is(':checked')) {
$("#node-input-tls").val("_ADD_");
}
}
});
</script>

View File

@ -16,13 +16,46 @@
module.exports = function(RED) {
"use strict";
var reconnectTime = RED.settings.socketReconnectTime||10000;
var socketTimeout = RED.settings.socketTimeout||null;
let reconnectTime = RED.settings.socketReconnectTime || 10000;
let socketTimeout = RED.settings.socketTimeout || null;
const msgQueueSize = RED.settings.tcpMsgQueueSize || 1000;
const Denque = require('denque');
var net = require('net');
const net = require('net');
const tls = require('tls');
var connectionPool = {};
let connectionPool = {};
function normalizeConnectArgs(listArgs) {
const args = net._normalizeArgs(listArgs);
const options = args[0];
const cb = args[1];
// If args[0] was options, then normalize dealt with it.
// If args[0] is port, or args[0], args[1] is host, port, we need to
// find the options and merge them in, normalize's options has only
// the host/port/path args that it knows about, not the tls options.
// This means that options.host overrides a host arg.
if (listArgs[1] !== null && typeof listArgs[1] === 'object') {
ObjectAssign(options, listArgs[1]);
} else if (listArgs[2] !== null && typeof listArgs[2] === 'object') {
ObjectAssign(options, listArgs[2]);
}
return cb ? [options, cb] : [options];
}
function getAllowUnauthorized() {
const allowUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
if (allowUnauthorized) {
process.emitWarning(
'Setting the NODE_TLS_REJECT_UNAUTHORIZED ' +
'environment variable to \'0\' makes TLS connections ' +
'and HTTPS requests insecure by disabling ' +
'certificate verification.');
}
return allowUnauthorized;
}
/**
* Enqueue `item` in `queue`
@ -53,13 +86,14 @@ module.exports = function(RED) {
this.topic = n.topic;
this.stream = (!n.datamode||n.datamode=='stream'); /* stream,single*/
this.datatype = n.datatype||'buffer'; /* buffer,utf8,base64 */
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r");
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.base64 = n.base64;
this.server = (typeof n.server == 'boolean')?n.server:(n.server == "server");
this.closing = false;
this.connected = false;
var node = this;
var count = 0;
if (n.tls) { var tlsNode = RED.nodes.getNode(n.tls); }
if (!node.server) {
var buffer = null;
@ -70,13 +104,25 @@ module.exports = function(RED) {
node.log(RED._("tcpin.status.connecting",{host:node.host,port:node.port}));
node.status({fill:"grey",shape:"dot",text:"common.status.connecting"});
var id = RED.util.generateId();
client = net.connect(node.port, node.host, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
client.setKeepAlive(true,120000);
var connOpts = {host: node.host};
if (n.tls) {
var connOpts = tlsNode.addTLSOptions({host: node.host});
client = tls.connect(node.port, connOpts, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("status.connected", {host: node.host, port: node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
}
else {
client = net.connect(node.port, node.host, function() {
buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected",_session:{type:"tcp",id:id}});
});
}
client.setKeepAlive(true, 120000);
connectionPool[id] = client;
client.on('data', function (data) {
@ -89,7 +135,7 @@ module.exports = function(RED) {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i]};
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd()};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@ -150,7 +196,13 @@ module.exports = function(RED) {
});
}
else {
var server = net.createServer(function (socket) {
let srv = net;
let connOpts;
if (n.tls) {
srv = tls;
connOpts = tlsNode.addTLSOptions({});
}
var server = srv.createServer(connOpts, function (socket) {
socket.setKeepAlive(true,120000);
if (socketTimeout !== null) { socket.setTimeout(socketTimeout); }
var id = RED.util.generateId();
@ -177,7 +229,7 @@ module.exports = function(RED) {
buffer = buffer+data;
var parts = buffer.split(node.newline);
for (var i = 0; i<parts.length-1; i+=1) {
msg = {topic:node.topic, payload:parts[i], ip:socket.remoteAddress, port:socket.remotePort};
msg = {topic:node.topic, payload:parts[i] + node.newline.trimEnd(), ip:socket.remoteAddress, port:socket.remotePort};
msg._session = {type:"tcp",id:id};
node.send(msg);
}
@ -269,8 +321,9 @@ module.exports = function(RED) {
this.closing = false;
this.connected = false;
var node = this;
if (n.tls) { var tlsNode = RED.nodes.getNode(n.tls); }
if (!node.beserver||node.beserver=="client") {
if (!node.beserver || node.beserver == "client") {
var reconnectTimeout;
var client = null;
var end = false;
@ -278,11 +331,24 @@ module.exports = function(RED) {
var setupTcpClient = function() {
node.log(RED._("tcpin.status.connecting",{host:node.host,port:node.port}));
node.status({fill:"grey",shape:"dot",text:"common.status.connecting"});
client = net.connect(node.port, node.host, function() {
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
if (n.tls) {
// connOpts = tlsNode.addTLSOptions(connOpts);
// client = tls.connect(connOpts, function() {
var connOpts = tlsNode.addTLSOptions({host: node.host});
client = tls.connect(node.port, connOpts, function() {
// buffer = (node.datatype == 'buffer') ? Buffer.alloc(0) : "";
node.connected = true;
node.log(RED._("status.connected", {host: node.host, port: node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
else {
client = net.connect(node.port, node.host, function() {
node.connected = true;
node.log(RED._("tcpin.status.connected",{host:node.host,port:node.port}));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
client.setKeepAlive(true,120000);
client.on('error', function (err) {
node.log(RED._("tcpin.errors.error",{error:err.toString()}));
@ -368,7 +434,13 @@ module.exports = function(RED) {
else {
var connectedSockets = [];
node.status({text:RED._("tcpin.status.connections",{count:0})});
var server = net.createServer(function (socket) {
let srv = net;
let connOpts;
if (n.tls) {
srv = tls;
connOpts = tlsNode.addTLSOptions({});
}
var server = srv.createServer(connOpts, function (socket) {
socket.setKeepAlive(true,120000);
if (socketTimeout !== null) { socket.setTimeout(socketTimeout); }
node.log(RED._("tcpin.status.connection-from",{host:socket.remoteAddress, port:socket.remotePort}));
@ -445,7 +517,11 @@ module.exports = function(RED) {
this.port = Number(n.port);
this.out = n.out;
this.ret = n.ret || "buffer";
this.newline = (n.newline||"").replace("\\n","\n").replace("\\r","\r").replace("\\t","\t");
this.splitc = n.splitc;
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
}
if (this.out === "immed") { this.splitc = -1; this.out = "time"; }
if (this.out !== "char") { this.splitc = Number(this.splitc); }
@ -500,12 +576,48 @@ module.exports = function(RED) {
}
else { buf = Buffer.alloc(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully
clients[connection_id].client = net.Socket();
var connOpts = {host:host, port:port};
if (n.tls) {
connOpts = tlsNode.addTLSOptions(connOpts);
const allowUnauthorized = getAllowUnauthorized();
let options = {
rejectUnauthorized: !allowUnauthorized,
ciphers: tls.DEFAULT_CIPHERS,
checkServerIdentity: tls.checkServerIdentity,
minDHSize: 1024,
...connOpts
};
if (!options.keepAlive) { options.singleUse = true; }
const context = options.secureContext || tls.createSecureContext(options);
clients[connection_id].client = new tls.TLSSocket(options.socket, {
allowHalfOpen: options.allowHalfOpen,
pipe: !!options.path,
secureContext: context,
isServer: false,
requestCert: false, // true,
rejectUnauthorized: false, // options.rejectUnauthorized !== false,
session: options.session,
ALPNProtocols: options.ALPNProtocols,
requestOCSP: options.requestOCSP,
enableTrace: options.enableTrace,
pskCallback: options.pskCallback,
highWaterMark: options.highWaterMark,
onread: options.onread,
signal: options.signal,
});
}
else {
clients[connection_id].client = net.Socket();
}
if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);}
if (host && port) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(port, host, function() {
clients[connection_id].client.connect(connOpts, function() {
//node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
if (clients[connection_id] && clients[connection_id].client) {
@ -528,17 +640,32 @@ module.exports = function(RED) {
else {
node.warn(RED._("tcpin.errors.no-host"));
}
var chunk = "";
clients[connection_id].client.on('data', function(data) {
if (node.out === "sit") { // if we are staying connected just send the buffer
if (clients[connection_id]) {
const msg = clients[connection_id].lastMsg || {};
msg.payload = RED.util.cloneMessage(data);
if (node.ret === "string") {
try { msg.payload = msg.payload.toString(); }
catch(e) { node.error("Failed to create string", msg); }
try {
if (node.newline && node.newline !== "" ) {
chunk += msg.payload.toString();
let parts = chunk.split(node.newline);
for (var p=0; p<parts.length-1; p+=1) {
let m = RED.util.cloneMessage(msg);
m.payload = parts[p] + node.newline.trimEnd();
nodeSend(m);
}
chunk = parts[parts.length-1];
}
else {
msg.payload = msg.payload.toString();
nodeSend(msg);
}
}
catch(e) { node.error(RED._("tcpin.errors.bad-string"), msg); }
}
nodeSend(msg);
else { nodeSend(msg); }
}
}
// else if (node.splitc === 0) {
@ -675,7 +802,13 @@ module.exports = function(RED) {
//node.warn(RED._("tcpin.errors.connect-timeout"));
if (clients[connection_id].client) {
clients[connection_id].connecting = true;
clients[connection_id].client.connect(port, host, function() {
var connOpts = {host:host, port:port};
if (n.tls) {
connOpts = tlsNode.addTLSOptions(connOpts);
}
clients[connection_id].client.connect(connOpts, function() {
clients[connection_id].connected = true;
clients[connection_id].connecting = false;
node.status({fill:"green",shape:"dot",text:"common.status.connected"});

View File

@ -49,7 +49,11 @@ module.exports = function(RED) {
}
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (typeof value === "string") {
if (typeof value === "string" || Buffer.isBuffer(value)) {
// if (Buffer.isBuffer(value) && node.action !== "obj") {
// node.warn(RED._("json.errors.dropped")); done();
// }
// else
if (node.action === "" || node.action === "obj") {
try {
RED.util.setMessageProperty(msg,node.property,JSON.parse(value));

View File

@ -71,9 +71,7 @@ module.exports = function(RED) {
node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg);
}
else {
if (RED.settings.verbose) {
node.log(RED._("file.status.deletedfile",{file:filename}));
}
node.debug(RED._("file.status.deletedfile",{file:filename}));
nodeSend(msg);
}
done();

View File

@ -499,7 +499,8 @@
"label": {
"type": "Typ",
"path": "Pfad",
"url": "URL"
"url": "URL",
"subprotocol": "Subprotokoll"
},
"listenon": "Lauschen (listen on)",
"connectto": "Verbinden mit",

View File

@ -442,7 +442,8 @@
"state": {
"connected": "Connected to broker: __broker__",
"disconnected": "Disconnected from broker: __broker__",
"connect-failed": "Connection failed to broker: __broker__"
"connect-failed": "Connection failed to broker: __broker__",
"broker-disconnected": "Broker __broker__ disconnected client: __reasonCode__ __reasonString__"
},
"retain": "Retain",
"output": {
@ -530,7 +531,8 @@
"label": {
"type": "Type",
"path": "Path",
"url": "URL"
"url": "URL",
"subprotocol": "Subprotocol"
},
"listenon": "Listen on",
"connectto": "Connect to",
@ -579,7 +581,9 @@
"server": "Server",
"return": "Return",
"ms": "ms",
"chars": "chars"
"chars": "chars",
"close": "Close",
"optional": "(optional)"
},
"type": {
"listen": "Listen on",
@ -596,7 +600,7 @@
"return": {
"timeout": "after a fixed timeout of",
"character": "when character received is",
"number": "a fixed number of chars",
"number": "after a fixed number of characters",
"never": "never - keep connection open",
"immed": "immediately - don't wait for reply"
},
@ -616,11 +620,11 @@
"timeout": "timeout closed socket port __port__",
"cannot-listen": "unable to listen on port __port__, error: __error__",
"error": "error: __error__",
"socket-error": "socket error from __host__:__port__",
"no-host": "Host and/or port not set",
"connect-timeout": "connect timeout",
"connect-fail": "connect failed"
"connect-fail": "connect failed",
"bad-string": "failed to convert to string"
}
},
"udp": {

View File

@ -442,7 +442,8 @@
"state": {
"connected": "ブローカへ接続しました: __broker__",
"disconnected": "ブローカから切断されました: __broker__",
"connect-failed": "ブローカへの接続に失敗しました: __broker__"
"connect-failed": "ブローカへの接続に失敗しました: __broker__",
"broker-disconnected": "ブローカ __broker__ がクライアントを切断しました: __reasonCode__ __reasonString__"
},
"retain": "保持",
"output": {
@ -530,7 +531,8 @@
"label": {
"type": "種類",
"path": "パス",
"url": "URL"
"url": "URL",
"subprotocol": "サブプロトコル"
},
"listenon": "待ち受け",
"connectto": "接続",
@ -579,7 +581,9 @@
"server": "サーバ",
"return": "戻り値",
"ms": "ミリ秒",
"chars": "文字"
"chars": "文字",
"close": "終了",
"optional": "(任意)"
},
"type": {
"listen": "待ち受け",
@ -618,7 +622,8 @@
"socket-error": "__host__:__port__ にてソケットのエラーが生じました",
"no-host": "ホスト名またはポートが設定されていません",
"connect-timeout": "接続がタイムアウトしました",
"connect-fail": "接続に失敗しました"
"connect-fail": "接続に失敗しました",
"bad-string": "文字列への変換に失敗しました"
}
},
"udp": {

View File

@ -433,7 +433,8 @@
"label": {
"type": "종류",
"path": "패스",
"url": "URL"
"url": "URL",
"subprotocol": "서브 프로토콜"
},
"listenon": "대기",
"connectto": "접속",

View File

@ -461,7 +461,8 @@
"label": {
"type": "Тип",
"path": "Путь",
"url": "URL"
"url": "URL",
"subprotocol": "Подпротокол"
},
"listenon": "Слушать на ...",
"connectto": "Присоединиться к ...",

View File

@ -454,7 +454,8 @@
"label": {
"type": "类型",
"path": "路径",
"url": "URL"
"url": "URL",
"subprotocol": "子协议"
},
"listenon": "监听",
"connectto": "连接",

View File

@ -458,7 +458,8 @@
"label": {
"type": "類型",
"path": "路徑",
"url": "URL"
"url": "URL",
"subprotocol": "子协议"
},
"listenon": "監聽",
"connectto": "連接",

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "2.1.6",
"version": "2.2.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
@ -17,7 +17,7 @@
"dependencies": {
"acorn": "8.7.0",
"acorn-walk": "8.2.0",
"ajv": "8.8.2",
"ajv": "8.9.0",
"body-parser": "1.19.1",
"cheerio": "1.0.0-rc.10",
"content-type": "1.0.4",
@ -37,13 +37,13 @@
"js-yaml": "3.14.1",
"media-typer": "1.1.0",
"mqtt": "4.3.4",
"multer": "1.4.3",
"multer": "1.4.4",
"mustache": "4.2.0",
"on-headers": "1.0.2",
"raw-body": "2.4.2",
"tough-cookie": "4.0.0",
"uuid": "8.3.2",
"ws": "7.5.1",
"ws": "7.5.6",
"xml2js": "0.4.23",
"iconv-lite": "0.6.3"
}

View File

@ -1,4 +1,4 @@
Copyright JS Foundation and other contributors, http://js.foundation
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
"version": "2.1.6",
"version": "2.2.0",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,11 +16,11 @@
}
],
"dependencies": {
"@node-red/util": "2.1.6",
"@node-red/util": "2.2.0",
"clone": "2.1.2",
"fs-extra": "10.0.0",
"semver": "7.3.5",
"tar": "6.1.11",
"uglify-js": "3.14.5"
"uglify-js": "3.15.0"
}
}

View File

@ -1,4 +1,4 @@
Copyright JS Foundation and other contributors, http://js.foundation
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004

View File

@ -99,6 +99,7 @@ var api = module.exports = {
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {String} opts.id - the id of the project to activate
* @param {boolean} opts.clearContext - whether to clear context
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<Object>} - resolves when complete
* @memberof @node-red/runtime_projects
@ -107,7 +108,7 @@ var api = module.exports = {
var currentProject = runtime.storage.projects.getActiveProject(opts.user);
runtime.log.audit({event: "projects.set",id:opts.id}, opts.req);
if (!currentProject || opts.id !== currentProject.name) {
return runtime.storage.projects.setActiveProject(opts.user, opts.id);
return runtime.storage.projects.setActiveProject(opts.user, opts.id, opts.clearContext);
}
},

View File

@ -424,6 +424,17 @@ class Flow {
*/
getGroupEnvSetting(node, group, name) {
if (group) {
if (name === "NR_GROUP_NAME") {
return [{
val: group.name
}, null];
}
if (name === "NR_GROUP_ID") {
return [{
val: group.id
}, null];
}
if (group.credentials === undefined) {
group.credentials = credentials.get(group.id) || {};
}
@ -498,7 +509,13 @@ class Flow {
* @return {[type]} [description]
*/
getSetting(key) {
const flow = this.flow;
const flow = this.flow;
if (key === "NR_FLOW_NAME") {
return flow.label;
}
if (key === "NR_FLOW_ID") {
return flow.id;
}
if (flow.credentials === undefined) {
flow.credentials = credentials.get(flow.id) || {};
}

View File

@ -371,6 +371,17 @@ class Subflow extends Flow {
name = name.substring(8);
}
const node = this.subflowInstance;
if (node) {
if (name === "NR_NODE_NAME") {
return node.name;
}
if (name === "NR_NODE_ID") {
return node.id;
}
if (name === "NR_NODE_PATH") {
return node._path;
}
}
if (node.g) {
const group = this.getGroupNode(node.g);
const [result, newName] = this.getGroupEnvSetting(node, group, name);

View File

@ -85,6 +85,7 @@ function createNode(flow,config) {
try {
Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true })
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
Object.defineProperty(conf,'_path', {value: `${flow.path}/${config._alias||config.id}`, enumerable: false, writable: true })
newNode = new nodeTypeConstructor(conf);
} catch (err) {
Log.log({

View File

@ -62,6 +62,9 @@ function Node(n) {
if (n._module) {
Object.defineProperty(this,'_module', {value: n._module, enumerable: false, writable: true })
}
if (n._path) {
Object.defineProperty(this,'_path', {value: n._path, enumerable: false, writable: true })
}
this.updateWires(n.wires);
}

View File

@ -14,30 +14,30 @@
* limitations under the License.
**/
var clone = require("clone");
var log = require("@node-red/util").log;
var util = require("@node-red/util").util;
var memory = require("./memory");
const clone = require("clone");
const log = require("@node-red/util").log;
const util = require("@node-red/util").util;
const memory = require("./memory");
var settings;
let settings;
// A map of scope id to context instance
var contexts = {};
let contexts = {};
// A map of store name to instance
var stores = {};
var storeList = [];
var defaultStore;
let stores = {};
let storeList = [];
let defaultStore;
// Whether there context storage has been configured or left as default
var hasConfiguredStore = false;
let hasConfiguredStore = false;
// Unknown Stores
var unknownStores = {};
let unknownStores = {};
function logUnknownStore(name) {
if (name) {
var count = unknownStores[name] || 0;
let count = unknownStores[name] || 0;
if (count == 0) {
log.warn(log._("context.unknown-store", {name: name}));
count++;
@ -52,8 +52,8 @@ function init(_settings) {
stores = {};
storeList = [];
hasConfiguredStore = false;
var seed = settings.functionGlobalContext || {};
contexts['global'] = createContext("global",seed);
initialiseGlobalContext();
// create a default memory store - used by the unit tests that skip the full
// `load()` initialisation sequence.
// If the user has any stores configured, this will be disgarded
@ -61,6 +61,11 @@ function init(_settings) {
defaultStore = "memory";
}
function initialiseGlobalContext() {
const seed = settings.functionGlobalContext || {};
contexts['global'] = createContext("global",seed);
}
function load() {
return new Promise(function(resolve,reject) {
// load & init plugins in settings.contextStorage
@ -233,12 +238,15 @@ function validateContextKey(key) {
function createContext(id,seed,parent) {
// Seed is only set for global context - sourced from functionGlobalContext
var scope = id;
var obj = seed || {};
var seedKeys;
var insertSeedValues;
const scope = id;
const obj = {};
let seedKeys;
let insertSeedValues;
if (seed) {
seedKeys = Object.keys(seed);
seedKeys.forEach(key => {
obj[key] = seed[key];
})
insertSeedValues = function(keys,values) {
if (!Array.isArray(keys)) {
if (values[0] === undefined) {
@ -540,8 +548,28 @@ function getContext(nodeId, flowId) {
return newContext;
}
/**
* Delete the context of the given node/flow/global
*
* If the user has configured a context store, this
* will no-op a request to delete node/flow context.
*/
function deleteContext(id,flowId) {
if(!hasConfiguredStore){
if (id === "global") {
// 1. delete global from all configured stores
var promises = [];
for(var plugin in stores){
if(stores.hasOwnProperty(plugin)){
promises.push(stores[plugin].delete('global'));
}
}
return Promise.all(promises).then(function() {
// 2. delete global context
delete contexts['global'];
// 3. reinitialise global context
initialiseGlobalContext();
})
} else if (!hasConfiguredStore) {
// only delete context if there's no configured storage.
var contextId = id;
if (flowId) {
@ -549,12 +577,19 @@ function deleteContext(id,flowId) {
}
delete contexts[contextId];
return stores["_"].delete(contextId);
}else{
} else {
return Promise.resolve();
}
}
/**
* Delete any contexts that are no longer in use
* @param flowConfig object includes allNodes as object of id->node
*
* If flowConfig is undefined, all flow/node contexts will be removed
**/
function clean(flowConfig) {
flowConfig = flowConfig || { allNodes: {} };
var promises = [];
for(var plugin in stores){
if(stores.hasOwnProperty(plugin)){
@ -572,6 +607,16 @@ function clean(flowConfig) {
return Promise.all(promises);
}
/**
* Deletes all contexts, including global and reinitialises global to
* initial state.
*/
function clear() {
return clean().then(function() {
return deleteContext('global')
})
}
function close() {
var promises = [];
for(var plugin in stores){
@ -594,5 +639,6 @@ module.exports = {
getFlowContext:getFlowContext,
delete: deleteContext,
clean: clean,
clear: clear,
close: close
};

View File

@ -206,6 +206,7 @@ module.exports = {
eachNode: flows.eachNode,
getContext: context.get,
clearContext: context.clear,
installerEnabled: registry.installerEnabled,
installModule: installModule,

View File

@ -377,8 +377,17 @@ function getActiveProject(user) {
return activeProject;
}
function reloadActiveProject(action) {
function reloadActiveProject(action, clearContext) {
// Stop the current flows
return runtime.nodes.stopFlows().then(function() {
if (clearContext) {
// Reset context to remove any old values
return runtime.nodes.clearContext()
} else {
return Promise.resolve()
}
}).then(function() {
// Load the new project flows and start them
return runtime.nodes.loadFlows(true).then(function() {
events.emit("runtime-event",{id:"project-update", payload:{ project: activeProject.name, action:action}});
}).catch(function(err) {
@ -387,6 +396,9 @@ function reloadActiveProject(action) {
events.emit("runtime-event",{id:"project-update", payload:{ project: activeProject.name, action:action}});
throw err;
});
}).catch(function(err) {
console.log(err.stack);
throw err;
});
}
function createProject(user, metadata) {
@ -424,7 +436,7 @@ function createProject(user, metadata) {
return getProject(user, metadata.name);
})
}
function setActiveProject(user, projectName) {
function setActiveProject(user, projectName, clearContext) {
return loadProject(projectName).then(function(project) {
var globalProjectSettings = settings.get("projects")||{};
globalProjectSettings.activeProject = project.name;
@ -434,7 +446,7 @@ function setActiveProject(user, projectName) {
// console.log("Updated file targets to");
// console.log(flowsFullPath)
// console.log(credentialsFile)
return reloadActiveProject("loaded");
return reloadActiveProject("loaded", clearContext);
})
});
}

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
"version": "2.1.6",
"version": "2.2.0",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@ -16,8 +16,8 @@
}
],
"dependencies": {
"@node-red/registry": "2.1.6",
"@node-red/util": "2.1.6",
"@node-red/registry": "2.2.0",
"@node-red/util": "2.2.0",
"async-mutex": "0.3.2",
"clone": "2.1.2",
"express": "4.17.2",

View File

@ -1,4 +1,4 @@
Copyright JS Foundation and other contributors, http://js.foundation
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004

View File

@ -522,6 +522,17 @@ function setObjectProperty(msg,prop,value,createMissing) {
* @return {String} value of env var
*/
function getSetting(node, name, flow_) {
if (node) {
if (name === "NR_NODE_NAME") {
return node.name;
}
if (name === "NR_NODE_ID") {
return node.id;
}
if (name === "NR_NODE_PATH") {
return node._path;
}
}
var flow = (flow_ ? flow_ : (node ? node._flow : null));
if (flow) {
if (node && node.g) {

View File

@ -1,6 +1,6 @@
{
"name": "@node-red/util",
"version": "2.1.6",
"version": "2.2.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
@ -16,7 +16,7 @@
],
"dependencies": {
"fs-extra": "10.0.0",
"i18next": "21.6.6",
"i18next": "21.6.10",
"json-stringify-safe": "5.0.1",
"jsonata": "1.8.5",
"lodash.clonedeep": "^4.5.0",

View File

@ -1,4 +1,4 @@
Copyright JS Foundation and other contributors, http://js.foundation
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004

View File

@ -2,12 +2,9 @@
http://nodered.org
[![Build Status](https://travis-ci.org/node-red/node-red.svg)](https://travis-ci.org/node-red/node-red)
[![Coverage Status](https://coveralls.io/repos/node-red/node-red/badge.svg?branch=master)](https://coveralls.io/r/node-red/node-red?branch=master)
Low-code programming for event-driven applications.
A visual tool for wiring the Internet of Things.
![Node-RED: A visual tool for wiring the Internet of Things](http://nodered.org/images/node-red-screenshot.png)
![Node-RED: Low-code programming for event-driven applications.](http://nodered.org/images/node-red-screenshot.png)
## Quick Start
@ -39,15 +36,14 @@ This project adheres to the [Contributor Covenant 1.4](http://contributor-covena
## Authors
Node-RED is a project of the [JS Foundation](http://js.foundation).
It was created by [IBM Emerging Technology](https://www.ibm.com/blogs/emerging-technology/).
* Nick O'Leary [@knolleary](http://twitter.com/knolleary)
* Dave Conway-Jones [@ceejay](http://twitter.com/ceejay)
Node-RED is a project of the [OpenJS Foundation](http://openjsf.org).
It is maintained by:
* Nick O'Leary [@knolleary](http://twitter.com/knolleary)
* Dave Conway-Jones [@ceejay](http://twitter.com/ceejay)
* And many others...
## Copyright and license
Copyright JS Foundation and other contributors, http://js.foundation under [the Apache 2.0 license](LICENSE).
Copyright OpenJS Foundation and other contributors, https://openjsf.org under [the Apache 2.0 license](LICENSE).

View File

@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "2.1.6",
"version": "2.2.0",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@ -31,10 +31,10 @@
"flow"
],
"dependencies": {
"@node-red/editor-api": "2.1.6",
"@node-red/runtime": "2.1.6",
"@node-red/util": "2.1.6",
"@node-red/nodes": "2.1.6",
"@node-red/editor-api": "2.2.0",
"@node-red/runtime": "2.2.0",
"@node-red/util": "2.2.0",
"@node-red/nodes": "2.2.0",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.17.2",

View File

@ -328,6 +328,12 @@ module.exports = {
* a collection of themes to chose from.
*/
//theme: "",
/** To disable the 'Welcome to Node-RED' tour that is displayed the first
* time you access the editor for each release of Node-RED, set this to false
*/
//tours: false,
palette: {
/** The following property can be used to order the categories in the editor
* palette. If a node's category is not in the list, the category will get
@ -336,6 +342,7 @@ module.exports = {
*/
//categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'],
},
projects: {
/** To enable the Projects feature, set this value to true */
enabled: false,
@ -349,6 +356,7 @@ module.exports = {
mode: "manual"
}
},
codeEditor: {
/** Select the text editor component used by the editor.
* Defaults to "ace", but can be set to "ace" or "monaco"

View File

@ -106,6 +106,142 @@ describe('inject node', function() {
});
});
it('inject name of node as environment variable ', function (done) {
var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_NAME", payloadType: "env", wires: [["n2"]], z: "flow"},
{id: "n2", type: "helper"}];
helper.load(injectNode, flow, function () {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "NAME");
done();
} catch (err) {
done(err);
}
});
n1.receive({});
});
});
it('inject id of node as environment variable ', function (done) {
var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_ID", payloadType: "env", wires: [["n2"]], z: "flow"},
{id: "n2", type: "helper"}];
helper.load(injectNode, flow, function () {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "n1");
done();
} catch (err) {
done(err);
}
});
n1.receive({});
});
});
it('inject path of node as environment variable ', function (done) {
var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_PATH", payloadType: "env", wires: [["n2"]], z: "flow"},
{id: "n2", type: "helper"}];
helper.load(injectNode, flow, function () {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "flow/n1");
done();
} catch (err) {
done(err);
}
});
n1.receive({});
});
});
it('inject name of flow as environment variable ', function (done) {
var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_FLOW_NAME", payloadType: "env", wires: [["n2"]], z: "flow"},
{id: "n2", type: "helper"},
{id: "flow", type: "tab", label: "FLOW" },
];
helper.load(injectNode, flow, function () {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "FLOW");
done();
} catch (err) {
done(err);
}
});
n1.receive({});
});
});
it('inject id of flow as environment variable ', function (done) {
var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_FLOW_ID", payloadType: "env", wires: [["n2"]], z: "flow"},
{id: "n2", type: "helper"},
{id: "flow", type: "tab", name: "FLOW" },
];
helper.load(injectNode, flow, function () {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "flow");
done();
} catch (err) {
done(err);
}
});
n1.receive({});
});
});
it('inject name of group as environment variable ', function (done) {
var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_GROUP_NAME", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"},
{id: "n2", type: "helper"},
{id: "g0", type: "group", name: "GROUP" },
];
helper.load(injectNode, flow, function () {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "GROUP");
done();
} catch (err) {
done(err);
}
});
n1.receive({});
});
});
it('inject id of group as environment variable ', function (done) {
var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_GROUP_ID", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"},
{id: "n2", type: "helper"},
{id: "g0", type: "group", name: "GROUP" },
];
helper.load(injectNode, flow, function () {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function (msg) {
try {
msg.should.have.property("payload", "g0");
done();
} catch (err) {
done(err);
}
});
n1.receive({});
});
});
it('sets the value of flow context property', function (done) {
var flow = [{id: "n1", type: "inject", topic: "t1", payload: "flowValue", payloadType: "flow", wires: [["n2"]], z: "flow"},
{id: "n2", type: "helper"}];
@ -149,6 +285,7 @@ describe('inject node', function() {
});
});
it('sets the value of two persistable flow context property', function (done) {
var flow = [{id: "n0", z: "flow", type: "inject", topic: "t0", payload: "#:(memory0)::val", payloadType: "flow", wires: [["n2"]]},
{id: "n1", z: "flow", type: "inject", topic: "t1", payload: "#:(memory1)::val", payloadType: "flow", wires: [["n2"]]},

View File

@ -366,6 +366,18 @@ describe('websocket Node', function() {
});
});
it('should handle protocol property', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws"), subprotocol: "testprotocol1, testprotocol2" }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property("subprotocol", []);
helper.getNode("n2").should.have.property("subprotocol", ["testprotocol1","testprotocol2"]);
done();
});
});
it('should connect to server', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
@ -378,6 +390,18 @@ describe('websocket Node', function() {
});
});
it('should initiate with subprotocol', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws"), subprotocol: "testprotocol" }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function (sock) {
sock.should.have.property("protocol", "testprotocol")
done();
});
});
});
it('should close on delete', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },

View File

@ -84,9 +84,17 @@ describe('TCP Request Node', function() {
n2.on("input", msg => {
try {
if (typeof result === 'object') {
msg.should.have.properties(Object.assign({}, result, {payload: Buffer.from(result.payload)}));
if (flow[0].ret === "string") {
msg.should.have.properties(Object.assign({}, result, {payload: result.payload}));
} else {
msg.should.have.properties(Object.assign({}, result, {payload: Buffer.from(result.payload)}));
}
} else {
msg.should.have.property('payload', Buffer.from(result));
if (flow[0].ret === "string") {
msg.should.have.property('payload', result);
} else {
msg.should.have.property('payload', Buffer.from(result));
}
}
done();
} catch(err) {
@ -245,10 +253,41 @@ describe('TCP Request Node', function() {
}, done);
});
it('should send & receive, then keep connection, and not split return strings', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", ret:"string", newline:"", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo",
topic: 'boo'
}, {
payload: "bar<A>\nfoo",
topic: 'boo'
}], {
payload: "ACK:foobar<A>\nfoo",
topic: 'boo'
}, done);
});
it('should send & receive, then keep connection, and split return strings', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", ret:"string", newline:"<A>\\n", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo",
topic: 'boo'
}, {
payload: "bar<A>\nfoo",
topic: 'boo'
}], {
payload: "ACK:foobar<A>",
topic: 'boo'
}, done);
});
it('should send & recv data to/from server:port from msg', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
testTCPMany(flow, [
{
payload: "f",
host: "localhost",
port: port

View File

@ -50,6 +50,24 @@ describe('JSON node', function() {
});
});
it('should convert a buffer of a valid json string to a javascript object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
msg.payload.should.have.property('employees');
msg.payload.employees[0].should.have.property('firstName', 'John');
msg.payload.employees[0].should.have.property('lastName', 'Smith');
done();
});
var jsonString = Buffer.from('{"employees":[{"firstName":"John", "lastName":"Smith"}]}');
jn1.receive({payload:jsonString,topic: "bar"});
});
});
it('should convert a javascript object to a json string', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
@ -166,29 +184,55 @@ describe('JSON node', function() {
});
});
it('should log an error if asked to parse something thats not json or js', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
it('should log an error if asked to parse an invalid json string in a buffer', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.eql('json.errors.dropped-object');
done();
} catch(err) {
done(err);
}
},50);
jn1.receive({payload:Buffer.from("a")});
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn1.receive({payload:Buffer.from('{"name":foo}'),topic: "bar"});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("Unexpected token o");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},20);
} catch(err) {
done(err);
}
});
});
// it('should log an error if asked to parse something thats not json or js and not in force object mode', function(done) {
// var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
// {id:"jn2", type:"helper"}];
// helper.load(jsonNode, flow, function() {
// var jn1 = helper.getNode("jn1");
// var jn2 = helper.getNode("jn2");
// setTimeout(function() {
// try {
// var logEvents = helper.log().args.filter(function(evt) {
// return evt[0].type == "json";
// });
// logEvents.should.have.length(1);
// logEvents[0][0].should.have.a.property('msg');
// logEvents[0][0].msg.toString().should.eql('json.errors.dropped');
// done();
// } catch(err) {
// done(err);
// }
// },50);
// jn1.receive({payload:Buffer.from("abcd")});
// });
// });
it('should pass straight through if no payload set', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];

View File

@ -567,4 +567,43 @@ describe('subflow', function() {
});
});
it('should access NR_NODE_PATH env var within subflow instance', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1",
env: [], wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.payload = env.get('NR_NODE_PATH'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("payload", "t0/n1/s1-n1");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
});

View File

@ -201,6 +201,64 @@ describe('Subflow', function() {
}
util.inherits(TestEnvNode,Node);
var TestNameEnvNode = function(n) {
Node.call(this,n);
this._index = createCount++;
this.scope = n.scope;
this.foo = n.foo;
var node = this;
this.stopped = false;
this.received = null;
currentNodes[node.id] = node;
this.on('input',function(msg) {
var flow = node._flow;
var val = flow.getSetting("NR_NODE_NAME");
node.received = val;
node.send({payload: val});
});
this.on('close',function() {
node.stopped = true;
stoppedNodes[node.id] = node;
delete currentNodes[node.id];
});
this.__updateWires = this.updateWires;
this.updateWires = function(newWires) {
rewiredNodes[node.id] = node;
node.newWires = newWires;
node.__updateWires(newWires);
};
}
util.inherits(TestNameEnvNode,Node);
var TestIDEnvNode = function(n) {
Node.call(this,n);
this._index = createCount++;
this.scope = n.scope;
this.foo = n.foo;
var node = this;
this.stopped = false;
this.received = null;
currentNodes[node.id] = node;
this.on('input',function(msg) {
var flow = node._flow;
var val = flow.getSetting("NR_NODE_ID");
node.received = val;
node.send({payload: val});
});
this.on('close',function() {
node.stopped = true;
stoppedNodes[node.id] = node;
delete currentNodes[node.id];
});
this.__updateWires = this.updateWires;
this.updateWires = function(newWires) {
rewiredNodes[node.id] = node;
node.newWires = newWires;
node.__updateWires(newWires);
};
}
util.inherits(TestIDEnvNode,Node);
before(function() {
getType = sinon.stub(typeRegistry,"get").callsFake(function(type) {
if (type=="test") {
@ -211,6 +269,10 @@ describe('Subflow', function() {
return TestStatusNode;
} else if (type=="testEnv"){
return TestEnvNode;
} else if (type=="testNameEnv"){
return TestNameEnvNode;
} else if (type=="testIDEnv"){
return TestIDEnvNode;
} else {
return TestAsyncNode;
}
@ -880,6 +942,65 @@ describe('Subflow', function() {
},150);
});
it("can access name of subflow as env var", function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",name:"SFN",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"t1.3",wires:[]},
{id:"sf1",type:"subflow",name:"Subflow 2",info:"",
"in":[ {wires:[{id:"sf1-1"}]} ],
"out":[ {wires:[{id:"sf1-2",port:0}]} ]},
{id:"sf1-1",type:"test",z:"sf1",foo:"sf1.1",x:166,y:99,wires:[["sf1-2"]]},
{id:"sf1-2",type:"testNameEnv",z:"sf1",foo:"sf1.2",x:166,y:99,wires:[[]]}
]);
var flow = Flow.create({
getSetting: k=> process.env[k],
handleError: (a,b,c) => { console.log(a,b,c); }
},config,config.flows["t1"]);
flow.start();
currentNodes["1"].receive({payload: "test"});
setTimeout(function() {
currentNodes["3"].should.have.a.property("received", "SFN");
flow.stop().then(function() {
done();
});
},150);
});
it("can access id of subflow as env var", function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",name:"SFN",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"test",foo:"t1.3",wires:[]},
{id:"sf1",type:"subflow",name:"Subflow 2",info:"",
"in":[ {wires:[{id:"sf1-1"}]} ],
"out":[ {wires:[{id:"sf1-2",port:0}]} ]},
{id:"sf1-1",type:"test",z:"sf1",foo:"sf1.1",x:166,y:99,wires:[["sf1-2"]]},
{id:"sf1-2",type:"testIDEnv",z:"sf1",foo:"sf1.2",x:166,y:99,wires:[[]]}
]);
var flow = Flow.create({
getSetting: k=> process.env[k],
handleError: (a,b,c) => { console.log(a,b,c); }
},config,config.flows["t1"]);
flow.start();
currentNodes["1"].receive({payload: "test"});
setTimeout(function() {
currentNodes["3"].should.have.a.property("received", "2");
flow.stop().then(function() {
done();
});
},150);
});
});
});

View File

@ -156,6 +156,22 @@ describe('context', function() {
});
});
it('deletes global context',function() {
Context.init({functionGlobalContext: {foo:"bar"}});
return Context.load().then(function(){
var globalContextA = Context.get("global")
globalContextA.get('foo').should.eql('bar')
globalContextA.set("another","value");
return Context.delete("global").then(function(){
var globalContextB = Context.get("global")
globalContextB.get('foo').should.eql('bar')
should.not.exist(globalContextB.get("another"));
});
});
});
it('enumerates context keys - sync', function() {
var flowContextA = Context.getFlowContext("flowA")
var context = Context.get("1","flowA");
@ -316,6 +332,31 @@ describe('context', function() {
});
});
describe("clear", function() {
it('clears all context',function() {
Context.init({functionGlobalContext: {foo:"bar"}});
return Context.load().then(function(){
var globalContextA = Context.get("global")
globalContextA.get('foo').should.eql('bar')
globalContextA.set("another","value");
var flowContextA = Context.getFlowContext("flowA")
flowContextA.set("foo","abc");
flowContextA.get("foo").should.equal("abc");
return Context.clear().then(function(){
var globalContextB = Context.getFlowContext("global")
globalContextB.get('foo').should.eql('bar')
should.not.exist(globalContextB.get("another"));
flowContextA = Context.getFlowContext("flowA")
should.not.exist(flowContextA.get("foo"))
});
});
});
})
});
describe('external context storage',function() {