diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json
index c1c9316d8..aa123b824 100644
--- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json
+++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json
@@ -476,6 +476,8 @@
"inputType": "Input type",
"selectType": "select types...",
"loadCredentials": "Loading node credentials",
+ "initiatorNodes": "During shutdown, start by stopping message initiator nodes:",
+ "failFast": "Cancel shutdown of listed flows if this flow fails to stop",
"inputs": {
"input": "input",
"select": "select",
@@ -1238,7 +1240,8 @@
"description": "Description",
"appearance": "Appearance",
"preview": "UI Preview",
- "defaultValue": "Default value"
+ "defaultValue": "Default value",
+ "shutdown": "Graceful Shutdown"
},
"tourGuide": {
"takeATour": "Take a tour",
diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js
index 4a114560b..1db933834 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js
@@ -112,7 +112,10 @@ RED.nodes = (function() {
disabled: {value: false},
locked: {value: false},
info: {value: ""},
- env: {value: []}
+ env: {value: []},
+ failFast: { value: false },
+ timeout: { value: 10000 },
+ initiatorNodes: { value: [] },
}
};
@@ -1141,7 +1144,10 @@ RED.nodes = (function() {
} else {
return errors
}
- }}
+ }},
+ failFast: { value: false },
+ timeout: { value: 10000 },
+ initiatorNodes: { value: [] },
},
icon: function() { return sf.icon||"subflow.svg" },
category: sf.category || "subflows",
@@ -1457,6 +1463,9 @@ RED.nodes = (function() {
node.in = [];
node.out = [];
node.env = n.env;
+ node.failFast = n.failFast;
+ node.timeout = n.timeout;
+ node.initiatorNodes = n.initiatorNodes;
node.meta = n.meta;
if (exportCreds) {
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
index f804e6de2..f6f52ccf2 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js
@@ -1963,6 +1963,11 @@ RED.editor = (function() {
'editor-tab-description',
'editor-tab-appearance'
];
+
+ if (RED.settings.gracefulShutdownEnabled) {
+ nodeEditPanes.splice(1, 0, 'editor-tab-shutdown');
+ }
+
prepareEditDialog(trayBody, nodeEditPanes, subflow, subflow._def, "subflow-input", defaultTab, function (_activeEditPanes) {
activeEditPanes = _activeEditPanes;
trayBody.i18n();
@@ -2215,6 +2220,10 @@ RED.editor = (function() {
'editor-tab-envProperties'
];
+ if (RED.settings.gracefulShutdownEnabled) {
+ nodeEditPanes.splice(1, 0, 'editor-tab-shutdown');
+ }
+
if (!workspace.hasOwnProperty("disabled")) {
workspace.disabled = false;
}
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/shutdown.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/shutdown.js
new file mode 100644
index 000000000..1335d965b
--- /dev/null
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/shutdown.js
@@ -0,0 +1,269 @@
+; (function () {
+ const tabcontent = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ RED.editor.registerEditPane("editor-tab-shutdown", function (node) {
+ return {
+ label: RED._("editor-tab.shutdown"),
+ name: RED._("editor-tab.shutdown"),
+ iconClass: "fa fa-power-off",
+
+ create: function (container) {
+ const dialogForm = $('').appendTo(container);
+
+ if (RED.settings.gracefulShutdownEnabled) {
+ $(tabcontent).appendTo(dialogForm);
+ buildScopeForm(dialogForm, node);
+ buildInitiatorForm(dialogForm, node);
+ $("#node-input-timeout").spinner({ min: 1 }).val((node.timeout || 10000) / 1000);
+ $("#node-input-failFast").prop("checked", node.failFast || false);
+ this._resize = function () {
+ const rows = $(dialogForm).find(">div:not(.node-input-initiator-list-row)");
+ let height = $(dialogForm).height();
+ for (let i = 0; i < rows.length; i++) {
+ height -= $(rows[i]).outerHeight(true);
+ }
+ $(dialogForm).find(">div.node-input-initiator-list-row").css("height", height + "px");
+ };
+ } else {
+ $("Graceful shutdown disabled
").appendTo(dialogForm);
+ }
+ },
+ close: function () { },
+ show: function () { },
+ resize: function (_size) {
+ if (this._resize) {
+ this._resize();
+ }
+ },
+ /** @type {(editState: { changes?: Record, changed?: boolean }) => void} */
+ apply: function (editState) {
+ const failFast = $("#node-input-failFast").prop("checked");
+ if (node.failFast != failFast) {
+ editState.changes = editState.changes || {};
+ editState.changes.failFast = node.failFast;
+ editState.changed = true;
+ node.failFast = failFast;
+ }
+
+ let timeout = parseFloat($("#node-input-timeout").val() || "10") * 1000;
+ if (Number.isNaN(timeout)) {
+ timeout = 10000;
+ }
+ if (node.timeout !== timeout) {
+ editState.changes = editState.changes || {};
+ editState.changes.timeout = node.timeout;
+ editState.changed = true;
+ node.timeout = timeout;
+ }
+
+ const initiatorNodes = $("#node-input-initiator-target-container-div").treeList("selected").map((i) => i.node.id);
+ if (JSON.stringify(initiatorNodes) !== JSON.stringify(node.initialNodes || [])) {
+ editState.changes = editState.changes || {};
+ editState.changes.initiatorNodes = node.initiatorNodes;
+ editState.changed = true;
+ node.initiatorNodes = initiatorNodes;
+ }
+ }
+ }
+ });
+
+ /** @type {(node: object) => JQuery} */
+ function getNodeLabel(node) {
+ var div = $('',{class:"red-ui-node-list-item red-ui-info-outline-item"});
+ RED.utils.createNodeIcon(node, true).appendTo(div);
+ div.find(".red-ui-node-label").addClass("red-ui-info-outline-item-label")
+ return div;
+ }
+
+ /** @type {(container: JQuery, node: object) => void} */
+ function buildScopeForm(container, node) {
+ const scope = getScope(node);
+
+ if (!scope.length) {
+ $(container).find(".node-input-scope-row").hide();
+ return;
+ }
+
+ const items = [];
+ const flowItemMap = {};
+ for (const id of scope) {
+ if (id === node.id) {
+ continue;
+ }
+
+ const isSuflow = id.startsWith("subflow:");
+ const workspace = isSuflow ? RED.nodes.subflow(id.substring(8)) : RED.nodes.workspace(id);
+ if (workspace) {
+ flowItemMap[workspace.id] = {
+ element: getNodeLabel(workspace),
+ selected: true,
+ };
+
+ items.push(flowItemMap[workspace.id]);
+ }
+ }
+
+ const dirList = $(container).find("#node-input-scope-target-container-div");
+ dirList.css({ width: "100%", height: "100%" }).treeList({ multi: true, data: items });
+ }
+
+ /** @type {(container: JQuery, node: object) => void} */
+ function buildInitiatorForm(container, node) {
+ const dirList = $(container).find("#node-input-initiator-target-container-div");
+
+ // We assume that a message initiator node must have at least one output
+ const nodeFilter = function (n) {
+ if (n.type.startsWith("link ")) {
+ // Link nodes transmits messages, but does not generate them.
+ return false;
+ }
+ if (n.hasOwnProperty("outputs")) {
+ return n.outputs > 0;
+ }
+ return true;
+ };
+ const candidateNodes = RED.nodes.filterNodes({ z: node.id }).filter(nodeFilter);
+ const search = $(container).find("#node-input-initiator-target-filter").searchBox({
+ style: "compact",
+ delay: 300,
+ change: function () {
+ const val = $(this).val().trim().toLowerCase();
+ if (val === "") {
+ dirList.treeList("filter", null);
+ search.searchBox("count", "");
+ } else {
+ const count = dirList.treeList("filter", function (item) {
+ return item.label.toLowerCase().indexOf(val) > -1 || item.node.type.toLowerCase().indexOf(val) > -1
+ });
+ search.searchBox("count", count + " / " + candidateNodes.length);
+ }
+ }
+ });
+
+ dirList.css({ width: "100%", height: "100%" })
+ .treeList({ multi: true }).on("treelistitemmouseover", function (e, item) {
+ item.node.highlighted = true;
+ item.node.dirty = true;
+ RED.view.redraw();
+ }).on("treelistitemmouseout", function (e, item) {
+ item.node.highlighted = false;
+ item.node.dirty = true;
+ RED.view.redraw();
+ });
+
+ const items = [];
+ const nodeItemMap = {};
+ const scope = node.initiatorNodes || [];
+ candidateNodes.forEach(function (n) {
+ const isChecked = scope.indexOf(n.id) !== -1;
+ const nodeDef = RED.nodes.getType(n.type);
+ let label, sublabel;
+ if (nodeDef) {
+ const l = nodeDef.label;
+ label = (typeof l === "function" ? l.call(n) : l) || "";
+ sublabel = n.type;
+ if (sublabel.indexOf("subflow:") === 0) {
+ const subflowId = sublabel.substring(8);
+ const subflow = RED.nodes.subflow(subflowId);
+ sublabel = "subflow : " + subflow.name;
+ }
+ }
+ if (!nodeDef || !label) {
+ label = n.type;
+ }
+ nodeItemMap[n.id] = {
+ node: n,
+ label: label,
+ sublabel: sublabel,
+ selected: isChecked,
+ checkbox: true
+ };
+
+ items.push(nodeItemMap[n.id]);
+ });
+
+ dirList.treeList("data", items);
+
+ $(container).find("#node-input-initiator-target-select").on("click", function (event) {
+ event.preventDefault();
+ const preselected = dirList.treeList("selected").map((item) => item.node.id);
+ RED.tray.hide();
+ RED.view.selectNodes({
+ selected: preselected,
+ onselect: function (selection) {
+ RED.tray.show();
+ const newlySelected = {};
+ selection.forEach(function (n) {
+ newlySelected[n.id] = true;
+ if (nodeItemMap[n.id]) {
+ nodeItemMap[n.id].treeList.select(true);
+ }
+ });
+ preselected.forEach(function (id) {
+ if (!newlySelected[id]) {
+ nodeItemMap[id].treeList.select(false);
+ }
+ });
+ },
+ oncancel: function () {
+ RED.tray.show();
+ },
+ filter: nodeFilter,
+ });
+ });
+ }
+
+ /** @type {(flow: object) => string[]} */
+ function getScope(flow) {
+ const activeWorkspace = flow.id;
+ /** @type {(node: object) => boolean} */
+ const nodeFilter = (n) => n.type.startsWith("link ") || n.type.startsWith("subflow:");
+ const nodes = RED.nodes.filterNodes({ z: activeWorkspace }).filter(nodeFilter);
+ const seen = new Set();
+ const scope = { [activeWorkspace]: true };
+ seen.add(activeWorkspace);
+ while (nodes.length) {
+ const node = nodes.pop();
+ if (seen.has(node.id)) {
+ continue;
+ }
+ seen.add(node.id);
+ if (node.links) {
+ node.links.forEach((id) => {
+ const link = RED.nodes.node(id);
+ if (link && !scope[link.z]) {
+ scope[link.z] = true;
+ nodes.push(...RED.nodes.filterNodes({ z: link.z }).filter(nodeFilter));
+ }
+ })
+ } else if (node.type.startsWith("subflow:") && !scope[node.type]) {
+ scope[node.type] = true;
+ nodes.push(...RED.nodes.filterNodes({ z: node.type.substring(8) }).filter(nodeFilter));
+ }
+ }
+ delete scope[activeWorkspace];
+ return Object.keys(scope);
+ }
+
+})();
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js
index 3e1b9a410..ae4243a09 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js
@@ -608,7 +608,10 @@ RED.subflow = (function() {
name:name,
info:"",
in: [],
- out: []
+ out: [],
+ failFast: { value: false },
+ timeout: { value: 10000 },
+ initiatorNodes: { value: [] },
};
RED.nodes.addSubflow(subflow);
RED.history.push({
@@ -777,7 +780,10 @@ RED.subflow = (function() {
i:index,
id:RED.nodes.id(),
wires:[{id:v.source.id,port:v.sourcePort}]
- }})
+ }}),
+ failFast: { value: false },
+ timeout: { value: 10000 },
+ initiatorNodes: { value: [] },
};
RED.nodes.addSubflow(subflow);
diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js
index 78e1399cd..da720a418 100644
--- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js
+++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js
@@ -84,6 +84,9 @@ RED.workspaces = (function() {
info: "",
label: RED._('workspace.defaultName',{number:workspaceIndex}),
env: [],
+ failFast: false,
+ timeout: 10000,
+ initiatorNodes: [],
hideable: true,
};
if (!skipHistoryEntry) {
diff --git a/packages/node_modules/@node-red/registry/lib/subflow.js b/packages/node_modules/@node-red/registry/lib/subflow.js
index 39fe083ab..29b786592 100644
--- a/packages/node_modules/@node-red/registry/lib/subflow.js
+++ b/packages/node_modules/@node-red/registry/lib/subflow.js
@@ -16,7 +16,10 @@ function generateSubflowConfig(subflow) {
const icon = subflow.icon || "arrow-in.svg";
const defaults = {
- name: {value: ""}
+ name: {value: ""},
+ failFast: { value: false },
+ timeout: { value: 10000 },
+ initiatorNodes: { value: [] },
}
const credentials = {}
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js
index f97b15b5e..e7c332bb3 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js
@@ -604,14 +604,14 @@ async function addFlow(flow, user) {
if (flow.hasOwnProperty('env')) {
tabNode.env = flow.env;
}
- if (flow.hasOwnProperty('shutdownScope')) {
- tabNode.shutdownScope = flow.shutdownScope;
+ if (flow.hasOwnProperty('failFast')) {
+ tabNode.failFast = flow.failFast;
}
- if (flow.hasOwnProperty('shutdownTimeout')) {
- tabNode.shutdownTimeout = flow.shutdownTimeout;
+ if (flow.hasOwnProperty('timeout')) {
+ tabNode.timeout = flow.timeout;
}
- if (flow.hasOwnProperty('messageInitiatorNodes')) {
- tabNode.messageInitiatorNodes = flow.messageInitiatorNodes;
+ if (flow.hasOwnProperty('initiatorNodes')) {
+ tabNode.initiatorNodes = flow.initiatorNodes;
}
var nodes = [tabNode];
@@ -676,14 +676,14 @@ function getFlow(id) {
if (flow.hasOwnProperty('env')) {
result.env = flow.env;
}
- if (flow.hasOwnProperty('shutdownScope')) {
- result.shutdownScope = flow.shutdownScope;
+ if (flow.hasOwnProperty('failFast')) {
+ result.failFast = flow.failFast;
}
- if (flow.hasOwnProperty('shutdownTimeout')) {
- result.shutdownTimeout = flow.shutdownTimeout;
+ if (flow.hasOwnProperty('timeout')) {
+ result.timeout = flow.timeout;
}
- if (flow.hasOwnProperty('messageInitiatorNodes')) {
- result.messageInitiatorNodes = flow.messageInitiatorNodes;
+ if (flow.hasOwnProperty('initiatorNodes')) {
+ result.initiatorNodes = flow.initiatorNodes;
}
if (id !== 'global') {
result.nodes = [];
@@ -813,14 +813,14 @@ async function updateFlow(id,newFlow, user) {
if (newFlow.hasOwnProperty('env')) {
tabNode.env = newFlow.env;
}
- if (flow.hasOwnProperty('shutdownScope')) {
- tabNode.shutdownScope = flow.shutdownScope;
+ if (flow.hasOwnProperty('failFast')) {
+ tabNode.failFast = flow.failFast;
}
- if (flow.hasOwnProperty('shutdownTimeout')) {
- tabNode.shutdownTimeout = flow.shutdownTimeout;
+ if (flow.hasOwnProperty('timeout')) {
+ tabNode.timeout = flow.timeout;
}
- if (flow.hasOwnProperty('messageInitiatorNodes')) {
- tabNode.messageInitiatorNodes = flow.messageInitiatorNodes;
+ if (flow.hasOwnProperty('initiatorNodes')) {
+ tabNode.initiatorNodes = flow.initiatorNodes;
}
if (newFlow.hasOwnProperty('credentials')) {
tabNode.credentials = newFlow.credentials;