UNPKG

node-red-contrib-uibuilder

Version:

Easily create web UI's for Node-RED using any (or no) front-end library. VueJS and bootstrap-vue included but change as desired.

1,104 lines (959 loc) 93.9 kB
<!-- Copyright (c) 2017-2021 Julian Knight (Totally Information) 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. --> <script type="text/javascript"> /* eslint-env browser es6 */ // Isolate this code ;(function () { 'use strict' /** @typedef {import("node-red").Red} Red */ //#region --------- "global" variables for the panel --------- // /** Module name must match this nodes html file @constant {string} moduleName */ var moduleName = 'uibuilder' /** Node's label @constant {string} paletteCategory */ var nodeLabel = 'uibuilder' /** Node's palette category @constant {string} paletteCategory */ var paletteCategory = 'uibuilder' /** Node's background color @constant {string} paletteColor */ var paletteColor = '#E6E0F8' /** Default session length (in seconds) if security is active @type {Number} */ var defaultSessionLength = 432000 /** Default JWT secret if security is active - to ensure it isn't blank @type {String} */ var defaultJwtSecret = 'Replace This With A Real Secret' /** List of installed packages - rebuilt when editor is opened, updates by library mgr */ var packages = [] /** placeholder for ACE editor vars - so that they survive close/reopen admin config ui * @typedef {Object} uiace * @property {string} format * @property {string} folder * @property {string} fname * @property {boolean} fullscreen Is the editor in fullscreen mode? */ var uiace = { 'format': 'html', 'folder': 'src', 'fname' : 'index.html', 'fullscreen': false } //#endregion ------------------------------------------------- // /** Initialise default values for package list - must be done before everything to give the ajax call time to finish * since the list is used to check if the template dependencies are installed. * NOTE: This is build dynamically each time the edit panel is opened * we are not saving this since external changes would result in * users having being prompted to deploy even when they've made * no changes themselves to a node instance. */ packageList() //#region --------- "global" functions for the panel --------- // /** AddItem function for package list * @param {JQuery<HTMLElement>} element the jQuery DOM element to which any row content should be added * @param {number} index the index of the row * @param {string|*} data data object for the row. {} if add button pressed, else data passed to addItem method */ function addPackageRow(element,index,data) { var hRow = '' if (Object.entries(data).length === 0) { // Add button was pressed so we have no packageName, create an input form instead hRow='<input type="text" id="packageList-input-' + index + '"> <button id="packageList-button-' + index + '">Install</button>' } else { // addItem method was called with a packageName passed hRow = data } // Build the output row var myRow = $('<div id="packageList-row-' + index + '" class="packageList-row-data">'+hRow+'</div>').appendTo(element) // Create a button click listener for the install button for this row $('#packageList-button-' + index).click(function(){ // show activity spinner $('i.spinner').show() var packageName = '' + $('#packageList-input-' + index).val() if ( packageName.length !== 0 ) { /** @type {Red} */ RED.notify('Installing npm package ' + packageName) // Call the npm installPackage API (it updates the package list) $.get( 'uibnpmmanage?cmd=install&package=' + packageName, function(data){ if ( data.success === true) { console.log('[uibuilder:addPackageRow:get] PACKAGE INSTALLED. ', packageName) RED.notify('Successful installation of npm package ' + packageName, 'success') // Replace the input field with the normal package name display myRow.html(packageName) // Update the master package list packages.push(packageName) } else { console.log('[uibuilder:addPackageRow:get] ERROR ON INSTALLATION OF PACKAGE ', packageName ) console.dir( data.result ) RED.notify('FAILED installation of npm package ' + packageName, 'error') } // Hide the progress spinner $('i.spinner').hide() }) .fail(function(_jqXHR, textStatus, errorThrown) { console.error( '[uibuilder:addPackageRow:get] Error ' + textStatus, errorThrown ) RED.notify('FAILED installation of npm package ' + packageName, 'error') $('i.spinner').hide() return 'addPackageRow failed' // TODO otherwise highlight input }) } // else Do nothing }) // -- end of button click -- // } // --- End of addPackageRow() ---- // /** RemoveItem function for package list */ function removePackageRow(packageName) { // If package name is an empty object - user removed an add row so ignore if ( (packageName === '') || (typeof packageName !== 'string') ) { return false } RED.notify('Starting removal of npm package ' + packageName) // show activity spinner $('i.spinner').show() // Call the npm installPackage API (it updates the package list) $.get( 'uibnpmmanage?cmd=remove&package=' + packageName, function(data){ if ( data.success === true) { console.log('[uibuilder:removePackageRow:get] PACKAGE REMOVED. ', packageName) RED.notify('Successfully uninstalled npm package ' + packageName, 'success') // Remove the entry from the master package list const i = packages.indexOf(packageName) if ( i > 0 ) packages.splice(i,1) } else { console.log('[uibuilder:removePackageRow:get] ERROR ON PACKAGE REMOVAL ', data.result ) RED.notify('FAILED to uninstall npm package ' + packageName, 'error') // Put the entry back again $('#node-input-packageList').editableList('addItem',packageName) } $('i.spinner').hide() }) .fail(function(_jqXHR, textStatus, errorThrown) { console.error( '[uibuilder:removePackageRow:get] Error ' + textStatus, errorThrown ) RED.notify('FAILED to uninstall npm package ' + packageName, 'error') // Put the entry back again $('#node-input-packageList').editableList('addItem',packageName) $('i.spinner').hide() return 'removePackageRow failed' // TODO otherwise highlight input }) } // ---- End of removePackageRow ---- // /** Get list of installed packages via API - save to master list */ function packageList() { $.getJSON('uibvendorpackages', function(vendorPaths) { packages = [] var pkgList = Object.keys(vendorPaths) pkgList.forEach(function(packageName,_index){ // Populate the master package list (used to check dependencies) packages.push(packageName) }) }) } // --- End of packageList --- // /** Return a file type from a file name (or default to txt) * ftype can be used in ACE editor modes */ function fileType(fname) { let ftype = 'text' let fparts = fname.split('.') // Take off the first entry if the file name started with a dot if ( fparts[0] === '' ) fparts.shift() if (fparts.length > 1) { // Get the last element of the array let fext = fparts.pop().toLowerCase().trim() switch (fext) { case 'js': ftype = 'javascript' break case 'html': case 'css': case 'json': ftype = fext break case 'vue': ftype = 'html' break case 'md': ftype = 'markdown' break case 'yaml': case 'yml': ftype = 'yaml' break default: ftype = fext } } return ftype } // --- End of fileType --- // /** Get the list of files for the chosen url & folder * @param {string} [selectedFile] Optional. If present will select this filename after refresh, otherwise 1st file is selected. */ function getFileList(selectedFile) { //#region --- Collect variables from Config UI --- var url = $('#node-input-url').val() var folder = $('#node-input-folder').val() var f = $('#node-input-filename').val() // Whether or not to force the index.(html|js|css) files to be copied over if missing var nodeInputCopyIndex = $('#node-input-copyIndex').is(':checked') //#endregion ------------------------------------- // Collect the current filename from various places if ( selectedFile === undefined ) selectedFile = f if ( selectedFile === null ) selectedFile = localStorage.getItem('uibuilder.'+url+'.selectedFile') || undefined if ( folder === null ) folder = localStorage.getItem('uibuilder.'+url+'.folder') || undefined // Clear out drop-downs ready for rebuilding $('#node-input-filename option').remove() $('#node-input-folder option').remove() // Get all files/folders for this uibuilder instance from API call $.ajax({ type: 'GET', dataType: 'json', url: './uibuilder/admin/' + url, data: { 'cmd': 'listall', }, }) .done(function(data, textStatus, jqXHR) { let firstFile = '', indexHtml = false, selected = false // build folder list, pre-select src if no current folder selected #node-input-folder - Object.keys(res) const folders = Object.keys(data).sort() // Rebuild folder drop-down $.each(folders, function (i, fldrname) { // For the root folder, use empty string for folders lookup but "root" for display if ( fldrname === '' ) fldrname = 'root' // Build the drop-down $('#node-input-folder').append($('<option>', { value: fldrname, text : fldrname, })) }) // if currently selected folder doesn't exist if ( !data[folder] ) { // Use 'src' if it exists otherwise use 'root' if ( data['src'] ) folder = 'src' else folder = root } // Selected folder $('#node-input-folder').val(folder) uiace.folder = folder // Persist the folder selection localStorage.setItem('uibuilder.'+url+'.folder', folder) let files = [] files = data[folder] $.each(files, function (i, filename) { // Build the drop-down $('#node-input-filename').append($('<option>', { value: filename, text : filename, })) // Choose the default file. In order: selectedFile param, index.html, 1st returned if ( i === 0 ) firstFile = filename if ( filename === 'index.html' ) indexHtml = true if ( filename === selectedFile ) selected = true }) // Set default file name/type. In order: selectedFile param, index.html, 1st returned if ( selected === true ) uiace.fname = selectedFile else if ( indexHtml === true ) uiace.fname = 'index.html' else uiace.fname = firstFile $('#node-input-filename').val(uiace.fname) uiace.format = fileType(uiace.fname) // Persist the file name selection localStorage.setItem('uibuilder.'+url+'.selectedFile', uiace.fname) }) .fail(function(jqXHR, textStatus, errorThrown) { console.error( '[uibuilder:getFileList:getJSON] Error ' + textStatus, errorThrown ) uiace.fname = '' uiace.format = 'text' RED.notify(`uibuilder: Folder and file listing error.<br>${errorThrown}`, {type:'error'}) }) .always(function() { getFileContents() }) } // --- End of getFileList --- // /** Get the chosen file contents & set up the ACE editor */ function getFileContents() { /** Get the chosen folder name - use the default/last saved on first load * @type {string} */ // @ts-ignore var folder = $('#node-input-folder').val() if ( folder === null ) folder = localStorage.getItem('uibuilder.'+url+'.folder') || uiace.folder /** Get the chosen filename - use the default/last saved on first load * @type {string} */ // @ts-ignore var fname = $('#node-input-filename').val() if ( fname === null ) fname = localStorage.getItem('uibuilder.'+url+'.selectedFile') || uiace.fname // Get the current url var url = $('#node-input-url').val() // Save the file & folder names uiace.folder = folder uiace.fname = fname // Persist the folder & file name selection localStorage.setItem('uibuilder.'+url+'.folder', uiace.folder) localStorage.setItem('uibuilder.'+url+'.selectedFile', uiace.fname) // Change mode to match file type var filetype = uiace.format = fileType(fname) $('#node-input-format').val(filetype) // Get the file contents via API defined in uibuilder.js $.get( 'uibgetfile?url=' + url + '&fname=' + fname + '&folder=' + folder, function(data){ $('#node-input-template-editor').show() $('#node-input-template-editor-no-file').hide() // Add the fetched data to the editor uiace.editorSession.setValue(data) // Set the editor file mode uiace.editorSession.setMode({ path: 'ace/mode/' + filetype, v: Date.now() }) // Mark the current session as clean uiace.editorSession.getUndoManager().isClean() // Position the cursor in the edit area uiace.editor.focus() }) .fail(function(_jqXHR, textStatus, errorThrown) { console.error( '[uibuilder:getFileContents:get] Error ' + textStatus, errorThrown ) uiace.editorSession.setValue('') $('#node-input-template-editor').hide() $('#node-input-template-editor-no-file').show() }) .always(function(){ fileIsClean(true) // Default the language selector in case it wasn't recognised if(!$('#node-input-format option:selected').length) $('#node-input-format').val('text') }) } // --- End of getFileContents --- // /** Call v3 admin API to create a new folder * @param {string} folder Name of new folder to create (combines with current uibuilder url) * @return {string} Status message * */ function createNewFolder(folder) { // Also get the current url var url = $('#node-input-url').val() $.ajax({ type: 'POST', dataType: 'json', url: './uibuilder/admin/' + url, data: { 'folder': folder, 'cmd': 'newfolder', }, }) .done(function(data, textStatus, jqXHR) { RED.notify(`uibuilder: Folder <code>${folder}</code> Created.`, {type:'success'}) // Rebuild the file list getFileList() return 'Create folder succeeded' }).fail(function(jqXHR, textStatus, errorThrown) { RED.notify(`uibuilder: Create Folder Error.<br>${errorThrown}`, {type:'error'}) return 'Create folder failed' }) } // --- End of createNewFile --- // /** Call v3 admin API to create a new file * @param {string} fname Name of new file to create (combines with current selected folder and the current uibuilder url) * @return {string} Status message * */ function createNewFile(fname) { // Also get the current folder & url var folder = $('#node-input-folder').val() || uiace.folder var url = $('#node-input-url').val() $.ajax({ type: 'POST', dataType: 'json', url: './uibuilder/admin/' + url, data: { 'folder': folder, 'fname': fname, 'cmd': 'newfile', }, }) .done(function(data, textStatus, jqXHR) { RED.notify(`uibuilder: File <code>${folder}/${fname}</code> Created.`, {type:'success'}) // Rebuild the file list getFileList(fname) return 'Create file succeeded' }).fail(function(jqXHR, textStatus, errorThrown) { console.error( '[uibuilder:createNewFile:post] Error ' + textStatus, errorThrown ) RED.notify(`uibuilder: Create File Error.<br>${errorThrown}`, {type:'error'}) return 'Create file failed' }) } // --- End of createNewFile --- // /** Call v3 admin API to delete the currently selected folder * @return {string} Status message */ function deleteFolder() { // Also get the current url & folder var url = $('#node-input-url').val() var folder = $('#node-input-folder').val() $.ajax({ type: 'DELETE', dataType: 'json', url: './uibuilder/admin/' + url, data: { 'folder': folder, 'cmd': 'deletefolder', }, }) .done(function(data, textStatus, jqXHR) { RED.notify(`uibuilder: Folder <code>${folder}</code> deleted.`, {type:'success'}) // Rebuild the file list getFileList() return 'Delete folder succeeded' }).fail(function(jqXHR, textStatus, errorThrown) { console.error( '[uibuilder:deleteFolder:delete] Error ' + textStatus, errorThrown ) RED.notify(`uibuilder: Delete Folder Error.<br>${errorThrown}`, {type:'error'}) return 'Delete folder failed' }) } // --- End of deleteFolder --- // /** Call v3 admin API to delete the currently selected file * @return {string} Status message */ function deleteFile() { // Get the current file, folder & url var folder = $('#node-input-folder').val() || uiace.folder var url = $('#node-input-url').val() var fname = $('#node-input-filename').val() $.ajax({ type: 'DELETE', dataType: 'json', url: './uibuilder/admin/' + url, data: { 'folder': folder, 'fname': fname, 'cmd': 'deletefile', }, }) .done(function(data, textStatus, jqXHR) { RED.notify(`uibuilder: File <code>${folder}/${fname}</code> Deleted.`, {type:'success'}) // Rebuild the file list getFileList(fname) return 'Delete file succeeded' }).fail(function(jqXHR, textStatus, errorThrown) { console.error( '[uibuilder:deleteFile:delete] Error ' + textStatus, errorThrown ) RED.notify(`uibuilder: Delete File Error.<br>${errorThrown}`, {type:'error'}) return 'Delete file failed' }) } // --- End of deleteFile --- // /** Enable/disable buttons if file has edits or not * @param {boolean} isClean true = the file is clean, else there are pending edits that need saving */ function fileIsClean(isClean) { // If clean, disable the save & reset buttons $('#edit-save').prop('disabled', isClean) $('#edit-reset').prop('disabled', isClean) // If clean, enable the delete and edit buttons //$('#edit-delete').prop('disabled', !isClean) $('#edit-close').prop('disabled', !isClean) $('#node-edit-file').prop('disabled', !isClean) $('#node-input-filename').prop('disabled', !isClean) // If not clean, disable main Done and Cancel buttons to prevent loss $('#node-dialog-ok').prop('disabled', !isClean) $('#node-dialog-cancel').prop('disabled', !isClean) // If not clean, Add a user hint if ( ! isClean ) { $('#file-action-message').text('Save Required') $('#node-dialog-ok').css( 'cursor', 'not-allowed' ) $('#node-dialog-cancel').css( 'cursor', 'not-allowed' ) } else { $('#node-dialog-ok').css( 'cursor', 'pointer' ) $('#node-dialog-cancel').css( 'cursor', 'pointer' ) } } // --- End of fileIsClean --- // /** Validation Function: Validate the url property * Max 20 chars, can't contain any of ['..', ] * @param {*} value The url value to validate * @returns {boolean} true = valid **/ function validateUrl(value) { // NB: `this` is the node instance configuration as at last press of Done // Validation fns are run on every node instance when the Editor is loaded (value is populated but jQuery val is undefined). // and again when the config panel is opened (because jquery change is fired. value, jquery val and this.url are all the same). /** If the url hasn't really changed - no need to validate but might need to allow Done (for other settings) * e.g. user changes is back after trying something different but didn't commit between * Initial flow run and Initial load of admin config ui - don't check for dups as it always will be a dup */ if ( value === this.url ) { $('#node-dialog-ok').prop('disabled', false) $('#node-dialog-ok').css( 'cursor', 'pointer' ) $('#node-dialog-ok').addClass('primary') return true } // Max 20 chars if ( value.length > 20 ) return false // Cannot contain .. if ( value.indexOf('..') !== -1 ) return false // cannot contain / or \ if ( value.indexOf('/') !== -1 ) return false if ( value.indexOf('\\') !== -1 ) return false // Cannot start with _ or . if ( value.substring(0,1) === '_' ) return false if ( value.substring(0,1) === '.' ) return false // Cannot be 'templates' as this is a reserved value (for v2) if ( value.toLowerCase().substring(0,9) === 'templates' ) return false // Check whether the url is already in use via a call to the admin API `uibindex` var exists = false $.ajax({ type: 'GET', async: false, dataType: 'json', url: './uibuilder/admin/' + value, data: { 'cmd': 'checkurls', }, success: function(check) { exists = check } }) /** If the url already exists - prevent the "Done" button from being pressed. */ if ( exists === true ) { $('#node-dialog-ok').prop('disabled', true) $('#node-dialog-ok').css( 'cursor', 'not-allowed' ) $('#node-dialog-ok').removeClass('primary') RED.notify(`<b>ERROR</b>: <p>The chosen URL (${value}) is already in use (or the folder exists).<br>It must be changed before you can save/commit</p>`, {type: 'error'}) return false } else { $('#node-dialog-ok').prop('disabled', false) $('#node-dialog-ok').css( 'cursor', 'pointer' ) $('#node-dialog-ok').addClass('primary') // Warn user when changing URL. NOTE: Set/reset old url in the onsave function not here if ( value !== this.url ) RED.notify(`<b>NOTE</b>: <p>You are renaming the url from ${this.url} to ${value}.<br>You <b>MUST</b> redeploy before doing anything else.</p>`, {type: 'warning'}) return true } } // --- End of validateUrl --- // /** Set the height of the ACE text editor box */ function setACEheight() { var height if ( uiace.editorLoaded === true ) { // If the editor is in full-screen ... if (document.fullscreenElement) { // Force background color and add some padding to keep away from edge $('#edit-props').css('background-color','#f6f6f6').css('padding','1em') // Start to calculate the available height and adjust the editor to fill the ht height = parseInt($('#edit-props').css('height')) // full available height height -= 25 // Replace the expand icon with a compress icon $('#node-function-expand-js').css('background-color','black').html('<i class="fa fa-compress"></i>') uiace.fullscreen = true } else { $('#edit-props').css('background-color','').css('padding','') // Full height height = parseInt($('#dialog-form').height()) // Subtract info lines height -= parseInt($('#info').height()) // Replace the compress icon with a expand icon $('#node-function-expand-js').css('background-color','').html('<i class="fa fa-expand"></i>') uiace.fullscreen = false } // everything but the edit box var rows = $('#edit-props > div:not(.node-text-editor-row)') // subtract height of each row from the total for (var i=0; i<rows.length; i++) { height -= $(rows[i]).outerHeight(true) } // Set the height of the edit box $('#node-input-template-editor').css('height',height+'px') // Get the content to match the edit box size uiace.editor.resize() } } // --- End of setACEheight --- // /** Validation Function: Validate the session length property * If security is on, must contain a number * @param {Number} value The session length value to validate * @returns {boolean} true = valid, false = not valid **/ function validateSessLen(value) { // NB: `this` is the node instance configuration as at last press of Done // TODO: Add display comment to help user var newVal = $('#node-input-sessionLength').val() var newSec = $('#node-input-useSecurity').is(':checked') if (newSec === true) { if ( newVal.length < 1 ) return false } return true } // --- End of validateSessLen --- // /** Validation Function: Validate the jwtSecret property * If security is on, must contain text * @param {String} value The jwt secret value to validate * @returns {boolean} true = valid, false = not valid **/ function validateSecret(value) { // NB: `this` is the node instance configuration as at last press of Done // TODO: Add display comment to help user var newVal = $('#node-input-jwtSecret').val() var newSec = $('#node-input-useSecurity').is(':checked') if (newSec === true) { if ( newVal.length < 1 ) return false } return true } // --- End of validateSecret --- // /** Populate the template selection dropdown * Uses a file that is `require`d in uibuilder.js */ function populateTemplateDropdown() { if ( $('#node-input-copyIndex').prop('checked') !== true ) return // Load each option - first entry will be the default Object.values(RED.settings.uibuilderTemplates).forEach( templ => { // Build the drop-down $('#node-input-templSel').append($('<option>', { value: templ.folder, text : templ.name, })) }) // 1st entry is default - populate the description help tip $('#node-templSel-info').text(Object.values(RED.settings.uibuilderTemplates)[0].description) } // --- end of function populateTemplateDropdown --- // /** Check whether the currently selected template's npm dependencies are installed * If not, warn the user to install them. */ function checkDependencies() { const folder = $('#node-input-templSel').val() const deps = RED.settings.uibuilderTemplates[folder].dependencies || [] const missing = [] deps.forEach( depName => { if ( ! packages.includes(depName) ) missing.push(depName) }) if ( missing.length > 0 ) { var myNotification = RED.notify(`WARNING<br /><br />The selected uibuilder template (${folder}) is MISSING the following dependencies:<div> ${missing.join(', ')}</div><br />Please install them using the uibuilder Library Manager or select a different template.`,{ type: 'warning', modal: true, fixed: true, buttons: [ { text: "OK", class:"primary", click: function(e) { myNotification.close(); } } ] }) } } //#endregion ------------------------------------------------- // // Register the node type, defaults and set up the edit fns RED.nodes.registerType(moduleName, { category: paletteCategory, color: paletteColor, defaults: { name: { value: '' }, topic: { value: '' }, url: { value: moduleName, required: true, validate: validateUrl }, fwdInMessages: { value: false }, // Should we send input msg's direct to output as well as the front-end? allowScripts: { value: false }, // Should we allow msg's to send JavaScript to the front-end? allowStyles: { value: false }, // Should we allow msg's to send CSS styles to the front-end? copyIndex: { value: true }, // Should the default template files be copied to the instance src folder? templateFolder: { value: undefined }, // Folder for selected template showfolder: { value: false }, // Should a web index view of all source files be made available? useSecurity: { value: false }, sessionLength: { value: defaultSessionLength, validate: validateSessLen }, // 5d - Must have content if useSecurity=true tokenAutoExtend: { value: false }, // TODO add validation if useSecurity=true oldUrl: { value: undefined }, // If the url has been changed, this is the previous url reload: { value: false }, // If true, all connected clients will be reloaded if a file is changed on the edit screens //jwtSecret: { value: defaultJwtSecret, validate: validateSecret }, // Must have content if useSecurity=true }, credentials: { jwtSecret: { type:'password' }, // text or password }, inputs: 1, inputLabels: 'Msg to send to front-end', outputs: 2, outputLabels: ['Data from front-end', 'Control Msgs from front-end'], icon: 'ui_template.png', paletteLabel: nodeLabel, label: function () { return this.url || this.name || nodeLabel }, /** Available methods: * oneditprepare: (function) called when the edit dialog is being built. * oneditsave: (function) called when the edit dialog is okayed. * oneditcancel: (function) called when the edit dialog is canceled. * oneditdelete: (function) called when the delete button in a configuration node’s edit dialog is pressed. * oneditresize: (function) called when the edit dialog is resized. * onpaletteadd: (function) called when the node type is added to the palette. * onpaletteremove: (function) called when the node type is removed from the palette. */ /** Prepares the Editor panel */ oneditprepare: function () { var that = this //#region Start with the edit section hidden & main section visible $('#main-props').show() $('#edit-props').hide() $('#npm-props').hide() $('#adv-props').hide() $('#sec-props').hide() $('#info-props').hide() //#endregion // Hide the folder buttons until needed //$('#fldr-buttons').hide() //#region Set the checkbox states $('#node-input-fwdInMessages').prop('checked', this.fwdInMessages) $('#node-input-allowScripts').prop('checked', this.allowScripts) $('#node-input-allowStyles').prop('checked', this.allowStyles) $('#node-input-copyIndex').prop('checked', this.copyIndex) $('#node-input-showfolder').prop('checked', this.showfolder) $('#node-input-useSecurity').prop('checked', this.useSecurity) $('#node-input-tokenAutoExtend').prop('checked', this.useSecurity) $('#node-input-reload').prop('checked', this.reload) //#endregion checkbox states /** When the url changes (NB: Also see the validation function) change visible folder names & links * NB: Actual URL change processing is done in validation which also happens on change * Change happens when config panel is opened as well as for a real change */ $('#node-input-url').change(function () { var thisurl = $(this).val() var eUrlSplit = window.origin.split(':') var nrPort = Number(eUrlSplit[2]) var nodeRoot = RED.settings.httpNodeRoot.replace(/^\//, '') $('#info-webserver').empty() // Is uibuilder using a custom server? if (RED.settings.uibuilderCustomServer.port) { // Use the correct protocol (http or https) eUrlSplit[0] = RED.settings.uibuilderCustomServer.type.replace('http2','https') // Use the correct port eUrlSplit[2] = RED.settings.uibuilderCustomServer.port // When using custom server, no base path is used nodeRoot = '' $('#info-webserver') .append(`<div class="form-tips node-help">uibuilder is using a custom webserver at ${eUrlSplit.join(':') + '/'} </div>`) } var urlPrefix = eUrlSplit.join(':') + '/' // Show the root URL $('#uibuilderurl').prop('href', urlPrefix + nodeRoot + thisurl).text('Open ' + nodeRoot + thisurl) $('#node-input-showfolder-url').empty().append('<a href="' + urlPrefix + nodeRoot + $(this).val() + '/idx" target="_blank">' + nodeRoot + $(this).val() + '/idx</a>') $('#uibinstanceconf').prop('href', `./uibuilder/instance/${thisurl}?cmd=showinstancesettings`) }) // Show/Hide the advanced settings $('#show-adv-props').css( 'cursor', 'pointer' ) $('#show-adv-props').click(function(_e) { $('#adv-props').toggle() if ( $('#adv-props').is(':visible') ) { $('#show-adv-props').html('<i class="fa fa-caret-down"></i> Advanced Settings') } else { $('#show-adv-props').html('<i class="fa fa-caret-right"></i> Advanced Settings') } }) // Show/Hide the security settings $('#show-security-props').css( 'cursor', 'pointer' ) $('#show-security-props').click(function(_e) { $('#sec-props').toggle() if ( $('#sec-props').is(':visible') ) { $('#show-security-props').html('<i class="fa fa-caret-down"></i> Security Settings') } else { $('#show-security-props').html('<i class="fa fa-caret-right"></i> Security Settings') } }) // One-off check for default settings if ( $('#node-input-useSecurity').is(':checked') ) { if ( $('#node-input-sessionLength').val().length === 0 ) { $('#node-input-sessionLength').val(defaultSessionLength) } if ( $('#node-input-jwtSecret').val().length === 0 ) { $('#node-input-jwtSecret').val(defaultJwtSecret) } } // Security turning on/off $('#node-input-useSecurity').change(function() { // security is requested, enable other settings and add warnings if needed if ( this.checked ) { // If in production, cannot turn on security without https, in dev, give a warning if (window.location.protocol !== 'https' || window.location.protocol !== 'https:') { if (RED.settings.uibuilderNodeEnv !== 'development') { console.error('HTTPS NOT IN USE BUT SECURITY REQUESTED AND Node environment is NOT "development"') $('#node-input-useSecurity').prop('checked', false); this.checked = false } else { console.warn('HTTPS NOT IN USE BUT SECURITY REQUESTED - Node environment is "development" so this is allowed but not recommended') } // TODO: Add user warnings } } if ( this.checked ) { $('#node-input-sessionLength').prop('disabled', false) $('#node-input-jwtSecret').prop('disabled', false) $('#node-input-tokenAutoExtend').prop('disabled', false) // Add defaults if fields are empty if ( $('#node-input-jwtSecret').val().length === 0 ) { $('#node-input-jwtSecret').addClass('input-error') } if ( $('#node-input-sessionLength').val().length === 0 ) { $('#node-input-sessionLength').val(defaultSessionLength) } if ( $('#node-input-jwtSecret').val().length === 0 ) { $('#node-input-jwtSecret').val(defaultJwtSecret) } } else { // security not requested, disable other settings $('#node-input-sessionLength').prop('disabled', true) $('#node-input-jwtSecret').prop('disabled', true) $('#node-input-tokenAutoExtend').prop('disabled', true) } }) // -- end of security change -- // // What mode is Node-RED running in? development or something else? $('#nrMode').text(RED.settings.uibuilderNodeEnv) // Populate the template selection drop-down and select default (in advanced) populateTemplateDropdown() if ( that.templateFolder ) $('#node-input-templSel').val(that.templateFolder) else $('#node-input-templSel').prop('selectedIndex',0) checkDependencies() // Change change of template $('#node-input-templSel').change(function(_e) { // save the selected template folder name that.templateFolder = $('#node-input-templSel').val() // update the help tip $('#node-templSel-info').text(RED.settings.uibuilderTemplates[that.templateFolder].description) // Check if the dependencies are installed, warn if not checkDependencies() }) //#region ---- File Editor ---- // // Mark edit save/reset buttons as disabled by default fileIsClean(true) // Show the edit section, hide the main, adv & sec sections $('#show-edit-props').click(function(e) { e.preventDefault() // don't trigger normal click event $('#main-props').hide() $('#sec-props').hide() $('#show-sec-props').html('<i class="fa fa-caret-right"></i> Advanced Settings') $('#show-adv-props').html('<i class="fa fa-caret-right"></i> Advanced Settings') $('#show-sec-props').html('<i class="fa fa-caret-right"></i> Advanced Settings') $('#edit-props').show() // Build the file list getFileList() if ( uiace.editorLoaded !== true ) { // Clear out the editor // @ts-ignore ts(2367) if ($('#node-input-template').val('') !== '') $('#node-input-template').val('') // Create the ACE editor component //@ts-ignore Cannot find name 'RED'.ts(2304) uiace.editor = RED.editor.createEditor({ id: 'node-input-template-editor', mode: 'ace/mode/' + uiace.format, value: that.template }) // Keep a reference to the current editor session uiace.editorSession = uiace.editor.getSession() /** If the editor has changes, enable the save & reset buttons * using input event instead of change since it's called with some timeout * which is needed by the undo (which takes some time to update) **/ uiace.editor.on('input', function() { // Is the editor clean? fileIsClean(uiace.editorSession.getUndoManager().isClean()) }) /*uiace.editorSession.on('change', function(delta) { // delta.start, delta.end, delta.lines, delta.action console.log('ACE Editor CHANGE Event', delta) }) */ uiace.editorLoaded = true // Resize to max available height setACEheight() // Be friendly and auto-load the initial file via the admin API getFileContents() fileIsClean(true) } }) // Hide the edit section, show the main section $('#edit-close').click(function(e) { e.preventDefault() // don't trigger normal click event $('#main-props').show() $('#edit-props').hide() }) // Handle the file editor change of folder/file (1st built on click of show edit button) $('#node-input-folder').change(function(_e) { // Rebuild the file list getFileList() }) $('#node-input-filename').change(function(_e) { // Get the content of the file via the admin API getFileContents() }) // Handle the folder new button $('#fldr-new-dialog_new').addClass('input-error').prop('disabled',true) $('#fldr-input-newname').addClass('input-error').on('input', function(){ if ( $('#fldr-input-newname').val().length === 0) { $('#fldr-input-newname').addClass('input-error') $('#fldr').addClass('input-error').prop('disabled',true) } else { $('#fldr-input-newname').removeClass('input-error') $('#fldr-new-dialog_new').removeClass('input-error').prop('disabled',false) } }) $('#fldr-new-dialog').dialog({ // define the dialog box autoOpen:false, modal:true, closeOnEscape:true, buttons: [ { text: 'Create', id: 'fldr-new-dialog_new', click: function() { // NB: Button is disabled unless name.length > 0 so don't need to check here // Call the new file API createNewFolder($('#fldr-input-newname').val()) $('#fldr-input-newname').val(null).addClass('input-error') $('#fldr-new-dialog_new').addClass('input-error').prop('disabled',true) $( this ).dialog( 'close' ) } },{ text: 'Cancel', click: function() { $('#fldr-input-newname').val(null).addClass('input-error') $('#fldr-new-dialog_new').addClass('input-error').prop('disabled',true) $( this ).dialog( 'close' ) } }, ] }).keyup(function(e) { // make enter key fire the create if (e.keyCode == $.ui.keyCode.ENTER) $('#fldr_new_dialog_new').click(); }) $('#btn-fldr-new').click(function(e) { $('#fldr_url').html( $('#node-input-url').val() ) $('#fldr-new-dialog').dialog('open') }) // Handle the folder delete button $('#fldr-del-dialog').dialog({ autoOpen:false, modal:true, closeOnEscape:true, buttons: [ { text: 'Delete', id: 'fldr-del-dialog_del', click: function() { // Call the delete folder API (uses the current folder) deleteFolder() $( this ).dialog( 'close' ) } },{ text: 'Cancel', click: function() { $( this ).dialog( 'close' ) } }, ] }) $('#btn-fldr-del').click(function(e) { $('#fldr-del-dialog_del').addClass('input-error').css('color','brown') $('#fldr-del-name').text($('#node-input-folder').val()) if ( $('#node-input-folder').val() === 'src' ) { if ( $('#node-input-copyIndex').is(':checked') ) { $('#fldr-del-recopy').css('color','').text('Copy flag is set so the src folder will be recreated and the index.(html|js|css) files will be recopied from the master template.') } else { $('#fldr-del-recopy').css('color','brown').text('Copy flag is NOT set so the src folder will NOT be recopied from the master template.') } } else { $('#fldr-del-recopy').css('color','').text('') } $('#fldr-del-dialog').dialog('open') }) // Handle the file editor reset button (reload the file) $('#edit-reset').click(function(e) { e.preventDefault() // don't trigger normal click event // Get the content of the file via the admin API getFileContents() $('#file-action-message').text('') }) // Handle the file editor save button $('#edit-save').click(function(e) { e.preventDefault() // don't trigger normal click event var authTokens = RED.settings.get('auth-tokens') // Post the updated content of the file via the admin API // NOTE: Cannot use jQuery POST function as it sets headers that trigger a CORS error. Do it using native requests only. // Clients will be reloaded if the reload checkbox is set. var request