mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge pull request #3719 from node-red/pr_3642
Allow flows to be stopped and started manually
This commit is contained in:
		| @@ -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 | 
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
|  | ||||
| /**  | ||||
| /** | ||||
|  * An Interface to nodes and utility functions for creating/adding/deleting nodes and links | ||||
|  * @namespace RED.nodes | ||||
| */ | ||||
|   | ||||
| @@ -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,9 +476,9 @@ var RED = (function() { | ||||
|             } else if (persistentNotifications.hasOwnProperty(notificationId)) { | ||||
|                 persistentNotifications[notificationId].close(); | ||||
|                 delete persistentNotifications[notificationId]; | ||||
|                 if (notificationId === 'runtime-state') { | ||||
|                     RED.events.emit("runtime-state",msg); | ||||
|                 } | ||||
|             } | ||||
|             if (notificationId === 'runtime-state') { | ||||
|                 RED.events.emit("runtime-state",msg); | ||||
|             } | ||||
|         }); | ||||
|         RED.comms.subscribe("status/#",function(topic,msg) { | ||||
| @@ -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: [ | ||||
|                       {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"}, | ||||
|  | ||||
|                   ] | ||||
|               }); | ||||
|             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 | ||||
|             ] | ||||
|             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; | ||||
|                 }); | ||||
|             }); | ||||
| @@ -399,12 +399,12 @@ module.exports = { | ||||
|     * @memberof @node-red/runtime | ||||
|     */ | ||||
|     version: externalAPI.version, | ||||
|      | ||||
|  | ||||
|     /** | ||||
|     * @memberof @node-red/diagnostics | ||||
|     */ | ||||
|     diagnostics:externalAPI.diagnostics, | ||||
|      | ||||
|  | ||||
|     storage: storage, | ||||
|     events: events, | ||||
|     hooks: hooks, | ||||
|   | ||||
| @@ -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__", | ||||
|   | ||||
							
								
								
									
										17
									
								
								packages/node_modules/node-red/settings.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								packages/node_modules/node-red/settings.js
									
									
									
									
										vendored
									
									
								
							| @@ -242,6 +242,7 @@ module.exports = { | ||||
| /******************************************************************************* | ||||
|  * Runtime Settings | ||||
|  *  - lang | ||||
|  *  - runtimeState | ||||
|  *  - diagnostics | ||||
|  *  - logging | ||||
|  *  - contextStorage | ||||
| @@ -260,14 +261,26 @@ module.exports = { | ||||
|      *   be available at http://localhost:1880/diagnostics   | ||||
|      * - ui: When `ui` is `true` (or unset), the action `show-system-info` will  | ||||
|      *   be available to logged in users of node-red editor   | ||||
|      */ | ||||
|     */ | ||||
|     diagnostics: { | ||||
|         /** enable or disable diagnostics endpoint. Must be set to `false` to disable */ | ||||
|         enabled: true, | ||||
|         /** 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', | ||||
|                          type: 'warning', | ||||
|                          text: 'notification.warnings.missing-modules', | ||||
|                          modules: [] } | ||||
|                      ); | ||||
|                     receivedEvent.should.have.property('payload', { | ||||
|                         state: 'stop', | ||||
|                         error: 'missing-modules', | ||||
|                         type: 'warning', | ||||
|                         text: 'notification.warnings.missing-modules', | ||||
|                         modules: [] | ||||
|                      }); | ||||
|  | ||||
|                     done(); | ||||
|                 }catch(err) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user