From 4840b5c0954a23ab24df47a36931bf2c79349c37 Mon Sep 17 00:00:00 2001 From: Julian Knight Date: Tue, 18 Apr 2017 16:53:53 +0100 Subject: [PATCH] Initial Commit --- .eslintrc.json | 28 ++++ .gitignore | 85 +++++++++++ CHANGELOG.md | 1 + CONTRIBUTING.md | 67 ++++++++ LICENSE | 178 ++++++++++++++++++++++ README.md | 76 ++++++++++ license.js | 15 ++ nodes/icons/ui_template.png | Bin 0 -> 413 bytes nodes/src/images/logo-red.png | Bin 0 -> 4324 bytes nodes/src/images/logo.png | Bin 0 -> 4178 bytes nodes/src/index.css | 0 nodes/src/index.html | 46 ++++++ nodes/src/index.js | 99 ++++++++++++ nodes/src/manifest.json | 8 + nodes/uibuilder.html | 148 ++++++++++++++++++ nodes/uibuilder.js | 277 ++++++++++++++++++++++++++++++++++ package.json | 39 +++++ 17 files changed, 1067 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 license.js create mode 100644 nodes/icons/ui_template.png create mode 100644 nodes/src/images/logo-red.png create mode 100644 nodes/src/images/logo.png create mode 100644 nodes/src/index.css create mode 100644 nodes/src/index.html create mode 100644 nodes/src/index.js create mode 100644 nodes/src/manifest.json create mode 100644 nodes/uibuilder.html create mode 100644 nodes/uibuilder.js create mode 100644 package.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..1e2f6a66 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "env": { + "node": true, + "browser": false, + "es6": true + }, + "extends": "eslint:recommended", + "rules": { + "no-console": 0, + "indent": [ + "warn", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "warn", + "single" + ], + "semi": [ + "warn", + "never" + ], + "no-unused-vars": "warn" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..290c8014 --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..27567f64 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +#### 0.0.1 - not a release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0b1df095 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to Node-RED-Dashboard + +We welcome contributions, but request you follow these guidelines. + + - [Raising issues](#raising-issues) + - [Feature requests](#feature-requests) + - [Pull-Requests](#pull-requests) + - [Contributor License Agreement](#contributor-license-agreement) + +This project adheres to the [Contributor Covenant 1.4](http://contributor-covenant.org/version/1/4/). +By participating, you are expected to uphold this code. Please report unacceptable +behaviour to any of the [project's core team](https://github.com/orgs/node-red/teams/core). + +## Raising issues + +Please raise any bug reports on the relevant project's issue tracker. Be sure to +search the list to see if your issue has already been raised. + +A good bug report is one that make it easy for us to understand what you were +trying to do and what went wrong. + +Provide as much context as possible so we can try to recreate the issue. +If possible, include the relevant part of your flow. To do this, select the +relevant nodes, press Ctrl-E and copy the flow data from the Export dialog. + +At a minimum, please include: + +- Version of Node-RED-Dashboard - either release number if you downloaded a zip, or the first few lines of `git log` if you are cloning the repository directly. + - Version of Node-RED - either release number if you downloaded a zip, or the first few lines of `git log` if you are cloning the repository directly. + - Version of node.js - what does `node -v` say? + +## Feature requests + +For feature requests, please raise them on the [mailing list](https://groups.google.com/forum/#!forum/node-red). + +## Pull-Requests + +If you want to raise a pull-request with a new feature, or a refactoring +of existing code, it may well get rejected if you haven't discussed it on +the [mailing list](https://groups.google.com/forum/#!forum/node-red) first. + +### Contributor License Agreement + +In order for us to accept pull-requests, the contributor must first complete +a Contributor License Agreement (CLA). This clarifies the intellectual +property license granted with any contribution. It is for your protection as a +Contributor as well as the protection of IBM and its customers; it does not +change your rights to use your own Contributions for any other purpose. + +You can download the CLAs here: + + - [individual](http://nodered.org/cla/node-red-cla-individual.pdf) + - [corporate](http://nodered.org/cla/node-red-cla-corporate.pdf) + +If you are an IBMer, please contact us directly as the contribution process is +slightly different. + +### Coding standards + +Please ensure you follow the coding standards used through-out the existing +code base. Some basic rules include: + + - all files must have the Apache license in the header. + - indent with 4-spaces, no tabs. No arguments. + - opening brace on same line as `if`/`for`/`function` and so on, closing brace + on its own line. + - There are .jshintrc and .jscsrc files included in the project which may help diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a16c46af --- /dev/null +++ b/LICENSE @@ -0,0 +1,178 @@ + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 00000000..1f8206b5 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# node-red-contrib-uibuilder + +A Node-RED UI web user interface builder. + +Designed as an *experimental* alternative to the Node-RED Dashboard. Be warned that this project is +currently very much **alpha** quality. It should pretty much work but only in a limited way. + +## Design + +- A single node is used to define an end-point (by its URL path) +- The node can be included as many times as you like - but each **must** have a unique URL path name +- Each node instance will have its own, dedicated Socket.IO connection (using Socket.IO namespacing) +- The node's module contains default html, JavaScript and CSS files that are used as master templates +- On deployment of the first instance, a new folder is created within your Node-RED user directory + (typically `~/.node-red`) with a fixed name of `uibuilder` +- On deployment of any new instance, a new sub-folder within `uibuilder` is created. The name is the same as the + URL path specified in the node instance's settings. (defaults to `uibuilder`) +- Any resource (html, css, js, image, etc) placed within this sub-folder is available to the browser + client. The default URL would be `http://localhost:1880/uibuilder` (where the path is set as per the point above) +- Any resource in this sub-folder that has the same name as resources in other resource paths will be + given preference - see *Preference Tree* below. +- Each node instance gets its own Socket.IO namespace matching the URL path setting. Note that Socket.IO + will efficiently share sockets while keeping traffic separated by namespace +- Any msg sent to a node instance is sent through unchanged to the UI via Socket.IO. NOTE that this may present + security and/or performance issues. In particular, you should remove msg.res and msg.req objects as they + are both very large and often contain circular references + +## Preference Tree + +This node adds a number of resource locations (physical file-system locations) to the URL path defined. +The order of preference is as follows: + +1. The node instance URL setting (default: `uibuilder`, default physical location: `~/.node-red\uibuilder\uibuilder`) +2. The node installations `dist` folder (default physical location: `~/.node-red\node_modules\node-red-contrib-uibuilder\nodes\dist`) + *only added if index.html exists in this folder* +3. The node installations `src` folder (default physical location: `~/.node-red\node_modules\node-red-contrib-uibuilder\nodes\src`) + *only added if index.html DOES NOT exist in the dist folder* + +In addition, this node uses the httpNodeMiddleware Node-RED setting allowing for ExpressJS middleware to be used. +For example, for implementing user security. + +## To Do + +- Add ability to create resources from the Node-RED admin UI - currently all resources have to be created in + the file system + +## Pre-requisites + +See the package.json file. Currently Socket.IO, normalize.css, serve.static and webpack are installed along +with the node. + +## Install + +Run the following command in your Node-RED user directory (typically `~/.node-red`): + +``` +npm install node-red-contrib-uibuilder +``` + +Run Node-RED and add an instance of the UI Builder node. Set the required URL path and deploy. + +The UI should then be available at the chosen path. The default would normally be +(if default Node-RED and node settings are used). + +## Discussions and suggestions + +Use the [Node-RED google group](https://groups.google.com/forum/#!forum/node-red) for general discussion about this node. Or use the +[GitHub issues log](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues) for raising issues or contributing suggestions and enhancements. + +## Contributing + +If you would like to contribute to this node, you can contact Totally Information via GitHub or raise a request in the GitHub issues log. + +## Developers/Contributors + +- [Julian Knight](https://github.com/TotallyInformation) \ No newline at end of file diff --git a/license.js b/license.js new file mode 100644 index 00000000..c9637bfa --- /dev/null +++ b/license.js @@ -0,0 +1,15 @@ +/* */ +/* Copyright (c) 2015, 2016 IBM Corp */ +/* */ +/* 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.*/ +/* */ diff --git a/nodes/icons/ui_template.png b/nodes/icons/ui_template.png new file mode 100644 index 0000000000000000000000000000000000000000..0e5ff114e640730d736cc48e8070d370eabf4984 GIT binary patch literal 413 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 literal 0 HcmV?d00001 diff --git a/nodes/src/index.css b/nodes/src/index.css new file mode 100644 index 00000000..e69de29b diff --git a/nodes/src/index.html b/nodes/src/index.html new file mode 100644 index 00000000..d2eebea1 --- /dev/null +++ b/nodes/src/index.html @@ -0,0 +1,46 @@ + + + + + + + Node-RED UI Builder + + + + + + + + + + + + + + + + + + + + + + + + +
+ ... waiting for content ... +
+ + + + + + \ No newline at end of file diff --git a/nodes/src/index.js b/nodes/src/index.js new file mode 100644 index 00000000..58acb07d --- /dev/null +++ b/nodes/src/index.js @@ -0,0 +1,99 @@ +/*global document */ +/* + Copyright (c) 2017 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. +*/ + +var debug = true, + ioChannels = {control: 'uiBuilderControl', client: 'uiBuilderClient', server: 'uiBuilder'}, + msgCounter = {control: 0, sent: 0, data: 0}, + msg = {}, + cookies = [], + ioNamespace = '/'+readCookie('uibuilder-namespace') + +console.log('ioNameSpace: '+ioNamespace) + +// Create the socket +var io = io(ioNamespace, {transports: ['polling', 'websocket']}) + +// send a msg back to Node-RED +// NR will generally expect the msg to contain a payload topic +// TODO: Needs a restrictor on it so it doesn't trigger on every keypress +var sendMsg = function(msg) { + // Track how many messages have been sent + msgCounter.sent++ + + io.emit(ioChannels.client, msg) +} + +// When the socket is connected ................. +io.on('connect', function() { + debug && console.log('SOCKET CONNECTED - Namespace: ' + ioNamespace) + + // When Node-RED vueui template node sends a msg over Socket.IO... + io.on(ioChannels.server, function(wsMsg) { + debug && console.info('uibuilder:io.connect:io.on.data - msg received - Namespace: ' + ioNamespace) + //console.dir(wsMsg) + + // Only process if the msg actually contains something useful + // TODO: Check whether msg is an object + if ( (wsMsg !== null) && (wsMsg !== '') ) { + if ( Object.getOwnPropertyNames(wsMsg).length > 0 ) { + // Track how many messages have been recieved + msgCounter.data++ + } + } + }) // -- End of websocket recieve DATA msg from Node-RED -- // + + // Recieve a CONTROL msg from Node-RED + io.on(ioChannels.control, function(wsMsg) { + debug && console.info('uibuilder:io.connect:io.on.control - msg received - Namespace: ' + ioNamespace) + //console.dir(wsMsg) + + // TODO: Check msg is an object + + switch(wsMsg.type) { + case 'shutdown': + // We are shutting down + break + default: + // ??? + } + }) // -- End of websocket recieve CONTROL msg from Node-RED -- // + +}) // --- End of socket connection processing --- + +// When the socket is disconnected .............. +io.on('disconnect', function() { + debug && console.log('SOCKET DISCONNECTED - Namespace: ' + ioNamespace) +}) // --- End of socket disconnect processing --- + +// ----- UTILITY FUNCTIONS ----- // +function readCookie(name,c,C,i){ + // @see http://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript + if(cookies.length > 0){ return cookies[name]; } + + c = document.cookie.split('; '); + cookies = {}; + + for(i=c.length-1; i>=0; i--){ + C = c[i].split('='); + cookies[C[0]] = C[1]; + } + + return cookies[name]; +} +// ----------------------------- // + +// EOF diff --git a/nodes/src/manifest.json b/nodes/src/manifest.json new file mode 100644 index 00000000..3d80f8de --- /dev/null +++ b/nodes/src/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "Node-RED VueUI", + "short_name": "VueUI", + "description": "A web template builder for Node-RED using VueJS", + "display": "standalone", + "lang": "en-GB", + "start_url": "." +} \ No newline at end of file diff --git a/nodes/uibuilder.html b/nodes/uibuilder.html new file mode 100644 index 00000000..0650bdfe --- /dev/null +++ b/nodes/uibuilder.html @@ -0,0 +1,148 @@ + + + + + + + diff --git a/nodes/uibuilder.js b/nodes/uibuilder.js new file mode 100644 index 00000000..7c5f44f8 --- /dev/null +++ b/nodes/uibuilder.js @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2017 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' + +// Module name must match this nodes html file +var moduleName = 'uibuilder' + +var debug = true + +//var inited = false; +var settings = {} + +var serveStatic = require('serve-static'), + socketio = require('socket.io'), + path = require('path'), + fs = require('fs'), + events = require('events'), + nodeVersion = require('../package.json').version + ; + +var io + +// Why? +var ev = new events.EventEmitter(); +ev.setMaxListeners(0); + +module.exports = function(RED) { + 'use strict'; + + function nodeGo(config) { + // Create the node + RED.nodes.createNode(this, config) + + // copy 'this' object in case we need it in context of callbacks of other functions. + var node = this + + // Create local copies of the node configuration (as defined in the .html file) + node.name = config.name || '' + // TODO: This needs to be limited to a single path element + node.url = config.url || 'uibuilder' + node.fwdInMessages = config.fwdInMessages || true + node.customFoldersReqd = config.customFoldersReqd || true + // NOTE: When a node is redeployed - e.g. after the template is changed + // it is totally torn down and rebuilt so we cannot ever know + // whether the template was changed. + node.template = config.template || '

{{ msg }}

'; + + // The channel names for Socket.IO + var ioChannels = {control: 'uiBuilderControl', client: 'uiBuilderClient', server: 'uiBuilder'} + + // Name of the fs path used to hold custom files & folders for all instances of uibuilder + var customAppFolder = path.join(RED.settings.userDir, 'uibuilder') + // Name of the fs path used to hold custom files & folders for THIS INSTANCE of uibuilder + // Files in this folder are also served to URL but take preference + // over those in the nodes folders (which act as defaults) + var customFolder = path.join(customAppFolder, node.url) + + var ioClientsCount = 0 + + // We need an http server to serve the page + var app = RED.httpNode || RED.httpAdmin + + // Use httNodeMiddleware function which is defined in settings.js + // as for the http in/out nodes + var httpMiddleware = function(req,res,next) { next() } + if (RED.settings.httpNodeMiddleware) { + if ( typeof RED.settings.httpNodeMiddleware === 'function' ) { + httpMiddleware = RED.settings.httpNodeMiddleware + } + } + + // This ExpressJS middleware runs when the vueui page loads - we'll use it at some point + // maybe to pass a "room" name in custom header for IO to use + // so that we can have multiple pages served + // @see https://expressjs.com/en/guide/using-middleware.html + function localMiddleware (req, res, next) { + // Tell the client what namespace to use + res.setHeader('x-uibuilder-namespace', node.url) + res.cookie('uibuilder-namespace', node.url, {path: node.url, sameSite: true}) + next() + } + + // ---- Add custom folder structure if requested ---- // + if ( node.customFoldersReqd ) { + // NOTE: May be better as async calls? + // Make sure the global custom folder exists first + try { + fs.mkdirSync(customAppFolder) + fs.accessSync( customAppFolder, fs.constants.W_OK ) + } catch (e) { + if ( e.code !== 'EEXIST' ) { + debug && RED.log.error('UIBUILDER - uibuilder custom folder ERROR, path: ' + path.join(RED.settings.userDir, customAppFolder) + ', error: ' + e.message) + } + } + // Then make sure the folder for this node instance exists + try { + fs.mkdirSync(customFolder) + fs.accessSync(customFolder, fs.constants.W_OK) + } catch (e) { + if ( e.code !== 'EEXIST' ) { + debug && RED.log.error('UIBUILDER - uibuilder local custom folder ERROR: ' + e.message) + } + } + // Add static path for local custom files + app.use( urlJoin(node.url), serveStatic(customFolder) ) + } + // -------------------------------------------------- // + + // Create a new, additional static http path to enable + // loading of central static resources for uibuilder + fs.stat(path.join(__dirname, 'dist', 'index.html'), function(err, stat) { + if (!err) { + // If the ./dist/index.html exists use the dist folder... + app.use( urlJoin(node.url), httpMiddleware, localMiddleware, serveStatic( path.join( __dirname, 'dist' ) ) ); + } else { + // ... otherwise, use dev resources at ./src/ + debug && RED.log.audit({ 'UIbuilder': node.url+' Using development folder' }); + app.use( urlJoin(node.url), httpMiddleware, localMiddleware, serveStatic( path.join( __dirname, 'src' ) ) ); + // Include vendor resource source paths if needed + var vendor_packages = [ + 'normalize.css', + //'sprintf-js', + //'jquery', 'jquery-ui' + ] + vendor_packages.forEach(function (packageName) { + //debug && RED.log.audit({ 'UIbuilder': 'Adding vendor paths', 'url': join(node.url, 'vendor', packageName), 'path': path.join(__dirname, 'node_modules', packageName)}); + app.use( urlJoin(node.url, 'vendor', packageName), serveStatic(path.join(__dirname, '..', 'node_modules', packageName)) ); + }) + // TODO: Add ~/.node-red (userDir?) level vendor packages too. + } + }) + + var fullPath = urlJoin( RED.settings.httpNodeRoot, node.url ); + if ( node.customFoldersReqd ) { + RED.log.info('UI Builder - Version ' + nodeVersion + ' started at ' + fullPath) + RED.log.info('UI Builder - Local file overrides at ' + customFolder) + } else { + RED.log.info('UI Builder - Version ' + nodeVersion + ' started at ' + fullPath) + RED.log.info('UI Builder - Local file overrides not requested') + } + + // Start Socket.IO with a namespace to match the url path + if (!io) { + io = socketio.listen(RED.server) // listen === attach + io.set('transports', ['polling', 'websocket']) + ioClientsCount = 0 + node.status({ fill: 'blue', shape: 'dot', text: 'Socket Created' }) + } + // Check that all incoming SocketIO data has the IO cookie + // TODO: Needs a bit more work to add some real security + io.use(function(socket, next){ + if (socket.request.headers.cookie) return next() + next(new Error('UIbuilder:NodeGo:io.use - Authentication error')) + }); + + // When someone loads the page, it will try to connect over Socket.IO + // note that the connection returns the socket instance to monitor for responses from + // the ui client instance + io.of(node.url).on('connection', function(socket) { + ioClientsCount++ + debug && RED.log.audit({ + 'UIbuilder': node.url+' Socket connected', 'clientCount': ioClientsCount, 'ID': socket.id, 'Cookie': socket.handshake.headers.cookie + }) + node.status({ fill: 'green', shape: 'dot', text: 'connected '+ioClientsCount }) + //console.log('--socket.request.connection.remoteAddress--') + //console.dir(socket.request.connection.remoteAddress) + //console.log('--socket.handshake.address--') + //console.dir(socket.handshake.address) + //console.dir(io.sockets.connected) + + //io.of('/'+node.url).emit( ioChannels.control, { 'type': 'initial-connection'} ) + + // if the client sends a specific msg channel... + socket.on(ioChannels.client, function(msg) { + debug && RED.log.audit({ + 'UIbuilder': node.url+' Data recieved from client', 'ID': socket.id, 'Cookie': socket.handshake.headers.cookie, 'data': msg + }) + + // Send out the message for downstream flows + // TODO: This should probably have safety validations! + node.send(msg) + }); + + socket.on('disconnect', function(reason) { + ioClientsCount-- + debug && RED.log.audit({ + 'UIbuilder': node.url+' Socket disconnected', 'clientCount': ioClientsCount, + 'reason': reason, 'ID': socket.id, 'Cookie': socket.handshake.headers.cookie + }) + node.status({ fill: 'green', shape: 'ring', text: 'connected ' + ioClientsCount }) + }) + }) + + // handler function for node input events (when a node instance receives a msg) + function nodeInputHandler(msg) { + debug && RED.log.info('UIbuilder:nodeGo:nodeInputHandler - emit received msg - Namespace: ' + node.url) //debug + + // pass the complete msg object to the vue ui client + // TODO: This should probably have some safety validation on it + io.of(node.url).emit(ioChannels.server, msg) + + } // -- end of msg recieved processing -- // + node.on('input', nodeInputHandler) + + // Do something when Node-RED is closing down + // which includes when this node instance is redeployed + node.on('close', function() { + //debug && RED.log.info('VUEUI:nodeGo:on-close') //debug + + // Let the clients know we are closing down + io.of(node.url).emit( ioChannels.control, { 'type': 'shutdown' } ) + + node.status({}) + node.removeListener('input', nodeInputHandler) + + // Disconnect all Socket.IO clients + // WARNING: TODO: If we do this, a client cannot reconnect after redeployment + // so the user has to reload the page + // They have to do this at the moment anyway so might as well. + const MyNamespace = io.of(node.url); // Get Namespace + const connectedNameSpaceSockets = Object.keys(MyNamespace.connected); // Get Object with Connected SocketIds as properties + connectedNameSpaceSockets.forEach(socketId => { + MyNamespace.connected[socketId].disconnect(); // Disconnect Each socket + }); + MyNamespace.removeAllListeners(); // Remove all Listeners for the event emitter + delete io.nsps[node.url]; // Remove from the server namespaces + + /* + //console.dir(io.of('/'+node.url).connected) + Object.keys(io.of(node.url).connected).forEach(function(id){ + console.log(id) // /someotherurl#7wHCCbdBTAFLyTOSAAAA + io.sockets.connected[id].disconnect(true) + }) + */ + io = null + + // TODO Do we need to remove the app.use paths too? YES! + // This code borrowed from the http nodes + app._router.stack.forEach(function(route,i,routes) { + if ( route.route && route.route.path === node.url ) { + routes.splice(i,1) + } + }); + + }) + + } // ---- End of nodeGo (initialised node instance) ---- // + + // Register the node by name. This must be called before overriding any of the + // Node functions. + RED.nodes.registerType(moduleName, nodeGo); +} + +// ========== UTILITY FUNCTIONS ================ // + +//from: http://stackoverflow.com/a/28592528/3016654 +function urlJoin() { + var trimRegex = new RegExp('^\\/|\\/$','g'); + var paths = Array.prototype.slice.call(arguments); + return '/'+paths.map(function(e){return e.replace(trimRegex,'');}).filter(function(e){return e;}).join('/'); +} + +// EOF diff --git a/package.json b/package.json new file mode 100644 index 00000000..20d2cf51 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "node-red-contrib-uibuilder", + "version": "0.0.1", + "description": "A node for Node-RED to create web UI's. - ALPHA only, not ready for use.", + "keywords": [ + "node-red", "ui", "gui" , "dashboard" + ], + "main": "none", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "homepage": "https://github.com/TotallyInformation/node-red-contrib-uibuilder", + "bugs": "https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues", + "author": {"name": "Julian Knight", "url": "https://github.com/TotallyInformation"}, + "contributors": [ + {"name": "Julian Knight", "url": "https://github.com/TotallyInformation"} + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/TotallyInformation/node-red-contrib-uibuilder.git" + }, + "node-red": { + "version": ">=0.16.0", + "nodes": { + "vueui_template": "nodes/uibuilder.js" + }, + "notyetnodes": {} + }, + "dependencies": { + "normalize.css": "*", + "serve-static": "^1.12.1", + "socket.io": "^1.7.3", + "webpack": "^2.4.1" + }, + "devDependencies": { + }, + "engines" : { "node" : ">=6" } +}