Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Widget: File Upload #981

Merged
merged 6 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
58 changes: 58 additions & 0 deletions docs/nodes/widgets/ui-file-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
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 <a href="https://pictogrammers.com/library/mdi/" target="_blank">here</a>.
Accept:
description: String representation of the "allow" file type selectors. See full list of options <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers" target="_blank">here</a>.
Multiple:
description: Allow end-users to upload multiple files at once. Each file will be sent as a unique message.
---

# 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

<PropsTable/>

## Output

```js
{
payload: <Buffer>,
file: {
name: <String>,
type: <String>,
size: <Number>
},
topic: <String>,
}
```

## 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}
_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"_
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions nodes/widgets/locales/en-US/ui_file_input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script type="text/html" data-help-name="ui-file-input">
<p>
The file input node will provide a file select widget, allowing users to upload files to Node-RED.
</p>
<h3>Properties</h3>
<dl class="message-properties">
<dt>Label <span class="property-type">string</span></dt>
<dd>The text shown to the user, explaining what the user should upload.</dd>
<dt>Icon <span class="property-type">string</span></dt>
<dd>Defaults to "paperclip". The icon shown to the left of the input field. See the full list of icons <a href="https://pictogrammers.com/library/mdi/" target="_blank">here</a>.</dd>
<dt>Accept <span class="property-type">string</span></dt>
<dd>String representation of the "allow" file type selectors. See full list of options <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers" target="_blank">here</a>.</dd>
<dt>Multiple <span class="property-type">boolean</span></dt>
<dd>Allow end-users to upload multiple files at once. Each file will be sent as a unique message.</dd>
</dl>
<h3>Output</h3>
<p><code>msg.payload</code> will contain a file buffer, and <code>msg.file</code> will provide meta data about the file itself.</p>
</script>
120 changes: 120 additions & 0 deletions nodes/widgets/ui_file_input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script type="text/javascript">
(function () {
function hasProperty (obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop)
}
RED.nodes.registerType('ui-file-input', {
category: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.label.category'),
color: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.colors.light'),
defaults: {
group: { type: 'ui-group', required: true },
name: { value: '' },
order: { value: 0 },
width: {
value: 0,
validate: function (v) {
const width = v || 0
const currentGroup = $('#node-input-group').val() || this.group
const groupNode = RED.nodes.node(currentGroup)
const valid = !groupNode || +width <= +groupNode.width
$('#node-input-size').toggleClass('input-error', !valid)
return valid
}
},
height: { value: 0 },
topic: { value: 'topic', validate: (hasProperty(RED.validators, 'typedInput') ? RED.validators.typedInput('topicType') : function (v) { return true }) },
topicType: { value: 'msg' },
label: { value: 'File Input' },
icon: { value: 'paperclip' },
allowMultiple: { value: false },
accept: { value: '' },
className: { value: '' }
},
inputs: 1,
outputs: 1,
icon: 'font-awesome/fa-upload',
paletteLabel: 'file input',
oneditprepare: function () {
// if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up
// as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
if (RED.nodes.subflow(this.z)) {
// change inputs from hidden to text & display them
$('#node-input-width').attr('type', 'text')
$('#node-input-height').attr('type', 'text')
$('div.form-row.nr-db-ui-element-sizer-row').hide()
$('div.form-row.nr-db-ui-manual-size-row').show()
} else {
// not in a subflow, use the elementSizer
$('div.form-row.nr-db-ui-element-sizer-row').show()
$('div.form-row.nr-db-ui-manual-size-row').hide()
$('#node-input-size').elementSizer({
width: '#node-input-width',
height: '#node-input-height',
group: '#node-input-group'
})
}
// topic
$('#node-input-topic').typedInput({
default: 'str',
typeField: $('#node-input-topicType'),
types: ['str', 'msg', 'flow', 'global']
})
},
label: function () {
return this.name || this.label || 'file input'
},
labelStyle: function () { return this.name ? 'node_label_italic' : '' }
})
})()
</script>

<script type="text/html" data-template-name="ui-file-input">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-icon"><i class="fa fa-picture-o"></i> Icon</label>
<input type="text" id="node-input-icon">
</div>
<div class="form-row">
<label for="node-input-accept"><i class="fa fa-paperclip"></i> Accept</label>
<input type="text" id="node-input-accept">
</div>
<div class="form-row" style="padding-left: 25px;">
<input type="checkbox" checked id="node-input-allowMultiple" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-allowMultiple"> Allow Multiple</label>
</div>
<div class="form-row">
<label for="node-input-className"><i class="fa fa-code"></i> Class</label>
<div style="display: inline;">
<input style="width: 70%;" type="text" id="node-input-className" placeholder="Optional CSS class name(s)" style="flex-grow: 1;">
<a
data-html="true"
title="Dynamic Property: Send msg.class to append new classes to this widget. NOTE: classes set at runtime will be applied in addition to any class(es) set in the nodes class field."
class="red-ui-button ui-node-popover-title"
style="margin-left: 4px; cursor: help; font-size: 0.625rem; border-radius: 50%; width: 24px; height: 24px; display: inline-flex; justify-content: center; align-items: center;">
<i style="font-family: ui-serif;">fx</i>
</a>
</div>
</div>
<div class="form-row" style="padding-left: 25px;">
<label for="node-input-topic" style="margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
<input type="hidden" id="node-input-topicType">
</div>
</script>
35 changes: 35 additions & 0 deletions nodes/widgets/ui_file_input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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
}

// 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)

node.on('close', async function (done) {
done()
})
}

RED.nodes.registerType('ui-file-input', FileInputNode)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion ui/src/stylesheets/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/* main */
--nrdb-main-padding: 12px;
/* widget sizing */
--widget-row-height: 42px;
--widget-row-height: 48px;
}

body {
Expand Down Expand Up @@ -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
*/
Expand Down
3 changes: 3 additions & 0 deletions ui/src/widgets/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,6 +25,7 @@ export {
UIControl,
UIDropdown,
UIEvent,
UIFileInput,
UIForm,
UIGauge,
UIMarkdown,
Expand All @@ -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,
Expand Down
Loading
Loading