From c26a9d45eb59eca5a39fe78f8fa46897808376ec Mon Sep 17 00:00:00 2001 From: Julian Knight <1591850+TotallyInformation@users.noreply.github.com> Date: Sat, 16 Oct 2021 13:49:05 +0100 Subject: [PATCH] Pkg mgt & route mgt rewritten. Some bugs fixed. Improved Edit panel. See changelog for details. --- CHANGELOG.md | 647 +--- docs/CHANGELOG-v3-v4.md | 582 ++++ docs/package-management-process.md | 67 +- global.d.ts | 5 +- nodes/libs/admin-api-v2.js | 344 +-- nodes/libs/admin-api-v3.js | 186 +- nodes/libs/package-mgt.js | 396 ++- nodes/libs/socket.js | 2 +- nodes/libs/web.js | 350 +-- nodes/uib-receiver.html | 170 - nodes/uib-receiver.js | 136 - nodes/uibuilder.html | 4604 +++++++++++++++------------- nodes/uibuilder.js | 46 +- src/editor/uibuilder/editor.js | 714 +++-- src/editor/uibuilder/main.html | 9 +- src/editor/uibuilder/panel.html | 19 +- typedefs.js | 17 +- 17 files changed, 4215 insertions(+), 4079 deletions(-) create mode 100644 docs/CHANGELOG-v3-v4.md delete mode 100644 nodes/uib-receiver.html delete mode 100644 nodes/uib-receiver.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d4871740..e39b5cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,21 +15,21 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### TODO * FIXES NEEDED: - * [ ] url rename fails if user updates template before committing url change - * [ ] Don't allow access to file editor if the node hasn't been deployed yet. - what is different if a node instance hasn't been deployed yet? * [ ] ERRORCHECK: Imported node with not default template had default template on first deploy * [ ] ERROR: Removing a module after install but without closing and reopening editor panel did nothing * [ ] When turning on idx, link won't work until node re-deployed - reflect in panel UI + * [ ] On connection, send current uib version to client * Move package management to a new singleton class * [x] Use execa promise-based calls to npm * [x] Allow any valid npm name spec in editor - * [ ] Add display of version on installed packages list - * [ ] Allow version spec on install - * [ ] Allow installation of GitHub and local packages - gh install partially works + * [x] Use `uibRoot`s package.json file for package management and add custom `uibuilder` property to it for managing package metadata and other config. + * [x] Add display of version & URL path on installed packages list + * [x] Allow version spec on install + * [x] Move package management out of web.js + * [x] Remove uib.vendorPaths + * [-] Allow installation of GitHub and local packages - Need to finish LOCAL installs * [ ] Check for new versions of installed packages when entering the library manager - * [ ] Use `uibRoot`s package.json file for package management and add custom `uibuilder` property to it for managing package metadata and other config. - * [ ] Move package management out of web.js * [x] Add uib-sender node * [x] Dropdown to choose uib URL @@ -43,6 +43,7 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * [x] Add `setOriginator` method to set default originator * [ ] How to add originator to the eventSend method? via an HTML data- attrib or use mapper? * [ ] Add mapper to map component id to originator & extend `eventSend` accordingly + * [ ] Add version check * uibuilder Panel * [x] Switch from hide/show interface to tabbed interface @@ -55,23 +56,28 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * [ ] Show template (instance root) folder * [ ] Add a new template and example to demonstrate the sender node. - -* *Maybe* Add caching option to uibuilder - as a shared service so that other nodes could also use it - allow control via msg so that any msg could use/avoid the cache - may need additional option to say whether to cache by msg.topic or just cache all msgs. May also need persistance (use context vars, allow access to all store types) - offer option to limit the number of msgs retained -* *Maybe* add in/out msg counts to status? -* *Maybe* add prev/curr version and checks? -* *Maybe* add alternate `uibDashboard` node that uses web components and data-driven composition. -* _Maybe_ On change of URL - signal other nodes? As no map currently being maintained - probably not possible -* [ ] _Maybe_ Add uib-receiver node - * [ ] Status msg to show node-id - * [ ] Have to manually send to by adding originator property -* _Maybe_ Add experimental flag - use settings.js and have an object of true/false values against a set of text keys for each feature. - * [ ] Update docs - * [ ] Add processing to nodes to be able to mark them as experimental. - +* [ ] Allow changes to socket.io config. [issue](https://discourse.nodered.org/t/uibuilderfe-socket-disconnect-reason-transport-close-when-receiving-json-from-node-red/52288/4) + * Templates - * Add ability to load an example flow from a template (add list to package.json and create a drop-down in the editor?) - * **Make sure that templates have a way of signalling to uibuilder what libraries they need.** - * Add example flows - using the pluggable libraries feature of Node-RED v2.1 + * [ ] **Add ability to specify library dependencies in package.json** - not using the dependencies prop because we dont want to install libraries in the instance root but rather the uibRoot. Will need matching code in the Editor panel & a suitable API. + * [ ] Add ability to load an example flow from a template (add list to package.json and create a drop-down in the editor?) + * [ ] Add example flows - using the pluggable libraries feature of Node-RED v2.1 + + +* *Maybe* + * *Maybe* Add caching option to uibuilder - as a shared service so that other nodes could also use it - allow control via msg so that any msg could use/avoid the cache - may need additional option to say whether to cache by msg.topic or just cache all msgs. May also need persistance (use context vars, allow access to all store types) - offer option to limit the number of msgs retained + * *Maybe* add in/out msg counts to status? + * *Maybe* add prev/curr version and checks? + * *Maybe* add alternate `uibDashboard` node that uses web components and data-driven composition. + * _Maybe_ On change of URL - signal other nodes? As no map currently being maintained - probably not possible + * [ ] _Maybe_ Add uib-receiver node + * [ ] Status msg to show node-id + * [ ] Have to manually send to by adding originator property + * _Maybe_ Add experimental flag - use settings.js and have an object of true/false values against a set of text keys for each feature. + * [ ] Update docs + * [ ] Add processing to nodes to be able to mark them as experimental. + * *Maybe* Find a way to support wildcard URL patterns which would automatically add structured data and make it available to uibuilder flows. Possibly by adding the param data to all output msg's. + * Security * Add roles/tags options to JWT? Or at least to the user session record @@ -96,6 +102,7 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Make sure localStorage _auth is always updated after control msg from server * Make bootstrap-vue toasts optional, add auth change notices * DOC UPDATES NEEDED: + * Add note about [default msg size](https://github.com/socketio/socket.io/issues/3946#issuecomment-850704139) * FE * Variables: * self.security - flag: indicates if server has security turned on @@ -104,6 +111,22 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * self.initSecurity * self.setStore +### BREAKING + +* **Installation of packages** for use in your front-end code has been **moved** from the `userDir` to `uibRoot`. + + Note that _only_ packages installed into the `uibRoot` folder will be recognised. + + Unfortunately, this means that you will need to re-install your packages in the correct location. You should uninstall them from the userDir. + + By default: userDir = `~/.node-red/`, uibRoot = `~/.node-red/uibuilder/`. However, both can, of course, be moved elsewhere. + + uibuilder will automatically create a suitable `package.json` file in `uibRoot`. That file not only lists the installed packages but also has a custom property `uibuilder` that contains metadata for the uibuilder modules. Specifically, it lists all of the necessary detailed data for the installed packages. + + The files `/.config/packageList.json` and `/.config/masterPackageList.json` are no longer used and may be deleted. + + You can now install not only packages from npmjs.com but also from GitHub and even local development packages. @scopes are fully supported and versions, tags, and branches are supported for both npmjs and GitHub installs. + ### New * **New node `uib-sender`** - this node allows you to send a msg to any uibuilder instance's connected front-end clients. @@ -120,8 +143,20 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The `uibuilderfe.js` library has been updated to allow easy use of the `originator` property for `uibuilder.send()`. See below for details. +* Package Management. You can now install not only packages from npmjs.com but also from GitHub and even local development packages. @scopes are fully supported and versions, tags, and branches are supported for both npmjs and GitHub installs. + + Note that _only_ packages installed into the `uibRoot` folder will be recognised. + +* New layout for the Editor panel. + + This is a much cleaner and clearer layout. It also blocks access to parts of the config that don't work until a newly added node has been Deployed for the first time so that its server folder has been created. + + There are also some additional error and warning messages to make things clearer. + * Updated node status display. Any instance of uibuilder will now show additional information in the status. In addition to the existing text information, the status icon will be YELLOW if security is turned on (default is blue). In addition, if _Allow unauthorised msg traffic_ is on, the icon will show as a ring instead of a dot. +* Added a version checker that allows uibuilder to notify users if a node instance must be updated due to a change of version. + ### Changed * `uibuilderfe.js` client library updated to allow for the use of an `originator` metadata property. This facilitates routing of messages back to an alternative node instead of the main uibuilder node. @@ -136,6 +171,10 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Note that at present, control messages from the front-end cannot be routed to a different originator node, they all go to the main uibuilder node. This will be reviewed in a future release. Let me know if you think that it is needed. +* Improvements to the Editor help panel. Should hopefully be clearer and includes all of the settings and custom msg properties. Now uses a tabbed interface. + +* Improvements to the "uibuilder details" page should make it easier to read. The data for ExpressJS Routes is much improved. + * Security improvements: * When security is active, a client that re-connects to Node-RED will attempt to reuse its existing authorisation (see the localStorage bullet below). @@ -189,549 +228,69 @@ uibuilder adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Tech docs - some minor improvements to the security process docs and bring into line with current process. -* Improvements to the Editor help panel. Should hopefully be clearer and includes all of the settings and custom msg properties. Now uses a tabbed interface. - -* Internal and development improvements: - - * Shared event handler implemented. This enables external nodes to send and receive data to/from uibuilder front-end clients. - - * Gulp implemented - - * initially for composing the `uibuilder.html` from the contents of `src/editor` - * and to replace the previously manual minify step for `uibuilderfe.js` - * _other tasks likely to be added in the future to make more efficient code and ease the release/publish process_. - - * New eslint rulesets implemented & config restructured. Along with the .html file decomposition, this makes for a much more accurate linting process. - - * Massive number of minor code improvements to `uibuilder.html` and `uibuilder.js` & to the supporting libs and `uibuilderfe.js` thanks to the impoved linting. - - * Even more massive restructuring of `uibuilder.js`. - - * Removing the need for the `node` object. This meant the use of some arrow functions to be able to retain the correct context in event handlers and callbacks. - * Destructuring the big exported function into a series of smaller functions. Makes the code a lot clearer and easier to follow. Also helped identify a few bits of logic that were not quite sane or not needed at all (the result of evolutionary growth of the code). - * Using named functions throughout should make future debugging a little easier. - * npm package handling moved to a separate singleton class in `package-mgt.js` - - * Removed `inputHandler` function from `uiblib.js`. Code folded into the `inputMsgHandler` function in `uibuilder.js` which has been destructured so is small enough to have it as a single function. - - * Package management rewritten. Should be faster and uses async/Promise functions. - - * v3 admin API changes - - * Moved v3 admin API to its own module (`libs/admin-api-v3.js`) and changed to be an ExpressJS router instance. - * Moved the setup from uibuilder.js to web.js - * New v3 admin API command added to list all of the deployed instances of uibuilder. Issue a GET with `cmd=listinstances`. This allows other nodes to get a list of all of the uibuilder instance URL's and the ID's of the nodes that create them. See the `uib-sender` node's html file for details. - - * v2 admin API changes - - * Moved v2 admin API to its own module (`libs/admin-api-v2.js`) and changed to be an ExpressJS router instance. - * Moved the setup from uibuilder.js to web.js - - * `web.js` changes - - * Both admin and user routes restructured, making use of Express.Router's to improve layout and control. - * Setup of v3 admin API moved to `web.js` class module (out of uibuilder.js). - * Setup of v2 admin API moved to `web.js` class module (out of uibuilder.js). - * New `web.dumpRoutes(print=true)` method added to `web.js` - dumpRoutes outputs a summary of all the relevant ExpressJS routes both for uib user facing web and Node-RED admin web servers. Also added individual methods: `web.dumpUserRoutes(print=true)`, `web.dumpAdminRoutes(print=true)`, and `web.dumpInstanceRoutes(print=true, url=null)` (where passing a uib url will just dump that one set of routes). - * Removed the separate `serve-static` npm package. This is now built into ExpressJS and not required separately. - * Removed the separate `body-parser` npm package. This is now built into ExpressJS and not required separately. - * Moved the user-facing API's to web.js from uibuilder.js and moved to their own Express.Router on `../uibuilder/...`. - * Added a new `this.routers` object - this helps with uibuilder live configuration documentation as it records all of the ExpressJS Routes that uibuilder adds. - -### Fixed - -* `uiblib.js` `logon()` - Fixed error that prevented logon from actually working due to misnamed JWT property. -* A number of hard to spot bugs in `uibuilder.html` thanks to better linting & disaggregation into component parts -* In `uibuilderfe.js`, security was being turned on even if the server set it to false. -* Fixed an issue when removing uibuilder nodes caused by the move to socket.io v4. Should fix the failure to remove unused uib instance root folders and fix renaming problems as well. - - - -## [4.1.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v4.1.0...v4.1.1) - -### New - -* [Issue #151](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/151)If the advanced option to "Show web view of source files" is selected, also show a link to the webpage. -### Changed - -* [Issue #149](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/149) If security is turned on, you can now run without Node-RED using TLS even in production. This is because you may wish to provide TLS via a reverse proxy. - - You still get a warning in the editor though. - -* Moved back-end libraries from `nodes` folder to `nodes/libs` to keep things tidier (especially if additional nodes added in the future) -* Add simple debug function to web.js to allow the ExpressJS routing stack to be dumped to stdout - -### Fixed - -* [Issue #150](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/150) Switching between src and dist folders now works without having to restart Node-RED. Existing routes are removed first then re-added. -* Common folder is only served once (previously it was been added to the ExpresJS router stack once for each node instance). - -## [4.1.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v4.0.1...v4.1.0) +### Internal and development improvements -### New - -* Add drop-down to adv settings that lets the served folder be changed between src and dist. [#147](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/147) - - - If the `/` folder does not exist, it will be silently created. - - If the `//index.html` file does not exist, a warning will be issued to the Node-RED log & the Node-RED debug panel. - -* Allow front-end code to update the `msg`. [#146](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/146) - - This allows your front-end code to be its own test harness by pretending that a msg has been `sent` from Node-RED. It would also let you have a single processing method even if you wanted to use a non-Node-RED data input (e.g. a direct MQTT connection or some other API). - - ```js - uibuilder.set( 'msg', { topic:'my/topic', payload: {a:1, b:'hello'} } ) - ``` - - When using this feature, the `uibuilder.onChange('msg', function(msg) { ... })` function is still triggered as expected. - -### Fixed - - * [#148](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/148) Editor node config cannot escape https check when not running in development mode - -## [4.0.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v4.0.0...v4.0.1) - -### Fixed - -* Minor bug stopping the logoff msg processing from working. - -### Updated +* Shared event handler implemented. This enables external nodes to send and receive data to/from uibuilder front-end clients. -* All dependencies and dev-dependencies updated - -## [4.0.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.3.1...v4.0.0) - -### Major Changes - -* Node.js v12+ is the minimum supported environment for Node-RED. -* Only "modern" browsers are now supported for both the Editor and the uibuilderfe front-end library as ES6 (ECMA2015) code is used. - - Let me know if this is a problem and I can build a backwards compatible version. +* Gulp implemented -#### Template handling is significantly changed in this major release + * initially for composing the `uibuilder.html` from the contents of `src/editor` + * and to replace the previously manual minify step for `uibuilderfe.js` + * _other tasks likely to be added in the future to make more efficient code and ease the release/publish process_. - New instances of uibuilder nodes will only be given the "blank" template which uses no front-end frameworks. +* New eslint rulesets implemented & config restructured. Along with the .html file decomposition, this makes for a much more accurate linting process. - You can load a different template using the "Template Settings" in the Editor. +* Massive number of minor code improvements to `uibuilder.html` and `uibuilder.js` & to the supporting libs and `uibuilderfe.js` thanks to the impoved linting. - **Loading a new template WILL overwrite any files with the same name**. A warning is given though so even if you press the button, you can still back out. +* Even more massive restructuring of `uibuilder.js`. - You can choose from the following internal templates: + * Removing the need for the `node` object. This meant the use of some arrow functions to be able to retain the correct context in event handlers and callbacks. + * Destructuring the big exported function into a series of smaller functions. Makes the code a lot clearer and easier to follow. Also helped identify a few bits of logic that were not quite sane or not needed at all (the result of evolutionary growth of the code). + * Using named functions throughout should make future debugging a little easier. + * npm package handling moved to a separate singleton class in `package-mgt.js` - * _VueJS & bootstrap-vue_ - The previous default template. - * _Simple VueJS_ - A minimal VueJS example. - * _Blank_ - The new default. - * _External_ - See below. - - **But**, you can now also chose an **EXTERNAL** template! This will let you choose from [any remote location supported by **degit**](https://github.com/Rich-Harris/degit#basics). You can use `TotallyInformation/uib-template-test` as an example (on [GitHub](https://github.com/TotallyInformation/uib-template-test)). - - **NOTE**: When using an external template, no check is currently done on dependencies, you must install these yourself. I will try to add this feature in the future. - -#### Changing the `uibRoot` folder - - You can now set uibuilder's root folder - that stores configuration, common, security and each node's front-end code - to a different location. The default location is in your userDir folder in a sub-folder called `uibuilder`. If you are using projects, the sub-folder will be in your projects root folder. See [docs/changing-uibroot.md](docs/changing-uibroot.md) for more detail. +* Removed `inputHandler` function from `uiblib.js`. Code folded into the `inputMsgHandler` function in `uibuilder.js` which has been destructured so is small enough to have it as a single function. -### Updated +* Package management rewritten. Should be faster and uses async/Promise functions. -* Update fs-extra to [v10](https://github.com/jprichardson/node-fs-extra/compare/9.1.0...10.0.0). No longer supports node.js v10, requires v12+. -* Make some class methods private in web.js and socket.js. Requires node.js v12 as a minimum as it uses an ECMA2018 feature. -* web.setup and socket.setup can only be called once. -* Socket.IO updated from v2 to v4. -* Added Admin API check for whether a url has a matching instance root folder. (Was an outstanding to-do) -* Reworked the info block that is printed to the log on startup. Much neater and with added info on the webserver being used. -* Technical Docs have been improved in line with some other work I did recently on enterprise standards. +* Editor changes - The docsify configuration has been greatly improved with a new theme and some automation for dates and document front-matter. + * Added a version checker that allows uibuilder to notify users if a node instance must be updated due to a change of version. - Added a new page on changing the uibRoot folder. - - Updated the front page with links and explanations of the different sections. - -### New - -* In the technical documentation, you can now access and search the main README as well as the current and archive changelogs (v1 & v2) in addition to everything else. +* v3 admin API changes - Don't forget that you can access the tech docs on the Internet from [GitHub](https://totallyinformation.github.io/node-red-contrib-uibuilder/#/) AND locally from within Node-RED. - -* `nodes/web.js` - Added web.isConfigured to allow a check to see whether web.setup has been called. -* `nodes/sockets.js` - Added socket.isConfigured to allow a check to see whether socket.setup has been called. -* Add a new icon to the main readme that allows editing of uibuilder code using VSCode either via a remote repository or via a Docker container. - -### Fixed + * Moved v3 admin API to its own module (`libs/admin-api-v3.js`) and changed to be an ExpressJS router instance. + * Moved the setup from uibuilder.js to web.js + * New v3 admin API command added to list all of the deployed instances of uibuilder. Issue a GET with `cmd=listinstances`. This allows other nodes to get a list of all of the uibuilder instance URL's and the ID's of the nodes that create them. See the `uib-sender` node's html file for details. -* Node-RED edge-case for credentials was causing node to be marked as changed whenever "Done" button pressed even if no changes made. Turns out to be an issue if you don't give a password-type credential an actual value (e.g. leave it blank). Gave the `JWTsecret` a default value even when it isn't really needed. -* Instance details page - CSS now loads correctly even if using a customer server port. Some Socket.IO details that were missing now returned. -* web.js - specifying a custom server port caused uibuilder to crash. Now fixed. -* Lots of tidying up of log messages, especially TRACE level. -* Accidentally include a node.js v14+ issue, now removed. -* Additional try/catch blocks to force better reporting if there is an error in the uibuilder module files. +* v2 admin API changes + * Moved v2 admin API to its own module (`libs/admin-api-v2.js`) and changed to be an ExpressJS router instance. + * Moved the setup from uibuilder.js to web.js ---- +* `web.js` changes -## [3.3.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.3.0...v3.3.1) + * Both admin and user routes restructured, making use of Express.Router's to improve layout and control. + * Setup of v3 admin API moved to `web.js` class module (out of uibuilder.js). + * Setup of v2 admin API moved to `web.js` class module (out of uibuilder.js). + * New `web.dumpRoutes(print=true)` method added to `web.js` - dumpRoutes outputs a summary of all the relevant ExpressJS routes both for uib user facing web and Node-RED admin web servers. Also added individual methods: `web.dumpUserRoutes(print=true)`, `web.dumpAdminRoutes(print=true)`, and `web.dumpInstanceRoutes(print=true, url=null)` (where passing a uib url will just dump that one set of routes). + * Removed the separate `serve-static` npm package. This is now built into ExpressJS and not required separately. + * Removed the separate `body-parser` npm package. This is now built into ExpressJS and not required separately. + * Moved the user-facing API's to web.js from uibuilder.js and moved to their own Express.Router on `../uibuilder/...`. + * Added a new `this.routers` object - this helps with uibuilder live configuration documentation as it records all of the ExpressJS Routes that uibuilder adds. ### Fixed -Added try/catch around Untrapped `JSON.stringify` in uiblib.js `showInstanceDetails()`. Prevent crash. - -## [3.3.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.2.1...v3.3.0) - -### New - -* Add [new pre-defined msg](./docs/pre-defined-msgs.md) from Node-RED that will cause the front-end client (browser) to reload. -* Add auto-reload flag to file editor - if set, any connected clients will automatically reload when a file is saved. (Only from the file editor in Node-RED for now, later I'll extend this to work if you are editing files using external editors). -* Add new function to uibuilderfe.js - `uibuilder.clearEventListeners()` - Will forcably clear any `onChange` event listeners that have been created. Partial update for [Issue #134](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/134). -* Added initial documentation for front-end build tooling to technical documentation (general info and Snowpack). - -### Fixed - -* [Issue #126](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/126) - Security not turning on even if TLS is used. -* Update security.js template to remove simple false return if authentication fails - this is no longer valid. - -### Updated - -* Bump dependencies to latest -* Add collapsible summaries to README.md -* Various updates to technical documentation -* Update chkAuth validation function to make it more robust -* Improve auth process logging and msg._auth.info checks -* Remove simple true/false return from auth processing as this is no longer valid -* uibuilderfe - - * Added check for `uibuilder.start()` having already been called and prevent it being run more than once. Partial update for [Issue #134](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/134). - * Add new function `uibuilder.clearEventListeners()` - see details in [New](#new) above. - * Added initial code for a simple alert - not yet ready for use. - -* Internal code refactoring - - * Prep for adding the ability for uibuilder to use its own independent ExpressJS server - * Rename uibuilder.js's `nodeGo()` function to `nodeInstance()` for clarity - * Add `dumpReq()` to tilib.js - returns the important bits of an ExpressJS REQ object - * Begin to add Node-RED type definitions - * Add ExpressJS type definitions - * Other linting improvements - * The refactoring has removed several hundred lines of code from the main js file and - simplified quite a few function calls. - - * **Moved Socket.IO processing to its own Singleton class module.** - - This means that any Node-RED related module can potentially `require` the `socket.js` module and get - access to the list of Socket.IO namespace's for all uibuilder node instances. All you need is the uibuilder URL name. - - It also means that any module can send messages to connected front-end clients simply by referencing the module and knowing - the url. - - Note that this currently only works once the class has been instantiated **and** a setup method called. - That requires a number of objects to be passed to it. This happens when you have added and deployed a uibuilder - node to your flows. - - But it does mean that, in theory at least, you could now write another custom node that could make use of the uibuilder communications - channel. Of course, it also opens the way for new nodes to be added to uibuilder. However, a slight caveat to that would be that - loading order would be important and you really must deploy uibuilder _before_ any other node that might want to use the module. - - * **Started moving ExpressJS web server handling to its own Singleton class module** - - Again, this will mean that any module running in Node-RED could potentially tie into the module - and be able to access/influence uibuilders web server capability. - - Works similarly to the Socket.IO class above. So it has to be initialised using a number of properties - from the core uibuilder node. - - Currently, only the core ExpressJS app and server references are handled by the class. More work - is required to move other processing into it. - -* Include PR #131 - add Socket.IO CORS support - -## [3.2.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.2.0...v3.2.1) - -### Fixed - -- [Issue #121](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/121) - Thanks to Sergio Rius for reporting and for [PR #122](https://github.com/TotallyInformation/node-red-contrib-uibuilder/pull/122) -- [Issue #123](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/123) - Allow for misuse of `browser` property in package.json for added libraries. Thanks to Steve McLaughlin for reporting and providing a potential fix. -- Technical Docs - Include favicon, expand search. Exclude missing file from search. - -## [3.2.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.3...v3.2.0) - -### New - -- You can now choose between front-end templates. - - Vue/Bootstrap-vue is still the default. - - Expand the "Advanced" settings to see the new dropdown. Note that uibuilder never overwrites your files so you either have to change the selection **before** the first deployment of the node or you have to delete the index.(html|js|css) and README.md files before changing the selection. - - Three templates are currently included, more may be added later: - - - An updated version of the existing default template that uses VueJS and bootstrap-vue. Contains an additional button demonstrating the new simple eventSend function. - - A new "Blank" template. This does not contain any front-end libraries or frameworks. It uses just the uibuilderfe library with raw DOM commands. - - A simplified Vue template. Contains the bare minimum to get you going. - - Templates are also now more comprehensive and flexible and contain README files for information. - - Templates will also warn you if you are missing a library that they depend on. Install them through the uibuilder library manager. - - -- The Editor will now tell you if you have missing dependencies for your chosen template. - - ![missing packages warning](docs/missing-packages-warning.png) - - Useful for people who forget to install vue and bootstrap-vue now that they have been removed from the default install. - -- When changing an existing node's URL: - - - **The existing source folder is renamed** - - No more losing track of existing code! - - - Folders as well as instances are checked for duplicates - - You are now warned to redeploy straight away, before doing anything else - -- When deleting a uibuilder instance, you are offered the chance to delete the source folder - -- In the `uibuilderfe` front-end library: - - - Added a new public method: `eventSend`. You can use this to attach to any HTML DOM event (e.g. a button click). - It will automatically send a msg back to Node-RED with details of the event. - - Details on how to use this are contained in the [technical docs](https://totallyinformation.github.io/node-red-contrib-uibuilder) in the `uibuilderfe-js` page. - You can access these docs directly in Node-RED either using the button in the configuration panel or the link - in the help panel. - - The updated default template also contains an example button that uses the new feature. - - Note that you can use more than just button clicks. It will work with _any_ DOM event that you attach it to. - -### Changed - -- Better warning if you set/change a URL to one that already exists. -- When changing URL: - - **The original folder (if it exists) will be renamed** - - The uibuilder instance folders are also checked. The change is rejected if the folder exists. - - You are warned that you need to redeploy before doing anything else. - - **NOTE**: You may have lots of old uibuilder folders lying around. If your url change is rejected and you can't think why, check the folders. - -- Check for duplicate url moved to v3 Admin API. API Test file updated. -- Further improvements to the techical documentation. This is now available [online](https://totallyinformation.github.io/node-red-contrib-uibuilder) as well as from the uibuilder node configuration panel and the help panel in the Editor. -- Improved links from the Node-RED Editor's help panel, particularly on how to use the uibuilderfe front-end library. -- Extensive improvements to the - [documentation for working with the `uibuilderfe` library](https://totallyinformation.github.io/node-red-contrib-uibuilder/#/front-end-library) in your front-end code. -- The default Vue template now defines the `data` section as a method instead of an object. This is recommended and prepares for Vue v3. - -## [3.1.3](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.1...v3.1.3) - -### Fix (kind of) - -[Issue #102](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/110) - -It seems that npm is incapable of safely being called from within a preinstall or postinstall npm script. - -Every effort at trying to achieve this in order to install `vue` and `bootstrap-vue` has failed. - -So I have removed this processing completely. - -The result of this is that you must install vue and bootstrap-vue yourself if they aren't already installed (and if you want to use them of course). - -You should instal v2 versions however, not v3 since there are a lot of breaking changes in vue v3 that have not been tested with uibuilder. -The installation command is: - -```bash -#cd -npm install vue@"2.*" bootstrap-vue@"2.*" -``` - -## [3.1.2](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.1...v3.1.2) - -This is a tweak to 3.1.1 to enable a workaround for the npm install issues. - -### Issue -- [Issue #102](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/110) The npm post install script has very unexpected and unwelcome side-effects that appear to be issues with npm itself. It seems that you cannot reliably run npm from within npm. - - There does not appear to be a reliable fix at this time. Set the environment variable `UIBNOPRE` to 'true' before installation to avoid the problem if you hit it. You should then install `vue` v2 and `bootstrap-vue` v2 manually if you need to: - - ```bash - #cd - npm install vue@"2.*" bootstrap-vue@"2.*" - ``` - - I will attempt to find another way to install vue and bootstrap-vue since in uibuilder v1/v2 you could not remove either of them. Some people don't want these libraries and so want to be able to remove them. - -### New -- Added environment variable `UIBNOPPRE` processing to the pre-install script -- Added environment variable `UIBDEBUG` processing to the pre-install script - -### Changed -- Removed Vue and bootstrap-vue peer dependencies from this package since they are actually dependencies for the userDir folder. Gets rid of the warnings. Vue and bootstrap-vue are installed by the pre-install script unless you set an environment variable `UIBNOPRE` to 'true' before installation. -- Post-install script is now a pre-install script. - -## [3.1.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.0...v3.1.1) - -Emergency fix. - -The permissions feature of the Node-RED Admin API does not seem to work as documented. - -``` -RED.httpAdmin.get('/uibgetfile', RED.auth.needsPermission('read'), function(req,res) { - // ..... -}) -``` - -Should allow you to have a user defined with "read" permission and they would be allowed to access the API endpoint. -However, as far as I can tell, this does not work. - -I have removed all permissions from the API endpoints until someone can work out how to do this correctly. - -Until then, all you can do is to remove the default user in settings.js so that defined users have no access until they have logged in. - -There is no longer a separation between read and write permissions I'm afraid. - -## [3.1.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.0.1...v3.1.0) - -### Fixed - -- [Issue #106](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/106) Editor: When editing files, a filename with a leading dot did not set the filetype correctly. - -- [Issue #105](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/105) Editor: Attempting to edit a hidden file (with a leading dot) resulted in an error and white screen. - -### New - -- [Issue #108](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/108) You can now view the uibuilder package docs (the ones in this package) by going to the url `/uibuilder/techdocs`. - - The package docs use [Docsify](https://docsify.js.org/#/?id=docsify) for formatting. The docs include a search feature as well. - - The docs are linked to from both the uibuilder help information panel and from a new button in the configuration panel. - -- The config editor has a new button Instance Details. clicking the button will show a new page in a new tab. The page contains debug details of the exact settings for the uibuilder instance. This should help people better understand all of the settings including folders and urls. - - -### Changed - -#### Editor, "Edit Source Files" improvements: - - **ALL** folders and files within the `/` folder can now be edited. - - - Soft- or Hard-linked folders and files can now be used. This lets you put your front-end resources wherever you like as long as you create a soft or hard link into the `/` folder. - - - Added better information toasts on file create/delete actions. - - Pop-up notifications are now given when you create/delete folders and files. - - - Made keyboard enter button do the default action in the create dialog windows. - - - Added more information to the create/delete dialog windows. (url, folder name, file name) - - - [Issue #102](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/102) Relaxed the file-type checks when editing files. Allows for use of more ACE file-types and prepares the way for the introduction of the Monaco editor in Node-RED v2. - - - [Issue #107](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/107) Allowed the selection of any folder or sub-folders in the file editor. - - The editor still constrains you to the folder for the instance but any folder within that root can be viewed. New sub-folders can be created and existing ones deleted. - - - [Issue #109](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/109) Persist the selection of folder and file when editing. - - This means that closing and reopening the editor will return to the last edited file. - - Uses browser local storage and so does not work with Internet Explorer (which hasn't been supported by uibuilder since v3.0.0). - - - Improved display when no file is available to edit or if the file cannot be opened. - - - Started moving to new v3 admin API's that are more consistent with less overheads. - - - Changed "Edit Source Files" button to say "Edit Files". Recognising the additional capabilities. - - - Changed button link names in the configuration panel to clarify and accommodate the 2 extra buttons for the instance details and technical docs links. - -#### uibuilder.js: - - Started to simplify and rationalise API checks and reporting. Deprecated `/uibfiles`, `/uibnewfile`, `/uibdeletefile` API's, replaced with new v3 admin API `/uibuilder/admin/:url`. Simplifies the admin API's, makes them more consistent and reduces the number of URL's. - - Added v3 admin API's to create new and delete files and folders - - Added `/uibuilder/instance/` admin API. Is created for each instance. Calling it will show a detailed information page for the given uibuilder instance. - -#### Other - -- Updated dependencies -- Installer: Improved the post-install console message (Post Install takes a while). Also forces VueJS to v2.x (not v3 as yet which will soon be the latest version because there are currently too many breaking changes). - -## [3.0.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.0.0...v3.0.1) - -### Changed - -* Fix for [Issue #100](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/100) - Detection of whether Node-RED is currently using https. -* Fix for [Issue #93](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/93) - Full screen editor doesn't work correctly for mobile users. Replaced custom code with equivalent feature from core. -* Remove test code from `uibuilder.html` - -## [3.0.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v2.0.8...v3.0.0) -### Summary - -As this contains rather a lot of changes, here is a summary of the key changes for users of the node. The details are in the following sections. - -- Breaking Changes - - - Minimum Node-RED version is now 1.0 - - Minimum Node.js version is 10 - - IE11 and other older browsers now no longer guaranteed to work. All modern browsers including mobile and Microsoft Edge (Chromium) should work. - -- New feature in uibuilderfe to be able to transparently feed data and configuration to VueJS components written to be compatible. -- New feature in uibuilderfe to be able to transparently create notification popovers (toasts) by sending a msg from Node-RED (no code needed). -- New security documentation - still evolving for the experimental security features -- `vue` and `bootstrap-vue` packages can now be removed (NB: if uibuilder previously installed, you need to remove and reinstall for this to be possible) -- Scoped packages can now be added and removed -- Improved Editor configuration panel layout for Advanced Settings -- Some simplification of the default VueJS JavaScript template. Makes it a little easier to read. -- New template file `/.config/security.js` - used to give you control over the security process, please read the caveats before attempting to use in this version. Do not use in a live environment, for development only right now. - -### New - -- By sending a msg from Node-RED with a pre-defined format, you can interact with VueJS with minimal or no front-end code - - - With no code at all, you can show a popover notification (toast) to the web page. - - With as little as a single line of HTML, you can control and send values to a custom uibuilder compatible VueJS component. - - Suitable components are in development. See the experimental module [uibuilder-vuejs-component-extras](https://github.com/TotallyInformation/uibuilder-vuejs-component-extras) for some example components. Specifically the `` component which is being developed as an exemplar and will be moved to a separate npm module at some point. - - The idea being to bridge some of the gap between the ease of use of Node-RED's Dashboard and the flexibility of uibuilder. Without needing to be a web development expert. - - This will be further enhanced in future releases - - - **NOTE** To use the Vue features, you need to pass a reference to your Vue app to uibuilder. - This is normally as simple as changing `uibuilder.start()` to `uibuilder.start(this)` - - - _This feature does not currently work with all Vue components._ See the [docs](./docs/vue-component-handling.md) for an alternate low-code version. - -- Moved pre-installed VueJS and bootstrap-vue to be installed into `` instead of into the uibuilder package folder. - - This allows the `vue` and `bootstrap-vue` packages to be uninstalled like everything else and resolves Issue [#75](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/75). - - Note that, at present, I have not added any clever code to remove the old installations of vue and bootstrap-vue. If you want to get them into the right place, remove and re-add uibuilder. Note that you don't need to do anything unless you want to be able to remove `vue` and `bootstrap-vue`. - -- uibuilderfe: Added msg._socketId to sent messages. - -- Added security documentation (Work in progress). - - Read these to understand how to use uibuilder security and how it works (respectively). - - [uibuilder Security Documentation](./docs/security.md) and [security.js Technical Documentation](./docs/securityjs.md). - -- Added new VueJS documentation [Vue Component Handling](./docs/vue-component-handling.md). +* `uiblib.js` `logon()` - Fixed error that prevented logon from actually working due to misnamed JWT property. +* A number of hard to spot bugs in `uibuilder.html` thanks to better linting & disaggregation into component parts +* In `uibuilderfe.js`, security was being turned on even if the server set it to false. +* Fixed an issue when removing uibuilder nodes caused by the move to socket.io v4. Should fix the failure to remove unused uib instance root folders and fix renaming problems as well. +* URL rename failed if user updates template before committing url change. This is now blocked. +* File editor failed if the node hadn't been deployed yet. Blocked if instance folder hasn't yet been created. -### Changed -- Documentation: Greatly improved documentation coverage in the `/docs` folder. This contains a lot of developer documentation which should make it easier to work on improvements to uibuilder in the future. Still a work in progress. -- Documentation now uses Docsify for presentation and easier reading. Open `./docs/index.html` in your browser. -- Editor: Tidy up the Advanced Settings section of the configuration panel. -- uibuilderfe: Internal improvements to get/set functions. -- uibuilderfe: Simplify default Vue templates. -- Further code tidy up. -- Add code isolation to Editor config code to prevent namespace clashes. -- Improve standardisation of output topic. -- Moved some serveStatic code back to instance level to allow caching to be changed by config. -- Changed palette category name from "UI Builder" to "uibuilder" and palette label to "uibuilder" from "UI Builder" for consistency with other nodes. -- Moved all front-end master code (e.g. `nodes/src` and `nodes/dist`) to new top-level folder `front-end` & refactored `uibuilder.js` accordingly. Folder references also changed to new properties in the `uib` variable. -- Moved the templates folder from `nodes` to its own top-level folder and refactored uibuilder.js accordingly. The folder reference is held in the `uib.masterTemplateFolder` variable. -- Change minify of uibuilderfe from uglify-js to bable-minify because uglify-js does not support ES6+ -### Fixed -- Running behind a proxy was causing Socket.IO namespace issues (see [Issue #84](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/84) - Removing `httpRoot` from the namespace should fix that. It is no longer required anyway since url uniqueness checks were added. ## Experimental and partially working new features @@ -810,8 +369,8 @@ _Security is mostly controlled via websocket messages, not by HTTP. The web UI i ---- -Because of the many changes in v3, the v2 changelog has been moved to a separate file: [v2 Changelog](/docs/CHANGELOG-v2.md). -Similarly, v1 chanegs are now in the [v1 Changelog](/docs/CHANGELOG-v1.md). +Because of the many changes in v5, the v3/v4 changelog has been moved to a separate file: [v3/v4 Changelog](/docs/CHANGELOG-v3-v4.md). +Similarly, older chanegs are in: [v1 Changelog](/docs/CHANGELOG-v1.md), [v2 Changelog](/docs/CHANGELOG-v2.md). ---- diff --git a/docs/CHANGELOG-v3-v4.md b/docs/CHANGELOG-v3-v4.md new file mode 100644 index 00000000..e2b17bdf --- /dev/null +++ b/docs/CHANGELOG-v3-v4.md @@ -0,0 +1,582 @@ +# Changelog Archive for v3 and v4 + +Current major version changelog can be found in the root of this package: `../CHANGELOG.md`. + +---- + +## [4.1.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v4.1.0...v4.1.1) + +### New + +* [Issue #151](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/151)If the advanced option to "Show web view of source files" is selected, also show a link to the webpage. +### Changed + +* [Issue #149](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/149) If security is turned on, you can now run without Node-RED using TLS even in production. This is because you may wish to provide TLS via a reverse proxy. + + You still get a warning in the editor though. + +* Moved back-end libraries from `nodes` folder to `nodes/libs` to keep things tidier (especially if additional nodes added in the future) +* Add simple debug function to web.js to allow the ExpressJS routing stack to be dumped to stdout + +### Fixed + +* [Issue #150](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/150) Switching between src and dist folders now works without having to restart Node-RED. Existing routes are removed first then re-added. +* Common folder is only served once (previously it was been added to the ExpresJS router stack once for each node instance). + +## [4.1.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v4.0.1...v4.1.0) + +### New + +* Add drop-down to adv settings that lets the served folder be changed between src and dist. [#147](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/147) + + - If the `/` folder does not exist, it will be silently created. + - If the `//index.html` file does not exist, a warning will be issued to the Node-RED log & the Node-RED debug panel. + +* Allow front-end code to update the `msg`. [#146](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/146) + + This allows your front-end code to be its own test harness by pretending that a msg has been `sent` from Node-RED. It would also let you have a single processing method even if you wanted to use a non-Node-RED data input (e.g. a direct MQTT connection or some other API). + + ```js + uibuilder.set( 'msg', { topic:'my/topic', payload: {a:1, b:'hello'} } ) + ``` + + When using this feature, the `uibuilder.onChange('msg', function(msg) { ... })` function is still triggered as expected. + +### Fixed + + * [#148](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/148) Editor node config cannot escape https check when not running in development mode + +## [4.0.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v4.0.0...v4.0.1) + +### Fixed + +* Minor bug stopping the logoff msg processing from working. + +### Updated + +* All dependencies and dev-dependencies updated + +## [4.0.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.3.1...v4.0.0) + +### Major Changes + +* Node.js v12+ is the minimum supported environment for Node-RED. +* Only "modern" browsers are now supported for both the Editor and the uibuilderfe front-end library as ES6 (ECMA2015) code is used. + + Let me know if this is a problem and I can build a backwards compatible version. + +#### Template handling is significantly changed in this major release + + New instances of uibuilder nodes will only be given the "blank" template which uses no front-end frameworks. + + You can load a different template using the "Template Settings" in the Editor. + + **Loading a new template WILL overwrite any files with the same name**. A warning is given though so even if you press the button, you can still back out. + + You can choose from the following internal templates: + + * _VueJS & bootstrap-vue_ - The previous default template. + * _Simple VueJS_ - A minimal VueJS example. + * _Blank_ - The new default. + * _External_ - See below. + + **But**, you can now also chose an **EXTERNAL** template! This will let you choose from [any remote location supported by **degit**](https://github.com/Rich-Harris/degit#basics). You can use `TotallyInformation/uib-template-test` as an example (on [GitHub](https://github.com/TotallyInformation/uib-template-test)). + + **NOTE**: When using an external template, no check is currently done on dependencies, you must install these yourself. I will try to add this feature in the future. + +#### Changing the `uibRoot` folder + + You can now set uibuilder's root folder - that stores configuration, common, security and each node's front-end code - to a different location. The default location is in your userDir folder in a sub-folder called `uibuilder`. If you are using projects, the sub-folder will be in your projects root folder. See [docs/changing-uibroot.md](docs/changing-uibroot.md) for more detail. + +### Updated + +* Update fs-extra to [v10](https://github.com/jprichardson/node-fs-extra/compare/9.1.0...10.0.0). No longer supports node.js v10, requires v12+. +* Make some class methods private in web.js and socket.js. Requires node.js v12 as a minimum as it uses an ECMA2018 feature. +* web.setup and socket.setup can only be called once. +* Socket.IO updated from v2 to v4. +* Added Admin API check for whether a url has a matching instance root folder. (Was an outstanding to-do) +* Reworked the info block that is printed to the log on startup. Much neater and with added info on the webserver being used. +* Technical Docs have been improved in line with some other work I did recently on enterprise standards. + + The docsify configuration has been greatly improved with a new theme and some automation for dates and document front-matter. + + Added a new page on changing the uibRoot folder. + + Updated the front page with links and explanations of the different sections. + +### New + +* In the technical documentation, you can now access and search the main README as well as the current and archive changelogs (v1 & v2) in addition to everything else. + + Don't forget that you can access the tech docs on the Internet from [GitHub](https://totallyinformation.github.io/node-red-contrib-uibuilder/#/) AND locally from within Node-RED. + +* `nodes/web.js` - Added web.isConfigured to allow a check to see whether web.setup has been called. +* `nodes/sockets.js` - Added socket.isConfigured to allow a check to see whether socket.setup has been called. +* Add a new icon to the main readme that allows editing of uibuilder code using VSCode either via a remote repository or via a Docker container. + +### Fixed + +* Node-RED edge-case for credentials was causing node to be marked as changed whenever "Done" button pressed even if no changes made. Turns out to be an issue if you don't give a password-type credential an actual value (e.g. leave it blank). Gave the `JWTsecret` a default value even when it isn't really needed. +* Instance details page - CSS now loads correctly even if using a customer server port. Some Socket.IO details that were missing now returned. +* web.js - specifying a custom server port caused uibuilder to crash. Now fixed. +* Lots of tidying up of log messages, especially TRACE level. +* Accidentally include a node.js v14+ issue, now removed. +* Additional try/catch blocks to force better reporting if there is an error in the uibuilder module files. + + +--- + +## [3.3.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.3.0...v3.3.1) + +### Fixed + +Added try/catch around Untrapped `JSON.stringify` in uiblib.js `showInstanceDetails()`. Prevent crash. + +## [3.3.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.2.1...v3.3.0) + +### New + +* Add [new pre-defined msg](./docs/pre-defined-msgs.md) from Node-RED that will cause the front-end client (browser) to reload. +* Add auto-reload flag to file editor - if set, any connected clients will automatically reload when a file is saved. (Only from the file editor in Node-RED for now, later I'll extend this to work if you are editing files using external editors). +* Add new function to uibuilderfe.js - `uibuilder.clearEventListeners()` - Will forcably clear any `onChange` event listeners that have been created. Partial update for [Issue #134](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/134). +* Added initial documentation for front-end build tooling to technical documentation (general info and Snowpack). + +### Fixed + +* [Issue #126](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/126) - Security not turning on even if TLS is used. +* Update security.js template to remove simple false return if authentication fails - this is no longer valid. + +### Updated + +* Bump dependencies to latest +* Add collapsible summaries to README.md +* Various updates to technical documentation +* Update chkAuth validation function to make it more robust +* Improve auth process logging and msg._auth.info checks +* Remove simple true/false return from auth processing as this is no longer valid +* uibuilderfe + + * Added check for `uibuilder.start()` having already been called and prevent it being run more than once. Partial update for [Issue #134](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/134). + * Add new function `uibuilder.clearEventListeners()` - see details in [New](#new) above. + * Added initial code for a simple alert - not yet ready for use. + +* Internal code refactoring + + * Prep for adding the ability for uibuilder to use its own independent ExpressJS server + * Rename uibuilder.js's `nodeGo()` function to `nodeInstance()` for clarity + * Add `dumpReq()` to tilib.js - returns the important bits of an ExpressJS REQ object + * Begin to add Node-RED type definitions + * Add ExpressJS type definitions + * Other linting improvements + * The refactoring has removed several hundred lines of code from the main js file and + simplified quite a few function calls. + + * **Moved Socket.IO processing to its own Singleton class module.** + + This means that any Node-RED related module can potentially `require` the `socket.js` module and get + access to the list of Socket.IO namespace's for all uibuilder node instances. All you need is the uibuilder URL name. + + It also means that any module can send messages to connected front-end clients simply by referencing the module and knowing + the url. + + Note that this currently only works once the class has been instantiated **and** a setup method called. + That requires a number of objects to be passed to it. This happens when you have added and deployed a uibuilder + node to your flows. + + But it does mean that, in theory at least, you could now write another custom node that could make use of the uibuilder communications + channel. Of course, it also opens the way for new nodes to be added to uibuilder. However, a slight caveat to that would be that + loading order would be important and you really must deploy uibuilder _before_ any other node that might want to use the module. + + * **Started moving ExpressJS web server handling to its own Singleton class module** + + Again, this will mean that any module running in Node-RED could potentially tie into the module + and be able to access/influence uibuilders web server capability. + + Works similarly to the Socket.IO class above. So it has to be initialised using a number of properties + from the core uibuilder node. + + Currently, only the core ExpressJS app and server references are handled by the class. More work + is required to move other processing into it. + +* Include PR #131 - add Socket.IO CORS support + +## [3.2.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.2.0...v3.2.1) + +### Fixed + +- [Issue #121](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/121) - Thanks to Sergio Rius for reporting and for [PR #122](https://github.com/TotallyInformation/node-red-contrib-uibuilder/pull/122) +- [Issue #123](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/123) - Allow for misuse of `browser` property in package.json for added libraries. Thanks to Steve McLaughlin for reporting and providing a potential fix. +- Technical Docs - Include favicon, expand search. Exclude missing file from search. + +## [3.2.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.3...v3.2.0) + +### New + +- You can now choose between front-end templates. + + Vue/Bootstrap-vue is still the default. + + Expand the "Advanced" settings to see the new dropdown. Note that uibuilder never overwrites your files so you either have to change the selection **before** the first deployment of the node or you have to delete the index.(html|js|css) and README.md files before changing the selection. + + Three templates are currently included, more may be added later: + + - An updated version of the existing default template that uses VueJS and bootstrap-vue. Contains an additional button demonstrating the new simple eventSend function. + - A new "Blank" template. This does not contain any front-end libraries or frameworks. It uses just the uibuilderfe library with raw DOM commands. + - A simplified Vue template. Contains the bare minimum to get you going. + + Templates are also now more comprehensive and flexible and contain README files for information. + + Templates will also warn you if you are missing a library that they depend on. Install them through the uibuilder library manager. + + +- The Editor will now tell you if you have missing dependencies for your chosen template. + + ![missing packages warning](docs/missing-packages-warning.png) + + Useful for people who forget to install vue and bootstrap-vue now that they have been removed from the default install. + +- When changing an existing node's URL: + + - **The existing source folder is renamed** + + No more losing track of existing code! + + - Folders as well as instances are checked for duplicates + - You are now warned to redeploy straight away, before doing anything else + +- When deleting a uibuilder instance, you are offered the chance to delete the source folder + +- In the `uibuilderfe` front-end library: + + - Added a new public method: `eventSend`. You can use this to attach to any HTML DOM event (e.g. a button click). + It will automatically send a msg back to Node-RED with details of the event. + + Details on how to use this are contained in the [technical docs](https://totallyinformation.github.io/node-red-contrib-uibuilder) in the `uibuilderfe-js` page. + You can access these docs directly in Node-RED either using the button in the configuration panel or the link + in the help panel. + + The updated default template also contains an example button that uses the new feature. + + Note that you can use more than just button clicks. It will work with _any_ DOM event that you attach it to. + +### Changed + +- Better warning if you set/change a URL to one that already exists. +- When changing URL: + - **The original folder (if it exists) will be renamed** + - The uibuilder instance folders are also checked. The change is rejected if the folder exists. + - You are warned that you need to redeploy before doing anything else. + + **NOTE**: You may have lots of old uibuilder folders lying around. If your url change is rejected and you can't think why, check the folders. + +- Check for duplicate url moved to v3 Admin API. API Test file updated. +- Further improvements to the techical documentation. This is now available [online](https://totallyinformation.github.io/node-red-contrib-uibuilder) as well as from the uibuilder node configuration panel and the help panel in the Editor. +- Improved links from the Node-RED Editor's help panel, particularly on how to use the uibuilderfe front-end library. +- Extensive improvements to the + [documentation for working with the `uibuilderfe` library](https://totallyinformation.github.io/node-red-contrib-uibuilder/#/front-end-library) in your front-end code. +- The default Vue template now defines the `data` section as a method instead of an object. This is recommended and prepares for Vue v3. + +## [3.1.3](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.1...v3.1.3) + +### Fix (kind of) + +[Issue #102](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/110) + +It seems that npm is incapable of safely being called from within a preinstall or postinstall npm script. + +Every effort at trying to achieve this in order to install `vue` and `bootstrap-vue` has failed. + +So I have removed this processing completely. + +The result of this is that you must install vue and bootstrap-vue yourself if they aren't already installed (and if you want to use them of course). + +You should instal v2 versions however, not v3 since there are a lot of breaking changes in vue v3 that have not been tested with uibuilder. +The installation command is: + +```bash +#cd +npm install vue@"2.*" bootstrap-vue@"2.*" +``` + +## [3.1.2](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.1...v3.1.2) + +This is a tweak to 3.1.1 to enable a workaround for the npm install issues. + +### Issue +- [Issue #102](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/110) The npm post install script has very unexpected and unwelcome side-effects that appear to be issues with npm itself. It seems that you cannot reliably run npm from within npm. + + There does not appear to be a reliable fix at this time. Set the environment variable `UIBNOPRE` to 'true' before installation to avoid the problem if you hit it. You should then install `vue` v2 and `bootstrap-vue` v2 manually if you need to: + + ```bash + #cd + npm install vue@"2.*" bootstrap-vue@"2.*" + ``` + + I will attempt to find another way to install vue and bootstrap-vue since in uibuilder v1/v2 you could not remove either of them. Some people don't want these libraries and so want to be able to remove them. + +### New +- Added environment variable `UIBNOPPRE` processing to the pre-install script +- Added environment variable `UIBDEBUG` processing to the pre-install script + +### Changed +- Removed Vue and bootstrap-vue peer dependencies from this package since they are actually dependencies for the userDir folder. Gets rid of the warnings. Vue and bootstrap-vue are installed by the pre-install script unless you set an environment variable `UIBNOPRE` to 'true' before installation. +- Post-install script is now a pre-install script. + +## [3.1.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.1.0...v3.1.1) + +Emergency fix. + +The permissions feature of the Node-RED Admin API does not seem to work as documented. + +``` +RED.httpAdmin.get('/uibgetfile', RED.auth.needsPermission('read'), function(req,res) { + // ..... +}) +``` + +Should allow you to have a user defined with "read" permission and they would be allowed to access the API endpoint. +However, as far as I can tell, this does not work. + +I have removed all permissions from the API endpoints until someone can work out how to do this correctly. + +Until then, all you can do is to remove the default user in settings.js so that defined users have no access until they have logged in. + +There is no longer a separation between read and write permissions I'm afraid. + +## [3.1.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.0.1...v3.1.0) + +### Fixed + +- [Issue #106](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/106) Editor: When editing files, a filename with a leading dot did not set the filetype correctly. + +- [Issue #105](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/105) Editor: Attempting to edit a hidden file (with a leading dot) resulted in an error and white screen. + +### New + +- [Issue #108](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/108) You can now view the uibuilder package docs (the ones in this package) by going to the url `/uibuilder/techdocs`. + + The package docs use [Docsify](https://docsify.js.org/#/?id=docsify) for formatting. The docs include a search feature as well. + + The docs are linked to from both the uibuilder help information panel and from a new button in the configuration panel. + +- The config editor has a new button Instance Details. clicking the button will show a new page in a new tab. The page contains debug details of the exact settings for the uibuilder instance. This should help people better understand all of the settings including folders and urls. + + +### Changed + +#### Editor, "Edit Source Files" improvements: + - **ALL** folders and files within the `/` folder can now be edited. + + - Soft- or Hard-linked folders and files can now be used. This lets you put your front-end resources wherever you like as long as you create a soft or hard link into the `/` folder. + + - Added better information toasts on file create/delete actions. + + Pop-up notifications are now given when you create/delete folders and files. + + - Made keyboard enter button do the default action in the create dialog windows. + + - Added more information to the create/delete dialog windows. (url, folder name, file name) + + - [Issue #102](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/102) Relaxed the file-type checks when editing files. Allows for use of more ACE file-types and prepares the way for the introduction of the Monaco editor in Node-RED v2. + + - [Issue #107](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/107) Allowed the selection of any folder or sub-folders in the file editor. + + The editor still constrains you to the folder for the instance but any folder within that root can be viewed. New sub-folders can be created and existing ones deleted. + + - [Issue #109](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/109) Persist the selection of folder and file when editing. + + This means that closing and reopening the editor will return to the last edited file. + + Uses browser local storage and so does not work with Internet Explorer (which hasn't been supported by uibuilder since v3.0.0). + + - Improved display when no file is available to edit or if the file cannot be opened. + + - Started moving to new v3 admin API's that are more consistent with less overheads. + + - Changed "Edit Source Files" button to say "Edit Files". Recognising the additional capabilities. + + - Changed button link names in the configuration panel to clarify and accommodate the 2 extra buttons for the instance details and technical docs links. + +#### uibuilder.js: + - Started to simplify and rationalise API checks and reporting. Deprecated `/uibfiles`, `/uibnewfile`, `/uibdeletefile` API's, replaced with new v3 admin API `/uibuilder/admin/:url`. Simplifies the admin API's, makes them more consistent and reduces the number of URL's. + - Added v3 admin API's to create new and delete files and folders + - Added `/uibuilder/instance/` admin API. Is created for each instance. Calling it will show a detailed information page for the given uibuilder instance. + +#### Other + +- Updated dependencies +- Installer: Improved the post-install console message (Post Install takes a while). Also forces VueJS to v2.x (not v3 as yet which will soon be the latest version because there are currently too many breaking changes). + +## [3.0.1](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v3.0.0...v3.0.1) + +### Changed + +* Fix for [Issue #100](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/100) - Detection of whether Node-RED is currently using https. +* Fix for [Issue #93](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/93) - Full screen editor doesn't work correctly for mobile users. Replaced custom code with equivalent feature from core. +* Remove test code from `uibuilder.html` + +## [3.0.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v2.0.8...v3.0.0) +### Summary + +As this contains rather a lot of changes, here is a summary of the key changes for users of the node. The details are in the following sections. + +- Breaking Changes + + - Minimum Node-RED version is now 1.0 + - Minimum Node.js version is 10 + - IE11 and other older browsers now no longer guaranteed to work. All modern browsers including mobile and Microsoft Edge (Chromium) should work. + +- New feature in uibuilderfe to be able to transparently feed data and configuration to VueJS components written to be compatible. +- New feature in uibuilderfe to be able to transparently create notification popovers (toasts) by sending a msg from Node-RED (no code needed). +- New security documentation - still evolving for the experimental security features +- `vue` and `bootstrap-vue` packages can now be removed (NB: if uibuilder previously installed, you need to remove and reinstall for this to be possible) +- Scoped packages can now be added and removed +- Improved Editor configuration panel layout for Advanced Settings +- Some simplification of the default VueJS JavaScript template. Makes it a little easier to read. +- New template file `/.config/security.js` - used to give you control over the security process, please read the caveats before attempting to use in this version. Do not use in a live environment, for development only right now. + +### New + +- By sending a msg from Node-RED with a pre-defined format, you can interact with VueJS with minimal or no front-end code + + - With no code at all, you can show a popover notification (toast) to the web page. + - With as little as a single line of HTML, you can control and send values to a custom uibuilder compatible VueJS component. + + Suitable components are in development. See the experimental module [uibuilder-vuejs-component-extras](https://github.com/TotallyInformation/uibuilder-vuejs-component-extras) for some example components. Specifically the `` component which is being developed as an exemplar and will be moved to a separate npm module at some point. + + The idea being to bridge some of the gap between the ease of use of Node-RED's Dashboard and the flexibility of uibuilder. Without needing to be a web development expert. + + This will be further enhanced in future releases + + - **NOTE** To use the Vue features, you need to pass a reference to your Vue app to uibuilder. + This is normally as simple as changing `uibuilder.start()` to `uibuilder.start(this)` + + - _This feature does not currently work with all Vue components._ See the [docs](./docs/vue-component-handling.md) for an alternate low-code version. + +- Moved pre-installed VueJS and bootstrap-vue to be installed into `` instead of into the uibuilder package folder. + + This allows the `vue` and `bootstrap-vue` packages to be uninstalled like everything else and resolves Issue [#75](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/75). + + Note that, at present, I have not added any clever code to remove the old installations of vue and bootstrap-vue. If you want to get them into the right place, remove and re-add uibuilder. Note that you don't need to do anything unless you want to be able to remove `vue` and `bootstrap-vue`. + +- uibuilderfe: Added msg._socketId to sent messages. + +- Added security documentation (Work in progress). + + Read these to understand how to use uibuilder security and how it works (respectively). + + [uibuilder Security Documentation](./docs/security.md) and [security.js Technical Documentation](./docs/securityjs.md). + +- Added new VueJS documentation [Vue Component Handling](./docs/vue-component-handling.md). + +### Changed + +- Documentation: Greatly improved documentation coverage in the `/docs` folder. This contains a lot of developer documentation which should make it easier to work on improvements to uibuilder in the future. Still a work in progress. +- Documentation now uses Docsify for presentation and easier reading. Open `./docs/index.html` in your browser. +- Editor: Tidy up the Advanced Settings section of the configuration panel. +- uibuilderfe: Internal improvements to get/set functions. +- uibuilderfe: Simplify default Vue templates. +- Further code tidy up. +- Add code isolation to Editor config code to prevent namespace clashes. +- Improve standardisation of output topic. +- Moved some serveStatic code back to instance level to allow caching to be changed by config. +- Changed palette category name from "UI Builder" to "uibuilder" and palette label to "uibuilder" from "UI Builder" for consistency with other nodes. +- Moved all front-end master code (e.g. `nodes/src` and `nodes/dist`) to new top-level folder `front-end` & refactored `uibuilder.js` accordingly. Folder references also changed to new properties in the `uib` variable. +- Moved the templates folder from `nodes` to its own top-level folder and refactored uibuilder.js accordingly. The folder reference is held in the `uib.masterTemplateFolder` variable. +- Change minify of uibuilderfe from uglify-js to bable-minify because uglify-js does not support ES6+ + +### Fixed + +- Running behind a proxy was causing Socket.IO namespace issues (see [Issue #84](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/84) + Removing `httpRoot` from the namespace should fix that. It is no longer required anyway since url uniqueness checks were added. + +## Experimental and partially working new features + +**WARNING**: _Consider these features **experimental**, some parts may not work and might even cause Node-RED to crash if used. Do not yet use on production._ + +**Leave the security flag OFF for production.** + +### NOT YET FULLY WORKING + +- Added configuration option to add browser/proxy caching control to all static assets - set the length of time before assets will be reloaded from the server. This may sometimes significantly improve performance in the browser. It depends on the performance of your server and the complexity of the UI. + + Added on options variable for serve-static to allow control of caching & other headers. `uib.staticOpts`. + + Some static folders are served at module level and so don't have access to instance settings. Would likely need to have different settings on global serves from instance ones. _Needs more thought_. + + This lets you control caching of your "static" assets like JavaScript, HTML, CSS, Images and any installed front-end library resources (Vue, etc). + + Note that this is **not** for caching the msg's coming through the node, see the caching examples in the WIKI for that. + +- If you use Node-RED's projects feature, restart Node-RED after changing projects otherwise uibuilder will not recognise the new root folder location. + +### New security features + +#### Summary + +_Security is mostly controlled via websocket messages, not by HTTP. The web UI itself is assumed to be non-sensitive. Only msg transfer is controlled. Read the security document for details._ **Don't put anything sensitive into your front-end code**. + +- Security features can be turned on via a flag in the node configuration. They are off by default. +- If running in Production mode but without using TLS encryption, the security won't turn on. This is to stop you sending secure information in plain-text over the wire. In Development mode, you will get a warning. + +- Added a new standardised property to uibuilder control msg's. `msg._auth`. This contains all the necessary data for logon and ongoing session maintenance. As a minimum, this must contain an `id` property which uniquely identifies the user. It will also contain the JWT token since websockets don't allow custom headers. + +- Security _does_ use JWT but only as a convenience. JWT is _NOT_ a security feature (despite what much of the web would have you believe). _Session processing_ is _required_ if you want real security. Again, see the security doc for details. +- Logon/logoff processing is done from the front-end using new `logon()` and `logoff()` functions in uibuilderfe. +- Logon/logoff and logon failure events are reported via uibuilder's control port (output port #2). +- Added security headers to protect against XSS and content sniffing. +- All custom security processing (validating user details - including password - and session validation/extension) is done via standard functions in the new `/.config/security.js` file. A simple template is provided for you to use as a starting point. You can also override this with custom processing for a single instance by using `//security.js`. + +#### Details + +- uibuilderfe: Added `.logon(...)` and `.logoff()` functions. + + The `logon` function takes a single parameter which must be an Object (schema not yet finalised). + At the least, it MUST contain an `id` property which will be used by the server to track sessions. + + Added new variables to the uibuilder object for use in your front-end code: + + - `isAuthorised` {boolean} - informs whether the current client connection is authenticated. + - `authTokenExpiry` {Date|null} - when the authentication token expires. + - `authData` {Object} - Additional data returned from logon/logoff requests. Can be used by front-end to display messages at logon/off or anything else desired. + - _`authToken` {String}_ - this is not externally accessible. It is sent back to the server on every msg sent and validated by the server. + + Added new control message types: + + - **'authorised'** - received from server after a successful logon request. Returns the token, expiry and any optional additional data (into `authData`). + - **'authorisation failure'** - received from server after an **un**successful logon request. + - **'logged off'** - received from server after a successful logoff request. Returns optional additional data (into `authData`). + +- Added `useSecurity` flag. If set, the uibuilder instance will apply security processes. + + Note that if not using TLS security to encrypt communications in Node-RED, you will get at least a warning (in development mode. In production mode, security will turn off as there is no point). + +- Added `security.js` template module and added processing from `//security.js` or `/.config/security.js`. + + In non-development node.js modes, logon processing will not work unless you have used your own security.js file. + + See the template `security.js` file for more information about what functions you need to export and about data schema's required. + + It isn't very hard to use and you don't need to know very much JavaScript/Node.js unless you want to get complex with your authentication and authorisation schemes. + + This is the core of the security processing. uibuilder enforces some standards for you but **you have to validate users and sessions**, uibuilder cannot do this for you. Instead, uibuilder makes + this as easy as it can so that you don't have to be a mega-coder to work with it. Your user validation for example could be as simple as a file-based lookup. + + I will add more examples as the code stabilises so that you should be able to copy & paste a solution if you want soething fairly simple. + + +---- + +Because of the many changes in v3, the v2 changelog has been moved to a separate file: [v2 Changelog](/docs/CHANGELOG-v2.md). +Similarly, v1 chanegs are now in the [v1 Changelog](/docs/CHANGELOG-v1.md). + +---- + +## Types of changes + +- **Added** for new features. +- **Changed** for changes in existing functionality. +- **Deprecated** for soon-to-be removed features. +- **Removed** for now removed features. +- **Fixed** for any bug fixes. +- **Security** in case of vulnerabilities. \ No newline at end of file diff --git a/docs/package-management-process.md b/docs/package-management-process.md index dcf76075..a4234bb1 100644 --- a/docs/package-management-process.md +++ b/docs/package-management-process.md @@ -5,7 +5,7 @@ This is usually `~/.node-red/` and is the main folder containing the settings and node packages for Node-RED. -Prio to v5 of uibuilder, this was used to install uibuilder front-end packages. As of v5+, this is no longer the case. +Prior to v5 of uibuilder, this was used to install uibuilder front-end packages. As of v5+, this is no longer the case. ### Folder: `/` @@ -15,14 +15,75 @@ Needs a package.json file and will contain a `node_modules` folder if any packag This is the local configuration folder for a uibuilder node instance (as defined by the node's `url` setting). -Needs a `package.json` file and will contain a `node_modules` folder if any packages are installed for this uibuilder node instance. +Needs a `package.json` file but currently, packages installed here are only used for processing the front-end code. Normally, you would not see any `dependencies`, only `dev-dependencies` for example Webpack. ## System startup +When the uibuilder module is loaded, it immediately sets up the required web server routes. This includes the front-end library (vendor) routes for any packages installed to `uibRoot`. + +``` +uibuilder.js[Uib->runtimeSetup]->web.js[setup->_webSetup->serveVendorPackages, serveVendorSocketIo] +``` + ## Editor ### Open panel for a uibuilder node instance +On opening a uibuilder node configuration panel in the editor: + +``` +uibuilder.html[oneditprepare->packageList]->admin-api-v2.js[uibvendorpackages]->web.js[serveVendorPackages] + +Sets the 'packages' variable. +``` + +On click on "Libraries" tab: + +``` +uibuilder.js[tabLibraries->] +``` + ### Add a new package -### Remove a package \ No newline at end of file +### Remove a package + +## Internal Variables + +You don't need to know these unless you are working on the uibuilder code. + +### `uibuilder.js` + +#### `packageMgt.uibPackageJson` + +Set by the `getUibRootPackageJson` method in `nodes/libs/package-mgt.js`. +It contains the contents of the `/package.json` file. + +The important properties are: + +* `dependencies` - the list of installed packages that are served up by uibuilder +* `uibuilder` - Metadata and config data for the uibuilder module. + + Specifically `uibuilder.packages` which contains the package metadata: + + ```json + { + "vue": { + "installFolder": "/src/uibRoot/node_modules/vue", + "installedVersion": "2.6.14", + "estimatedEntryPoint": "dist/vue.js", + "homepage": "https://github.com/vuejs/vue#readme", + "packageUrl": "/vue", + "url": "../uibuilder/vendor/vue/dist/vue.js", + "spec": "^2.6.14" + }, + .... + } + ``` + +### `uibuilder.html` + +#### `packages` + +Set when panel is opened. See Editor section above. + +A copy of the `uibuilder.packages` data shown above. \ No newline at end of file diff --git a/global.d.ts b/global.d.ts index b5ee3f75..8001779b 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,7 +1,8 @@ -/** Define Global RED object so that VScode typescript stops whinging. */ - declare var RED:any; + +/** Define Global RED object so that VScode typescript stops whinging. */ + //import { EditorRED } from "node-red"; // declare global { diff --git a/nodes/libs/admin-api-v2.js b/nodes/libs/admin-api-v2.js index 3f29b3b2..ac693cad 100644 --- a/nodes/libs/admin-api-v2.js +++ b/nodes/libs/admin-api-v2.js @@ -335,49 +335,7 @@ function adminRouterV2(uib, log) { } // default to 'html' output type default: { - //console.log('Expresss 3.x - app.routes: ', app.routes) // Expresss 3.x - //console.log('Expresss 3.x with express.router - app.router.stack: ', app.router.stack) // Expresss 3.x with express.router - //console.log('Expresss 4.x - app._router.stack: ', app._router.stack) // Expresss 4.x - //console.log('Restify - server.router.mounts: ', server.router.mounts) // Restify - - // Update the uib.vendorPaths master variable - web.checkInstalledPackages() - - // Include socket.io as a client library (but don't add to vendorPaths) - // let sioFolder = packageMgt.findPackage('socket.io', userDir) - // let sioVersion = packageMgt.readPackageJson( sioFolder ).version - - // Collate current ExpressJS urls and details - var otherPaths = [], uibPaths = [] - var urlRe = new RegExp('^' + tilib.escapeRegExp('/^\\/uibuilder\\/vendor\\') + '.*$') - // req.app._router.stack.forEach( function(r, i, stack) { // shows Node-RED admin server paths - // eslint-disable-next-line no-unused-vars - web.app._router.stack.forEach( function(r, i, stack) { // shows Node-RED user server paths - let rUrl = r.regexp.toString().replace(urlRe, '') - if ( rUrl === '' ) { - uibPaths.push( { - 'name': r.name, - 'regex': r.regexp.toString(), - 'route': r.route, - 'path': r.path, - 'params': r.params, - 'keys': r.keys, - 'method': r.route ? Object.keys(r.route.methods)[0].toUpperCase() : 'ANY', - 'handle': r.handle.toString(), - } ) - } else { - otherPaths.push( { - 'name': r.name, - 'regex': r.regexp.toString(), - 'route': r.route, - 'path': r.path, - 'params': r.params, - 'keys': r.keys, - 'method': r.route ? Object.keys(r.route.methods)[0].toUpperCase() : 'ANY', - 'handle': r.handle.toString(), - } ) - } - }) + const routes = web.dumpRoutes(false) // Build the web page @@ -395,6 +353,9 @@ function adminRouterV2(uib, log) {

