node-red-contrib-sensecraft-application
Version:
This project is a Node-RED contribution for the Sensecraft application, allowing users to integrate Sensecraft functionalities into their Node-RED flows.
463 lines (401 loc) • 20 kB
HTML
<style>
.ui-button {
line-height: 30px ;
}
.mission-manager-diff-type {
background-color: #ffffff;
font-size: large;
text-decoration: underline;
}
#mission-manager-remote-diff-dialog {
max-height: 94vh ;
}
#mission-manager-remote-diff-dialog table,
#mission-manager-remote-diff-dialog td,
#mission-manager-remote-diff-dialog th {
border: 1pt #3e3e3e solid;
padding: 5pt;
white-space: nowrap;
}
#mission-manager-remote-diff-dialog td {
min-width: 200px;
}
.mission-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);
});
}
function paramObj(url) {
if (url.indexOf('#flow') != -1) url = url.slice(0, url.indexOf('#flow'))
const search = url.split('?')[1]
if (!search) {
return {}
}
return JSON.parse(
'{"' +
decodeURIComponent(search)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"')
.replace(/\+/g, ' ') +
'"}'
)
}
async function urlToFile(url, filename, mimeType) {
let fileName1 = url.split('/')
fileName1 = fileName1[fileName1.length - 1]
// Fetch the file data from the URL
const response = await fetch(url);
// Convert response to Blob
const blob = await response.blob();
// Create a File object
return new File([blob], filename || fileName1, { type: mimeType || blob.type });
}
let baseUrl = window.location.host
if (baseUrl.indexOf(':') != -1) baseUrl = baseUrl.slice(0, baseUrl.indexOf(':'))
baseUrl = 'http://' + baseUrl + ':8090'
// baseUrl = 'http://192.168.118.71:8090'
// console.log(baseUrl, 'baseUrl')
const apiUrl = {
getDeviceInfo: {
url: baseUrl + '/device/info',
method: 'GET'
},
restart: {
url: baseUrl + '/container/restart/',
method: 'POST'
},
uploadFile: {
url: baseUrl + '/sensecraft-application/flow-static/', // :appId
method: 'POST'
},
getLocalState: {
url: 'sensecraft-application/states/',
method: 'GET'
},
getRemoteState: {
url: 'sensecraft-application/applicaiton/sate/', // :appId
method: 'POST',
},
applyFlow: {
url: 'sensecraft-application/flow-files/', // :type/:fileName
method: 'POST',
},
getFlowInfo: {
url: 'sensecraft-application/applicaiton/', // :type/:fileName
method: 'POST'
},
reloadState: {
url: 'sensecraft-application/states/',
method: 'POST',
},
}
RED.comms.subscribe('sensecraft-application/sensecraft-application-envnodes-npm-update', function (topic, msgData) {
// console.log(topic, msgData)
if (topic) {
// RED.view.redraw();
try {
ajax({
url: apiUrl.restart.url + `sensecraft-nodered`,
method: apiUrl.restart.method,
contentType: 'application/json',
});
const myNotification = RED.notify(
`The plugin has been updated and will be restarted for you soon`, {
type: "success",
timeout: 30000,
buttons: [
{
text: "Thanks",
click: function (e) {
myNotification.close();
}
}
]
});
} catch (error) {
console.log('Failed to restart:' + JSON.stringify(error))
}
}
});
async function uploadFn(arr, appId, author) {
for (let i = 0; i < arr.length; i++) {
try {
const formData = new FormData();
const file = await urlToFile(arr[i]);
formData.append('file', file)
if (author) formData.append('author', author)
await ajax({
url: apiUrl.uploadFile.url + `${appId}`,
method: apiUrl.uploadFile.method,
data: formData, processData: false, // 必须为 false
contentType: false, // 必须为 false
});
} catch (error) {
console.log(arr[i] + ' upload failed')
}
}
}
async function getdeviceInfo() {
try {
// get device info
const deviceInfo = await ajax({
url: apiUrl.getDeviceInfo.url,
method: apiUrl.getDeviceInfo.method,
contentType: "application/json; charset=utf-8"
})
if (deviceInfo && deviceInfo.result) {
const _nr1Header = $('ul.red-ui-header-toolbar');
const prependInfo = `
<div style="padding: 0 10px; line-height: 20px; text-align: right;">
<div style="color: #8cc020; font-size: 12px; border-bottom: 1px dashed #888888">IP: ${deviceInfo.result && deviceInfo.result.ip}</div>
<div style="color: #8cc020; font-size: 12px;">SN: ${deviceInfo.result && deviceInfo.result.sn}</div>
</div>`
if (_nr1Header.length) {
_nr1Header.prepend(prependInfo)
} else {
$('.header-toolbar').prepend(prependInfo)
}
console.log(deviceInfo, 'deviceInfo')
}
} catch (error) {
console.log('Failed to get device information:' + JSON.stringify(error))
}
}
getdeviceInfo()
const queryObj = await paramObj(window.location.href)
let targetJson = null;
let staticInfo = null;
let remoteState = null;
if (queryObj && queryObj.appId) {
// get the target file
try {
const targetUrl = 'https://sensecraft-statics.seeed.cc/mission-pack-test/' + queryObj.appId + '/flow.json';
const data = await ajax({
url: targetUrl,
dataType: 'json', // 指定数据类型为JSON
method: 'GET',
contentType: "application/json; charset=utf-8"
});
targetJson = data;
for (let i = 0; i < targetJson.length; i++) {
if (targetJson[i].type === "comment" && targetJson[i].name === "sensecraft-libs") {
// 找到静态文件
staticInfo = targetJson[i].info ? JSON.parse(targetJson[i].info) : null
}
}
} catch (error) {
alert(`Failed to get the target file (Application Id: ${queryObj.appId}), Discontinuing.`);
console.log(error)
}
}
if (!targetJson) return false;
try {
remoteState = await ajax({
url: apiUrl.getRemoteState.url + `${queryObj.appId}`,
method: apiUrl.getRemoteState.method,
data: JSON.stringify(targetJson),
contentType: 'application/json',
});
for (const key in remoteState) {
if (remoteState[key] && JSON.stringify(remoteState[key]) === '{}') {
delete remoteState[key]
}
}
if (remoteState && JSON.stringify(remoteState) === '{}') remoteState = null;
} catch (error) {
alert(`Failed to retrieve flow state from ${queryObj.appId}`);
console.log(error)
}
if (!remoteState) return false;
// Push progress dialog
$('body').prepend(`
<div id="mission-manager-push-progress-dialog">
<div id="mission-manager-push-progress-label" class="progress-label">Starting download...</div><br/>
<div id="mission-manager-push-progress-progressbar"></div>
</div>`);
// Add diff popup
$('body').prepend(`
<div id="mission-manager-remote-diff-dialog" style="width: auto; text-align: center">
<div id="mission-manager-remote-no-diff" style="color: darkgreen">No differences were found.</div>
<div id="mission-manager-remote-diff-controllers">
<button id="mission-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: #666; color: #fff !important">
<tr>
<td class="mission-manager-remote-diff-field-header">Action</td>
<td class="mission-manager-remote-diff-field-header">Name</td>
</tr>
</thead>
<tbody><tr><td colspan="2" class="mission-manager-diff-type" id="mission-manager-diff-type-flow" style="background:#fff">Flows</td></tr></tbody>
<tbody id="mission-manager-diff-flow"></tbody>
<tr><td colspan="2" class="mission-manager-diff-type" id="mission-manager-diff-type-subflow">Subflows</td></tr></tbody>
<tbody id="mission-manager-diff-subflow"></tbody>
<tbody><tr><td colspan="2" class="mission-manager-diff-type" id="mission-manager-diff-type-global">Global</tr></tbody>
<tbody id="mission-manager-diff-global"></tbody>
</table>
<br/><button id="mission-manager-remote-diff-push-button" class="ui-button ui-widget ui-corner-all" href="#" style="background: #3c7eff !important; color: #fff !important">Apply Selected Actions</button>
</div>
</div>`);
$('#mission-manager-remote-diff-toggle-all').click(function () {
const inputs = $('#mission-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
}
});
$('#mission-manager-remote-diff-push-button').click(async function () {
$("#mission-manager-push-progress-dialog").dialog({ title: 'Push in progress...', modal: true });
// upload static
if (staticInfo && staticInfo.static) {
$('#mission-manager-push-progress-label').text(`Uploading Static Files ...`);
await uploadFn(staticInfo.static, queryObj.appId, staticInfo.author)
}
$('#mission-manager-push-progress-label').text(`Applying ...`);
const inputs = $('#mission-manager-remote-diff-dialog input');
let progress = 0;
for (const input of inputs) {
progress++;
if (!input.checked) continue;
const flowType = input.getAttribute('data-flowType');
const flowName = input.getAttribute('data-flowName');
const action = input.getAttribute('data-action') // DEPLOY, DELETE, POST
if (action === 'DEPLOY') continue;
try {
$('#fmission-manager-push-progress-label').text(`${action}ing ${flowType} ${flowName || ''}`);
await ajax({
url: apiUrl.applyFlow.url + flowType + '/' + flowName,
method: apiUrl.applyFlow.method,
dataType: "json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(await ajax({
url: apiUrl.getFlowInfo.url + flowType + '/' + flowName,
method: apiUrl.applyFlow.method,
dataType: "json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(targetJson)
}))
});
} catch (e) {
alert(`Failed to ${action} file ${flowType} ${flowName || ''}, Discontinuing.`);
break;
}
$("#mission-manager-push-progress-progressbar").progressbar({ value: (progress / (inputs.length + 1)) * 100 });
}
$('#mission-manager-push-progress-label').text(`Deploying ...`);
try {
await ajax({
url: apiUrl.reloadState.url,
method: 'POST',
data: JSON.stringify({ "action": "reloadOnly" }),
dataType: "json",
contentType: "application/json; charset=utf-8"
});
} catch (error) {
console.log('Failed to reload state:' + JSON.stringify(error))
}
$("#mission-manager-push-progress-progressbar").progressbar({ value: 100 });
$("#mission-manager-push-progress-dialog").dialog('close');
$("#mission-manager-remote-diff-dialog").dialog('close');
if (window.location.href.indexOf('?appId') != -1) window.location.href = window.location.href.slice(0, window.location.href.indexOf('?appId'))
});
function prepareDiffTable(opts) {
console.log(opts, 'opts')
let foundDifferencesInAnyType = false;
['flow', 'subflow', 'global'].forEach(flowType => {
let foundDifferencesInThisType = false;
const rowsContainerElementForType = $(`#mission-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 statusColor, forceChecked = false, isChecked = false, action, actionDesc;
if (!localFileState && remoteFileState) {
actionDesc = 'add';
statusColor = '#8CC020';
action = 'POST';
forceChecked = false;
isChecked = true;
} else if (localFileState && !remoteFileState) {
// Actions are already available locally and do not need to be operated
action = null;
} else {
// Both have
if (localFileState.rev !== remoteFileState.rev) {
actionDesc = 'update';
statusColor = '#f5e15b';
isChecked = true;
action = 'POST';
} else {
// Equal files: in this case, do not list row
action = null;
}
}
if (action) {
foundDifferencesInAnyType = true;
foundDifferencesInThisType = true;
const lRev = localFileState ? localFileState.rev : '';
const rRev = remoteFileState ? remoteFileState.rev : '';
const inputId = `mission-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 || isChecked ? 'checked' : ''} type="checkbox" data-remoteName="${opts.remoteName}" data-action="${action}" data-flowName="${flowName}" data-flowType="${flowType}"><label for="${inputId}">${actionDesc}</label></td>
<td onclick="javascript:alert('Remote Rev Checksum: ${rRev}')">${flowName}</td>
</tr>
`)
}
})
$(`#mission-manager-diff-type-${flowType}`).css('display', foundDifferencesInThisType ? '' : 'none');
})
$('#mission-manager-remote-diff-dialog').dialog({
title: `Please select the actions to apply to your Node-RED`,
height: 'auto', width: 'auto'
});
$('#mission-manager-remote-diff-controllers').css('display', foundDifferencesInAnyType ? '' : 'none')
$('#mission-manager-remote-no-diff').css('display', foundDifferencesInAnyType ? 'none' : '')
if (!foundDifferencesInAnyType) $("#mission-manager-remote-diff-dialog").dialog('close');
}
const progressDialog = $("#mission-manager-push-progress-dialog");
const progressLabel = $('#mission-manager-push-progress-label');
const progressBar = $('#mission-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: apiUrl.getLocalState.url });
progressBar.progressbar({ value: 50 });
progressLabel.text('Applying...');
try {
$('#mission-manager-remote-dialog').dialog('close');
progressBar.progressbar({ value: 100 });
prepareDiffTable({
localState,
remoteState
});
} catch (e) {
alert(`Failed to retrieve flow state from ${queryObj.appId}`);
}
} catch (e) {
alert(`Failed to retrieve local flow state`);
}
progressDialog.dialog('close');
})()
</script>