From d9c728c5c57374f9cb36d67089ab8c8b68c38c05 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Sat, 15 Jun 2024 19:52:22 +0100 Subject: [PATCH 1/6] Widget: File Upload --- nodes/widgets/ui_file_input.html | 125 ++++++++++++++++ nodes/widgets/ui_file_input.js | 29 ++++ package.json | 1 + ui/src/stylesheets/common.css | 14 +- ui/src/widgets/index.mjs | 3 + ui/src/widgets/ui-file-input/UIFileInput.vue | 142 +++++++++++++++++++ 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 nodes/widgets/ui_file_input.html create mode 100644 nodes/widgets/ui_file_input.js create mode 100644 ui/src/widgets/ui-file-input/UIFileInput.vue diff --git a/nodes/widgets/ui_file_input.html b/nodes/widgets/ui_file_input.html new file mode 100644 index 000000000..c646d87a4 --- /dev/null +++ b/nodes/widgets/ui_file_input.html @@ -0,0 +1,125 @@ + + + diff --git a/nodes/widgets/ui_file_input.js b/nodes/widgets/ui_file_input.js new file mode 100644 index 000000000..194be0591 --- /dev/null +++ b/nodes/widgets/ui_file_input.js @@ -0,0 +1,29 @@ +// const datastore = require('../store/data.js') + +module.exports = function (RED) { + function FileInputNode (config) { + const node = this + + // create node in Node-RED + RED.nodes.createNode(this, config) + + // this ndoe need to store content/value from UI + node.value = null + + // which group are we rendering this widget + const group = RED.nodes.getNode(config.group) + + const evts = { + onAction: true + } + + // inform the dashboard UI that we are adding this node + group.register(node, config, evts) + + node.on('close', async function (done) { + done() + }) + } + + RED.nodes.registerType('ui-file-input', FileInputNode) +} diff --git a/package.json b/package.json index 10635dcf8..9dc492f68 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "ui-theme": "nodes/config/ui_theme.js", "ui-form": "nodes/widgets/ui_form.js", "ui-text-input": "nodes/widgets/ui_text_input.js", + "ui-file-input": "nodes/widgets/ui_file_input.js", "ui-button": "nodes/widgets/ui_button.js", "ui-button-group": "nodes/widgets/ui_button_group.js", "ui-dropdown": "nodes/widgets/ui_dropdown.js", diff --git a/ui/src/stylesheets/common.css b/ui/src/stylesheets/common.css index 7883f47bc..ee0e20382 100644 --- a/ui/src/stylesheets/common.css +++ b/ui/src/stylesheets/common.css @@ -13,7 +13,7 @@ /* main */ --nrdb-main-padding: 12px; /* widget sizing */ - --widget-row-height: 42px; + --widget-row-height: 48px; } body { @@ -60,6 +60,18 @@ main { fill: #bbb; } +/** +* Anchor +*/ + +.nrdb-anchor { + cursor: pointer; + color: rgb(var(--v-theme-primary)); +} +.nrdb-anchor:hover { + color: rgb(var(--v-theme-primary-darken-1)); +} + /** * Placeholder */ diff --git a/ui/src/widgets/index.mjs b/ui/src/widgets/index.mjs index 238a988e8..853c7c332 100644 --- a/ui/src/widgets/index.mjs +++ b/ui/src/widgets/index.mjs @@ -4,6 +4,7 @@ import UIChart from './ui-chart/UIChart.vue' import UIControl from './ui-control/UIControl.vue' import UIDropdown from './ui-dropdown/UIDropdown.vue' import UIEvent from './ui-event/UIEvent.vue' +import UIFileInput from './ui-file-input/UIFileInput.vue' import UIForm from './ui-form/UIForm.vue' import UIGauge from './ui-gauge/UIGauge.vue' import UIMarkdown from './ui-markdown/UIMarkdown.vue' @@ -24,6 +25,7 @@ export { UIControl, UIDropdown, UIEvent, + UIFileInput, UIForm, UIGauge, UIMarkdown, @@ -48,6 +50,7 @@ export default { 'ui-control': UIControl, 'ui-dropdown': UIDropdown, 'ui-event': UIEvent, + 'ui-file-input': UIFileInput, 'ui-form': UIForm, 'ui-gauge': UIGauge, 'ui-markdown': UIMarkdown, diff --git a/ui/src/widgets/ui-file-input/UIFileInput.vue b/ui/src/widgets/ui-file-input/UIFileInput.vue new file mode 100644 index 000000000..54b8141a1 --- /dev/null +++ b/ui/src/widgets/ui-file-input/UIFileInput.vue @@ -0,0 +1,142 @@ + + + + + From 726a8c3b71ef8655055afd21695b688c7e55f559 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Sat, 15 Jun 2024 20:01:06 +0100 Subject: [PATCH 2/6] Remove "Show File Size" option as it's just going to cause height problems --- nodes/widgets/ui_file_input.html | 5 ----- ui/src/widgets/ui-file-input/UIFileInput.vue | 1 - 2 files changed, 6 deletions(-) diff --git a/nodes/widgets/ui_file_input.html b/nodes/widgets/ui_file_input.html index c646d87a4..8fe5763be 100644 --- a/nodes/widgets/ui_file_input.html +++ b/nodes/widgets/ui_file_input.html @@ -26,7 +26,6 @@ topicType: { value: 'msg' }, label: { value: 'File Input' }, icon: { value: 'paperclip' }, - showFileSize: { value: false }, allowMultiple: { value: false }, accept: { value: '' }, className: { value: '' } @@ -100,10 +99,6 @@ -
- - -
diff --git a/ui/src/widgets/ui-file-input/UIFileInput.vue b/ui/src/widgets/ui-file-input/UIFileInput.vue index 54b8141a1..dbb197f7d 100644 --- a/ui/src/widgets/ui-file-input/UIFileInput.vue +++ b/ui/src/widgets/ui-file-input/UIFileInput.vue @@ -7,7 +7,6 @@ :disabled="!state.enabled" :label="label" :prepend-icon="icon" :accept="accept" - :show-size="showFileSize" variant="outlined" hide-details="auto" /> From d5b401b5c517c48b3f3082eb4023217e0c7b854f Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Mon, 17 Jun 2024 09:04:14 +0100 Subject: [PATCH 3/6] Docs: Add docs for file-input --- docs/.vitepress/config.js | 1 + docs/nodes/widgets/ui-file-input.md | 30 ++++++++++++++++++ .../node-examples/ui-file-input-chosen.png | Bin 0 -> 26032 bytes .../node-examples/ui-file-input-select.png | Bin 0 -> 15152 bytes .../widgets/locales/en-US/ui_file_input.html | 16 ++++++++++ 5 files changed, 47 insertions(+) create mode 100644 docs/nodes/widgets/ui-file-input.md create mode 100644 docs/public/images/node-examples/ui-file-input-chosen.png create mode 100644 docs/public/images/node-examples/ui-file-input-select.png create mode 100644 nodes/widgets/locales/en-US/ui_file_input.html diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 801cf41c2..ea3133031 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -136,6 +136,7 @@ export default ({ mode }) => { { text: 'ui-chart', link: '/nodes/widgets/ui-chart' }, { text: 'ui-dropdown', link: '/nodes/widgets/ui-dropdown' }, { text: 'ui-event', link: '/nodes/widgets/ui-event' }, + { text: 'ui-file-input', link: '/nodes/widgets/ui-file-input' }, { text: 'ui-form', link: '/nodes/widgets/ui-form' }, { text: 'ui-gauge', link: '/nodes/widgets/ui-gauge' }, { text: 'ui-markdown', link: '/nodes/widgets/ui-markdown' }, diff --git a/docs/nodes/widgets/ui-file-input.md b/docs/nodes/widgets/ui-file-input.md new file mode 100644 index 000000000..f80e180ef --- /dev/null +++ b/docs/nodes/widgets/ui-file-input.md @@ -0,0 +1,30 @@ +--- +description: The File Upload widget allows users to upload files to Node-RED. +props: + Group: Defines which group of the UI Dashboard this widget will render in. + Size: Controls the width of the dropdown with respect to the parent group. Maximum value is the width of the group. + Label: + description: The text shown to the user, explaining what the user should upload. + Icon: + description: Defaults to "paperclip". The icon shown to the left of the input field. See the full list of icons here. + Accept: + description: String representation of the "allow" file type selectors. See full list of options here. + Multiple: + description: Allow end-users to upload multiple files at once. +--- + +# File Upload + +The File Upload widget allows users to upload files to Node-RED. The widget can be configured to accept specific file types and allow for multiple files. + +## Properties + + + +## Example + +![Example of a File Upload](/images/node-examples/ui-file-input-select.png "Example of a File Upload"){data-zoomable} +_Screenshot to show an example file input, when ready to have a file selected_ + +![Example of a File Upload](/images/node-examples/ui-file-input-chosen.png "Example of a File Upload"){data-zoomable} +_Screenshot to show an example file input, when a file has been selected, and is ready for "Upload"_ diff --git a/docs/public/images/node-examples/ui-file-input-chosen.png b/docs/public/images/node-examples/ui-file-input-chosen.png new file mode 100644 index 0000000000000000000000000000000000000000..24650f91f8db0f4b926442e3b380398169affcfc GIT binary patch literal 26032 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 tWt literal 0 HcmV?d00001 diff --git a/docs/public/images/node-examples/ui-file-input-select.png b/docs/public/images/node-examples/ui-file-input-select.png new file mode 100644 index 0000000000000000000000000000000000000000..b6c6836bec26ce45920a3c0187e19b13655b7aef GIT binary patch literal 15152 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 T9NgPi(GjF26vWF!_5J@ZpCM<9 literal 0 HcmV?d00001 diff --git a/nodes/widgets/locales/en-US/ui_file_input.html b/nodes/widgets/locales/en-US/ui_file_input.html new file mode 100644 index 000000000..c68e767aa --- /dev/null +++ b/nodes/widgets/locales/en-US/ui_file_input.html @@ -0,0 +1,16 @@ + \ No newline at end of file From 07d46704abb54c2fae9820165d74fd391b14645e Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Wed, 19 Jun 2024 17:28:58 +0100 Subject: [PATCH 4/6] Cap file selection to maximum file size per NR settings.js --- docs/nodes/widgets/ui-file-input.md | 14 +++++++++++++ nodes/widgets/ui_file_input.js | 6 ++++++ ui/src/widgets/ui-file-input/UIFileInput.vue | 22 +++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/nodes/widgets/ui-file-input.md b/docs/nodes/widgets/ui-file-input.md index f80e180ef..5d79c64dc 100644 --- a/docs/nodes/widgets/ui-file-input.md +++ b/docs/nodes/widgets/ui-file-input.md @@ -21,6 +21,20 @@ The File Upload widget allows users to upload files to Node-RED. The widget can +## Current Limitations + +_Currently_, the File Upload widget is limited by a maximum file size defined by the Websocket connection. The default maximum here is 5MB. This can be increased by modifying the `maxHttpBufferSize` property in the `settings.js` file in the Node-RED installation directory: + +``` +dashboard: { + maxHttpBufferSize: 1e8 // size in bytes, example: 100 MB +} +``` + +Read more about Dashboard configuration in the `settings.js` [here](/user/settings.html#maxhttpbuffersize). + +Note that we do have plans to improve this behavior by chunking files into smaller parts, and reassembling them on the server side. This will allow for larger files to be uploaded, and will be implemented in a future release. + ## Example ![Example of a File Upload](/images/node-examples/ui-file-input-select.png "Example of a File Upload"){data-zoomable} diff --git a/nodes/widgets/ui_file_input.js b/nodes/widgets/ui_file_input.js index 194be0591..301ce4433 100644 --- a/nodes/widgets/ui_file_input.js +++ b/nodes/widgets/ui_file_input.js @@ -17,6 +17,12 @@ module.exports = function (RED) { onAction: true } + // get max file size supported + const MAX_FILESIZE_DEFAULT = 1e6 + const maxFileSize = RED.settings.dashboard?.maxHttpBufferSize || MAX_FILESIZE_DEFAULT + + config.maxFileSize = maxFileSize + // inform the dashboard UI that we are adding this node group.register(node, config, evts) diff --git a/ui/src/widgets/ui-file-input/UIFileInput.vue b/ui/src/widgets/ui-file-input/UIFileInput.vue index dbb197f7d..606d54ab7 100644 --- a/ui/src/widgets/ui-file-input/UIFileInput.vue +++ b/ui/src/widgets/ui-file-input/UIFileInput.vue @@ -7,8 +7,10 @@ :disabled="!state.enabled" :label="label" :prepend-icon="icon" :accept="accept" + :multiple="multiple" variant="outlined" hide-details="auto" + :rules="[maxFileSize]" />
@@ -24,7 +26,7 @@ Upload Another File
- + {{ uploading ? 'Uploading...' : 'Upload' }}
@@ -74,6 +76,14 @@ export default { } }, methods: { + formatFileSize (bytes) { + if (bytes === 0) return '0 Bytes' + const k = 1000 + const dm = 2 + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] + }, reset: function () { this.files = null this.uploading = false @@ -115,6 +125,13 @@ export default { }, send (msg) { this.$socket.emit('widget-send', this.id, msg) + }, + maxFileSize (files) { + if (files) { + const size = files[0]?.size + const maxSize = this.props.maxFileSize + return !files || !files.length || size < maxSize || `File size should be less than ${this.formatFileSize(maxSize)}!` + } } } } @@ -125,6 +142,9 @@ export default { display: flex; gap: 12px; } +.nrdb-ui-file-input .v-btn { + max-height: var(--widget-row-height); +} .nrdb-ui-file-input--progress { width: 100%; display: flex; From 106c2163ca9222eba2a10aa7e582dd6ce3dc9e58 Mon Sep 17 00:00:00 2001 From: Joe Pavitt <99246719+joepavitt@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:54:56 +0100 Subject: [PATCH 5/6] Update ui/src/widgets/ui-file-input/UIFileInput.vue Co-authored-by: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> --- ui/src/widgets/ui-file-input/UIFileInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/widgets/ui-file-input/UIFileInput.vue b/ui/src/widgets/ui-file-input/UIFileInput.vue index 606d54ab7..b6c934519 100644 --- a/ui/src/widgets/ui-file-input/UIFileInput.vue +++ b/ui/src/widgets/ui-file-input/UIFileInput.vue @@ -26,7 +26,7 @@ Upload Another File
- + {{ uploading ? 'Uploading...' : 'Upload' }} From 1fe18a05881f60fd743016f375281a02e8f3cb81 Mon Sep 17 00:00:00 2001 From: Joe Pavitt Date: Thu, 20 Jun 2024 12:58:19 +0100 Subject: [PATCH 6/6] Support "multiple" file upload option --- docs/nodes/widgets/ui-file-input.md | 16 ++++- .../widgets/locales/en-US/ui_file_input.html | 4 +- ui/src/widgets/ui-file-input/UIFileInput.vue | 72 +++++++++++-------- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/docs/nodes/widgets/ui-file-input.md b/docs/nodes/widgets/ui-file-input.md index 5d79c64dc..d4550b341 100644 --- a/docs/nodes/widgets/ui-file-input.md +++ b/docs/nodes/widgets/ui-file-input.md @@ -10,7 +10,7 @@ props: Accept: description: String representation of the "allow" file type selectors. See full list of options here. Multiple: - description: Allow end-users to upload multiple files at once. + description: Allow end-users to upload multiple files at once. Each file will be sent as a unique message. --- # File Upload @@ -21,6 +21,20 @@ The File Upload widget allows users to upload files to Node-RED. The widget can +## Output + +```js +{ + payload: , + file: { + name: , + type: , + size: + }, + topic: , +} +``` + ## Current Limitations _Currently_, the File Upload widget is limited by a maximum file size defined by the Websocket connection. The default maximum here is 5MB. This can be increased by modifying the `maxHttpBufferSize` property in the `settings.js` file in the Node-RED installation directory: diff --git a/nodes/widgets/locales/en-US/ui_file_input.html b/nodes/widgets/locales/en-US/ui_file_input.html index c68e767aa..ef4000587 100644 --- a/nodes/widgets/locales/en-US/ui_file_input.html +++ b/nodes/widgets/locales/en-US/ui_file_input.html @@ -11,6 +11,8 @@

Properties

Accept string
String representation of the "allow" file type selectors. See full list of options here.
Multiple boolean
-
Allow end-users to upload multiple files at once.
+
Allow end-users to upload multiple files at once. Each file will be sent as a unique message.
+

Output

+

msg.payload will contain a file buffer, and msg.file will provide meta data about the file itself.

\ No newline at end of file diff --git a/ui/src/widgets/ui-file-input/UIFileInput.vue b/ui/src/widgets/ui-file-input/UIFileInput.vue index b6c934519..5ca1f7ad1 100644 --- a/ui/src/widgets/ui-file-input/UIFileInput.vue +++ b/ui/src/widgets/ui-file-input/UIFileInput.vue @@ -21,12 +21,12 @@
- +
- + {{ uploading ? 'Uploading...' : 'Upload' }} @@ -72,7 +72,22 @@ export default { return this.props.showFileSize }, multiple: function () { - return this.props.multiple + return this.props.allowMultiple + }, + canUpload: function () { + // no file selected yet + if (!this.files || (Array.isArray(this.files) && !this.files.length)) return false + + let tooLarge = false + if (Array.isArray(this.files)) { + tooLarge = this.files.some(file => file.size > this.props.maxFileSize) + } else { + tooLarge = this.files.size > this.props.maxFileSize + } + + if (tooLarge) return false + + return !this.uploading && !this.uploaded } }, methods: { @@ -90,37 +105,38 @@ export default { this.uploaded = false this.progress = 0 }, - upload: function (file) { - if (file && !this.multiple) { - // Create a FileReader instance to read the file - const reader = new FileReader() + uploadFile (file) { + // Create a FileReader instance to read the file + const reader = new FileReader() - // When the file is read, send it to Node-RED - reader.onload = () => { - // Prepare the payload to send - const msg = { - payload: file, // File content - file: { - name: file.name, // File name - size: file.size, // File size - type: file.type // File type - } + // When the file is read, send it to Node-RED + reader.onload = () => { + // Prepare the payload to send + const msg = { + payload: file, // File content + file: { + name: file.name, // File name + size: file.size, // File size + type: file.type // File type } + } - this.uploading = false - this.uploaded = true + this.uploading = false + this.uploaded = true - this.send(msg) - } + this.send(msg) + } - // Track progress of file reading - reader.onprogress = (event) => { - this.progress = event.loaded // Update progress - } - this.uploading = true + this.uploading = true - // readAsText alternative? - reader.readAsArrayBuffer(file) + // readAsText alternative? + reader.readAsArrayBuffer(file) + }, + upload: function (files) { + if (Array.isArray(files)) { + files.forEach(file => this.uploadFile(file)) + } else { + this.uploadFile(files) } }, send (msg) {