UNPKG

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
<style> .ui-button { line-height: 30px !important; } .mission-manager-diff-type { background-color: #ffffff; font-size: large; text-decoration: underline; } #mission-manager-remote-diff-dialog { max-height: 94vh !important; } #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>