Skip to content

Commit

Permalink
Merge pull request #981 from FlowFuse/44-file-upload
Browse files Browse the repository at this point in the history
Widget: File Upload
  • Loading branch information
joepavitt authored Jun 20, 2024
2 parents 8882129 + 1fe18a0 commit ceb0327
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 1 deletion.
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

0 comments on commit ceb0327

Please sign in to comment.