UNPKG

node-red-contrib-flow-manager

Version:

Flow Manager separates your flow json to multiple files

525 lines (442 loc) 24.7 kB
<style> .toolbar-button { border: none; color: #eee; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: auto 0 auto 12pt; padding: 3.5pt; } .toolbar-button.toolbar-button-disabled { color: #999 !important; background-color: #444 !important; cursor: default !important; } .flow-manager-remote-button { background-color: #4b5627 !important; } button.flow-manager-remote-option { background-color: #e6f3e2; font-size: x-large !important; } button.flow-manager-remote-option:active{ background: #4e4e4b; } button.toolbar-button.flow-manager-filter-flows-button { background-color: #176670 !important; } #flow-manager-flows-dialog-options .ui-selecting { background: #feca40; } #flow-manager-flows-dialog-options .ui-selected { background: #7c9cec; } #flow-manager-flows-dialog-options { list-style-type: none; margin: 0; padding: 0; width: 60%; } #flow-manager-flows-dialog-options li { margin: 1px; padding: 0.2em; font-size: 1.0em; height: 18px} .flow-manager-diff-type { background-color: #ffffff; font-size: large; text-decoration: underline; } #flow-manager-remote-diff-dialog table, #flow-manager-remote-diff-dialog td, #flow-manager-remote-diff-dialog th { border: 1pt #3e3e3e solid; padding: 5pt; } .flow-manager-remote-diff-field-header { font-size: large; } </style> <script type="text/javascript"> (async ()=>{ // We use this function for ajax and not the jquery one ($.ajax) because on older nodereds, jquery version of this call does not return promise function ajax(options) { return new Promise(function (resolve, reject) { $.ajax(options).done(resolve).fail(reject); }); } const _nr1Header = $('ul.red-ui-header-toolbar'); if(_nr1Header.length) { _nr1Header.prepend(` <li> <button class="button-group red-ui-deploy-button toolbar-button flow-manager-filter-flows-button" href="#"> <i class="fa fa-filter"></i> Filter Flows</button> </button> </li> `); } else { // Add "filter flows" button $('.header-toolbar').prepend(` <span> <span> <button class="toolbar-button flow-manager-filter-flows-button"><i class="fa fa-filter"></i> Filter Flows</button> </span> </span> `); } $('body').prepend(` <div id="flow-manager-flows-dialog" style="text-align: center"> <div style="margin-bottom: 10pt"> <input id="filter-flows-all-flows-input" type="checkbox" name="all" value="all" /> <label for="filter-flows-all-flows-input">Load All</label> </div> <ol id="flow-manager-flows-dialog-options" style="width: 100%;"></ol> <br/> <button id="filter-flows-confirm">Confirm</button> </div>`); RED.comms.subscribe('flow-manager/flow-manager-envnodes-override-attempt', function (topic, envNodePropsChangedByUser) { let text = '<br/><br/><p>' // Revert properties on view (those defined by envnodes) for(const nodeId of Object.keys(envNodePropsChangedByUser)) { const node = RED.nodes.node(nodeId); const envNodeCfg = envNodePropsChangedByUser[nodeId]; text += '<hr/>'; text += `<div><span style="color:#7e7d2c">${node.name || node.label || 'Unnamed'} (${nodeId})</span></div>`; text += `<ul>`; for(const envNodeCfgKey of Object.keys(envNodeCfg)) { text += `<li><strong>${envNodeCfgKey}</strong>: attempt=<span style="color: #962d2d">${JSON.stringify(node[envNodeCfgKey])}</span> original=<span style="color: #55a23a">${JSON.stringify(envNodeCfg[envNodeCfgKey])}</span></li>` node[envNodeCfgKey] = envNodeCfg[envNodeCfgKey]; } text += `</ul>`; RED.editor.updateNodeProperties(node); RED.view.redraw(); } text += '<hr/>'; text += `</p>`; const myNotification = RED.notify( `Warning!<br/>Detected user attempt to change envnodes controlled properties.<br/>Don't worry, these properties were restored to envnodes values.${text}`, { type: "warn", timeout: 30000, buttons: [ { text: "Thanks", click: function(e) { myNotification.close(); } } ] }); }); let flowsToLoad = []; // Empty array means "all" $('#filter-flows-confirm').click(async function flowManagerDialogConfirmed() { const selectAllIsChecked = $('#filter-flows-all-flows-input').prop('checked'); const filterProgressDialog = $('#flow-manager-filter-progress-dialog'); const filterProgressBar = $('#flow-manager-filter-progress-bar'); filterProgressDialog.dialog({modal:true, title:'Filter Flows'}) filterProgressBar.progressbar({}); filterProgressBar.progressbar("option","value", false); try { await ajax({ url: 'flow-manager/filter-flows', contentType: "application/json", dataType:'json', data: JSON.stringify(selectAllIsChecked? [] : flowsToLoad), type: 'PUT' }); location.reload(); } catch (error) { alert(`Failed to filter algos ${error}`); } filterProgressDialog.dialog('close'); }); $('#filter-flows-all-flows-input').change(function flowManagerOnSelectAllChanged(target) { const allChecked = target.currentTarget.checked; $('.flow-manager-load-flow-select-item').css('display', allChecked?'none':''); }); var filterFlowsButton = document.getElementsByClassName('flow-manager-filter-flows-button')[0]; var selectFlowsDialog = document.getElementById('flow-manager-flows-dialog'); filterFlowsButton.addEventListener('click', function() { Promise.all([ ajax({url: 'flow-manager/flow-names'}), ajax({url: 'flow-manager/filter-flows'}).catch(e=>Promise.resolve([])) ]).then(([flowsArray, flowsFromConfig])=>{ flowsToLoad = flowsFromConfig; const $selectEl = $('#flow-manager-flows-dialog-options'); $selectEl.empty(); $selectEl.selectable({ stop: function() { flowsToLoad = []; $( ".ui-selected", this ).each(function() { const flowName = this.innerText; flowsToLoad.push(flowName); }); } }); flowsArray = flowsArray.map(flowName=>flowName.substring(0, flowName.lastIndexOf('.'))) flowsArray.forEach(flowName=>{ const thisFlowIsSelected = flowsFromConfig.indexOf(flowName) !== -1; $selectEl.append(` <li class="flow-manager-load-flow-select-item ui-widget-content ${thisFlowIsSelected?'ui-selected':''}"> ${flowName} </li> `); }); $( selectFlowsDialog ).dialog({title: "Select which flows should be loaded"}); $('#filter-flows-all-flows-input').prop('checked', !flowsFromConfig || !flowsFromConfig.length ) $('#filter-flows-all-flows-input').trigger('change') }).catch(alert); }); const flowManagerCfg = await ajax({url: 'flow-manager/cfg'}); if(flowManagerCfg?.remoteDeploy) { const _nr1Header = $('ul.red-ui-header-toolbar'); if(_nr1Header.length) { _nr1Header.prepend(` <li> <button class="button-group red-ui-deploy-button toolbar-button flow-manager-remote-button" href="#"> <i class="fa fa-rocket"></i> Remote Deploy</button> </button> </li> `); } else { $('.header-toolbar').prepend(` <span> <span> <button class="toolbar-button flow-manager-remote-button"><i class="fa fa-rocket"></i> Remote Deploy</button> </span> </span> `); } // Add remotes list popup $('body').prepend(` <div id="flow-manager-remote-dialog" style="width: 100%; text-align: center"> <p> ${flowManagerCfg.remoteDeploy.remotes.map(remote=>{ return `<button class="flow-manager-remote-option">${remote.name}</button><br/><br/>` })} </ol> </p> </div>`); // Push progress dialog $('body').prepend(` <div id="flow-manager-push-progress-dialog"> <div id="flow-manager-push-progress-label" class="progress-label">Starting download...</div><br/> <div id="flow-manager-push-progress-progressbar"></div> </div>`); // Filter Flows modal dialog $('body').prepend(` <div id="flow-manager-filter-progress-dialog"> <div id="flow-manager-filter-progress-label" class="progress-label">Loading flows selection...</div><br/> <div id="flow-manager-filter-progress-bar"></div> </div>`); // Add diff popup $('body').prepend(` <div id="flow-manager-remote-diff-dialog" style="width: auto; text-align: center"> <div id="flow-manager-remote-no-diff" style="color: darkgreen">No differences were found.</div> <div id="flow-manager-remote-diff-controllers"> <button id="flow-manager-remote-diff-toggle-all" class="ui-button ui-widget ui-corner-all" href="#">Toggle all actions</button> <br/><br/> <table style="border-color: #7c9cec; border-width: 2px;"> <thead style="background-color: #999999"> <tr> <td class="flow-manager-remote-diff-field-header">Action</td> <td class="flow-manager-remote-diff-field-header">Name</td> <td class="flow-manager-remote-diff-field-header">Status</td> <td class="flow-manager-remote-diff-field-header">Modified Time</td> <td class="flow-manager-remote-diff-field-header">Remote Modified Time</td> </tr> </thead> <tbody><tr><td colspan="6" class="flow-manager-diff-type" id="flow-manager-diff-type-flow">Flows</td></tr></tbody> <tbody id="flow-manager-diff-flow"></tbody> <tr><td colspan="6" class="flow-manager-diff-type" id="flow-manager-diff-type-subflow">Subflows</td></tr></tbody> <tbody id="flow-manager-diff-subflow"></tbody> <tbody><tr><td colspan="6" class="flow-manager-diff-type" id="flow-manager-diff-type-global">Global</tr></tbody> <tbody id="flow-manager-diff-global"></tbody> </table> <br/><button id="flow-manager-remote-diff-push-button" class="ui-button ui-widget ui-corner-all" href="#">Execute Selected Actions</button> </div> </div>`); $('#flow-manager-remote-diff-toggle-all').click(function () { const inputs = $('#flow-manager-remote-diff-dialog input'); let allSelected = true for(const input of inputs) { if(!input.checked && !input.disabled) { allSelected = false; break; } } for(const input of inputs) { input.checked = !allSelected || input.disabled } }); $('#flow-manager-remote-diff-push-button').click(async function () { $("#flow-manager-push-progress-dialog").dialog({title: 'Push in progress...', modal: true}); const inputs = $('#flow-manager-remote-diff-dialog input'); let remoteNameToDeploy = null; let progress = 0; for(const input of inputs) { progress++; if(!input.checked) continue; const remoteName = input.getAttribute('data-remoteName'); remoteNameToDeploy = remoteName; const action = input.getAttribute('data-action') // DEPLOY, DELETE, POST if(action === 'DEPLOY') continue; const flowType = input.getAttribute('data-flowType'); const flowName = input.getAttribute('data-flowName'); const localMtime = input.getAttribute('data-mtime'); const [localUrl, remoteUrl] = [false,true].map(isRemote=>{ let url = `flow-manager/${isRemote?('remotes/'+remoteName+'/'):''}flow-files/`; if(flowType === 'global') url += flowType; else { url += `${flowType}s/${flowName}`; } url += '?mtime='+localMtime; return url; }) try { $('#flow-manager-push-progress-label').text(`${action}ing ${flowType} ${flowName||''}`); await ajax({ url: remoteUrl, ...(action==='POST'? { contentType: "text/plain", data: await ajax({url: localUrl, headers:{accept:'text/plain'}}) } :{}), type: action }); } catch (e) { alert(`Failed to ${action} file ${flowType} ${flowName||''}, Discontinuing.`); break; } $("#flow-manager-push-progress-progressbar").progressbar({value: (progress/(inputs.length+1))*100}); } if(remoteNameToDeploy) { $('#flow-manager-push-progress-label').text(`Deploying changes to ${remoteNameToDeploy}...`); try { await ajax({ url: `flow-manager/remotes/${remoteNameToDeploy}/states`, method: 'POST', data: JSON.stringify({"action":"reloadOnly"}), dataType: "json", contentType: "application/json; charset=utf-8" }); } catch (e) {} $( "#flow-manager-push-progress-progressbar" ).progressbar({value: 100}); } $("#flow-manager-push-progress-dialog").dialog('close'); $("#flow-manager-remote-diff-dialog").dialog('close'); }); function prepareDiffTable(opts) { let foundDifferencesInAnyType = false; ['flow', 'subflow', 'global'].forEach(flowType=>{ let foundDifferencesInThisType = false; const rowsContainerElementForType = $(`#flow-manager-diff-${flowType}`); rowsContainerElementForType.empty() const allFlowFileNamesFromBoth = flowType === 'global'? ["global"]: new Set([...Object.keys(opts.localState[flowType]), ...Object.keys(opts.remoteState[flowType])]); allFlowFileNamesFromBoth.forEach(flowName=>{ const localFileState = flowType==='global'? opts.localState[flowType] : opts.localState[flowType][flowName]; const remoteFileState = flowType==='global'? opts.remoteState[flowType] : opts.remoteState[flowType][flowName]; let status, statusColor, forceChecked = false, action, actionDesc; if(localFileState && !remoteFileState) { actionDesc = 'add'; status = "Missing in remote"; statusColor = '#6f8aae'; action = 'POST'; } else if(!localFileState && remoteFileState) { // Missing in local actionDesc = 'delete'; status = "Missing in local"; statusColor = '#d9c648'; action = 'DELETE'; } else { // Both have if(localFileState.rev !== remoteFileState.rev) { const localDate = new Date(localFileState.mtime); const remoteDate = new Date(remoteFileState.mtime); if(localDate > remoteDate) { actionDesc = 'update'; status = "Local file is newer"; statusColor = '#469b40'; } else if(localDate < remoteDate) { actionDesc = 'overwrite ⚠️'; status = "Remote file is newer"; // "Danger" tell use he will override more recent change statusColor = '#f54646'; } else { actionDesc = 'overwrite ⚠️'; status = "Different"; // Not sure, just say it is different. should not happen. statusColor = '#ff9b9b'; } action = 'POST'; } else if(remoteFileState.hasUpdate) { // Remote and Local file are the same, but Remote did not load it yet action = 'DEPLOY'; actionDesc = 'deploy'; forceChecked = true; status = "Files equal<br/>But not deployed remotely (deploy)<br/>"; statusColor = '#c0d6bd'; } else { // Equal files: in this case, do not list row action = null; } } if(action) { foundDifferencesInAnyType = true; foundDifferencesInThisType = true; const lMtime = (localFileState && localFileState.mtime)? new Date(localFileState.mtime).toLocaleString():''; const rMtime = (remoteFileState && remoteFileState.mtime)? new Date(remoteFileState.mtime).toLocaleString():''; const lRev = localFileState? localFileState.rev:''; const rRev = remoteFileState? remoteFileState.rev:''; const inputId = `flow-manager-diff-action-${flowType}${flowName?('-'+flowName):''}` rowsContainerElementForType.append(` <tr ${statusColor?'style="background-color: '+statusColor+'"':''}> <td style="text-align: left"><input id="${inputId}" ${forceChecked?'disabled':''} ${forceChecked?'checked':''} type="checkbox" data-mtime="${lMtime}" data-remoteName="${opts.remoteName}" data-action="${action}" data-flowName="${flowName}" data-flowType="${flowType}"><label for="${inputId}">${actionDesc}</label></td> <td>${flowName}</td> <td>${status}</td> <td onclick="javascript:alert('Local Rev Checksum: ${lRev}')">${lMtime}</td> <td onclick="javascript:alert('Remote Rev Checksum: ${rRev}')">${rMtime}</td> </tr> `) } }) $(`#flow-manager-diff-type-${flowType}`).css('display', foundDifferencesInThisType?'':'none'); }) $('#flow-manager-remote-diff-dialog').dialog({ title: `Remote diff from: ${opts.remoteName}`, height: 'auto', width: 'auto' }); $('#flow-manager-remote-diff-controllers').css('display', foundDifferencesInAnyType?'':'none') $('#flow-manager-remote-no-diff').css('display', foundDifferencesInAnyType?'none':'') } $('.flow-manager-remote-button').click(function (){ $('#flow-manager-remote-dialog').dialog({title: "Select a remote"}); }); $('.flow-manager-remote-option').click(async function (el) { const progressDialog = $("#flow-manager-push-progress-dialog"); const progressLabel = $('#flow-manager-push-progress-label'); const progressBar = $('#flow-manager-push-progress-progressbar'); progressDialog.dialog({title: 'Loading & Comparing states', modal: true}); progressLabel.text(''); progressBar.progressbar({value: 0}); try { progressLabel.text('Retrieving Local State'); const localState = await ajax({url: 'flow-manager/states'}); progressBar.progressbar({value: 50}); progressLabel.text('Retrieving Remote State'); const remoteName = el.currentTarget.innerText; try { const remoteStatesUrl = `flow-manager/remotes/${remoteName}/states`; const remoteState = await ajax({url: remoteStatesUrl}); $('#flow-manager-remote-dialog').dialog('close'); progressBar.progressbar({value: 100}); prepareDiffTable({ remoteName, remoteStatesUrl, localState, remoteState }); } catch (e) { alert(`Failed to retrieve flow state from ${remoteName}`); } } catch (e) { alert(`Failed to retrieve local flow state`); } progressDialog.dialog('close'); }); } })() </script>