/** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ RED.deploy = (function() { var deploymentTypes = { "full":{img:"red/images/deploy-full-o.svg"}, "nodes":{img:"red/images/deploy-nodes-o.svg"}, "flows":{img:"red/images/deploy-flows-o.svg"} } var ignoreDeployWarnings = { unknown: false, unusedConfig: false, invalid: false } var deploymentType = "full"; var deployInflight = false; var currentDiff = null; function changeDeploymentType(type) { deploymentType = type; $("#red-ui-header-button-deploy-icon").attr("src",deploymentTypes[type].img); } /** * options: * type: "default" - Button with drop-down options - no further customisation available * type: "simple" - Button without dropdown. Customisations: * label: the text to display - default: "Deploy" * icon : the icon to use. Null removes the icon. default: "red/images/deploy-full-o.svg" */ function init(options) { options = options || {}; var type = options.type || "default"; if (type == "default") { $('
  • '+ ''+ ''+ ' '+ ''+RED._("deploy.deploy")+''+ ''+ ''+ ''+ ''+ ''+ ''+ '
  • ').prependTo(".red-ui-header-toolbar"); 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 !== false) { 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'; if (options.hasOwnProperty('icon')) { icon = options.icon; } $('
  • '+ ''+ ''+ (icon?' ':'')+ ''+label+''+ ''+ ''+ ''+ ''+ ''+ '
  • ').prependTo(".red-ui-header-toolbar"); } $('#red-ui-header-button-deploy').on("click", function(event) { event.preventDefault(); save(); }); RED.actions.add("core:deploy-flows",save); if (type === "default") { 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); }); RED.actions.add("core:set-deploy-type-to-modified-nodes",function() { RED.menu.setSelected("deploymenu-item-node",true); }); } RED.events.on('workspace:dirty',function(state) { if (state.dirty) { window.onbeforeunload = function() { return RED._("deploy.confirm.undeployedChanges"); } $("#red-ui-header-button-deploy").removeClass("disabled"); } else { window.onbeforeunload = null; $("#red-ui-header-button-deploy").addClass("disabled"); } }); var activeNotifyMessage; RED.comms.subscribe("notification/runtime-deploy",function(topic,msg) { if (!activeNotifyMessage) { var currentRev = RED.nodes.version(); if (currentRev === null || deployInflight || currentRev === msg.revision) { return; } var message = $('

    ').text(RED._('deploy.confirm.backgroundUpdate')); activeNotifyMessage = RED.notify(message,{ modal: true, fixed: true, buttons: [ { text: RED._('deploy.confirm.button.ignore'), click: function() { activeNotifyMessage.close(); activeNotifyMessage = null; } }, { text: RED._('deploy.confirm.button.review'), class: "primary", click: function() { activeNotifyMessage.close(); var nns = RED.nodes.createCompleteNodeSet(); resolveConflict(nns,false); activeNotifyMessage = null; } } ] }); } }); } function getNodeInfo(node) { var tabLabel = ""; if (node.z) { var tab = RED.nodes.workspace(node.z); if (!tab) { tab = RED.nodes.subflow(node.z); tabLabel = tab.name; } else { tabLabel = tab.label; } } var label = RED.utils.getNodeLabel(node,node.id); return {tab:tabLabel,type:node.type,label:label}; } function sortNodeInfo(A,B) { if (A.tab < B.tab) { return -1;} if (A.tab > B.tab) { return 1;} if (A.type < B.type) { return -1;} if (A.type > B.type) { return 1;} if (A.name < B.name) { return -1;} if (A.name > B.name) { return 1;} return 0; } function resolveConflict(currentNodes, activeDeploy) { var message = $('

    '); $('

    ').appendTo(message); var conflictCheck = $('
    '+ '
    '+ '
    ').appendTo(message); var conflictAutoMerge = $('
    '+ '
    '+ '
    ').hide().appendTo(message); var conflictManualMerge = $('
    '+ '
    '+ '
    ').hide().appendTo(message); message.i18n(); currentDiff = null; var buttons = [ { text: RED._("common.label.cancel"), click: function() { conflictNotification.close(); } }, { id: "red-ui-deploy-dialog-confirm-deploy-review", text: RED._("deploy.confirm.button.review"), class: "primary disabled", click: function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-review").hasClass('disabled')) { RED.diff.showRemoteDiff(); conflictNotification.close(); } } }, { id: "red-ui-deploy-dialog-confirm-deploy-merge", text: RED._("deploy.confirm.button.merge"), class: "primary disabled", click: function() { if (!$("#red-ui-deploy-dialog-confirm-deploy-merge").hasClass('disabled')) { RED.diff.mergeDiff(currentDiff); conflictNotification.close(); } } } ]; if (activeDeploy) { buttons.push({ id: "red-ui-deploy-dialog-confirm-deploy-overwrite", text: RED._("deploy.confirm.button.overwrite"), class: "primary", click: function() { save(true,activeDeploy); conflictNotification.close(); } }) } var conflictNotification = RED.notify(message,{ modal: true, fixed: true, width: 600, buttons: buttons }); var now = Date.now(); RED.diff.getRemoteDiff(function(diff) { var ellapsed = Math.max(1000 - (Date.now()-now), 0); currentDiff = diff; setTimeout(function() { conflictCheck.hide(); var d = Object.keys(diff.conflicts); if (d.length === 0) { conflictAutoMerge.show(); $("#red-ui-deploy-dialog-confirm-deploy-merge").removeClass('disabled') } else { conflictManualMerge.show(); } $("#red-ui-deploy-dialog-confirm-deploy-review").removeClass('disabled') },ellapsed); }) } function cropList(list) { if (list.length > 5) { var remainder = list.length - 5; list = list.slice(0,5); list.push(RED._("deploy.confirm.plusNMore",{count:remainder})); } return list; } function sanitize(html) { return html.replace(/&/g,"&").replace(//g,">") } 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(); RED.runtime.updateState(state); $.ajax({ url:"flows/state", type: "POST", data: {state: state}, headers: { "Node-RED-Flow-Run-State-Change": state } }).done(function(data,textStatus,xhr) { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled"); } RED.runtime.updateState((data && data.state) || "unknown" ) RED.notify('

    Done

    ',"success"); }).fail(function(xhr,textStatus,err) { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled"); } if (xhr.status === 401) { RED.notify("Not authorized" ,"error"); } else if (xhr.responseText) { RED.notify("Operation failed: " + xhr.responseText,"error"); } else { RED.notify("Operation failed: no response","error"); } RED.runtime.requestState() }).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", headers: { "Node-RED-Deployment-Type":"reload" } }).done(function(data,textStatus,xhr) { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled"); } RED.notify('

    '+RED._("deploy.successfulRestart")+'

    ',"success"); }).fail(function(xhr,textStatus,err) { if (deployWasEnabled) { $("#red-ui-header-button-deploy").removeClass("disabled"); } if (xhr.status === 401) { RED.notify(RED._("deploy.deployFailed",{message:RED._("user.notAuthorized")}),"error"); } else if (xhr.status === 409) { resolveConflict(nns, true); } else if (xhr.responseText) { RED.notify(RED._("deploy.deployFailed",{message:xhr.responseText}),"error"); } else { RED.notify(RED._("deploy.deployFailed",{message:RED._("deploy.errors.noResponse")}),"error"); } }).always(function() { var delta = Math.max(0,300-(Date.now()-startTime)); setTimeout(function() { deployButtonClearBusy(); deployInflight = false; },delta); }); } function save(skipValidation, force) { if ($("#red-ui-header-button-deploy").hasClass("disabled")) { return; //deploy is disabled } if ($("#red-ui-header-shade").is(":visible")) { return; //deploy is shaded } if (!RED.user.hasPermission("flows.write")) { RED.notify(RED._("user.errors.deploy"), "error"); return; } let hasUnusedConfig = false; if (!skipValidation) { let hasUnknown = false; let hasInvalid = false; const unknownNodes = []; const invalidNodes = []; RED.nodes.eachConfig(function (node) { if (node.valid === undefined) { RED.editor.validateNode(node); } if (!node.valid && !node.d) { invalidNodes.push(getNodeInfo(node)); } if (node.type === "unknown") { if (unknownNodes.indexOf(node.name) == -1) { unknownNodes.push(node.name); } } }); RED.nodes.eachNode(function (node) { if (!node.valid && !node.d) { invalidNodes.push(getNodeInfo(node)); } if (node.type === "unknown") { if (unknownNodes.indexOf(node.name) == -1) { unknownNodes.push(node.name); } } }); hasUnknown = unknownNodes.length > 0; hasInvalid = invalidNodes.length > 0; const unusedConfigNodes = []; RED.nodes.eachConfig(function (node) { if ((node._def.hasUsers !== false) && (node.users.length === 0)) { unusedConfigNodes.push(getNodeInfo(node)); hasUnusedConfig = true; } }); let showWarning = false; let notificationMessage; let notificationButtons = []; let notification; if (hasUnknown && !ignoreDeployWarnings.unknown) { showWarning = true; notificationMessage = "

    " + RED._('deploy.confirm.unknown') + "

    " + '

    " + RED._('deploy.confirm.confirm') + "

    "; notificationButtons = [ { text: RED._("deploy.unknownNodesButton"), class: "pull-left", click: function() { notification.close(); RED.actions.invoke("core:search","type:unknown "); } }, { id: "red-ui-deploy-dialog-confirm-deploy-deploy", text: RED._("deploy.confirm.button.confirm"), class: "primary", click: function () { save(true); notification.close(); } } ]; } else if (hasInvalid && !ignoreDeployWarnings.invalid) { showWarning = true; invalidNodes.sort(sortNodeInfo); notificationMessage = "

    " + RED._('deploy.confirm.improperlyConfigured') + "

    " + '

    " + RED._('deploy.confirm.confirm') + "

    "; notificationButtons = [ { text: RED._("deploy.invalidNodesButton"), class: "pull-left", click: function() { notification.close(); RED.actions.invoke("core:search","is:invalid "); } }, { id: "red-ui-deploy-dialog-confirm-deploy-deploy", text: RED._("deploy.confirm.button.confirm"), class: "primary", click: function () { save(true); notification.close(); } } ]; } if (showWarning) { notificationButtons.unshift( { text: RED._("common.label.cancel"), click: function () { notification.close(); } } ); notification = RED.notify(notificationMessage, { modal: true, fixed: true, buttons: notificationButtons }); return; } } const nns = RED.nodes.createCompleteNodeSet(); const startTime = Date.now(); deployButtonSetBusy(); const data = { flows: nns }; data.runtimeState = RED.runtime.state; if (data.runtimeState === RED.runtime.states.STOPPED || force) { data._rev = RED.nodes.version(); } else { data.rev = RED.nodes.version(); } deployInflight = true; shadeShow(); $.ajax({ url: "flows", type: "POST", data: JSON.stringify(data), contentType: "application/json; charset=utf-8", headers: { "Node-RED-Deployment-Type": deploymentType } }).done(function (data, textStatus, xhr) { RED.nodes.dirty(false); RED.nodes.version(data.rev); RED.nodes.originalFlow(nns); if (hasUnusedConfig) { let notification; const opts = { type: "success", fixed: false, timeout: 6000, buttons: [ { text: RED._("deploy.unusedConfigNodesButton"), class: "pull-left", click: function() { notification.close(); RED.actions.invoke("core:search","is:config is:unused "); } }, { text: RED._("common.label.close"), class: "primary", click: function () { save(true); notification.close(); } } ] } notification = RED.notify( '

    ' + RED._("deploy.successfulDeploy") + '

    ' + '

    ' + RED._("deploy.unusedConfigNodes") + '

    ', opts); } else { RED.notify('

    ' + RED._("deploy.successfulDeploy") + '

    ', "success"); } RED.nodes.eachNode(function (node) { if (node.changed) { node.dirty = true; node.changed = false; } if (node.moved) { node.dirty = true; node.moved = false; } if (node.credentials) { delete node.credentials; } }); RED.nodes.eachConfig(function (confNode) { confNode.changed = false; if (confNode.credentials) { delete confNode.credentials; } }); RED.nodes.eachSubflow(function (subflow) { subflow.changed = false; }); RED.nodes.eachWorkspace(function (ws) { ws.changed = false; }); // Once deployed, cannot undo back to a clean state RED.history.markAllDirty(); RED.view.redraw(); RED.events.emit("deploy"); }).fail(function (xhr, textStatus, err) { RED.nodes.dirty(true); $("#red-ui-header-button-deploy").removeClass("disabled"); if (xhr.status === 401) { RED.notify(RED._("deploy.deployFailed", { message: RED._("user.notAuthorized") }), "error"); } else if (xhr.status === 409) { resolveConflict(nns, true); } else if (xhr.responseText) { RED.notify(RED._("deploy.deployFailed", { message: xhr.responseText }), "error"); } else { RED.notify(RED._("deploy.deployFailed", { message: RED._("deploy.errors.noResponse") }), "error"); } }).always(function () { const delta = Math.max(0, 300 - (Date.now() - startTime)); setTimeout(function () { deployInflight = false; deployButtonClearBusy() shadeHide() }, delta); }); } return { init: init, setDeployInflight: function(state) { deployInflight = state; } } })();