node-red-contrib-flow-manager
Version:
Flow Manager separates your flow json to multiple files
525 lines (442 loc) • 24.7 kB
HTML
<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 ;
background-color: #444 ;
cursor: default ;
}
.flow-manager-remote-button {
background-color: #4b5627 ;
}
button.flow-manager-remote-option {
background-color: #e6f3e2;
font-size: x-large ;
}
button.flow-manager-remote-option:active{
background: #4e4e4b;
}
button.toolbar-button.flow-manager-filter-flows-button {
background-color: #176670 ;
}
#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>