mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #3719 from node-red/pr_3642
Allow flows to be stopped and started manually
This commit is contained in:
commit
51684d18cf
@ -142,6 +142,7 @@ module.exports = function(grunt) {
|
||||
"packages/node_modules/@node-red/editor-client/src/js/settings.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/user.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/comms.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/runtime.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/text/bidi.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/text/format.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/state.js",
|
||||
|
@ -68,5 +68,28 @@ module.exports = {
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
getState: function(req,res) {
|
||||
const opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.flows.getState(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
},
|
||||
postState: function(req,res) {
|
||||
const opts = {
|
||||
user: req.user,
|
||||
state: req.body.state || "",
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.flows.setState(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(function(err) {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,12 @@ module.exports = {
|
||||
adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler);
|
||||
adminApp.post("/flows",needsPermission("flows.write"),flows.post,apiUtil.errorHandler);
|
||||
|
||||
// Flows/state
|
||||
adminApp.get("/flows/state", needsPermission("flows.read"), flows.getState, apiUtil.errorHandler);
|
||||
if (settings.runtimeState && settings.runtimeState.enabled === true) {
|
||||
adminApp.post("/flows/state", needsPermission("flows.write"), flows.postState, apiUtil.errorHandler);
|
||||
}
|
||||
|
||||
// Flow
|
||||
adminApp.get("/flow/:id",needsPermission("flows.read"),flow.get,apiUtil.errorHandler);
|
||||
adminApp.post("/flow",needsPermission("flows.write"),flow.post,apiUtil.errorHandler);
|
||||
|
@ -169,6 +169,10 @@
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"state": {
|
||||
"flowsStopped": "Flows stopped",
|
||||
"flowsStarted": "Flows started"
|
||||
},
|
||||
"warning": "<strong>Warning</strong>: __message__",
|
||||
"warnings": {
|
||||
"undeployedChanges": "node has undeployed changes",
|
||||
|
4
packages/node_modules/@node-red/editor-client/src/images/start.svg
vendored
Normal file
4
packages/node_modules/@node-red/editor-client/src/images/start.svg
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path color="#000" fill="#8c101c" d="M0 0h32v32H0z"></path>
|
||||
<path style="fill:#ffffff;stroke:#000000;stroke-width:0" d="M 24,16 8,24 8,8 Z" fill="none" stroke="#000" stroke-width="1.5"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 271 B |
4
packages/node_modules/@node-red/editor-client/src/images/stop.svg
vendored
Normal file
4
packages/node_modules/@node-red/editor-client/src/images/stop.svg
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path color="#000" fill="#8c101c" d="M0 0h32v32H0z"></path>
|
||||
<rect style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0;" width="15" height="15" x="8" y="8.5"></rect>
|
||||
</svg>
|
After Width: | Height: | Size: 256 B |
@ -297,6 +297,10 @@ var RED = (function() {
|
||||
// handled below
|
||||
return;
|
||||
}
|
||||
if (notificationId === "flows-run-state") {
|
||||
// handled in editor-client/src/js/runtime.js
|
||||
return;
|
||||
}
|
||||
if (notificationId === "project-update") {
|
||||
loader.start(RED._("event.loadingProject"), 0);
|
||||
RED.nodes.clear();
|
||||
@ -332,7 +336,6 @@ var RED = (function() {
|
||||
id: notificationId
|
||||
}
|
||||
if (notificationId === "runtime-state") {
|
||||
RED.events.emit("runtime-state",msg);
|
||||
if (msg.error === "safe-mode") {
|
||||
options.buttons = [
|
||||
{
|
||||
@ -473,10 +476,10 @@ var RED = (function() {
|
||||
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
|
||||
persistentNotifications[notificationId].close();
|
||||
delete persistentNotifications[notificationId];
|
||||
}
|
||||
if (notificationId === 'runtime-state') {
|
||||
RED.events.emit("runtime-state",msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
RED.comms.subscribe("status/#",function(topic,msg) {
|
||||
var parts = topic.split("/");
|
||||
@ -747,6 +750,7 @@ var RED = (function() {
|
||||
RED.keyboard.init(buildMainMenu);
|
||||
|
||||
RED.nodes.init();
|
||||
RED.runtime.init()
|
||||
RED.comms.connect();
|
||||
|
||||
$("#red-ui-main-container").show();
|
||||
|
36
packages/node_modules/@node-red/editor-client/src/js/runtime.js
vendored
Normal file
36
packages/node_modules/@node-red/editor-client/src/js/runtime.js
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
RED.runtime = (function() {
|
||||
let state = ""
|
||||
let settings = { ui: false, enabled: false };
|
||||
const STOPPED = "stop"
|
||||
const STARTED = "start"
|
||||
const SAFE = "safe"
|
||||
|
||||
return {
|
||||
init: function() {
|
||||
// refresh the current runtime status from server
|
||||
settings = Object.assign({}, settings, RED.settings.runtimeState);
|
||||
RED.events.on("runtime-state", function(msg) {
|
||||
if (msg.state) {
|
||||
const currentState = state
|
||||
state = msg.state
|
||||
$(".red-ui-flow-node-button").toggleClass("red-ui-flow-node-button-stopped", state !== STARTED)
|
||||
if(settings.enabled === true && settings.ui === true) {
|
||||
RED.menu.setVisible("deploymenu-item-runtime-stop", state === STARTED)
|
||||
RED.menu.setVisible("deploymenu-item-runtime-start", state !== STARTED)
|
||||
}
|
||||
// Do not notify the user about this event if:
|
||||
// - This is the very first event we've received after loading the editor (currentState = '')
|
||||
// - The state matches what we already thought was the case (state === currentState)
|
||||
// - The event was triggered by a deploy (msg.deploy === true)
|
||||
// - The event is a safe mode event - that gets notified separately
|
||||
if (currentState !== '' && state !== currentState && !msg.deploy && state !== SAFE) {
|
||||
RED.notify(RED._("notification.state.flows"+(state === STOPPED?'Stopped':'Started'), msg), "success")
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
get started() {
|
||||
return state === STARTED
|
||||
}
|
||||
}
|
||||
})()
|
@ -167,6 +167,9 @@ RED.menu = (function() {
|
||||
if (opt.disabled) {
|
||||
item.addClass("disabled");
|
||||
}
|
||||
if (opt.visible === false) {
|
||||
item.addClass("hide");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -303,6 +306,14 @@ RED.menu = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function setVisible(id,state) {
|
||||
if (!state) {
|
||||
$("#"+id).parent().addClass("hide");
|
||||
} else {
|
||||
$("#"+id).parent().removeClass("hide");
|
||||
}
|
||||
}
|
||||
|
||||
function addItem(id,opt) {
|
||||
var item = createMenuItem(opt);
|
||||
if (opt !== null && opt.group) {
|
||||
@ -359,6 +370,7 @@ RED.menu = (function() {
|
||||
isSelected: isSelected,
|
||||
toggleSelected: toggleSelected,
|
||||
setDisabled: setDisabled,
|
||||
setVisible: setVisible,
|
||||
addItem: addItem,
|
||||
removeItem: removeItem,
|
||||
setAction: setAction,
|
||||
|
@ -63,16 +63,18 @@ RED.deploy = (function() {
|
||||
'</a>'+
|
||||
'<a id="red-ui-header-button-deploy-options" class="red-ui-deploy-button" href="#"><i class="fa fa-caret-down"></i></a>'+
|
||||
'</span></li>').prependTo(".red-ui-header-toolbar");
|
||||
RED.menu.init({id:"red-ui-header-button-deploy-options",
|
||||
options: [
|
||||
const mainMenuItems = [
|
||||
{id:"deploymenu-item-full",toggle:"deploy-type",icon:"red/images/deploy-full.svg",label:RED._("deploy.full"),sublabel:RED._("deploy.fullDesc"),selected: true, onselect:function(s) { if(s){changeDeploymentType("full")}}},
|
||||
{id:"deploymenu-item-flow",toggle:"deploy-type",icon:"red/images/deploy-flows.svg",label:RED._("deploy.modifiedFlows"),sublabel:RED._("deploy.modifiedFlowsDesc"), onselect:function(s) {if(s){changeDeploymentType("flows")}}},
|
||||
{id:"deploymenu-item-node",toggle:"deploy-type",icon:"red/images/deploy-nodes.svg",label:RED._("deploy.modifiedNodes"),sublabel:RED._("deploy.modifiedNodesDesc"),onselect:function(s) { if(s){changeDeploymentType("nodes")}}},
|
||||
null,
|
||||
{id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"},
|
||||
|
||||
null
|
||||
]
|
||||
});
|
||||
if (RED.settings.runtimeState && RED.settings.runtimeState.ui === true) {
|
||||
mainMenuItems.push({id:"deploymenu-item-runtime-start", icon:"red/images/start.svg",label:"Start"/*RED._("deploy.startFlows")*/,sublabel:"Start Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:start-flows", visible:false})
|
||||
mainMenuItems.push({id:"deploymenu-item-runtime-stop", icon:"red/images/stop.svg",label:"Stop"/*RED._("deploy.startFlows")*/,sublabel:"Stop Flows" /*RED._("deploy.startFlowsDesc")*/,onselect:"core:stop-flows", visible:false})
|
||||
}
|
||||
mainMenuItems.push({id:"deploymenu-item-reload", icon:"red/images/deploy-reload.svg",label:RED._("deploy.restartFlows"),sublabel:RED._("deploy.restartFlowsDesc"),onselect:"core:restart-flows"})
|
||||
RED.menu.init({id:"red-ui-header-button-deploy-options", options: mainMenuItems });
|
||||
} else if (type == "simple") {
|
||||
var label = options.label || RED._("deploy.deploy");
|
||||
var icon = 'red/images/deploy-full-o.svg';
|
||||
@ -100,6 +102,10 @@ RED.deploy = (function() {
|
||||
|
||||
RED.actions.add("core:deploy-flows",save);
|
||||
if (type === "default") {
|
||||
if (RED.settings.runtimeState && RED.settings.runtimeState.ui === true) {
|
||||
RED.actions.add("core:stop-flows",function() { stopStartFlows("stop") });
|
||||
RED.actions.add("core:start-flows",function() { stopStartFlows("start") });
|
||||
}
|
||||
RED.actions.add("core:restart-flows",restart);
|
||||
RED.actions.add("core:set-deploy-type-to-full",function() { RED.menu.setSelected("deploymenu-item-full",true);});
|
||||
RED.actions.add("core:set-deploy-type-to-modified-flows",function() { RED.menu.setSelected("deploymenu-item-flow",true); });
|
||||
@ -270,18 +276,73 @@ RED.deploy = (function() {
|
||||
function sanitize(html) {
|
||||
return html.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")
|
||||
}
|
||||
function restart() {
|
||||
var startTime = Date.now();
|
||||
$(".red-ui-deploy-button-content").css('opacity',0);
|
||||
$(".red-ui-deploy-button-spinner").show();
|
||||
var deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled");
|
||||
$("#red-ui-header-button-deploy").addClass("disabled");
|
||||
deployInflight = true;
|
||||
|
||||
function shadeShow() {
|
||||
$("#red-ui-header-shade").show();
|
||||
$("#red-ui-editor-shade").show();
|
||||
$("#red-ui-palette-shade").show();
|
||||
$("#red-ui-sidebar-shade").show();
|
||||
|
||||
}
|
||||
function shadeHide() {
|
||||
$("#red-ui-header-shade").hide();
|
||||
$("#red-ui-editor-shade").hide();
|
||||
$("#red-ui-palette-shade").hide();
|
||||
$("#red-ui-sidebar-shade").hide();
|
||||
}
|
||||
function deployButtonSetBusy(){
|
||||
$(".red-ui-deploy-button-content").css('opacity',0);
|
||||
$(".red-ui-deploy-button-spinner").show();
|
||||
$("#red-ui-header-button-deploy").addClass("disabled");
|
||||
}
|
||||
function deployButtonClearBusy(){
|
||||
$(".red-ui-deploy-button-content").css('opacity',1);
|
||||
$(".red-ui-deploy-button-spinner").hide();
|
||||
}
|
||||
function stopStartFlows(state) {
|
||||
const startTime = Date.now()
|
||||
const deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled")
|
||||
deployInflight = true
|
||||
deployButtonSetBusy()
|
||||
shadeShow()
|
||||
$.ajax({
|
||||
url:"flows/state",
|
||||
type: "POST",
|
||||
data: {state: state}
|
||||
}).done(function(data,textStatus,xhr) {
|
||||
if (deployWasEnabled) {
|
||||
$("#red-ui-header-button-deploy").removeClass("disabled")
|
||||
}
|
||||
}).fail(function(xhr,textStatus,err) {
|
||||
if (deployWasEnabled) {
|
||||
$("#red-ui-header-button-deploy").removeClass("disabled")
|
||||
}
|
||||
if (xhr.status === 401) {
|
||||
RED.notify(RED._("notification.error", { message: RED._("user.notAuthorized") }), "error")
|
||||
} else if (xhr.responseText) {
|
||||
const errorDetail = { message: err ? (err + "") : "" }
|
||||
try {
|
||||
errorDetail.message = JSON.parse(xhr.responseText).message
|
||||
} finally {
|
||||
errorDetail.message = errorDetail.message || xhr.responseText
|
||||
}
|
||||
RED.notify(RED._("notification.error", errorDetail), "error")
|
||||
} else {
|
||||
RED.notify(RED._("notification.error", { message: RED._("deploy.errors.noResponse") }), "error")
|
||||
}
|
||||
}).always(function() {
|
||||
const delta = Math.max(0, 300 - (Date.now() - startTime))
|
||||
setTimeout(function () {
|
||||
deployButtonClearBusy()
|
||||
shadeHide()
|
||||
deployInflight = false
|
||||
}, delta);
|
||||
});
|
||||
}
|
||||
function restart() {
|
||||
var startTime = Date.now();
|
||||
var deployWasEnabled = !$("#red-ui-header-button-deploy").hasClass("disabled");
|
||||
deployInflight = true;
|
||||
deployButtonSetBusy();
|
||||
$.ajax({
|
||||
url:"flows",
|
||||
type: "POST",
|
||||
@ -307,15 +368,10 @@ RED.deploy = (function() {
|
||||
RED.notify(RED._("deploy.deployFailed",{message:RED._("deploy.errors.noResponse")}),"error");
|
||||
}
|
||||
}).always(function() {
|
||||
deployInflight = false;
|
||||
var delta = Math.max(0,300-(Date.now()-startTime));
|
||||
setTimeout(function() {
|
||||
$(".red-ui-deploy-button-content").css('opacity',1);
|
||||
$(".red-ui-deploy-button-spinner").hide();
|
||||
$("#red-ui-header-shade").hide();
|
||||
$("#red-ui-editor-shade").hide();
|
||||
$("#red-ui-palette-shade").hide();
|
||||
$("#red-ui-sidebar-shade").hide();
|
||||
deployButtonClearBusy();
|
||||
deployInflight = false;
|
||||
},delta);
|
||||
});
|
||||
}
|
||||
@ -450,21 +506,14 @@ RED.deploy = (function() {
|
||||
const nns = RED.nodes.createCompleteNodeSet();
|
||||
const startTime = Date.now();
|
||||
|
||||
$(".red-ui-deploy-button-content").css('opacity', 0);
|
||||
$(".red-ui-deploy-button-spinner").show();
|
||||
$("#red-ui-header-button-deploy").addClass("disabled");
|
||||
|
||||
deployButtonSetBusy();
|
||||
const data = { flows: nns };
|
||||
|
||||
if (!force) {
|
||||
data.rev = RED.nodes.version();
|
||||
}
|
||||
|
||||
deployInflight = true;
|
||||
$("#red-ui-header-shade").show();
|
||||
$("#red-ui-editor-shade").show();
|
||||
$("#red-ui-palette-shade").show();
|
||||
$("#red-ui-sidebar-shade").show();
|
||||
shadeShow();
|
||||
$.ajax({
|
||||
url: "flows",
|
||||
type: "POST",
|
||||
@ -550,15 +599,11 @@ RED.deploy = (function() {
|
||||
RED.notify(RED._("deploy.deployFailed", { message: RED._("deploy.errors.noResponse") }), "error");
|
||||
}
|
||||
}).always(function () {
|
||||
deployInflight = false;
|
||||
const delta = Math.max(0, 300 - (Date.now() - startTime));
|
||||
setTimeout(function () {
|
||||
$(".red-ui-deploy-button-content").css('opacity', 1);
|
||||
$(".red-ui-deploy-button-spinner").hide();
|
||||
$("#red-ui-header-shade").hide();
|
||||
$("#red-ui-editor-shade").hide();
|
||||
$("#red-ui-palette-shade").hide();
|
||||
$("#red-ui-sidebar-shade").hide();
|
||||
deployInflight = false;
|
||||
deployButtonClearBusy()
|
||||
shadeHide()
|
||||
}, delta);
|
||||
});
|
||||
}
|
||||
|
@ -4877,6 +4877,9 @@ RED.view = (function() {
|
||||
if (d._def.button) {
|
||||
var buttonEnabled = isButtonEnabled(d);
|
||||
this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-disabled", !buttonEnabled);
|
||||
if (RED.runtime && Object.hasOwn(RED.runtime,'started')) {
|
||||
this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-stopped", !RED.runtime.started);
|
||||
}
|
||||
|
||||
var x = d._def.align == "right"?d.w-6:-25;
|
||||
if (d._def.button.toggle && !d[d._def.button.toggle]) {
|
||||
|
@ -176,6 +176,13 @@
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
&.red-ui-flow-node-button-stopped {
|
||||
opacity: 0.4;
|
||||
.red-ui-flow-node-button-button {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.red-ui-flow-node-button-button {
|
||||
cursor: pointer;
|
||||
|
@ -219,7 +219,7 @@
|
||||
span.red-ui-menu-sublabel {
|
||||
color: var(--red-ui-header-menu-sublabel-color);
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
display: block;
|
||||
text-indent: 0px;
|
||||
}
|
||||
}
|
||||
|
@ -255,5 +255,82 @@ var api = module.exports = {
|
||||
}
|
||||
}
|
||||
return sendCredentials;
|
||||
},
|
||||
/**
|
||||
* Gets running state of runtime flows
|
||||
* @param {Object} opts
|
||||
* @param {User} opts.user - the user calling the api
|
||||
* @param {Object} opts.req - the request to log (optional)
|
||||
* @return {{state:string, started:boolean}} - the current run state of the flows
|
||||
* @memberof @node-red/runtime_flows
|
||||
*/
|
||||
getState: async function(opts) {
|
||||
runtime.log.audit({event: "flows.getState"}, opts.req);
|
||||
const result = {
|
||||
state: runtime.flows.state()
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Sets running state of runtime flows
|
||||
* @param {Object} opts
|
||||
* @param {Object} opts.req - the request to log (optional)
|
||||
* @param {User} opts.user - the user calling the api
|
||||
* @param {string} opts.state - the requested state. Valid values are "start" and "stop".
|
||||
* @return {Promise<Flow>} - the active flow configuration
|
||||
* @memberof @node-red/runtime_flows
|
||||
*/
|
||||
setState: async function(opts) {
|
||||
opts = opts || {};
|
||||
const makeError = (error, errcode, statusCode) => {
|
||||
const message = typeof error == "object" ? error.message : error
|
||||
const err = typeof error == "object" ? error : new Error(message||"Unexpected Error")
|
||||
err.status = err.status || statusCode || 400;
|
||||
err.code = err.code || errcode || "unexpected_error"
|
||||
runtime.log.audit({
|
||||
event: "flows.setState",
|
||||
state: opts.state || "",
|
||||
error: errcode || "unexpected_error",
|
||||
message: err.code
|
||||
}, opts.req);
|
||||
return err
|
||||
}
|
||||
|
||||
const getState = () => {
|
||||
return {
|
||||
state: runtime.flows.state()
|
||||
}
|
||||
}
|
||||
|
||||
if(!runtime.settings.runtimeState || runtime.settings.runtimeState.enabled !== true) {
|
||||
throw (makeError("Method Not Allowed", "not_allowed", 405))
|
||||
}
|
||||
switch (opts.state) {
|
||||
case "start":
|
||||
try {
|
||||
try {
|
||||
runtime.settings.set('runtimeFlowState', opts.state);
|
||||
} catch(err) {}
|
||||
if (runtime.settings.safeMode) {
|
||||
delete runtime.settings.safeMode
|
||||
}
|
||||
await runtime.flows.startFlows("full")
|
||||
return getState()
|
||||
} catch (err) {
|
||||
throw (makeError(err, err.code, 500))
|
||||
}
|
||||
case "stop":
|
||||
try {
|
||||
try {
|
||||
runtime.settings.set('runtimeFlowState', opts.state);
|
||||
} catch(err) {}
|
||||
await runtime.flows.stopFlows("full")
|
||||
return getState()
|
||||
} catch (err) {
|
||||
throw (makeError(err, err.code, 500))
|
||||
}
|
||||
default:
|
||||
throw (makeError(`Cannot change flows runtime state to '${opts.state}'}`, "invalid_run_state", 400))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -148,6 +148,18 @@ var api = module.exports = {
|
||||
enabled: (runtime.settings.diagnostics && runtime.settings.diagnostics.enabled === false) ? false : true,
|
||||
ui: (runtime.settings.diagnostics && runtime.settings.diagnostics.ui === false) ? false : true
|
||||
}
|
||||
if(safeSettings.diagnostics.enabled === false) {
|
||||
safeSettings.diagnostics.ui = false; // cannot have UI without endpoint
|
||||
}
|
||||
|
||||
safeSettings.runtimeState = {
|
||||
//unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false.
|
||||
enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true,
|
||||
ui: !!runtime.settings.runtimeState && runtime.settings.runtimeState.ui === true
|
||||
}
|
||||
if(safeSettings.runtimeState.enabled !== true) {
|
||||
safeSettings.runtimeState.ui = false; // cannot have UI without endpoint
|
||||
}
|
||||
|
||||
runtime.settings.exportNodeSettings(safeSettings);
|
||||
runtime.plugins.exportPluginSettings(safeSettings);
|
||||
|
@ -36,6 +36,8 @@ var activeFlowConfig = null;
|
||||
|
||||
var activeFlows = {};
|
||||
var started = false;
|
||||
var state = 'stop'
|
||||
|
||||
var credentialsPendingReset = false;
|
||||
|
||||
var activeNodesToFlow = {};
|
||||
@ -50,6 +52,7 @@ function init(runtime) {
|
||||
storage = runtime.storage;
|
||||
log = runtime.log;
|
||||
started = false;
|
||||
state = 'stop';
|
||||
if (!typeEventRegistered) {
|
||||
events.on('type-registered',function(type) {
|
||||
if (activeFlowConfig && activeFlowConfig.missingTypes.length > 0) {
|
||||
@ -214,19 +217,26 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) {
|
||||
// Flows are running (or should be)
|
||||
|
||||
// Stop the active flows (according to deploy type and the diff)
|
||||
return stop(type,diff,muteLog).then(() => {
|
||||
return stop(type,diff,muteLog,true).then(() => {
|
||||
// Once stopped, allow context to remove anything no longer needed
|
||||
return context.clean(activeFlowConfig)
|
||||
}).then(() => {
|
||||
if (!isLoad) {
|
||||
log.info(log._("nodes.flows.updated-flows"));
|
||||
}
|
||||
// Start the active flows
|
||||
start(type,diff,muteLog).then(() => {
|
||||
start(type,diff,muteLog,true).then(() => {
|
||||
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true});
|
||||
});
|
||||
// Return the new revision asynchronously to the actual start
|
||||
return flowRevision;
|
||||
}).catch(function(err) { })
|
||||
} else {
|
||||
if (!isLoad) {
|
||||
log.info(log._("nodes.flows.updated-flows"));
|
||||
}
|
||||
events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true});
|
||||
return flowRevision;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -259,9 +269,10 @@ function getFlows() {
|
||||
return activeConfig;
|
||||
}
|
||||
|
||||
async function start(type,diff,muteLog) {
|
||||
type = type||"full";
|
||||
async function start(type,diff,muteLog,isDeploy) {
|
||||
type = type || "full";
|
||||
started = true;
|
||||
state = 'start'
|
||||
var i;
|
||||
// If there are missing types, report them, emit the necessary runtime event and return
|
||||
if (activeFlowConfig.missingTypes.length > 0) {
|
||||
@ -283,7 +294,7 @@ async function start(type,diff,muteLog) {
|
||||
log.info(log._("nodes.flows.missing-type-install-2"));
|
||||
log.info(" "+settings.userDir);
|
||||
}
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true});
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -297,7 +308,7 @@ async function start(type,diff,muteLog) {
|
||||
missingModules.push({module:err[i].module.module, error: err[i].error.code || err[i].error.toString()})
|
||||
log.info(` - ${err[i].module.spec} [${err[i].error.code || "unknown_error"}]`);
|
||||
}
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{state: 'stop', error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -306,10 +317,23 @@ async function start(type,diff,muteLog) {
|
||||
log.info("*****************************************************************")
|
||||
log.info(log._("nodes.flows.safe-mode"));
|
||||
log.info("*****************************************************************")
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true});
|
||||
state = 'safe'
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{state: 'safe', error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true});
|
||||
return;
|
||||
}
|
||||
|
||||
let runtimeState
|
||||
try {
|
||||
runtimeState = settings.get('runtimeFlowState') || 'start'
|
||||
} catch (err) {}
|
||||
if (runtimeState === 'stop') {
|
||||
log.info(log._("nodes.flows.stopped-flows"));
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{ state: 'stop', deploy:isDeploy },retain:true});
|
||||
state = 'stop'
|
||||
started = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!muteLog) {
|
||||
if (type !== "full") {
|
||||
log.info(log._("nodes.flows.starting-modified-"+type));
|
||||
@ -364,12 +388,10 @@ async function start(type,diff,muteLog) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Having created or updated all flows, now start them.
|
||||
for (id in activeFlows) {
|
||||
if (activeFlows.hasOwnProperty(id)) {
|
||||
try {
|
||||
activeFlows[id].start(diff);
|
||||
|
||||
// Create a map of node id to flow id and also a subflowInstance lookup map
|
||||
var activeNodes = activeFlows[id].getActiveNodes();
|
||||
Object.keys(activeNodes).forEach(function(nid) {
|
||||
@ -387,7 +409,7 @@ async function start(type,diff,muteLog) {
|
||||
if (credentialsPendingReset === true) {
|
||||
credentialsPendingReset = false;
|
||||
} else {
|
||||
events.emit("runtime-event",{id:"runtime-state",retain:true});
|
||||
events.emit("runtime-event",{id:"runtime-state", payload:{ state: 'start', deploy:isDeploy}, retain:true});
|
||||
}
|
||||
|
||||
if (!muteLog) {
|
||||
@ -400,7 +422,7 @@ async function start(type,diff,muteLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
function stop(type,diff,muteLog) {
|
||||
function stop(type,diff,muteLog,isDeploy) {
|
||||
if (!started) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@ -420,6 +442,7 @@ function stop(type,diff,muteLog) {
|
||||
}
|
||||
}
|
||||
started = false;
|
||||
state = 'stop'
|
||||
var promises = [];
|
||||
var stopList;
|
||||
var removedList = diff.removed;
|
||||
@ -471,6 +494,8 @@ function stop(type,diff,muteLog) {
|
||||
}
|
||||
}
|
||||
events.emit("flows:stopped",{config: activeConfig, type: type, diff: diff});
|
||||
|
||||
events.emit("runtime-event",{ id:"runtime-state", payload:{ state: 'stop', deploy:isDeploy }, retain:true });
|
||||
// Deprecated event
|
||||
events.emit("nodes-stopped");
|
||||
});
|
||||
@ -790,7 +815,7 @@ module.exports = {
|
||||
stopFlows: stop,
|
||||
|
||||
get started() { return started },
|
||||
|
||||
state: () => { return state },
|
||||
// handleError: handleError,
|
||||
// handleStatus: handleStatus,
|
||||
|
||||
|
@ -215,7 +215,7 @@ function start() {
|
||||
}
|
||||
}
|
||||
return redNodes.loadContextsPlugin().then(function () {
|
||||
redNodes.loadFlows().then(redNodes.startFlows).catch(function(err) {});
|
||||
redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {});
|
||||
started = true;
|
||||
});
|
||||
});
|
||||
|
@ -122,6 +122,7 @@
|
||||
"stopped-flows": "Stopped flows",
|
||||
"stopped": "Stopped",
|
||||
"stopping-error": "Error stopping node: __message__",
|
||||
"updated-flows": "Updated flows",
|
||||
"added-flow": "Adding flow: __label__",
|
||||
"updated-flow": "Updated flow: __label__",
|
||||
"removed-flow": "Removed flow: __label__",
|
||||
|
15
packages/node_modules/node-red/settings.js
vendored
15
packages/node_modules/node-red/settings.js
vendored
@ -242,6 +242,7 @@ module.exports = {
|
||||
/*******************************************************************************
|
||||
* Runtime Settings
|
||||
* - lang
|
||||
* - runtimeState
|
||||
* - diagnostics
|
||||
* - logging
|
||||
* - contextStorage
|
||||
@ -267,7 +268,19 @@ module.exports = {
|
||||
/** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */
|
||||
ui: true,
|
||||
},
|
||||
|
||||
/** Configure runtimeState options
|
||||
* - enabled: When `enabled` is `true` flows runtime can be Started/Stoped
|
||||
* by POSTing to available at http://localhost:1880/flows/state
|
||||
* - ui: When `ui` is `true`, the action `core:start-flows` and
|
||||
* `core:stop-flows` will be available to logged in users of node-red editor
|
||||
* Also, the deploy menu (when set to default) will show a stop or start button
|
||||
*/
|
||||
runtimeState: {
|
||||
/** enable or disable flows/state endpoint. Must be set to `false` to disable */
|
||||
enabled: false,
|
||||
/** show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */
|
||||
ui: false,
|
||||
},
|
||||
/** Configure the logging output */
|
||||
logging: {
|
||||
/** Only console logging is currently supported */
|
||||
|
@ -32,7 +32,9 @@ describe("api/admin/flows", function() {
|
||||
app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.get("/flows",flows.get);
|
||||
app.get("/flows/state",flows.getState);
|
||||
app.post("/flows",flows.post);
|
||||
app.post("/flows/state",flows.postState);
|
||||
});
|
||||
|
||||
it('returns flow - v1', function(done) {
|
||||
@ -208,4 +210,99 @@ describe("api/admin/flows", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('returns flows run state', function (done) {
|
||||
var setFlows = sinon.spy(function () { return Promise.resolve(); });
|
||||
flows.init({
|
||||
flows: {
|
||||
setFlows,
|
||||
getState: async function () {
|
||||
return { started: true, state: "started" };
|
||||
}
|
||||
}
|
||||
});
|
||||
request(app)
|
||||
.get('/flows/state')
|
||||
.set('Accept', 'application/json')
|
||||
.set('Node-RED-Deployment-Type', 'reload')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
try {
|
||||
res.body.should.have.a.property('started', true);
|
||||
res.body.should.have.a.property('state', "started");
|
||||
done();
|
||||
} catch (e) {
|
||||
return done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
it('sets flows run state - stopped', function (done) {
|
||||
var setFlows = sinon.spy(function () { return Promise.resolve(); });
|
||||
flows.init({
|
||||
flows: {
|
||||
setFlows: setFlows,
|
||||
getState: async function () {
|
||||
return { started: true, state: "started" };
|
||||
},
|
||||
setState: async function () {
|
||||
return { started: false, state: "stopped" };
|
||||
},
|
||||
}
|
||||
});
|
||||
request(app)
|
||||
.post('/flows/state')
|
||||
.set('Accept', 'application/json')
|
||||
.send({state:'stop'})
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
try {
|
||||
res.body.should.have.a.property('started', false);
|
||||
res.body.should.have.a.property('state', "stopped");
|
||||
done();
|
||||
} catch (e) {
|
||||
return done(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
it('sets flows run state - bad value', function (done) {
|
||||
var setFlows = sinon.spy(function () { return Promise.resolve(); });
|
||||
const makeError = (error, errcode, statusCode) => {
|
||||
const message = typeof error == "object" ? error.message : error
|
||||
const err = typeof error == "object" ? error : new Error(message||"Unexpected Error")
|
||||
err.status = err.status || statusCode || 400;
|
||||
err.code = err.code || errcode || "unexpected_error"
|
||||
return err
|
||||
}
|
||||
flows.init({
|
||||
flows: {
|
||||
setFlows: setFlows,
|
||||
getState: async function () {
|
||||
return { started: true, state: "started" };
|
||||
},
|
||||
setState: async function () {
|
||||
var err = (makeError("Cannot set runtime state. Invalid state", "invalid_run_state", 400))
|
||||
var p = Promise.reject(err);
|
||||
p.catch(()=>{});
|
||||
return p;
|
||||
},
|
||||
}
|
||||
});
|
||||
request(app)
|
||||
.post('/flows/state')
|
||||
.set('Accept', 'application/json')
|
||||
.send({state:'bad-state'})
|
||||
.expect(400)
|
||||
.end(function(err,res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
res.body.should.have.property("code","invalid_run_state");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -427,4 +427,123 @@ describe("runtime-api/flows", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("flow run state", function() {
|
||||
var startFlows, stopFlows, runtime;
|
||||
beforeEach(function() {
|
||||
let flowsStarted = true;
|
||||
let flowsState = "start";
|
||||
startFlows = sinon.spy(function(type) {
|
||||
if (type !== "full") {
|
||||
var err = new Error();
|
||||
// TODO: quirk of internal api - uses .code for .status
|
||||
err.code = 400;
|
||||
var p = Promise.reject(err);
|
||||
p.catch(()=>{});
|
||||
return p;
|
||||
}
|
||||
flowsStarted = true;
|
||||
flowsState = "start";
|
||||
return Promise.resolve();
|
||||
});
|
||||
stopFlows = sinon.spy(function(type) {
|
||||
if (type !== "full") {
|
||||
var err = new Error();
|
||||
// TODO: quirk of internal api - uses .code for .status
|
||||
err.code = 400;
|
||||
var p = Promise.reject(err);
|
||||
p.catch(()=>{});
|
||||
return p;
|
||||
}
|
||||
flowsStarted = false;
|
||||
flowsState = "stop";
|
||||
return Promise.resolve();
|
||||
});
|
||||
runtime = {
|
||||
log: mockLog(),
|
||||
settings: {
|
||||
runtimeState: {
|
||||
enabled: true,
|
||||
ui: true,
|
||||
},
|
||||
},
|
||||
flows: {
|
||||
get started() {
|
||||
return flowsStarted;
|
||||
},
|
||||
startFlows,
|
||||
stopFlows,
|
||||
getFlows: function() { return {rev:"currentRev",flows:[]} },
|
||||
state: function() { return flowsState}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("gets flows run state", async function() {
|
||||
flows.init(runtime);
|
||||
const state = await flows.getState({})
|
||||
state.should.have.property("state", "start")
|
||||
});
|
||||
it("permits getting flows run state when setting disabled", async function() {
|
||||
runtime.settings.runtimeState.enabled = false;
|
||||
flows.init(runtime);
|
||||
const state = await flows.getState({})
|
||||
state.should.have.property("state", "start")
|
||||
});
|
||||
it("start flows", async function() {
|
||||
flows.init(runtime);
|
||||
const state = await flows.setState({state:"start"})
|
||||
state.should.have.property("state", "start")
|
||||
stopFlows.called.should.not.be.true();
|
||||
startFlows.called.should.be.true();
|
||||
});
|
||||
it("stop flows", async function() {
|
||||
flows.init(runtime);
|
||||
const state = await flows.setState({state:"stop"})
|
||||
state.should.have.property("state", "stop")
|
||||
stopFlows.called.should.be.true();
|
||||
startFlows.called.should.not.be.true();
|
||||
});
|
||||
it("rejects starting flows when setting disabled", async function() {
|
||||
let err;
|
||||
runtime.settings.runtimeState.enabled = false;
|
||||
flows.init(runtime);
|
||||
try {
|
||||
await flows.setState({state:"start"})
|
||||
} catch (error) {
|
||||
err = error
|
||||
}
|
||||
stopFlows.called.should.not.be.true();
|
||||
startFlows.called.should.not.be.true();
|
||||
should(err).have.property("code", "not_allowed")
|
||||
should(err).have.property("status", 405)
|
||||
});
|
||||
it("rejects stopping flows when setting disabled", async function() {
|
||||
let err;
|
||||
runtime.settings.runtimeState.enabled = false;
|
||||
flows.init(runtime);
|
||||
try {
|
||||
await flows.setState({state:"stop"})
|
||||
} catch (error) {
|
||||
err = error
|
||||
}
|
||||
stopFlows.called.should.not.be.true();
|
||||
startFlows.called.should.not.be.true();
|
||||
should(err).have.property("code", "not_allowed")
|
||||
should(err).have.property("status", 405)
|
||||
});
|
||||
it("rejects setting invalid flows run state", async function() {
|
||||
let err;
|
||||
flows.init(runtime);
|
||||
try {
|
||||
await flows.setState({state:"bad-state"})
|
||||
} catch (error) {
|
||||
err = error
|
||||
}
|
||||
stopFlows.called.should.not.be.true();
|
||||
startFlows.called.should.not.be.true();
|
||||
should(err).have.property("code", "invalid_run_state")
|
||||
should(err).have.property("status", 400)
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -396,12 +396,13 @@ describe('flows/index', function() {
|
||||
try {
|
||||
flowCreate.called.should.be.false();
|
||||
receivedEvent.should.have.property('id','runtime-state');
|
||||
receivedEvent.should.have.property('payload',
|
||||
{ error: 'missing-modules',
|
||||
receivedEvent.should.have.property('payload', {
|
||||
state: 'stop',
|
||||
error: 'missing-modules',
|
||||
type: 'warning',
|
||||
text: 'notification.warnings.missing-modules',
|
||||
modules: [] }
|
||||
);
|
||||
modules: []
|
||||
});
|
||||
|
||||
done();
|
||||
}catch(err) {
|
||||
|
Loading…
Reference in New Issue
Block a user