Note that this page is only accessible to users with Node-RED admin authority.

+

+ If this page looks ugly, try using the uibuilder library manager to install bootstrap. +

` /** Index of uibuilder instances */ @@ -436,7 +397,7 @@ function adminRouterV2(uib, log) { page += `

Vendor Client Libraries

- You can include these libraries in any uibuilder served web page. + You can include these libraries (packages) in any uibuilder served web page. Note though that you need to find out the correct file and relative folder either by looking on your Node-RED server in the location shown or by looking at the packages source online.

@@ -444,32 +405,20 @@ function adminRouterV2(uib, log) { Package Version - uibuilder URL (1) - Browser Entry Point (est.) (2) + Browser Entry Point (est.) (1) (2) Server Filing System Folder ` - Object.keys(uib.installedPackages).forEach(packageName => { - let pj = uib.installedPackages[packageName] + let installedPackages = packageMgt.uibPackageJson.uibuilder.packages + Object.keys(installedPackages).forEach(packageName => { + let pj = installedPackages[packageName] - /** Are either the `browser` or `main` properties set in package.json? - * If so, add them to the output as an indicator of where to look. - */ - let mainTxt = 'Not Supplied' - //console.log('==>> ',uib.nodeRoot, pj.url,pj.browser) - if ( pj.browser !== '' ) { - mainTxt = `${pj.url}/${pj.browser}` - } else if ( pj.main !== '' ) { - mainTxt = `${pj.url}/${pj.main}` - } - page += ` - ${packageName} - ${pj.version} - ${pj.url} - ${mainTxt} - ${pj.folder} + ${packageName} + ${pj.installedVersion} + ${pj.url} + ${pj.installFolder} ` }) @@ -591,55 +540,51 @@ function adminRouterV2(uib, log) {
${tilib.syntaxHighlight( web.app.mountpath )}
` - /** Installed Packages */ - page += ` -

Installed Packages

-

- These are the front-end libraries uibuilder knows to be installed and made available via ExpressJS serve-static. - This is the raw view of the Vendor Client Libraries table above. -

-
${tilib.syntaxHighlight( uib.installedPackages )}
- ` - // Show the ExpressJS paths currently defined + // web.routers contains descriptive info on routes, routes.user contains the ExpressJS route stack info. page += ` +

uibuilder ExpressJS Routes

+

These tables show all of the web URL routes for uibuilder.

` - page += `

User-Facing Routes

- ` - page += ` -

Application Routes (../*)

+ ${web.htmlBuildTable( web.routers.user, ['name', 'desc', 'path', 'type', 'folder'] )} +

ExpressJS technical route data for admin routes

+
Application Routes (../*)
${web.htmlBuildTable( routes.user.app, ['name','path', 'folder', 'route'] )} - ` - page += ` -

uibuilder generic Routes (../uibuilder/*)

+
uibuilder generic Routes (../uibuilder/*)
${web.htmlBuildTable( routes.user.uibRouter, ['name','path', 'folder', 'route'] )} - ` - page += ` -

Vendor Routes (../uibuilder/vendor/*)

+
Vendor Routes (../uibuilder/vendor/*)
${web.htmlBuildTable( routes.user.vendorRouter, ['name','path', 'folder', 'route'] )} ` page += ` -

Other ExpressJS Paths

-

A raw view of all other app.use paths being served.

-
${tilib.syntaxHighlight( otherPaths )}
- ` - - page += ` +

Per-Instance User-Facing Routes

` Object.keys(routes.instances).forEach( url => { - + page += ` +

${url}

+ ${web.htmlBuildTable( web.routers.instances[url], ['name', 'desc', 'path', 'type', 'folder'] )} +
ExpressJS technical route data for ${url} (../${url}/*)
+ ${web.htmlBuildTable( routes.instances[url], ['name','path', 'folder', 'route'] )} + ` } ) - page += ` +

Admin-Facing Routes

- ` - page += ` -

Application Routes (../*)

- ${web.htmlBuildTable( routes.user.app, ['name','path', 'folder', 'route'] )} + ${web.htmlBuildTable( web.routers.admin, ['name', 'desc', 'path', 'type', 'folder'] )} +

ExpressJS technical route data for admin routes

+
Node-RED Admin Routes (../*)
+

Note: Shows ALL Node-RED top-level admin routes, not just uibuilder

+ ${web.htmlBuildTable( routes.admin.app, ['name','path', 'folder', 'route'] )} +
Admin uibuilder Routes (../uibuilder/*)
+ ${web.htmlBuildTable( routes.admin.admin, ['name','path', 'folder', 'route'] )} +
Admin v3 API Routes (../uibuilder/admin)
+

Note: This route uses the following methods: all, get, put, post, delete.

+ ${web.htmlBuildTable( routes.admin.v3, ['name','path', 'folder', 'route'] )} +
Admin v2 API Routes (../uibuilder/*)
+ ${web.htmlBuildTable( routes.admin.v2, ['name','path', 'folder', 'route'] )} ` page += '' @@ -664,14 +609,10 @@ function adminRouterV2(uib, log) { return } - // TODO: get rid of web. and consolidate output - // Update the installed packages list - web.checkInstalledPackages('', /** @type {string} */ (params.url) ) - // res.json(uib.installedPackages) + web.serveVendorPackages() - let pkgs = packageMgt.updateInstalledPackages(params.url) - res.json( { ...uib.installedPackages, pkgs: pkgs} ) + res.json( packageMgt.uibPackageJson.uibuilder.packages ) }) // ---- End of uibvendorpackages ---- // @@ -724,75 +665,68 @@ function adminRouterV2(uib, log) { return } - // package location must exist and be either 'userdir', 'common' or 'local' - if ( params.loc === undefined ) { - log.error('[uibuilder:API:uibnpmmanage] uibuilder Admin API. No location provided for npm management.') - res.statusMessage = 'npm loc parameter not provided' - res.status(500).end() - return - } - switch (params.loc) { - case 'userdir': - case 'common': - case 'local': - break - - default: - log.error('[uibuilder:API:uibnpmmanage] Admin API. Invalid location provided for npm management.') - res.statusMessage = 'npm loc parameter is invalid' + // For install/upd, package location must exist and be either 'npmjs', 'github' or 'local' + if ( params.cmd !== 'remove' ) { + if ( params.loc === undefined ) { + log.error('[uibuilder:API:uibnpmmanage] uibuilder Admin API. No location provided for npm management.') + res.statusMessage = 'npm loc parameter not provided' res.status(500).end() return + } + switch (params.loc) { + case 'npmjs': + case 'github': + case 'local': + break + + default: + log.error(`[uibuilder:API:uibnpmmanage] Admin API. Invalid location provided for npm management. "${params.loc}"`) + res.statusMessage = 'npm loc parameter is invalid' + res.status(500).end() + return + } } - // We need the node instance as well - // @ts-ignore - const chkUrl = chkParamUrl(params) - if ( chkUrl.status !== 0 ) { - log.error(`[uibuilder:API:uibnpmmanage] Admin API. ${chkUrl.statusMessage}`) - res.statusMessage = chkUrl.statusMessage - res.status(chkUrl.status).end() - return + // If install/update, we need the node instance as well + if ( params.cmd !== 'remove' ) { + // @ts-ignore + const chkUrl = chkParamUrl(params) + if ( chkUrl.status !== 0 ) { + log.error(`[uibuilder:API:uibnpmmanage] Admin API. ${chkUrl.statusMessage}`) + res.statusMessage = chkUrl.statusMessage + res.status(chkUrl.status).end() + return + } } //#endregion ---- ---- const folder = RED.settings.userDir - log.info(`[uibuilder:API:uibnpmmanage] Admin API. Running npm ${params.cmd} for package ${params.package} in location ${params.loc}`) + log.info(`[uibuilder:API:uibnpmmanage] Admin API. Running npm ${params.cmd} for package ${params.package} from '${params.loc}' with tag/version '${params.tag}'`) // delete package lock file as it seems to mess up sometimes - no error if it fails fs.removeSync(path.join(folder, 'package-lock.json')) // Formulate the command to be run - //var command = '' switch (params.cmd) { case 'update': case 'install': { - // npm install --no-audit --no-update-notifier --save --production --color=false --no-fund --json @latest // --save-prefix="~" - //command = `npm install --no-audit --no-update-notifier --save --production --color=false --no-fund --json ${params.package}@latest` - packageMgt.npmInstallPackage(params.url, params.loc, params.package) - .then((val) => { - let success = false + packageMgt.npmInstallPackage(params.url, params.package, params.loc, params.tag) + .then((npmOutput) => { + //let success = false // Update the packageList - uib.installedPackages = web.checkInstalledPackages(/** @type {string} */ (params.package), /** @type {string} */ (params.url) ) - console.log('>> uib.installedPackages >>', uib.installedPackages) - - // package name should exist in uib.installedPackages - if ( Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package) ) { - // Add an ExpressJS URL (only for install since update should already be served) - if (params.cmd==='install') web.servePackage( /** @type {string} */ (params.package) ) - success = true - log.info(`[uibuilder:API:uibnpmmanage:install] Admin API. npm command success. npm ${params.cmd} for package ${params.package}`) - } else { - log.error(`[uibuilder:API:uibnpmmanage:install] Admin API. npm command failed. npm ${params.cmd} for package ${params.package}`) - } - - res.json({'success':success, 'result': val}) - return success + web.serveVendorPackages() + + res.json({'success':true, 'result': [npmOutput, packageMgt.uibPackageJson.uibuilder.packages]}) + + //return success + return true }) .catch((err) => { //log.warn(`[uibuilder:API:uibnpmmanage] Admin API. ERROR Running npm ${params.cmd} for package ${params.package}`, err.stdout) + console.dir(err) log.warn(`[uibuilder:API:uibnpmmanage:install] Admin API. ERROR Running: \n'${err.command}' \n${err.all}`) res.json({'success':false, 'result':err.all}) return false @@ -800,24 +734,15 @@ function adminRouterV2(uib, log) { break } case 'remove': { - // npm remove --no-audit --no-update-notifier --color=false --json // --save-prefix="~" - //command = `npm remove --no-audit --no-update-notifier --color=false --json ${params.package}` - packageMgt.npmRemovePackage(RED.settings.userDir, params.package) - .then((val) => { + packageMgt.npmRemovePackage(params.package) + .then((npmOutput) => { + + // TODO remove - just send back success + // Update the packageList - uib.installedPackages = web.checkInstalledPackages(/** @type {string} */ (params.package)) - - // package name should NOT exist in uib.installedPackages - if ( ! Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package) ) { - log.info(`[uibuilder:API:uibnpmmanage:remove] Admin API. npm command success. npm ${params.cmd} for package ${params.package}`) - // Remove ExpressJS URL - web.unservePackage(/** @type {string} */(params.package)) - res.json({'success':true, 'result': val}) - } else { - log.error(`[uibuilder:API:uibnpmmanage:remove] Admin API. npm command failed. npm ${params.cmd} for package ${params.package}`) - res.json({'success':false, 'result': val}) - } + web.serveVendorPackages() + res.json({'success':true, 'result': npmOutput}) return true }) .catch((err) => { @@ -836,99 +761,8 @@ function adminRouterV2(uib, log) { } } - // if ( command === '' ) { - // log.error('[uibuilder:API:uibnpmmanage] Admin API. No valid command available for npm management.') - // res.statusMessage = 'No valid npm command available' - // res.status(500).end() - // return - // } - - // if ( command !== '' ) { - // // Run the command - against the correct instance or userDir (cwd) - // var output = [], errOut = null, success = false - // child_process.exec(command, {'cwd': folder}, (error, stdout, stderr) => { - // if ( error ) { - // log.warn(`[uibuilder:API:uibnpmmanage] Admin API. ERROR Running npm ${params.cmd} for package ${params.package}`, error) - // } - - // // try to force output & error output to JSON (or split by newline) - // try { - // output.push(JSON.parse(stdout)) - // } catch (err) { - // output.push(stdout.split('\n')) - // } - // try { - // errOut = JSON.parse(stderr) - // } catch (err) { - // errOut = stderr.split('\n') - // } - - // // Find the actual JSON output in amongst all the other crap that npm can produce - // var result = null - // try { - // result = stdout.slice(stdout.search(/^\{/m), stdout.search(/^\}/m)+1) //stdout.match(/\n\{.*\}\n/) - // } catch (e) { - // result = e - // } - // var jResult = null - // try { - // jResult = JSON.parse(result) - // } catch (e) { - // jResult = {'ERROR': e, 'RESULT': result} - // } - - // //log.trace(`[uibuilder:API:uibnpmmanage] Writing stdout to ${path.join(uib.rootFolder,uib.configFolder,'npm-out-latest.txt')}`) - // //fs.writeFile(path.join(uib.configFolder,'npm-out-latest.txt'), stdout, 'utf8', function(){}) - - // // Update the packageList - // // @ts-ignore - // uib.installedPackages = web.checkInstalledPackages(params.package) - - // // Check the results of the command - // switch (params.cmd) { - // // check pkg exiss in uib.installedPackages, if so, serve it up - // case 'install': { - // // package name should exist in uib.installedPackages - // if ( Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package) ) success = true - // if (success === true) { - // // Add an ExpressJS URL - // web.servePackage( /** @type {string} */ (params.package) ) - // } - // break - // } - // // Check pkg does not exist in uib.installedPackages, if so, remove served url - // case 'remove': { - // // package name should NOT exist in uib.installedPackages - // if ( ! Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package) ) success = true - // if (success === true) { - // // Remove ExpressJS URL - // // @ts-ignore - // web.unservePackage(params.package) - // } - // break - // } - // // Check pkg still exists in uib.installedPackages - // case 'update': { - // // package name should exist in uib.installedPackages - // if ( Object.prototype.hasOwnProperty.call(uib.installedPackages, params.package) ) success = true - // break - // } - // } - - // if (success === true) { - // log.info(`[uibuilder:API:uibnpmmanage] Admin API. npm command success. npm ${params.cmd} for package ${params.package}`) - // } else { - // log.error(`[uibuilder:API:uibnpmmanage] Admin API. npm command failed. npm ${params.cmd} for package ${params.package}`, jResult) - // } - - // res.json({'success':success,'result':jResult,'output':output,'errOut':errOut}) - - // }) - // } - }) // ---- End of npmmanage ---- // - return admin_Router_V2 } diff --git a/nodes/libs/admin-api-v3.js b/nodes/libs/admin-api-v3.js index c50a6fd3..2fda263c 100644 --- a/nodes/libs/admin-api-v3.js +++ b/nodes/libs/admin-api-v3.js @@ -194,81 +194,122 @@ function adminRouterV3(uib, log) { params.type = 'get' // Commands ... - if ( params.cmd === 'listall' ) { // List all folders and files for this uibuilder instance - log.trace(`[uibuilder:admin-router:GET] Admin API. List all folders and files. url=${params.url}, root fldr=${uib.rootFolder}`) - - // get list of all (sub)folders (follow symlinks as well) - const out = {'root':[]} - const root2 = uib.rootFolder.replace(/\\/g, '/') - fg.stream([`${root2}/${params.url}/**`], { dot: true, onlyFiles: false, deep: 10, followSymbolicLinks: true, markDirectories: true }) - .on('data', entry => { - entry = entry.replace(`${root2}/${params.url}/`, '') - let fldr - if ( entry.endsWith('/') ) { - // remove trailing / - fldr = entry.slice(0, -1) - // For the root folder of the instance, use "root" as the name (matches editor processing) - if ( fldr === '' ) fldr = 'root' - out[fldr] = [] - } else { - let splitEntry = entry.split('/') - let last = splitEntry.pop() - fldr = splitEntry.join('/') - if ( fldr === '' ) fldr = 'root' - out[fldr].push(last) + switch (params.cmd) { + // List all folders and files for this uibuilder instance + case 'listall': { + log.trace(`[uibuilder:admin-router:GET] Admin API. List all folders and files. url=${params.url}, root fldr=${uib.rootFolder}`) + + // get list of all (sub)folders (follow symlinks as well) + const out = {'root':[]} + const root2 = uib.rootFolder.replace(/\\/g, '/') + fg.stream([`${root2}/${params.url}/**`], { dot: true, onlyFiles: false, deep: 10, followSymbolicLinks: true, markDirectories: true }) + .on('data', entry => { + entry = entry.replace(`${root2}/${params.url}/`, '') + let fldr + if ( entry.endsWith('/') ) { + // remove trailing / + fldr = entry.slice(0, -1) + // For the root folder of the instance, use "root" as the name (matches editor processing) + if ( fldr === '' ) fldr = 'root' + out[fldr] = [] + } else { + let splitEntry = entry.split('/') + let last = splitEntry.pop() + fldr = splitEntry.join('/') + if ( fldr === '' ) fldr = 'root' + out[fldr].push(last) + } + }) + .on('end', () => { + res.statusMessage = 'Folders and Files listed successfully' + res.status(200).json(out) + }) + + break + } // -- end of listall -- // + + // Check if URL is already in use + case 'checkurls': { + log.trace(`[uibuilder:admin-router:GET:checkurls] Check if URL is already in use. URL: ${params.url}`) + + /** @returns {boolean} True if the given url exists, else false */ + let chkInstances = Object.values(uib.instances).includes(params.url) + let chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url)) + + res.statusMessage = 'Instances and Folders checked' + res.status(200).json( chkInstances || chkFolders ) + + break + } // -- end of checkurls -- // + + // List all of the deployed instance urls + case 'listinstances': { + + log.trace('[uibuilder:admin-router:GET:listinstances] Returning a list of deployed URLs (instances of uib).') + + /** @returns {boolean} True if the given url exists, else false */ + // let chkInstances = Object.values(uib.instances).includes(params.url) + // let chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url)) + + res.statusMessage = 'Instances listed' + res.status(200).json( uib.instances ) + + break + } // -- end of listinstances -- // + + // Return a list of all user urls in use by ExpressJS + case 'listurls': { + // TODO Not currently working + var route, routes = [] + web.app._router.stack.forEach( (middleware) => { + if(middleware.route){ // routes registered directly on the app + let path = middleware.route.path + let methods = middleware.route.methods + routes.push({path: path, methods: methods}) + } else if(middleware.name === 'router'){ // router middleware + middleware.handle.stack.forEach(function(handler){ + route = handler.route + route && routes.push(route) + }) } }) - .on('end', () => { - res.statusMessage = 'Folders and Files listed successfully' - res.status(200).json(out) - }) - // -- end of listall -- // - } else if ( params.cmd === 'checkurls' ) { // Check if URL is already in use - log.trace(`[uibuilder:admin-router:GET:checkurls] Check if URL is already in use. URL: ${params.url}`) - - /** @returns {boolean} True if the given url exists, else false */ - let chkInstances = Object.values(uib.instances).includes(params.url) - let chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url)) + console.log(web.app._router.stack[0]) + + log.trace('[uibuilder:admin-router:GET:listurls] Admin API. List of all user urls in use.') + res.statusMessage = 'URLs listed successfully' + //res.status(200).json(routes) + res.status(200).json(web.app._router.stack) + + break + } // -- end of listurls -- // + + // See if a node's custom folder exists. Return true if it does, else false + case 'checkfolder': { + log.trace(`[uibuilder:admin-router:GET:checkfolder] See if a node's custom folder exists. URL: ${params.url}`) - res.statusMessage = 'Instances and Folders checked' - res.status(200).json( chkInstances || chkFolders ) - // -- end of checkurls -- // - } else if ( params.cmd === 'listinstances' ) { // List all of the deployed instance urls + const folder = path.join( uib.rootFolder, params.url) - log.trace('[uibuilder:admin-router:GET:listinstances] Returning a list of deployed URLs (instances of uib).') - - /** @returns {boolean} True if the given url exists, else false */ - // let chkInstances = Object.values(uib.instances).includes(params.url) - // let chkFolders = fs.existsSync(path.join(uib.rootFolder, params.url)) - - res.statusMessage = 'Instances listed' - res.status(200).json( uib.instances ) - - } else if ( params.cmd === 'listurls' ) { // Return a list of all user urls in use by ExpressJS - // TODO Not currently working - var route, routes = [] - web.app._router.stack.forEach( (middleware) => { - if(middleware.route){ // routes registered directly on the app - let path = middleware.route.path - let methods = middleware.route.methods - routes.push({path: path, methods: methods}) - } else if(middleware.name === 'router'){ // router middleware - middleware.handle.stack.forEach(function(handler){ - route = handler.route - route && routes.push(route) + fs.access(folder, fs.constants.F_OK) + .then( () => { + res.statusMessage = 'Folder checked' + res.status(200).json( true ) + return true }) - } - }) - console.log(web.app._router.stack[0]) - - log.trace('[uibuilder:admin-router:GET:listurls] Admin API. List of all user urls in use.') - res.statusMessage = 'URLs listed successfully' - //res.status(200).json(routes) - res.status(200).json(web.app._router.stack) - // -- end of listurls -- // + .catch( () => { //err) => { + res.statusMessage = 'Folder checked' + res.status(200).json( false ) + return false + }) + + break + } // -- end of checkfolder -- // + + default: { + break + } } - }) + // TODO Write file contents .put(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) { // @ts-ignore @@ -294,6 +335,7 @@ function adminRouterV3(uib, log) { }) }) + // Load new template or Create a new folder or file .post(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) { // @ts-ignore @@ -390,6 +432,7 @@ function adminRouterV3(uib, log) { } // end of else }) // --- End of POST processing --- // + // Delete a folder or a file .delete(function(/** @type {express.Request} */ req, /** @type {express.Response} */ res) { // @ts-ignore ts(2339) @@ -459,9 +502,10 @@ function adminRouterV3(uib, log) { 'params': params, }) }) - /** @see https://expressjs.com/en/4x/api.html#app.METHOD for other methods - * patch, report, search ? - */ + + /** @see https://expressjs.com/en/4x/api.html#app.METHOD for other methods + * patch, report, search ? + */ return admin_Router_V3 } diff --git a/nodes/libs/package-mgt.js b/nodes/libs/package-mgt.js index f3745e9e..f13fd16d 100644 --- a/nodes/libs/package-mgt.js +++ b/nodes/libs/package-mgt.js @@ -1,3 +1,4 @@ +/* eslint-disable class-methods-use-this */ /** Manage npm packages * * Copyright (c) 2021 Julian Knight (Totally Information) @@ -61,6 +62,9 @@ class UibPackages { /** @type {string} The name of the package.json file 'package.json' */ this.packageJson = 'package.json' + /** @type {*} The uibRoot package.json contents */ + this.uibPackageJson + } // ---- End of constructor ---- // /** Gets the global install folder for npm & saves to a class variable @@ -117,7 +121,6 @@ class UibPackages { uib.RED.log.warn('[uibuilder:UibPackages:setup] Setup has already been called, it cannot be called again.') return } - this._isConfigured = true if ( ! uib ) { throw new Error('[uibuilder:UibPackages.js:setup] Called without required uib parameter') @@ -126,49 +129,108 @@ class UibPackages { this.RED = uib.RED this.uib = uib this.log = uib.RED.log - - // Make sure we have the list as soon as possible - this.updateMergedPackageList('') - this.updateInstalledPackages() + // At this point we have the refs to uib and RED + this._isConfigured = true } // ---- End of setup ---- // - /** Update all of the installed packages - * NB: Not including Global installs because they are not managed the same way by npm and there is no matching master package.json - * @param {string=} url A uibuilder node instance url - * @returns {object} List of installed packages from userDir, uibRoot and the node instance + /** Create/Update, record & return /package.json (create it if it doesn't exist) + * @returns {object|null} Parsed version of /package.json with uibuilder specific updates */ - updateInstalledPackages(url) { - - // See which front-end packages are already installed (compare uib.me.dependencies with master package list? Better to scan the node_modules folder?) - this.userDirPackageList = ( this.readPackageJson( path.join(this.RED.settings.userDir) ) ).dependencies || {} - this.uibRootPackageList = ( this.readPackageJson( path.join(this.uib.rootFolder) ) ).dependencies || {} + getUibRootPackageJson() { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:UibPackages:getUibRootPackageJson] Cannot run. Setup has not been called.') + return + } - // console.log('>> userDir >> ', this.userDirPackageList, this.RED.settings.userDir) - // console.log('>> uibRoot >> ', this.uibRootPackageList, this.uib.rootFolder) + const uibRoot = this.uib.rootFolder + const fileName = path.join( uibRoot, 'package.json' ) + + // Make sure it exists & contains valid JSON + if ( ! fs.existsSync(fileName) ) { + // Create a minimal one + fs.writeJsonSync(fileName, { + 'name': 'uib_root', + 'version': this.uib.version, + 'description': 'Root configuration and data folder for uibuilder', + 'scripts': {}, + 'dependencies': {}, + 'homepage': '', + 'bugs': '', + 'author': '', + 'license': 'Apache-2.0', + 'repository': '', + 'uibuilder': { + 'packages': {}, + }, + }) + } - if ( url && url !== undefined && url !== 'undefined' ) { - this.uibNodePackageList[url] = ( this.readPackageJson( path.join(this.uib.rootFolder, url) ) ).dependencies || {} - console.log('>> Node >>', this.uibNodePackageList, url, path.join(this.uib.rootFolder, url)) - } else { - this.uibNodePackageList = {} + // Get it + let pj = {} + try { + pj = this.readPackageJson(uibRoot) + } catch (e) { + this.log.error(`[uibuilder:package-mgt:getUibRootPackageJson] Error reading ${fileName}. ${e.message}`) + this.uibPackageJson = null + return null } - //console.log('>> uib.installedPackages >> ', this.uib.installedPackages) - // return { - // userDir: this.userDirPackageList, - // uibRoot: this.uibRootPackageList, - // node: this.uibNodePackageList[url], - // } - return { - ...this.userDirPackageList, - ...this.uibRootPackageList, - ...this.uibNodePackageList[url], + // Make sure there is a dependencies prop + if ( ! pj.dependencies ) pj.dependencies = {} + // Make sure there is a uibuilder prop + if ( ! pj.uibuilder ) pj.uibuilder = {} + // Reset the packages list, we rebuild it below + pj.uibuilder.packages = {} + + // Update the version string to match uibuilder version + pj.version = this.uib.version + + // Make sure we have package details for all installed packages + Object.keys(pj.dependencies).forEach( packageName => { + // Get/Update package details + pj.uibuilder.packages[packageName] = this.getPackageDetails2(packageName, this.uib.rootFolder) + // And save the version/location spec from the dependencies prop so everything is together + pj.uibuilder.packages[packageName].spec = pj.dependencies[packageName] + + //Frig to pick up the version of Bootstrap installed with bootstrap-vue + if (packageName === 'bootstrap-vue' && ! pj.dependencies.bootstrap ) { + pj.dependencies.bootstrap = pj.uibuilder.packages[packageName].bootstrap + pj.uibuilder.packages.bootstrap = this.getPackageDetails2('bootstrap', this.uib.rootFolder) + pj.uibuilder.packages.bootstrap.spec = pj.dependencies.bootstrap + } + }) + + // Save it for use elsewhere + this.uibPackageJson = pj + + // Update the /package.json file with updated details + this.setUibRootPackageJson(pj) + + // Return it + return pj + } // ----- End of getUibRootPackageJson() ----- // + + /** Write updated /package.json + * @param {object} json The Object data to write to the file + */ + setUibRootPackageJson(json) { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:UibPackages:setUibRootPackageJson] Cannot run. Setup has not been called.') + return } - } // ---- End of updateInstalledPackages ---- // + const uibRoot = this.uib.rootFolder + const fileName = path.join( uibRoot, 'package.json' ) + // Save it for use elsewhere + this.uibPackageJson = json + + // TODO Add try & error message + fs.writeJsonSync(fileName, json) + } + /** Find install folder for a package * NOTE: require.resolve can be a little ODD! * When run from a linked package, it uses the link root not the linked location, @@ -176,199 +238,125 @@ class UibPackages { * Also, it finds the "main" script name which might not be in the package root. * Also, it won't find ANYTHING if a `main` entry doesn't exist :( * So we no longer use it, just search for folder names. - * @param {string} packageName - Name of the package who's root folder we are looking for. - * @param {uibNode=} node A uibuilder node instance - will search in node's root folder first + * @param {string} packageName - Name of the package who's install folder we are looking for. + * @param {string} installRoot A uibuilder node instance - will search in node's root folder first * @returns {null|string} Actual filing system path to the installed package */ - getPackagePath(packageName, node) { + getPackagePath2(packageName, installRoot) { if ( this._isConfigured !== true ) { this.log.warn('[uibuilder:UibPackages:getPackagePath] Cannot run. Setup has not been called.') return } - let uib = this.uib - let userDir = this.RED.settings.userDir - - let tracePkg = false // Use this to debug package finding if needed - let mylog = tracePkg ? tilib.mylog : function(){} - - let found = false, packagePath = '' - - // May as well just search the folder names first. require.resolve is too unreliable and quirky. - let loc - // 1) If a node instance is provided, search there first - if (node) { - loc = path.join(uib.rootFolder, node.url, 'node_modules', packageName) - if (fs.existsSync( loc )) { - found = true - packagePath = loc - mylog(`>> FOUND ${packageName} at ${loc}`) - } else { - mylog(`>> NOT FOUND ${packageName} at ${loc}`) - } - } - // 2) Check the Common modules next - if (!found) { - loc = path.join(userDir, 'node_modules', packageName) - if (fs.existsSync( loc )) { - found = true - packagePath = loc - mylog(`>> FOUND ${packageName} at ${loc}`) - } else { - mylog(`>> NOT FOUND ${packageName} at ${loc}`) - } - } - // 3) Check the userDir modules next - if (!found) { - loc = path.join(userDir, 'node_modules', packageName) - if (fs.existsSync( loc )) { - found = true - packagePath = loc - mylog(`>> FOUND ${packageName} at ${loc}`) - } else { - mylog(`>> NOT FOUND ${packageName} at ${loc}`) - } - } - // 4) Then check uibuilder's modules - if (!found) { - loc = path.join(__dirname, '..', '..', 'node_modules', packageName) - if (fs.existsSync( loc )) { - found = true - packagePath = loc - mylog(`>> FOUND ${packageName} at ${loc}`) - } else { - mylog(`>> NOT FOUND ${packageName} at ${loc}`) - } - } - // 4) Finally try the global modules - no, don't because there is no package.json for the globals parent - // if (!found) { - // loc = path.join(this.globalPrefix, 'node_modules', packageName) - // if (fs.existsSync( loc )) { - // found = true - // packagePath = loc - // mylog(`>> FOUND ${packageName} at ${loc}`) - // } else { - // mylog(`>> NOT FOUND ${packageName} at ${loc}`) - // } - // } - - /* DEPRECATED: Using unreliable require.resolve - // Try in userDir first - if (!found) try { - packagePath = path.dirname( require.resolve(packageName, {paths: [userDir]}) ) - mylog(`[UIBUILDER] ${packageName} found from userDir`, packagePath) - found = true - } catch (e) { - mylog(`[UIBUILDER] ${packageName} not found from userDir. Path: ${userDir}`) - } - // Then try without a path - if (found === false) try { - packagePath = path.dirname( require.resolve(packageName) ) - mylog(`[UIBUILDER] ${packageName} found (no path)`, packagePath) - found = true - } catch (e) { - mylog(`[UIBUILDER] ${packageName} not found (no path)`) - } - // Then try this uibuilder instance's root folder - if (!found) try { - packagePath = path.dirname( require.resolve(packageName, {paths: [userDir]}) ) - mylog(`[UIBUILDER] ${packageName} found from userDir`, packagePath) - found = true - } catch (e) { - mylog(`[UIBUILDER] ${packageName} not found from userDir. Path: ${userDir}`) - } - // Finally try in the uibuilder source folder - if (found === false) try { - packagePath = path.dirname( require.resolve(packageName, {paths: [path.join(__dirname,'..')]}) ) - mylog(`[UIBUILDER] ${packageName} found from uibuilder path`, packagePath) - found = true - } catch (e) { - mylog(`[UIBUILDER] ${packageName} not found from uibuilder path. Path: ${path.join(__dirname,'..')}`) - } */ - - if ( found === false ) { - mylog(`>> ${packageName} not found anywhere\n`) - return null + //let loc = path.join(__dirname, '..', '..', 'node_modules', packageName) + let loc = path.join(installRoot, 'node_modules', packageName) + if (fs.existsSync( loc )) { + return loc } - /** require.resolve returns the "main" script, this may not be in the root folder for the package - * so we change that here. We check whether the last element of the path matches the package - * name. If not, we walk back up the tree until it is or we run out of tree. - * If we don't do this, when it is used with express.static, we may not get everything we need served. - * NB: Only assuming 3 levels here. - * NB2: Added packageName split to allow for more complex npm package names. - */ - /* DEPRECATED: Only needed when using require.resolve - let pathSplit = packagePath.split(path.sep) - let packageLast = packageName.split('/').pop() // Allow for package names like `@riophae/vue-treeselect` - if ( (pathSplit.length > 1) && (pathSplit[pathSplit.length - 1] !== packageLast) ) pathSplit.pop() - if ( (pathSplit.length > 1) && (pathSplit[pathSplit.length - 1] !== packageLast) ) pathSplit.pop() - if ( (pathSplit.length > 1) && (pathSplit[pathSplit.length - 1] !== packageLast) ) pathSplit.pop() - packagePath = pathSplit.join(path.sep) */ + this.log.warn(`[uibuilder:package-mgt:getPackagePath2] PACKAGE NOT FOUND ${packageName} at ${loc}`) + return null + } // ---- End of getPackagePath2 ---- // - return packagePath - - } // ---- End of getPackagePath ---- // - - /** Update the master name list of possible packages that could be served to the front-end - * Called initially from this.setup (early in uibuilder setup) - * and then repeatedly from other places that trigger an update. - * @param {string} newPkg Optional new package to add to the list + /** Get the details for an installed package & update uibuilder specific details before returning it + * @param {string} packageName - Name of the package who's install folder we are looking for. + * @param {string} installRoot A uibuilder node instance - will search in node's root folder first + * @returns {object} Details object for an installed package */ - updateMergedPackageList(newPkg='') { + getPackageDetails2(packageName, installRoot) { if ( this._isConfigured !== true ) { - this.log.warn('[uibuilder:UibPackages:npmInstallPackage] Cannot run. Setup has not been called.') + this.log.warn('[uibuilder:UibPackages:getPackagePath] Cannot run. Setup has not been called.') return } - let uib = this.uib - //let log = this.log - let installedPackages = uib.installedPackages - let pkgList = [] - let masterPkgList = [] + // Trim the input just in case + packageName = packageName.trim() - // Read packageList and masterPackageList from their files - try { - pkgList = fs.readJsonSync(path.join(uib.configFolder, uib.packageListFilename)) - } catch (err) { - // not an issue - } - try { - masterPkgList = fs.readJsonSync(path.join(uib.configFolder, uib.masterPackageListFilename)) - } catch (err) { - // no op + const folder = this.getPackagePath2(packageName, installRoot) + const pkgJson = this.readPackageJson(folder) + + const pkgDetails = { 'installFolder': folder } + if (pkgJson.version) pkgDetails.installedVersion = pkgJson.version + + /** If we can, lets work out what resource is actually needed + * when using one of these packages in the browser. + * If we can't, leave a ? to make it obvious + */ + if (pkgJson.browser) pkgDetails.estimatedEntryPoint = pkgJson.browser + else if (pkgJson.jsdelivr) pkgDetails.estimatedEntryPoint = pkgJson.jsdelivr + else if (pkgJson.unpkg) pkgDetails.estimatedEntryPoint = pkgJson.unpkg + else if (pkgJson.main) pkgDetails.estimatedEntryPoint = pkgJson.main + else pkgDetails.estimatedEntryPoint = '?' + if ( pkgDetails.estimatedEntryPoint === 'none') pkgDetails.estimatedEntryPoint = '?' + + // Homepage - used for a help ref in the Editor + if (pkgJson.homepage) pkgDetails.homepage = pkgJson.homepage + else pkgDetails.homepage = `https://www.npmjs.com/search?q=${packageName}` + + // The base url used by uib - note this is changed if this is a scoped package + pkgDetails.packageUrl = '/' + packageName + + // Work out what kind of package this is + + // If the package name is npm @scoped, remove the scope, add leading / & track scope name + if ( pkgDetails.packageUrl.startsWith('@') ) { + pkgDetails.packageUrl = '/' + packageName.replace(/^@.*\//, '') + pkgDetails.scope = packageName.replace(pkgDetails.packageUrl, '') } - // // If neither can be found, that's an error - // if ( (pkgList.length === 0) && (masterPkgList.length === 0) ) { - // log.error(`[uibuilder:web:checkInstalledPackages] Neither packageList nor masterPackageList could be read from: ${uib.configFolder}`) - // return null - // } - - // Make sure we have socket.io in the list - masterPkgList.push('socket.io') - - // Add in the new package as well if requested - if (newPkg !== '') { - pkgList.push(newPkg) + + // As the url may have changed (by removing scope), record the usable url + pkgDetails.url = `../uibuilder/vendor${pkgDetails.packageUrl}/${pkgDetails.estimatedEntryPoint}` + + //Frig to pick up the version of Bootstrap installed with bootstrap-vue + if (packageName === 'bootstrap-vue') { + pkgDetails.bootstrap = pkgJson.dependencies.bootstrap } - // Merge and de-dup to get a complete list - this.mergedPkgMasterList = tilib.mergeDedupe(Object.keys(installedPackages), pkgList, masterPkgList) + return pkgDetails + } // ---- End of getPackageDetails2 ---- // + + // ---- + + /** DEPRECATED Update all of the installed packages + */ + updateInstalledPackages() { + console.trace('package-mgt.js:updateInstalledPackages') + } // ---- End of updateInstalledPackages ---- // + + /** DEPRECATED Find install folder for a package + */ + getPackagePath() { + console.trace('package-mgt.js:getPackagePath') + } // ---- End of getPackagePath ---- // + + /** DEPRECATED Update the master name list of possible packages that could be served to the front-end + */ + updateMergedPackageList() { + console.trace('package-mgt.js:updateMergedPackageList') } // ---- End of updateMergedPackageList ---- // + // ------ + /** Install an npm package + * NOTE: This fn does not update the list of packages + * because that is built from the package.json file + * and that is updated by calling web.serveVendorPackages() + * which can't be done here - The calling admin API's do that + * Editor->API->This fn->API cont.->web.serveVendorPackages->getUibRootPackageJson->API cont2->Editor * @param {string} url Node instance url - * @param {string} location One of 'userdir', 'common' or 'local' * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed) - * @returns {Promise} Combined stdout/stderr + * @param {string} fromLocation One of 'npmjs', 'github' or 'local', where is the source of the package? + * @param {string} tag Specifier for a version, tag, branch, etc. with leading @ for npm and # for GitHub installs + * @returns {Promise} [Combined stdout/stderr, updated list of package details] */ - async npmInstallPackage(url, location, pkgName) { + async npmInstallPackage(url, pkgName, fromLocation, tag='') { if ( this._isConfigured !== true ) { this.log.warn('[uibuilder:UibPackages:npmInstallPackage] Cannot run. Setup has not been called.') return } - let folder + //? Maybe - allow installs in other locations? + /* let folder switch (location) { case 'userdir': { folder = this.RED.settings.userDir @@ -387,11 +375,13 @@ class UibPackages { folder = this.RED.settings.userDir break } - } + } */ + + // TODO process tag - check if it starts with '#' or '@' // https://github.com/sindresorhus/execa#options const opts = { - 'cwd': folder, + 'cwd': this.uib.rootFolder, 'all': true, } const args = [ // `npm install --no-audit --no-update-notifier --save --production --color=false --no-fund --json ${params.package}@latest` @@ -403,24 +393,22 @@ class UibPackages { '--color=false', '--no-fund', //'--json', - pkgName, + pkgName + tag, ] // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected const {all} = await execa('npm', args, opts) - this.updateInstalledPackages(url, location, pkgName) - return all } // ---- End of installPackage ---- // /** Install an npm package - * @param {string} folder Path of the folder in which to install the package + * NOTE: This fn does not update the list of packages - see install above for reasons. * @param {string} pkgName The npm name of the package (with scope prefix, version, etc if needed) * @returns {Promise} Combined stdout/stderr */ - async npmRemovePackage(folder, pkgName) { + async npmRemovePackage(pkgName) { if ( this._isConfigured !== true ) { this.log.warn('[uibuilder:UibPackages:npmRemovePackage] Cannot run. Setup has not been called.') return @@ -428,7 +416,7 @@ class UibPackages { // https://github.com/sindresorhus/execa#options const opts = { - 'cwd': folder, + 'cwd': this.uib.rootFolder, 'all': true, } const args = [ // `npm remove --no-audit --no-update-notifier --color=false --json ${params.package}` // --save-prefix="~" @@ -443,8 +431,6 @@ class UibPackages { // Don't need a try since we don't do any processing on an execa error - if cmd fails, the promise is rejected const {all} = await execa('npm', args, opts) - this.updateInstalledPackages() - return all } // ---- End of removePackage ---- // diff --git a/nodes/libs/socket.js b/nodes/libs/socket.js index e008acd5..9999729d 100644 --- a/nodes/libs/socket.js +++ b/nodes/libs/socket.js @@ -101,7 +101,7 @@ class UibSockets { } if ( ! uib || ! server ) { - throw new Error('[uibuilder:socket.js:setup] Called without required parameters') + throw new Error(`[uibuilder:socket.js:setup] Called without required parameters. uib=${uib}, server=${server}`) } /** @type {runtimeRED} reference to Core Node-RED runtime object */ diff --git a/nodes/libs/web.js b/nodes/libs/web.js index 202a7eff..f6e02f91 100644 --- a/nodes/libs/web.js +++ b/nodes/libs/web.js @@ -133,17 +133,26 @@ class UibWeb { // @ts-ignore if ( RED.settings.uibuilder && RED.settings.uibuilder.port && RED.settings.uibuilder.port != RED.settings.uiPort) uib.customServer.port = RED.settings.uibuilder.port // eslint-disable-line eqeqeq + // At this point we have the refs to uib and RED + this._isConfigured = true + // TODO: Replace _XXX with #XXX once node.js v14 is the minimum supported version this._adminApiSetup() this._webSetup() this._userApiSetup() this._setMasterStaticFolder() - this._isConfigured = true } // --- End of setup() --- // + //#region ==== Setup - these are called AFTER _isConfigured=true ==== // + /** Add routes for uibuilder's admin REST API's */ _adminApiSetup() { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:web.js:_adminApiSetup] Cannot run. Setup has not been called.') + return + } + this.adminRouter = express.Router({mergeParams:true}) // eslint-disable-line new-cap /** Serve up the v3 admin apis on //uibuilder/admin/ */ @@ -170,6 +179,11 @@ class UibWeb { * @protected */ _webSetup() { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:web.js:_webSetup] Cannot run. Setup has not been called.') + return + } + // Reference static vars const uib = this.uib const RED = this.RED @@ -248,14 +262,11 @@ class UibWeb { // Create Express Router to handle routes on `/uibuilder/` this.uibRouter = express.Router({mergeParams:true}) // eslint-disable-line new-cap - // Create Express Router to handle routes on `/uibuilder/vendor/` - this.vendorRouter = express.Router({mergeParams:true}) // eslint-disable-line new-cap - - // Assign the vendorRouter to the ../uibuilder/vendor url path (via uibRouter) - this.uibRouter.use( '/vendor', this.vendorRouter ) - this.routers.user.push( {name: 'Vendor Routes', path:`${this.uib.httpRoot}/uibuilder/vendor/*`, desc: 'Front-end libraries are mounted under here', type:'Router'} ) - // TODO: Add vendor paths - from `/package.json` + // Add vendor paths for installed front-end libraries - from `/package.json` + this.serveVendorPackages() + // Add socket.io client (../uibuilder/vendor/socket.io/socket.io.js) + this.serveVendorSocketIo() //TODO: This needs some tweaking to allow the cache settings to change - currently you'd have to restart node-red. // Serve up the master common folder (e.g. /uibuilder/common/) @@ -350,7 +361,12 @@ class UibWeb { // TODO - lots of work to do here /** Set up user-facing REST API's */ - _userApiSetup() { // eslint-disable-line class-methods-use-this + _userApiSetup() { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:web.js:_userApiSetup] Cannot run. Setup has not been called.') + return + } + //const RED = uib.RED //app = RED.httpNode @@ -420,6 +436,11 @@ class UibWeb { * in the uibuilder module folders. Services standard images, ico file and fall-back index pages * @protected */ _setMasterStaticFolder() { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:web.js:_setMasterStaticFolder] Cannot run. Setup has not been called.') + return + } + // Reference static vars const uib = this.uib //const RED = this.RED @@ -440,6 +461,81 @@ class UibWeb { } } // --- End of setMasterStaticFolder() --- // + /** Add ExpressJS Route for Socket.IO client */ + serveVendorSocketIo() { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:web.js:serveVendorSocketIo] Cannot run. Setup has not been called.') + return + } + + // Add socket.io client (../uibuilder/vendor/socket.io/socket.io.js) + const sioPath = packageMgt.getPackagePath2('socket.io', this.RED.settings.userDir) + if ( sioPath !== null ) { + this.vendorRouter.use( '/socket.io', express.static( sioPath, this.uib.staticOpts ) ) + } else { + // Error: Can't find Socket.IO + this.log.error(`[uibuilder:web.js:serveVendorSocketIo] Cannot find installation of Socket.IO. It should be in userDir (${this.RED.settings.userDir}) but is not. Check that uibuilder is installed correctly. Run 'npm ls socket.io'.`) + } + } // --- End of serveVendorSocketIo() --- // + + /** Add ExpressJS Routes for all installed packages & ensure /package.json is up-to-date. */ + serveVendorPackages() { + if ( this._isConfigured !== true ) { + this.log.warn('[uibuilder:web.js:serveVendorPackages] Cannot run. Setup has not been called.') + return + } + + // TODO Add some trace messages + + // Create Express Router to handle routes on `/uibuilder/vendor/` + this.vendorRouter = express.Router({mergeParams:true}) // eslint-disable-line new-cap + this.vendorRouter.myname = 'uibVendorRouter' + + // Remove the vendor router if it already exists - we will recreate it. `some` stops once it has found a result + this.uibRouter.stack.some((layer, i, aStack) => { + if ( layer.regexp.toString() === '/^\\/vendor\\/?(?=\\/|$)/i' ) { + aStack.splice(i,1) + return true + } + return false + } ) + this.routers.user.some((entry, i, uRoutes) => { + if ( entry.name === 'Vendor Routes' ) { + uRoutes.splice(i,1) + return true + } + return false + } ) + + // Assign the vendorRouter to the ../uibuilder/vendor url path (via uibRouter) + this.uibRouter.use( '/vendor', this.vendorRouter ) + this.routers.user.push( {name: 'Vendor Routes', path:`${this.uib.httpRoot}/uibuilder/vendor/*`, desc: 'Front-end libraries are mounted under here', type:'Router'} ) + + // Get the installed packages from the `/package.json` file + // If it doesn't exist, this will create it. + const pj = packageMgt.getUibRootPackageJson() + + Object.keys(pj.dependencies).forEach( packageName => { + const pkgDetails = pj.uibuilder.packages[packageName] + + // Add a route for each package to this.vendorRouter + this.vendorRouter.use( + pkgDetails.packageUrl, + express.static( + pkgDetails.installFolder, + this.uib.staticOpts + ) + ) + + }) + + // Update the /package.json file with updated details + packageMgt.setUibRootPackageJson(pj) + + } // ---- End of serveVendorPackages ---- // + + //#endregion ==== End of Setup ==== // + /** Allow the isConfigured flag to be read (not written) externally * @returns {boolean} True if this class as been configured */ @@ -447,8 +543,6 @@ class UibWeb { return this._isConfigured } - //? Consider adding isConfigered checks on each method? - //#region ====== Per-node instance processing ====== // /** Setup the web resources for a specific uibuilder instance @@ -459,7 +553,12 @@ class UibWeb { const uib = this.uib //const RED = this.RED //const log = this.log - this.routers.instances[node.url] = [] // Track routes + + // NOTE: When an instance is renamed or deleted, the routes are removed + // See the relevant parts of uibuilder.js for details. + + // Reset the routes for this instance + this.routers.instances[node.url] = [] /** Make sure that the common static folder is only loaded once */ node.commonStaticLoaded = false @@ -471,10 +570,6 @@ class UibWeb { // We want to add services in the right order - first load takes preference // Middleware first (1), then front-end user code(2)(4), master (3) and common (5) folders last - // TODO: Is this actually needed any more? - // Remove existing middleware so that it can be redone - allows for changing of src/dist folder - this.removeInstanceMiddleware(node) - // (1a) httpMiddleware - Optional middleware from a custom file this.addMiddlewareFile(node) // (1b) masterMiddleware - Generic dynamic middleware to add uibuilder specific headers & cookie @@ -502,8 +597,6 @@ class UibWeb { // Apply this instances router to the url path on `//` this.app.use( tilib.urlJoin(node.url), this.instanceRouters[node.url]) - console.log('>> ROUTES >>'); console.dir(this.routers, {depth:3}) - } /** (1a) Optional middleware from a file @@ -821,202 +914,6 @@ class UibWeb { //#endregion ====== Per-node instance processing ====== // - //#region ====== Package Management ====== // - // NB: Packages are actually installed via a v2 API `uibnpmmanage` - - /** Compare the in-memory package list against packages actually installed. - * Also check common packages installed against the master package list in case any new ones have been added. - * Updates the package list file and uib.installedPackages - * param {Object} vendorPaths Schema: {'': {'url': vendorPath, 'path': installFolder, 'version': packageVersion, 'main': mainEntryScript} } - * @param {string} newPkg Default=''. Name of a new package to be checked for in addition to existing. - * @param {string} url The node instance url if wanting to check for locally installed packages. - * @returns {object} uib.installedPackages - */ - checkInstalledPackages(newPkg='', url='') { - // Reference static vars - const uib = this.uib - const log = this.log - const app = this.app - - let installedPackages = uib.installedPackages - - packageMgt.updateMergedPackageList(newPkg) - - // For each entry in the complete list ... - packageMgt.mergedPkgMasterList.forEach( (pkgName, _i) => { // eslint-disable-line no-unused-vars - // flags - let pkgExists = false - - let pj = null // package details if found - - // Check to see if folder names present in /node_modules - const pkgFolder = packageMgt.getPackagePath(pkgName) - - // Check whether package is really installed (exists) - if ( pkgFolder !== null ) { - - // Get the package.json - pj = packageMgt.readPackageJson( pkgFolder ) - - /** The folder delete for npm remove happens async so it may - * still exist when we check. But the package.json will have been removed - * so we don't process the entry unless package.json actually exists - */ - if ( ! Object.prototype.hasOwnProperty.call(pj, 'ERROR') ) { - // We only know for sure package exists now - pkgExists = true - } - } - - // Check to see if the package is in the current list - const isInCurrent = Object.prototype.hasOwnProperty.call(installedPackages, pkgName) - - if ( pkgExists ) { - // If package is installed but does NOT exist in current list - add it now - if ( ! isInCurrent ) { - // Add to current & mark for loading (serving) - installedPackages[pkgName] = {} - installedPackages[pkgName].loaded = false - } - - // Update package info - installedPackages[pkgName].folder = pkgFolder - installedPackages[pkgName].url = ['..', uib.moduleName, 'vendor', pkgName].join('/') - // Find installed version - installedPackages[pkgName].version = pj.version - // Find homepage - installedPackages[pkgName].homepage = pj.homepage - // Find main entry point (or '') - installedPackages[pkgName].main = pj.main || '' - - /** Try to guess the browser entry point (or '') - * since v3.2.1 Fix for packages misusing the browser property - might be an object see #123 - */ - let browserEntry = '' - if ( pj.browser && typeof pj.browser === 'string' ) { - browserEntry = pj.browser - } - if ( browserEntry === '' ) { - browserEntry = pj.jsdelivr || pj.unpkg || '' - } - installedPackages[pkgName].browser = browserEntry - - // Replace generic entry points with specific if we know them - if ( pkgName === 'socket.io' ) { - //installedPackages[pkgName].url = '../uibuilder/socket.io/socket.io.js' - installedPackages[pkgName].main = 'socket.io.js' - } - - // If we need to load it & we have app available - if ( (installedPackages[pkgName].loaded === false) && (app !== undefined) ) { - /** Add a static path to serve up the files */ - installedPackages[pkgName].loaded = this.servePackage(pkgName) - } - - } else { // (package not actually installed) - // If in current, flag for unloading then delete from current - if ( isInCurrent ) { // eslint-disable-line no-lonely-if - if ( app !== undefined) { - installedPackages[pkgName].loaded = this.unservePackage(pkgName) - log.trace('[uibuilder:web:checkInstalledPackages] package unserved ', pkgName) - } - delete installedPackages[pkgName] - log.trace('[uibuilder:web:checkInstalledPackages] package deleted from installedPackages ', pkgName) - } - } - }) - - //uib.installedPackages = installedPackages - - // Write packageList back to file - try { - fs.writeJsonSync(path.join(uib.configFolder,uib.packageListFilename), Object.keys(installedPackages), {spaces:2}) - } catch(e) { - log.error(`[uibuilder:web:checkInstalledPackages] Could not write ${uib.packageListFilename} in ${uib.configFolder}`, e) - } - - return uib.installedPackages - - } // ---- End of checkInstalledPackages ---- // - - /** Add an installed package to the ExpressJS app to allow access from URLs - * @param {string} packageName Name of the front-end npm package we are trying to add - * @returns {boolean} True if loaded, false otherwise - */ - servePackage(packageName) { - // Reference static vars - const uib = this.uib - const RED = this.RED - const log = this.log - - let userDir = RED.settings.userDir - let pkgDetails = null - let installedPackages = uib.installedPackages - - // uib.installedPackages[packageName] MUST exist and be populated (done by uiblib.checkInstalledPackages()) - if ( Object.prototype.hasOwnProperty.call(installedPackages, packageName) ) { - pkgDetails = installedPackages[packageName] - } else { - log.error('[uibuilder:web:servePackage] Failed to find package in uib.installedPackages') - return false - } - - // Where is the node_modules folder for this package? - const installFolder = pkgDetails.folder - - if (installFolder === '' ) { - log.error(`[uibuilder:web:servePackage] Failed to add user vendor path - no install folder found for ${packageName}. Try doing "npm install ${packageName} --save" from ${userDir}`) - return false - } - - // What is the URL for this package? Remove the leading "../" - var vendorPath - try { - vendorPath = pkgDetails.url.replace('../','/') // "../uibuilder/vendor/socket.io" tilib.urlJoin(uib.moduleName, 'vendor', packageName) - } catch (e) { - log.error(`[uibuilder:web:servePackage] ${packageName} `, e) - return false - } - log.trace(`[uibuilder:web:servePackage] Adding user vendor path: ${util.inspect({'url': vendorPath, 'path': installFolder})}`) - - try { - this.vendorRouter.use( vendorPath.replace('/uibuilder/vendor', ''), express.static(installFolder, uib.staticOpts) ) - return true - } catch (e) { - log.error(`[uibuilder:web:servePackage] app.use failed. vendorPath: ${vendorPath}, installFolder: ${installFolder}`, e) - return false - } - } // ---- End of servePackage ---- // - - /** Remove an installed package from the ExpressJS app - * @param {string} packageName Name of the front-end npm package we are trying to add - * @returns {boolean} True if unserved, false otherwise - */ - unservePackage(packageName) { - // Reference static vars - //const uib = this.uib - //const RED = this.RED - //const log = this.log - const app = this.app - - let pkgReStr = `/^\\/uibuilder\\/vendor\\/${packageName}\\/?(?=\\/|$)/i` - let done = false - // For each entry on ExpressJS's server stack... - app._router.stack.some( function(r, i) { - if ( r.regexp.toString() === pkgReStr ) { - // We can splice inside the array only because we will only do a single one. - app._router.stack.splice(i,1) - done = true - return true - } - return false - }) - - return done - } // ---- End of unservePackage ---- // - - //#endregion ====== Package Management ====== // - //#region ==== ExpressJS Route Reporting ==== // /** Summarise Express route properties @@ -1186,23 +1083,40 @@ class UibWeb { /** Build a raw HTML table from an input * @param {*} input Input object - * @param {array} [cols] List of columns + * @param {Array} [cols] List of columns * @returns {string} HTML Table */ htmlBuildTable(input, cols) { // eslint-disable-line class-methods-use-this if (!cols) { cols = Object.keys(input[0]) } - let html = '' + let html = '
' + + const escapeHTML = str => + str.replace(/[&<>'"]/g, + tag => ({ + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"' + }[tag]) + ) + /** The HTML for a single cell + * @param {*} col _ + * @param {*} entry _ + * @returns {string} HTML for a single cell + */ function cell(col, entry) { // eslint-disable-line require-jsdoc let html = '' return html } + // Show the headings cols.forEach( (col) => { html += '' } - html += '
' - html += entry[col] ? entry[col] : ' ' + html += entry[col] ? escapeHTML(entry[col]) : ' ' html += '' html += col @@ -1220,7 +1134,7 @@ class UibWeb { html += '
' + html += '' return html } diff --git a/nodes/uib-receiver.html b/nodes/uib-receiver.html deleted file mode 100644 index c204bc99..00000000 --- a/nodes/uib-receiver.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - diff --git a/nodes/uib-receiver.js b/nodes/uib-receiver.js deleted file mode 100644 index 72f7c1a6..00000000 --- a/nodes/uib-receiver.js +++ /dev/null @@ -1,136 +0,0 @@ -/** Takes a msg input and sends it to the chosen uibuilder instance - * Destructured to make for easier and more consistent logic. - * - * Copyright (c) 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. - */ -'use strict' - -/** --- Type Defs --- - * @typedef {import('../typedefs.js').runtimeRED} runtimeRED - * @typedef {import('../typedefs.js').runtimeNodeConfig} runtimeNodeConfig - * @typedef {import('../typedefs.js').runtimeNode} runtimeNode - * @typedef {import('../typedefs.js').senderNode1} senderNode - * typedef {import('../typedefs.js').myNode} myNode - */ - -//#region ----- Module level variables ---- // - -/** Main (module) variables - acts as a configuration object - * that can easily be passed around. - */ -const mod = { - /** @type {runtimeRED} Reference to the master RED instance */ - RED: undefined, - /** @type {string} Custom Node Name - has to match with html file and package.json `red` section */ - nodeName: 'uib-receiver', -} - -//#endregion ----- Module level variables ---- // - -//#region ----- Module-level support functions ----- // - -/** 3) Run whenever a node instance receives a new input msg - * NOTE: `this` context is still the parent (nodeInstance). - * See https://nodered.org/blog/2019/09/20/node-done - * @param {object} msg The msg object received. - * @param {Function} send Per msg send function, node-red v1+ - * @param {Function} done Per msg finish function, node-red v1+ - */ -function inputMsgHandler(msg, send, done) { // eslint-disable-line no-unused-vars - // As a module-level named function, it will inherit `mod` and other module-level variables - - // If you need it - or just use mod.RED if you prefer: - //const RED = mod.RED - - // TODO send the msg to the front-end - const sockets = global.totallyInformationShared.uibsockets - console.log('>>>>', sockets.uib.ioChannels.client) - msg._fromSender = true - if ( global.totallyInformationShared.uibsockets ) { - sockets.sendToFe(msg, this.url, sockets.uib.ioChannels.client) - } - - - // If passthrough is enabled, send the msg - if ( this.passthrough === true ) send(msg) - // We are done - done() - -} // ----- end of inputMsgHandler ----- // - -/** 2) This is run when an actual instance of our node is committed to a flow - * type {function(this:runtimeNode&senderNode, runtimeNodeConfig & senderNode):void} - * @param {runtimeNodeConfig & senderNode} config The Node-RED node instance config object - * @this {runtimeNode & senderNode} - */ -function nodeInstance(config) { - // As a module-level named function, it will inherit `mod` and other module-level variables - - // If you need it - which you will here - or just use mod.RED if you prefer: - const RED = mod.RED - - // Create the node instance - `this` can only be referenced AFTER here - RED.nodes.createNode(this, config) - - /** Transfer config items from the Editor panel to the runtime */ - this.name = config.name - this.topic = config.topic || '' - this.passthrough = config.passthrough - this.url = config.url || '' - - /** Handle incoming msg's - note that the handler fn inherits `this` */ - this.on('input', inputMsgHandler) - - /** Put things here if you need to do anything when a node instance is removed - * Or if Node-RED is shutting down. - * Note the use of an arrow function, ensures that the function keeps the - * same `this` context and so has access to all of the node instance properties. - */ - // this.on('close', (removed, done) => { - // console.log('>>>=[IN 4]=>>> [nodeInstance:close] Closing. Removed?: ', removed) - - // done() - // }) - - /** Properties of `this` - * Methods: updateWires(wires), context(), on(event,callback), emit(event,...args), removeListener(name,listener), removeAllListeners(name), close(removed) - * send(msg), receive(msg), log(msg), warn(msg), error(logMessage,msg), debug(msg), trace(msg), metric(eventname, msg, metricValue), status(status) - * Other: credentials, id, type, z, wires, x, y - * + any props added manually from config, typically at least name and topic - */ - //console.log('>>>> TI GLOBAL <<<<', global.totallyInformationShared) -} - -//#endregion ----- Module-level support functions ----- // - -/** 1) Complete module definition for our Node. This is where things actually start. - * @param {runtimeRED} RED The Node-RED runtime object - */ -function EventOut(RED) { - // As a module-level named function, it will inherit `mod` and other module-level variables - - // Save a reference to the RED runtime for convenience - mod.RED = RED - - /** Register a new instance of the specified node type (2) - * - */ - RED.nodes.registerType(mod.nodeName, nodeInstance) -} - -// Export the module definition (1), this is consumed by Node-RED on startup. -module.exports = EventOut - -//EOF diff --git a/nodes/uibuilder.html b/nodes/uibuilder.html index 7656660f..795d173d 100644 --- a/nodes/uibuilder.html +++ b/nodes/uibuilder.html @@ -14,2298 +14,2526 @@ limitations under the License. --> - - } else { - // Don't bother if the top of the editor is still auto - if ( $('#edit-outer').css('top') === 'auto' ) return + - /** 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 + - - - - + \ No newline at end of file diff --git a/nodes/uibuilder.js b/nodes/uibuilder.js index 33085add..d20b6f98 100644 --- a/nodes/uibuilder.js +++ b/nodes/uibuilder.js @@ -74,7 +74,6 @@ const uib = { instances: {}, masterPackageListFilename: 'masterPackageList.json', packageListFilename: 'packageList.json', - installedPackages: {}, masterTemplateFolder: path.join( __dirname, '..', 'templates' ), masterStaticDistFolder: path.join( __dirname, '..', 'front-end', 'dist' ), masterStaticSrcFolder: path.join( __dirname, '..', 'front-end', 'src' ), @@ -96,6 +95,7 @@ const uib = { /** @type {undefined|string} The host name of the Node-RED server */ hostName: undefined, }, + reDeployNeeded: '4.1.2', degitEmitter: undefined, RED: undefined, } @@ -146,7 +146,7 @@ function runtimeSetup() { RED.log.info(`| ${uib.customServer.type}://${uib.customServer.host}:${port}/ or ${uib.customServer.type}://localhost:${port}/`) RED.log.info('| Installed packages:') - const pkgs = Object.keys(uib.installedPackages) + const pkgs = Object.keys(packageMgt.uibPackageJson.uibuilder.packages) for (let i = 0; i < pkgs.length; i+=4) { const k = [] for (let j = 0; j <= 3; j++) { @@ -265,13 +265,6 @@ function runtimeSetup() { /** Pass core objects to the Socket.IO handler module */ sockets.setup(uib, web.server) // Singleton wrapper for Socket.IO - /** Serve up vendor packages. Updates uib.installedPackages - * This is the first check, the installed packages are rechecked at various times. - * Reads the packageList and masterPackageList files - * Adds ExpressJS static paths for each found FE package & saves the details to the vendorPaths variable. - */ - web.checkInstalledPackages() - } // --- end of runtimeSetup --- // /** Create external event listeners @@ -401,7 +394,7 @@ function nodeInstance(config) { // NB: this.id and this.type are also available this.name = config.name || '' this.topic = config.topic || '' - this.url = config.url || 'uibuilder' + this.url = config.url // Undefined or '' is not valid this.oldUrl = config.oldUrl this.fwdInMessages = config.fwdInMessages === undefined ? false : config.fwdInMessages this.allowScripts = config.allowScripts === undefined ? false : config.allowScripts @@ -417,11 +410,20 @@ function nodeInstance(config) { this.tokenAutoExtend = config.tokenAutoExtend === undefined ? false : config.tokenAutoExtend this.reload = config.reload === undefined ? false : config.reload this.sourceFolder = config.sourceFolder // NB: Do not add a default here as undefined triggers a check for index.html in web.js:setupInstanceStatic + this.deployedVersion = config.deployedVersion //#endregion ====== Local node config copy ====== // log.trace(`[uibuilder:nodeInstance:${this.url}] ================ instance registered ================`) log.trace(`[uibuilder:nodeInstance:${this.url}] node keys: ${JSON.stringify(Object.keys(this))}`) log.trace(`[uibuilder:nodeInstance:${this.url}] config keys: ${JSON.stringify(Object.keys(config))}`) + log.trace(`[uibuilder:nodeInstance:${this.url}] Deployed Version: ${this.deployedVersion}`) + + if ( !this.url || typeof this.url !== 'string' || this.url.length < 1 ) { + log.error('[uibuilder:nodeInstance] No valid URL provided. Cannot set up this uibuilder instance') + this.statusDisplay = { fill: 'red', shape: 'dot', text: 'ERROR:NOT CONFIGURED - No URL' } + uiblib.setNodeStatus( this ) + return + } this.statusDisplay = { fill: 'blue', shape: 'dot', text: 'Configuring node' } if ( this.useSecurity === true ) this.statusDisplay.fill = 'yellow' @@ -466,6 +468,9 @@ function nodeInstance(config) { log.error(`[uibuilder:nodeInstance] RENAME OF INSTANCE FOLDER FAILED. Fatal. url=${this.url}, oldUrl=${this.oldUrl}, Fldr=${this.customFolder}. Error=${e.message}`, e) } } + // Remove the old router and remove from the routes list + delete web.routers.instances[this.oldUrl] + delete web.instanceRouters[this.oldUrl] // we continue to do the normal checks in case something failed or if this is an initial deploy (so no original folder exists) } @@ -551,7 +556,7 @@ function nodeInstance(config) { // 3) Add event handler to process inbound messages this.on('input', inputMsgHandler) - // 3rd-party node (non-flow) Event handlers + // 3rd-party node (non-flow) Event handlers (e.g. uib-sender) externalEvents(this) /** Do something when Node-RED is closing down which includes when this node instance is redeployed @@ -565,22 +570,29 @@ function nodeInstance(config) { // Cancel any event listeners for this node tiEvents.removeAllListeners(`node-red-contrib-uibuilder/${this.url}`) + // Tody up the ExpressJS routes if a node is removed + if (removed) { + delete web.routers.instances[this.url] + delete web.instanceRouters[this.url] + } + // Do any complex close processing here if needed - MUST BE LAST uiblib.instanceClose(this, uib, sockets, web, done) //done() }) + // TODO Move to web // Shows an instance details debug page RED.httpAdmin.get(`/uibuilder/instance/${this.url}`, (/** @type {Express.Request} */ req, /** @type {Express.Response} */ res) => { let page = web.showInstanceDetails(req, this) res.status(200).send( page ) }) - // TODO: Remove this debug info - setTimeout(function(){ - tilib.dumpMem('Instance') - web.dumpRoutes(true) - }, 2000) + // // TODO: Remove this debug info + // setTimeout(function(){ + // tilib.dumpMem('Instance') + // web.dumpRoutes(true) + // }, 2000) } // ----- end of nodeInstance ----- // @@ -603,6 +615,8 @@ function Uib(RED) { uibuilderNodeEnv: { value: process.env.NODE_ENV, exportable: true }, uibuilderTemplates: { value: templateConf, exportable: true }, // See require's uibuilderCustomServer: { value: (uib.customServer), exportable: true }, + uibuilderCurrentVersion: { value: (uib.version), exportable: true }, // Current version of uibuilder + uibuilderRedeployNeeded: { value: uib.reDeployNeeded, exportable: true }, }, }) diff --git a/src/editor/uibuilder/editor.js b/src/editor/uibuilder/editor.js index 49266f1e..a79a9c6b 100644 --- a/src/editor/uibuilder/editor.js +++ b/src/editor/uibuilder/editor.js @@ -40,6 +40,12 @@ var packages = [] /** Default template name */ var defaultTemplate = 'blank' + /** Set to true if we want to force a (re)deploy */ + var mustChange = false + /** Is the URL valid? */ + var urlValid = false + /** Does the server folder for this instance exist? */ + var folderExists = false /** placeholder for ACE editor vars - so that they survive close/reopen admin config ui * @typedef {object} uiace Options for the ACE/Monaco code editor @@ -59,6 +65,8 @@ //#region --------- "global" functions for the panel --------- // + //#region ==== Package Management Functions ==== // + /** AddItem function for package list * @param {object} node A reference to the panel's `this` object * @param {JQuery} element the jQuery DOM element to which any row content should be added @@ -66,32 +74,69 @@ * @param {string|*} data data object for the row. {} if add button pressed, else data passed to addItem method */ function addPackageRow(node, element, index, data) { - var hRow = '' + let hRow = '', pkgSpec = null if (Object.entries(data).length === 0) { // Add button was pressed so we have no packageName, create an input form instead - hRow = `` - hRow += ` ` - hRow += '
Enter one of: (a) an npm package name (optionally with leading scope and/or trailing version), (b) a GitHub user/repo (with optional branch spec), (c) A filing system path to a local package. Select where to install.
' + hRow = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ ` } else { + /* + npm install [<@scope>/] + npm install [<@scope>/]@ + npm install [<@scope>/]@ + npm install [<@scope>/]@ + npm install @npm: + npm install :/[#branch-tag-name] + npm install + npm install + npm install + npm install + */ + pkgSpec = packages[data] // addItem method was called with a packageName passed - hRow = data + hRow = ` +
+ ${data}, ${pkgSpec.installedVersion} +
+ URL to use: + ${pkgSpec.url} + +
+ ` } // Build the output row - var myRow = $('
'+hRow+'
').appendTo(element) + $(hRow).appendTo(element) + // Add tooltips for the input fields if (Object.entries(data).length === 0) { - // Add a tooltip // @ts-expect-error ts(2339) $(`#packageList-input-${index}`).tooltip({ - content: 'Enter one of:
  • An npm package name (optionally with leading scope and/or trailing version)
  • A GitHub user/repo (with optional branch spec)
  • A filing system path to a local package
' + content: 'Enter one of:
  • An npm package name (optionally with leading)
  • A GitHub user/repo
  • A filing system path to a local package
Ensure that you select the correct "From" dropdown.' }) // @ts-expect-error ts(2339) - $(`#pkg-sel-${index}`).tooltip({ - content: 'Install for:
  • All uibuilder instances (common)
  • Just for this one (local)
' + $(`#packageList-ver-${index}`).tooltip({ + content: 'Optional. Branch, Tag or Version to install (defaults to @latest)' }) } @@ -100,29 +145,30 @@ // show activity spinner $('i.spinner').show() - var packageName = String($('#packageList-input-' + index).val()) - var packageLoc = String($('#pkg-sel-' + index).val()) + var packageName = String($(`#packageList-input-${index}`).val()) + var packageLoc = String($(`#packageList-src-${index}`).val()) + var packageTag = String($(`#packageList-tag-${index}`).val()) if ( packageName.length !== 0 ) { - console.log( '>>', packageName, packageLoc ) - RED.notify('Installing npm package ' + packageName) // Call the npm installPackage API (it updates the package list) - $.get( `uibuilder/uibnpmmanage?cmd=install&package=${packageName}&url=${node.url}&loc=${packageLoc}`, function(data){ + $.get( `uibuilder/uibnpmmanage?cmd=install&package=${packageName}&url=${node.url}&loc=${packageLoc}&tag=${packageTag}`, function(data){ + const npmOutput = data.result[0] if ( data.success === true) { - console.log('[uibuilder:addPackageRow:get] PACKAGE INSTALLED. ', packageName, packageLoc, node.url) + packages = data.result[1] + + console.log('[uibuilder:addPackageRow:get] PACKAGE INSTALLED. ', packageName, packageLoc, node.url, '\n\n', npmOutput, '\n ') RED.notify(`Successful installation of npm package ${packageName} in ${packageLoc} for ${node.url}`, 'success') - // Replace the input field with the normal package name display - myRow.html(packageName) + // reset and populate the list + $('#node-input-packageList').editableList('empty') + // @ts-ignore + $('#node-input-packageList').editableList('addItems', Object.keys(packages)) - // Update the master package list - packages.push(packageName) } else { - console.log('[uibuilder:addPackageRow:get] ERROR ON INSTALLATION OF PACKAGE ', packageName, packageLoc, node.url ) - console.dir( data.result ) + console.log('[uibuilder:addPackageRow:get] ERROR ON INSTALLATION OF PACKAGE ', packageName, packageLoc, node.url, '\n\n', npmOutput, '\n ' ) RED.notify(`FAILED installation of npm package ${packageName} in ${packageLoc} for ${node.url}`, 'error') } @@ -165,10 +211,7 @@ 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) + if ( packages[packageName] ) delete packages[packageName] } else { console.log('[uibuilder:removePackageRow:get] ERROR ON PACKAGE REMOVAL ', data.result ) RED.notify('FAILED to uninstall npm package ' + packageName, 'error') @@ -199,36 +242,27 @@ * @param {string} url Used to find locally install packages for this node from uibRoot/url/ */ function packageList(url) { + $.ajax({ + dataType: 'json', method: 'get', url: `uibuilder/uibvendorpackages?url=${url}`, async: false, //data: { url: node.url}, + success: function(vendorPaths) { - packages = [] - var pkgList = Object.keys(vendorPaths) - console.log('>> Packages >> ', vendorPaths) - // eslint-disable-next-line no-unused-vars - pkgList.forEach(function(packageName,_index){ - // Populate the master package list (used to check dependencies) - packages.push(packageName) - }) + packages = vendorPaths } + }) - // $.getJSON('uibvendorpackages', function(vendorPaths) { - // packages = [] - // var pkgList = Object.keys(vendorPaths) - // console.log('>> Packages >> ', vendorPaths) - // // eslint-disable-next-line no-unused-vars - // pkgList.forEach(function(packageName,_index){ - // // Populate the master package list (used to check dependencies) - // packages.push(packageName) - // }) - // }) } // --- End of packageList --- // + //#endregion ==== Package Management Functions ==== // + + //#region ==== File Management Functions ==== // + /** Return a file type from a file name (or default to txt) * ftype can be used in ACE editor modes * @param {string} fname File name for which to return the type @@ -587,6 +621,66 @@ } // --- End of deleteFile --- // + /** Set the height of the ACE text editor box */ + function setACEheight() { + let 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'), 10) // full available height + height -= 25 + + // Replace the expand icon with a compress icon + $('#node-function-expand-js').css('background-color','black') + .html('') + + uiace.fullscreen = true + + } else { + // Don't bother if the top of the editor is still auto + if ( $('#edit-outer').css('top') === 'auto' ) return + + $('#edit-props').css('background-color','') + .css('padding','') + + height = ($('.red-ui-tray-footer').position()).top - ($('#edit-outer').offset()).top - 35 + + // Replace the compress icon with a expand icon + $('#node-function-expand-js').css('background-color','') + .html('') + + 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 20 ) return false // Cannot contain .. if ( value.indexOf('..') !== -1 ) return false - // cannot contain / or \ + // Cannot contain / or \ if ( value.indexOf('/') !== -1 ) return false if ( value.indexOf('\\') !== -1 ) return false + // Cannot contain spaces + 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 var exists = false $.ajax({ type: 'GET', async: false, dataType: 'json', - url: './uibuilder/admin/' + value, + url: `./uibuilder/admin/${value}`, data: { 'cmd': 'checkurls', }, @@ -640,82 +738,20 @@ /** If the url already exists - prevent the "Done" button from being pressed. */ // @ts-ignore if ( exists === true ) { - $('#node-dialog-ok').prop('disabled', true) - $('#node-dialog-ok').css( 'cursor', 'not-allowed' ) - $('#node-dialog-ok').removeClass('primary') RED.notify(`ERROR:

The chosen URL (${value}) is already in use (or the folder exists).
It must be changed before you can save/commit

`, {type: 'error'}) return false } - - $('#node-dialog-ok').prop('disabled', false) - $('#node-dialog-ok').css( 'cursor', 'pointer' ) - $('#node-dialog-ok').addClass('primary') + + urlValid = true // Warn user when changing URL. NOTE: Set/reset old url in the onsave function not here - if ( value !== this.url ) + if ( this.url !== undefined && value !== this.url ) RED.notify(`NOTE:

You are renaming the url from ${this.url} to ${value}.
You MUST redeploy before doing anything else.

`, {type: 'warning'}) return true - } // --- End of validateUrl --- // - /** Set the height of the ACE text editor box */ - function setACEheight() { - let 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'), 10) // full available height - height -= 25 - - // Replace the expand icon with a compress icon - $('#node-function-expand-js').css('background-color','black') - .html('') - - uiace.fullscreen = true - - } else { - // Don't bother if the top of the editor is still auto - if ( $('#edit-outer').css('top') === 'auto' ) return - - $('#edit-props').css('background-color','') - .css('padding','') - - height = ($('.red-ui-tray-footer').position()).top - ($('#edit-outer').offset()).top - 35 - - // Replace the compress icon with a expand icon - $('#node-function-expand-js').css('background-color','') - .html('') - - 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 RED.settings.uibuilderRedeployNeeded) { + RED.notify(`uibuilder ${this.url}
uibuilder has been updated since you last deployed this instance. Please deploy now.`,{ + modal: false, + fixed: false, + type: 'warning', // 'compact', 'success', 'warning', 'error' + }) + mustChange = true + return false + } + } + return true + } + + //#endregion ==== Validation Functions ==== // + + //#region ==== Template Management Functions ==== // + /** Populate the template selection dropdown * Uses a file that is `require`d in uibuilder.js * @param {object} node Pass in this @@ -803,7 +864,7 @@ const missing = [] deps.forEach( depName => { - if ( ! packages.includes(depName) ) missing.push(depName) + if ( ! packages[depName] ) missing.push(depName) }) if ( missing.length > 0 ) { @@ -888,6 +949,49 @@ } // --- End of btnTemplate() --- // + /** Configure the template dropdown & setup button handlers (called from onEditPrepare) + * @param {object} node A reference to the panel's `this` object + */ + function templateSettings(node) { + + $('#adv-templ').hide() + $('#show-templ-props').css( 'cursor', 'pointer' ) + $('#show-templ-props').on('click', function() { // (e) { + $('#adv-templ').toggle() + if ( $('#adv-templ').is(':visible') ) { + $('#show-templ-props').html(' Template Settings') + } else { + $('#show-templ-props').html(' Template Settings') + } + }) + // Populate the template selection drop-down and select default (in advanced) + populateTemplateDropdown(node) + checkDependencies() + // Unhide the external template name input if external selected + if ( $('#node-input-templateFolder').val() === 'external' ) $('#et-input').show() + // Handle change of template + $('#node-input-templateFolder').on('change', function() { // (e) { + // update the help tip + if ( RED.settings.uibuilderTemplates[node.templateFolder] ) + $('#node-templSel-info').text(RED.settings.uibuilderTemplates[node.templateFolder].description) + // Check if the dependencies are installed, warn if not + checkDependencies() + // Unhide the external template name input if external selected + if ( $('#node-input-templateFolder').val() === 'external' ) + $('#et-input').show() + else + $('#et-input').hide() + }) + // Button press for loading new template + $('#btn-load-template').on('click', function(e){ + e.preventDefault() // don't trigger normal click event + btnTemplate() + }) + + } + + //#endregion ==== Template Management Functions ==== // + /** Set initial hidden & checkbox states (called from onEditPrepare) * @param {object} node A reference to the panel's `this` object */ @@ -904,12 +1008,130 @@ $('#node-input-reload').prop('checked', node.reload) } + /** (Dis)Allow uibuilder configuration other than URL changes + * @param {boolean} enable True=Enable config editing, false=disable + */ + function enableEdit(enable=true) { + if (enable) { + // $('#node-dialog-ok') + // .prop('disabled', false) + // .css( 'cursor', 'pointer' ) + // .addClass('primary') + + $('#node-input-templateFolder, #btn-load-template') + .prop('disabled', false) + + $('#red-ui-tab-tab-files, #red-ui-tab-tab-libraries, #red-ui-tab-tab-security, #red-ui-tab-tab-advanced, info') + .css('pointer-events', 'auto') + $('#red-ui-tab-tab-files>a, #red-ui-tab-tab-libraries>a, #red-ui-tab-tab-security>a, #red-ui-tab-tab-advanced>a, info>a') + .css('color', 'var(--nr-db-dark-text)') + + $('#uibuilderurl, #uibinstanceconf') + .css({ + 'pointer-events': 'auto', + 'background-color': '', + }) + + } else { + // Don't disable the Done button if the folder doesn't exist + // if (!folderExists) + // $('#node-dialog-ok') + // .prop('disabled', true) + // .css( 'cursor', 'not-allowed' ) + // .removeClass('primary') + + $('#node-input-templateFolder, #btn-load-template') + .prop('disabled', true) + + $('#red-ui-tab-tab-files, #red-ui-tab-tab-libraries, #red-ui-tab-tab-security, #red-ui-tab-tab-advanced, info') + .css('pointer-events', 'none') + $('#red-ui-tab-tab-files>a, #red-ui-tab-tab-libraries>a, #red-ui-tab-tab-security>a, #red-ui-tab-tab-advanced>a, info>a') + .css('color', 'var(--nr-db-disabled-text)') + + $('#uibuilderurl, #uibinstanceconf') + .css({ + 'pointer-events': 'none', + 'background-color': 'var(--red-ui-secondary-background-selected)', + }) + } + + } // ---- End of enableEdit ---- // + + /** Find out if a server folder exists for this url + * @param {*} url URL to check + * @returns {boolean} Whether the folder exists + */ + function queryFolderExists(url) { + if (url === undefined) return false + let check = false + $.ajax({ + type: 'GET', + async: false, + dataType: 'json', + url: `./uibuilder/admin/${url}`, + data: { + 'cmd': 'checkfolder', + }, + success: function(data) { + check = data + }, + error: function(jqXHR, textStatus, errorThrown) { + if (errorThrown !== 'Not Found') + console.error( '[uibuilder:queryFolderExists] Error ' + textStatus, errorThrown ) + check = false + }, + }) + return check + } // ---- end of queryFolderExists ---- // + /** Handle URL changes (called from onEditPrepare) * `this` is the selected jQuery object $('#node-input-url') + * @param {object} node A reference to the panel's `this` object + * @param {object} jqThis A jQuery object */ - function urlChange() { + function urlChange(node, jqThis) { + var thisurl = $(jqThis).val() + + // Find out if the server folder for this instance exists yet + folderExists = queryFolderExists(thisurl) + + if ( urlValid && folderExists ) { + enableEdit(true) + } else { + enableEdit(false) + } + + if (!urlValid && (thisurl !== undefined && thisurl !== '')) { + $('#folder-not-exist').remove() + $('#url-input').after(` +
+ Invalid URL.
+ Cannot contain space, .., /, \\.
+ Must be <=20 characters, not start with _ or ..
+ Must not be templates or an existing URL. +
+ `) + } else if (!folderExists) { + $('#folder-not-exist').remove() + if (node.url === undefined) { + $('#url-input').after(` +
+ Server folder has not yet been created.
+ You must Deploy before you can make other changes. +
+ `) + } else { + $('#url-input').after(` +
+ Server folder does not exist.
+ You must Deploy before you can make other changes. +
+ `) + } + } else { + $('#folder-not-exist').remove() + } - var thisurl = $(this).val() var eUrlSplit = window.origin.split(':') //var nrPort = Number(eUrlSplit[2]) var nodeRoot = RED.settings.httpNodeRoot.replace(/^\//, '') @@ -934,49 +1156,9 @@ $('#uibinstanceconf').prop('href', `./uibuilder/instance/${thisurl}?cmd=showinstancesettings`) // NB: The index url link is only shown if the option is turned on $('#show-src-folder-idx-url').empty() - .append('') - } + .append('') - /** Configure the template dropdown & setup button handlers (called from onEditPrepare) - * @param {object} node A reference to the panel's `this` object - */ - function templateSettings(node) { - - $('#adv-templ').hide() - $('#show-templ-props').css( 'cursor', 'pointer' ) - $('#show-templ-props').on('click', function() { // (e) { - $('#adv-templ').toggle() - if ( $('#adv-templ').is(':visible') ) { - $('#show-templ-props').html(' Template Settings') - } else { - $('#show-templ-props').html(' Template Settings') - } - }) - // Populate the template selection drop-down and select default (in advanced) - populateTemplateDropdown(node) - checkDependencies() - // Unhide the external template name input if external selected - if ( $('#node-input-templateFolder').val() === 'external' ) $('#et-input').show() - // Handle change of template - $('#node-input-templateFolder').on('change', function() { // (e) { - // update the help tip - if ( RED.settings.uibuilderTemplates[node.templateFolder] ) - $('#node-templSel-info').text(RED.settings.uibuilderTemplates[node.templateFolder].description) - // Check if the dependencies are installed, warn if not - checkDependencies() - // Unhide the external template name input if external selected - if ( $('#node-input-templateFolder').val() === 'external' ) - $('#et-input').show() - else - $('#et-input').hide() - }) - // Button press for loading new template - $('#btn-load-template').on('click', function(e){ - e.preventDefault() // don't trigger normal click event - btnTemplate() - }) - - } + } // ---- end of urlChange ---- // /** Setup for security settings (called from onEditPrepare) */ function securitySettings() { @@ -1052,12 +1234,97 @@ } // ---- end of securitySettings ---- // + /** Run when switching to the Files tab + * @param {object} node A reference to the panel's `this` object + */ + function tabFiles(node) { + // Build the file list + getFileList() + + // We only need to do all of this once + if ( uiace.editorLoaded !== true ) { + // @ts-expect-error ts(2352) Clear out the editor + if ( /** @type {string} */ ($('#node-input-template').val('')) !== '') $('#node-input-template').val('') + + // Create the ACE editor component + uiace.editor = RED.editor.createEditor({ + id: 'node-input-template-editor', + mode: 'ace/mode/' + uiace.format, + value: node.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) + } + } // ---- End of tabFiles() ---- // + + /** Return the correct height of the libraries list + * @returns {number} Calculated height of the libraries list + */ + function getLibrariesListHeight() { + return ($('.red-ui-tray-footer').position()).top - ($('#package-list-container').offset()).top + 25 + } // ---- End of getLibrariesListHeight() ---- // + + /** Run when switching to the Libraries tab + * @param {object} node A reference to the panel's `this` object + */ + function tabLibraries(node) { + + //! TODO Improve feedback + + // Setup the package list https://nodered.org/docs/api/ui/editableList/ + $('#node-input-packageList').editableList({ + addItem: function addItem(element,index,data) { + addPackageRow(node, element,index, data) + }, + removeItem: removePackageRow, // function(data){}, + //resizeItem: function() {}, // function(_row,_index) {}, + header: $('
').append('Installed Packages'), + height: getLibrariesListHeight(), + addButton: true, + removable: true, + scrollOnAdd: true, + sortable: false, + }) + + // reset and populate the list + $('#node-input-packageList').editableList('empty') + // @ts-ignore + $('#node-input-packageList').editableList('addItems', Object.keys(packages)) + + // spinner + $('.red-ui-editableList-addButton').after(' ') + $('i.spinner').hide() + + } // ---- End of tabLibraries() ---- // + /** Prep tabs * @param {object} node A reference to the panel's `this` object */ function prepTabs(node) { const tabs = RED.tabs.create({ id: 'tabs', + scrollable: false, + collapsible: false, onchange: function(tab) { // Show only the content (i.e. the children) of the selected tabsheet, and hide the others $('#tabs-content').children().hide() // eslint-disable-line newline-per-chained-call @@ -1065,76 +1332,24 @@ //? Could move these to their own show event. Might even unload some stuff on hide? - if ( tab.id === 'tab-files' ) { - // Build the file list - getFileList() - - if ( uiace.editorLoaded !== true ) { - // @ts-expect-error ts(2352) Clear out the editor - if ( /** @type {string} */ ($('#node-input-template').val('')) !== '') $('#node-input-template').val('') - - // Create the ACE editor component - uiace.editor = RED.editor.createEditor({ - id: 'node-input-template-editor', - mode: 'ace/mode/' + uiace.format, - value: node.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) + switch (tab.id) { + case 'tab-files': { + tabFiles(node) + break + } + + case 'tab-libraries': { + tabLibraries(node) + break + } + + default: { + break } - } else if ( tab.id === 'tab-libraries' ) { - - //! TODO Improve feedback - - // Setup the package list - $('#node-input-packageList').editableList({ - addItem: function addItem(element,index,data) { - addPackageRow(node, element,index, data) - }, - removeItem: removePackageRow, // function(data){}, - resizeItem: function() {}, // function(_row,_index) {}, - header: $('
').append('Installed Packages'), - height: 'auto', - addButton: true, - removable: true, - scrollOnAdd: true, - sortable: false, - }) - - // reset and populate the list - $('#node-input-packageList').editableList('empty') - packages.forEach( packageName => { - if ( packageName !== 'socket.io' ) // ignore socket.io - $('#node-input-packageList').editableList('addItem',packageName) - }) - - // spinner - $('.red-ui-editableList-addButton').after(' ') - $('i.spinner').hide() - } - } + }, }) + tabs.addTab({ id: 'tab-core', label: 'Core' }) tabs.addTab({ id: 'tab-files', label: 'Files' }) tabs.addTab({ id: 'tab-libraries', label: 'Libraries' }) @@ -1148,6 +1363,16 @@ */ function onEditPrepare(node) { + // RED.events.on('deploy', function() { + // console.log('Deployed') + // }) + // RED.events.on('workspace:dirty', function(data) { + // console.log('Workspace dirty:', data) + // }) + // RED.events.on('nodes:change', function(data) { + // console.log('nodes:change:', data) + // }) + packageList(node.url) // Bug fix for messed up recording of template up to uib v3.3, fixed in v4 @@ -1166,7 +1391,9 @@ * 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').on('change', urlChange) + $('#node-input-url').on('change', function() { + urlChange(node, this) + }) // When the show web view (index) of source files changes $('#node-input-showfolder').on('change', function() { @@ -1180,6 +1407,7 @@ // security settings securitySettings() + // TODO Move to separate function //#region ---- File Editor ---- // // Mark edit save/reset buttons as disabled by default @@ -1487,15 +1715,6 @@ //#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() - // Register the node type, defaults and set up the edit fns RED.nodes.registerType(moduleName, { //#region --- options --- // @@ -1504,7 +1723,7 @@ defaults: { name: { value: '' }, topic: { value: '' }, - url: { value: moduleName, required: true, validate: validateUrl }, + url: { 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? @@ -1520,6 +1739,7 @@ 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 sourceFolder: { value: 'src', required: true, }, // Which folder to use for front-end code? (src or dist) + deployedVersion: { validate: validateVersion }, //jwtSecret: { value: defaultJwtSecret, validate: validateSecret }, // Must have content if useSecurity=true }, credentials: { @@ -1547,7 +1767,7 @@ /** Prepares the Editor panel */ oneditprepare: function() { onEditPrepare(this) }, - /** Runs before save + /** Runs before save (Actually before Done button pressed) * @this {RED} */ oneditsave: function() { @@ -1567,6 +1787,10 @@ this.oldUrl = undefined } + // If something has changed or if something must change, update the deployed version + if ( this.changed || mustChange ) + this.deployedVersion = RED.settings.uibuilderCurrentVersion + }, // ---- End of oneditsave ---- // /** Runs before cancel */ @@ -1584,6 +1808,12 @@ setACEheight() + // Set correct height of the libraries list + try { + $('#node-input-packageList').editableList('height', getLibrariesListHeight()) + } catch (e) {} + + }, // ---- End of oneditcancel ---- // /** Show notification warning before allowing delete */ diff --git a/src/editor/uibuilder/main.html b/src/editor/uibuilder/main.html index d9b176c5..07c5d560 100644 --- a/src/editor/uibuilder/main.html +++ b/src/editor/uibuilder/main.html @@ -14,10 +14,6 @@ limitations under the License. --> - - @@ -25,3 +21,8 @@ + + + \ No newline at end of file diff --git a/src/editor/uibuilder/panel.html b/src/editor/uibuilder/panel.html index 10066fb2..509d66ad 100644 --- a/src/editor/uibuilder/panel.html +++ b/src/editor/uibuilder/panel.html @@ -1,9 +1,9 @@
    -
    +
    -
    +
    - +
    @@ -166,19 +166,16 @@

    Template Settings

    - Front-End Library Manager

    Install, remove or update npm packages that provide front-end libraries such as VueJS, jQuery, MoonJS, etc. +
    + Search for packages on official npm site + or npms.io.

    -

    - Search for packages: official npm site - or npms.io.
    - Copy the name, click on "+ add" below and paste in the input. -

    - -
    +
    +
      diff --git a/typedefs.js b/typedefs.js index 15d89eab..f9deac80 100644 --- a/typedefs.js +++ b/typedefs.js @@ -207,6 +207,7 @@ * @property {string} ioChannels.server SIO Server channel name 'uiBuilder' * @property {string} ioNamespace Make sure each node instance uses a separate Socket.IO namespace * @property {boolean} allowUnauth Allow unauthorised messaging + * @property {string} deployedVersion The version of uibuilder when this node was last deployed */ /** uibNode @@ -239,6 +240,7 @@ * @property {string} ioChannels.client SIO Client channel name 'uiBuilderClient' * @property {string} ioChannels.server SIO Server channel name 'uiBuilder' * @property {string} ioNamespace Make sure each node instance uses a separate Socket.IO namespace + * @property {string} deployedVersion The version of uibuilder when this node was last deployed * * @property {Function} send Send a Node-RED msg to an output port * @property {Function=} done Dummy done Function for pre-Node-RED 1.0 servers @@ -283,19 +285,6 @@ * @property {string} packageListFilename File name of the installed package list. * * Default 'packageList.json' - * @property {Object} installedPackages Track the vendor packages installed and their paths - - * updated by `uiblib.checkInstalledPackages()` Populated initially from packageList file once the configFolder - * is known & master list has been copied. - * - * Schema: - * ```json - * {"": { - * "": "", - * "": "", - * "": "", - * "
      ": "" - * } } - * ``` * @property {string} masterTemplateFolder Location of master template folders (containing default front-end code). * * Default `../templates` @@ -340,6 +329,8 @@ * @property {undefined|object} degitEmitter Event emitter for degit, populated on 1st use. See POST admin API * @property {undefined|runtimeRED} RED Keep a reference to RED for convenience. Set at the start of Uib * @property {string=} version The deployed version of uibuilder (from `package.json`) + * @property {string=} httpRoot Copy of RED.settings.httpRoot for ease of use + * @property {string=} reDeployNeeded If the last deployed version is this version or earlier and the current version is greater than this, tell the Editor that a redeploy is needed */ /** senderNode1