mirror of https://github.com/node-red/node-red.git
Add the Graceful Shutdown UI
parent
af6d7c2b9b
commit
5113851189
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
269
packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/shutdown.js
vendored
Normal file
269
packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/shutdown.js
vendored
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
; (function () {
|
||||
const tabcontent = `
|
||||
<div class="form-row">
|
||||
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="node-red:exec.label.timeout"></span></label>
|
||||
<input type="text" id="node-input-timeout" placeholder="10" style="width: 70px; margin-right: 5px;"> <span data-i18n="node-red:inject.seconds"></span>
|
||||
</div>
|
||||
<div class="form-row node-input-scope-row" style="margin-bottom: 0px;">
|
||||
<input type="checkbox" id="node-input-failFast" style="display: inline-block; width: auto; vertical-align: top; margin-left: 30px; margin-right: 5px;">
|
||||
<label for="node-input-failFast" style="width: auto;"><span data-i18n="editor.failFast"></span></label>
|
||||
</div>
|
||||
<div class="form-row node-input-scope-row" style="min-height: 100px; height: 10%;">
|
||||
<div id="node-input-scope-target-container-div"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label style="width: auto" data-i18n="editor:editor.initiatorNodes""></label>
|
||||
</div>
|
||||
<div class="form-row node-input-target-row">
|
||||
<button type="button" id="node-input-initiator-target-select" class="red-ui-button" data-i18n="node-red:common.label.selectNodes"></button>
|
||||
</div>
|
||||
<div class="form-row node-input-target-row node-input-initiator-list-row" style="position: relative; min-height: 200px;">
|
||||
<div style="position: absolute; top: -30px; right: 0;"><input type="text" id="node-input-initiator-target-filter"></div>
|
||||
<div id="node-input-initiator-target-container-div"></div>
|
||||
</div>`;
|
||||
|
||||
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 = $('<form class="dialog-form form-horizontal" autocomplete="off"></form>').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 {
|
||||
$("<p>Graceful shutdown disabled</p>").appendTo(dialogForm);
|
||||
}
|
||||
},
|
||||
close: function () { },
|
||||
show: function () { },
|
||||
resize: function (_size) {
|
||||
if (this._resize) {
|
||||
this._resize();
|
||||
}
|
||||
},
|
||||
/** @type {(editState: { changes?: Record<string, unknown>, 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 = $('<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);
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ RED.workspaces = (function() {
|
|||
info: "",
|
||||
label: RED._('workspace.defaultName',{number:workspaceIndex}),
|
||||
env: [],
|
||||
failFast: false,
|
||||
timeout: 10000,
|
||||
initiatorNodes: [],
|
||||
hideable: true,
|
||||
};
|
||||
if (!skipHistoryEntry) {
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue