@flowfuse/node-red-dashboard 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +53 -0
  3. package/dist/css/app.d047b42b.css +1 -0
  4. package/dist/css/chunk-vendors.2378ce49.css +24 -0
  5. package/dist/fonts/materialdesignicons-webfont.3de8526e.woff +0 -0
  6. package/dist/fonts/materialdesignicons-webfont.477c6ab0.woff2 +0 -0
  7. package/dist/fonts/materialdesignicons-webfont.48a1ce0c.eot +0 -0
  8. package/dist/fonts/materialdesignicons-webfont.dfd403cf.ttf +0 -0
  9. package/dist/index.html +1 -0
  10. package/dist/js/app.854a8cd5.js +2 -0
  11. package/dist/js/app.854a8cd5.js.map +1 -0
  12. package/dist/js/chunk-vendors.174e8921.js +43 -0
  13. package/dist/js/chunk-vendors.174e8921.js.map +1 -0
  14. package/nodes/config/locales/en-US/ui_base.json +19 -0
  15. package/nodes/config/locales/en-US/ui_group.html +4 -0
  16. package/nodes/config/locales/en-US/ui_group.json +16 -0
  17. package/nodes/config/ui_base.html +807 -0
  18. package/nodes/config/ui_base.js +678 -0
  19. package/nodes/config/ui_group.html +55 -0
  20. package/nodes/config/ui_group.js +34 -0
  21. package/nodes/config/ui_page.html +84 -0
  22. package/nodes/config/ui_page.js +33 -0
  23. package/nodes/config/ui_theme.html +101 -0
  24. package/nodes/config/ui_theme.js +15 -0
  25. package/nodes/store/index.js +34 -0
  26. package/nodes/utils/index.js +35 -0
  27. package/nodes/widgets/locales/en-US/ui_button.html +7 -0
  28. package/nodes/widgets/locales/en-US/ui_button.json +24 -0
  29. package/nodes/widgets/locales/en-US/ui_chart.html +41 -0
  30. package/nodes/widgets/locales/en-US/ui_chart.json +17 -0
  31. package/nodes/widgets/locales/en-US/ui_dropdown.html +24 -0
  32. package/nodes/widgets/locales/en-US/ui_form.html +16 -0
  33. package/nodes/widgets/locales/en-US/ui_form.json +36 -0
  34. package/nodes/widgets/locales/en-US/ui_markdown.html +10 -0
  35. package/nodes/widgets/locales/en-US/ui_slider.html +9 -0
  36. package/nodes/widgets/locales/en-US/ui_switch.html +32 -0
  37. package/nodes/widgets/locales/en-US/ui_template.html +59 -0
  38. package/nodes/widgets/locales/en-US/ui_template.json +18 -0
  39. package/nodes/widgets/locales/en-US/ui_text.html +16 -0
  40. package/nodes/widgets/locales/en-US/ui_text_input.html +19 -0
  41. package/nodes/widgets/ui_button.html +146 -0
  42. package/nodes/widgets/ui_button.js +65 -0
  43. package/nodes/widgets/ui_chart.html +314 -0
  44. package/nodes/widgets/ui_chart.js +195 -0
  45. package/nodes/widgets/ui_dropdown.html +199 -0
  46. package/nodes/widgets/ui_dropdown.js +19 -0
  47. package/nodes/widgets/ui_form.html +368 -0
  48. package/nodes/widgets/ui_form.js +18 -0
  49. package/nodes/widgets/ui_markdown.html +134 -0
  50. package/nodes/widgets/ui_markdown.js +14 -0
  51. package/nodes/widgets/ui_notification.html +139 -0
  52. package/nodes/widgets/ui_notification.js +14 -0
  53. package/nodes/widgets/ui_radio_group.html +186 -0
  54. package/nodes/widgets/ui_radio_group.js +20 -0
  55. package/nodes/widgets/ui_slider.html +162 -0
  56. package/nodes/widgets/ui_slider.js +31 -0
  57. package/nodes/widgets/ui_switch.html +194 -0
  58. package/nodes/widgets/ui_switch.js +98 -0
  59. package/nodes/widgets/ui_table.html +149 -0
  60. package/nodes/widgets/ui_table.js +16 -0
  61. package/nodes/widgets/ui_template.html +283 -0
  62. package/nodes/widgets/ui_template.js +19 -0
  63. package/nodes/widgets/ui_text.html +358 -0
  64. package/nodes/widgets/ui_text.js +98 -0
  65. package/nodes/widgets/ui_text_input.html +141 -0
  66. package/nodes/widgets/ui_text_input.js +37 -0
  67. package/package.json +114 -0
@@ -0,0 +1,195 @@
1
+ const datastore = require('../store/index.js')
2
+
3
+ module.exports = function (RED) {
4
+ function ChartNode (config) {
5
+ const node = this
6
+
7
+ // create node in Node-RED
8
+ RED.nodes.createNode(this, config)
9
+
10
+ // which group are we rendering this widget
11
+ const group = RED.nodes.getNode(config.group)
12
+
13
+ function getProperty (value, property) {
14
+ const props = property.split('.')
15
+ props.forEach((prop) => {
16
+ if (value) {
17
+ value = value[prop]
18
+ }
19
+ })
20
+ return value
21
+ }
22
+
23
+ const evts = {
24
+ beforeSend: function (msg) {
25
+ const p = msg.payload
26
+
27
+ let series = RED.util.evaluateNodeProperty(config.category, config.categoryType, node, msg)
28
+ // if receiving a object payload, the series could be a within the payload
29
+ if (config.categoryType === 'property') {
30
+ series = getProperty(p, config.category)
31
+ }
32
+
33
+ if (config.chartType === 'line' || config.chartType === 'scatter') {
34
+ // possible that we haven't received any x-data in the payload,
35
+ // so let's make sure we append something
36
+
37
+ // single point or array of data?
38
+ if (Array.isArray(p)) {
39
+ // array of data
40
+ msg._datapoint = p.map((point) => {
41
+ // series available on a msg by msg basis - ensure we check for each msg
42
+ if (config.categoryType === 'property') {
43
+ series = getProperty(point, config.category)
44
+ }
45
+ return addToLine(point, series)
46
+ })
47
+ } else {
48
+ // single point
49
+ msg._datapoint = addToLine(p, series)
50
+ }
51
+ } else if (config.chartType === 'bar') {
52
+ // single point or array of data?
53
+ if (Array.isArray(p)) {
54
+ // array of data
55
+ msg._datapoint = p.map((point) => {
56
+ if (config.categoryType === 'property') {
57
+ series = getProperty(point, config.category)
58
+ }
59
+ return addToBar(point, series)
60
+ })
61
+ } else {
62
+ // single point
63
+ msg._datapoint = addToBar(p, series)
64
+ }
65
+ }
66
+
67
+ // function to process a data point being appended to a line/scatter chart
68
+ function addToLine (payload, series) {
69
+ const datapoint = {}
70
+ datapoint.category = series
71
+ // construct our datapoint
72
+ if (typeof payload === 'number') {
73
+ // just a number, assume we're plotting a time series
74
+ datapoint.x = (new Date()).getTime()
75
+ datapoint.y = payload
76
+ } else if (typeof payload === 'object') {
77
+ // may have been given an x/y object already
78
+ let x = getProperty(payload, config.xAxisProperty)
79
+ if (x === undefined || x === null) {
80
+ x = (new Date()).getTime()
81
+ }
82
+ datapoint.x = x
83
+ datapoint.y = payload.y
84
+ }
85
+ return datapoint
86
+ }
87
+
88
+ // the only server-side computed var we need is the category for a Bar Chart
89
+ function addToBar (payload, series) {
90
+ const datapoint = {}
91
+ datapoint.category = series
92
+ if (typeof payload === 'number') {
93
+ datapoint.y = payload
94
+ }
95
+ return datapoint
96
+ }
97
+
98
+ return msg
99
+ },
100
+ onInput: function (msg, send, done) {
101
+ // use our own custom onInput in order to store history of msg payloads
102
+ if (!datastore.get(node.id)) {
103
+ datastore.save(node.id, [])
104
+ }
105
+ if (Array.isArray(msg.payload) && !msg.payload.length) {
106
+ // clear history
107
+ datastore.save(node.id, [])
108
+ } else {
109
+ if (!Array.isArray(msg.payload)) {
110
+ // quick clone of msg, and store in history
111
+ datastore.append(node.id, {
112
+ ...msg
113
+ })
114
+ } else {
115
+ // we have an array in msg.payload, let's split them
116
+ msg.payload.forEach((p, i) => {
117
+ const payload = JSON.parse(JSON.stringify(p))
118
+ const d = msg._datapoint ? msg._datapoint[i] : null
119
+ const m = {
120
+ ...msg,
121
+ payload,
122
+ _datapoint: d
123
+ }
124
+ datastore.append(node.id, m)
125
+ })
126
+ }
127
+
128
+ const maxPoints = parseInt(config.removeOlderPoints)
129
+
130
+ if (config.xAxisType === 'category') {
131
+ const _msg = datastore.get(node.id)
132
+
133
+ // filters the ._msg array so that we keep just the latest msg with each category/series
134
+ const seen = {}
135
+ _msg.forEach((m, index) => {
136
+ const series = m._datapoint.category
137
+ // loop through and record the latest index seen for each topic/label
138
+ seen[series] = index
139
+ })
140
+ const indices = Object.values(seen)
141
+ datastore.save(node.id, _msg.filter((msg, index) => {
142
+ // return only the msgs with the latest index for each topic/label
143
+ return indices.includes(index)
144
+ }))
145
+ console.log(datastore.get(node.id))
146
+ } else if (maxPoints && config.removeOlderPoints) {
147
+ // account for multiple lines?
148
+ // client-side does this for _each_ line
149
+ // remove older points
150
+ const lineCounts = {}
151
+ const _msg = datastore.get(node.id)
152
+ // trawl through in reverse order, and only keep the latest points (up to maxPoints) for each label
153
+ for (let i = _msg.length - 1; i >= 0; i--) {
154
+ const msg = _msg[i]
155
+ const label = msg.topic
156
+ lineCounts[label] = lineCounts[label] || 0
157
+ if (lineCounts[label] >= maxPoints) {
158
+ _msg.splice(i, 1)
159
+ } else {
160
+ lineCounts[label]++
161
+ }
162
+ }
163
+ datastore.save(node.id, _msg)
164
+ }
165
+
166
+ if (config.xAxisType === 'time' && config.removeOlder && config.removeOlderUnit) {
167
+ // remove any points older than the specified time
168
+ const removeOlder = parseFloat(config.removeOlder)
169
+ const removeOlderUnit = parseFloat(config.removeOlderUnit)
170
+ const ago = (removeOlder * removeOlderUnit) * 1000 // milliseconds ago
171
+ const cutoff = (new Date()).getTime() - ago
172
+ const _msg = datastore.get(node.id).filter((msg) => {
173
+ let timestamp = msg._datapoint.x
174
+ // is x already a millisecond timestamp?
175
+ if (typeof (msg._datapoint.x) === 'string') {
176
+ timestamp = (new Date(msg._datapoint.x)).getTime()
177
+ }
178
+ return timestamp > cutoff
179
+ })
180
+ datastore.save(node.id, _msg)
181
+ }
182
+
183
+ // check sizing limits
184
+ }
185
+
186
+ send(msg)
187
+ }
188
+ }
189
+
190
+ // inform the dashboard UI that we are adding this node
191
+ group.register(node, config, evts)
192
+ }
193
+
194
+ RED.nodes.registerType('ui-chart', ChartNode)
195
+ }
@@ -0,0 +1,199 @@
1
+ <script type="text/javascript">
2
+ (function () {
3
+ function hasProperty (obj, prop) {
4
+ return Object.prototype.hasOwnProperty.call(obj, prop)
5
+ }
6
+ RED.nodes.registerType('ui-dropdown', {
7
+ category: RED._('@flowforge/node-red-dashboard/ui-base:ui-base.label.category'),
8
+ color: RED._('@flowforge/node-red-dashboard/ui-base:ui-base.colors.light'),
9
+ defaults: {
10
+ group: { type: 'ui-group', required: true },
11
+ name: { value: '' },
12
+ label: { value: 'Select Option:' },
13
+ tooltip: { value: '' },
14
+ order: { value: 0 },
15
+ width: {
16
+ value: 0,
17
+ validate: function (v) {
18
+ const width = v || 0
19
+ const currentGroup = $('#node-input-group').val() || this.group
20
+ const groupNode = RED.nodes.node(currentGroup)
21
+ const valid = !groupNode || +width <= +groupNode.width
22
+ $('#node-input-size').toggleClass('input-error', !valid)
23
+ return valid
24
+ }
25
+ },
26
+ height: { value: 0 },
27
+ passthru: { value: false },
28
+ multiple: { value: false },
29
+ options: {
30
+ value: [{ value: '', label: '' }],
31
+ validate: function (v) {
32
+ const unique = new Set(v.map(function (o) { return o.value }))
33
+ return v.length === unique.size
34
+ }
35
+ },
36
+ payload: { value: '' },
37
+ topic: { value: 'topic', validate: (hasProperty(RED.validators, 'typedInput') ? RED.validators.typedInput('topicType') : function (v) { return true }) },
38
+ topicType: { value: 'msg' },
39
+ className: { value: '' }
40
+ },
41
+ inputs: 1,
42
+ outputs: 1,
43
+ icon: 'font-awesome/fa-bars',
44
+ paletteLabel: 'dropdown',
45
+ label: function () { return this.name || (~this.label.indexOf('{' + '{') ? null : this.label) || 'dropdown' },
46
+ labelStyle: function () { return this.name ? 'node_label_italic' : '' },
47
+ oneditprepare: function () {
48
+ if (this.multiple === undefined) {
49
+ $('#node-input-multiple').prop('checked', false)
50
+ }
51
+ $('#node-input-size').elementSizer({
52
+ width: '#node-input-width',
53
+ height: '#node-input-height',
54
+ group: '#node-input-group'
55
+ })
56
+ const un = new Set(this.options.map(function (o) { return o.value }))
57
+ if (this.options.length === un.size) { $('#valWarning').hide() } else { $('#valWarning').show() }
58
+
59
+ function generateOption (i, option) {
60
+ const container = $('<li/>', { style: 'background: var(--red-ui-secondary-background, #fff); margin:0; padding:8px 0px 0px; border-bottom: 1px solid var(--red-ui-form-input-border-color, #ccc);' })
61
+ const row = $('<div/>').appendTo(container)
62
+ $('<div/>', { style: 'padding-top:5px; padding-left:175px;' }).appendTo(container)
63
+ $('<div/>', { style: 'padding-top:5px; padding-left:120px;' }).appendTo(container)
64
+
65
+ $('<i style="color: var(--red-ui-form-text-color, #eee); cursor:move; margin-left:3px;" class="node-input-option-handle fa fa-bars"></i>').appendTo(row)
66
+
67
+ $('<input/>', { class: 'node-input-option-value', type: 'text', style: 'margin-left:7px; width:calc(50% - 32px);', placeholder: 'Value', value: option.value }).appendTo(row).typedInput({ default: option.type || 'str', types: ['str', 'num', 'bool'] })
68
+ $('<input/>', { class: 'node-input-option-label', type: 'text', style: 'margin-left:7px; width:calc(50% - 32px);', placeholder: 'Label', value: option.label }).appendTo(row)
69
+
70
+ const finalSpan = $('<span/>', { style: 'float:right; margin-right:8px;' }).appendTo(row)
71
+ const deleteButton = $('<a/>', { href: '#', class: 'editor-button editor-button-small', style: 'margin-top:7px; margin-left:5px;' }).appendTo(finalSpan)
72
+ $('<i/>', { class: 'fa fa-remove' }).appendTo(deleteButton)
73
+
74
+ deleteButton.click(function () {
75
+ container.css({ background: 'var(--red-ui-secondary-background-inactive, #fee)' })
76
+ container.fadeOut(300, function () {
77
+ $(this).remove()
78
+ })
79
+ })
80
+
81
+ $('#node-input-option-container').append(container)
82
+ }
83
+
84
+ $('#node-input-add-option').click(function () {
85
+ generateOption($('#node-input-option-container').children().length + 1, {})
86
+ $('#node-input-option-container-div').scrollTop($('#node-input-option-container-div').get(0).scrollHeight)
87
+ })
88
+
89
+ for (let i = 0; i < this.options.length; i++) {
90
+ const option = this.options[i]
91
+ generateOption(i + 1, option)
92
+ }
93
+
94
+ $('#node-input-option-container').sortable({
95
+ axis: 'y',
96
+ handle: '.node-input-option-handle',
97
+ cursor: 'move'
98
+ })
99
+
100
+ $('#node-input-topic').typedInput({
101
+ default: 'str',
102
+ typeField: $('#node-input-topicType'),
103
+ types: ['str', 'msg', 'flow', 'global']
104
+ })
105
+
106
+ // use jQuery UI tooltip to convert the plain old title attribute to a nice tooltip
107
+ $('.ui-node-popover-title').tooltip({
108
+ show: {
109
+ effect: 'slideDown',
110
+ delay: 150
111
+ }
112
+ })
113
+ },
114
+ oneditsave: function () {
115
+ const options = $('#node-input-option-container').children()
116
+ const node = this
117
+ node.options = []
118
+ options.each(function (i) {
119
+ const option = $(this)
120
+ const o = {
121
+ label: option.find('.node-input-option-label').val(),
122
+ value: option.find('.node-input-option-value').typedInput('value'),
123
+ type: option.find('.node-input-option-value').typedInput('type')
124
+ }
125
+ if (option.find('.node-input-option-value').typedInput('type') === 'num') {
126
+ o.value = Number(o.value)
127
+ }
128
+ if (option.find('.node-input-option-value').typedInput('type') === 'bool') {
129
+ o.value = (o.value === 'true')
130
+ }
131
+ node.options.push(o)
132
+ })
133
+ },
134
+ oneditresize: function () {
135
+ }
136
+ })
137
+ })()
138
+ </script>
139
+
140
+ <script type="text/html" data-template-name="ui-dropdown">
141
+ <div class="form-row">
142
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
143
+ <input type="text" id="node-input-name">
144
+ </div>
145
+ <div class="form-row">
146
+ <label for="node-input-group"><i class="fa fa-table"></i> Group</label>
147
+ <input type="text" id="node-input-group">
148
+ </div>
149
+ <div class="form-row">
150
+ <label><i class="fa fa-object-group"></i> Size</label>
151
+ <input type="hidden" id="node-input-width">
152
+ <input type="hidden" id="node-input-height">
153
+ <button class="editor-button" id="node-input-size"></button>
154
+ </div>
155
+ <div class="form-row">
156
+ <label for="node-input-label"><i class="fa fa-tag"></i> Label</label>
157
+ <input type="text" id="node-input-label" placeholder="optional label">
158
+ </div>
159
+ <div class="form-row">
160
+ <label for="node-input-className"><i class="fa fa-code"></i> Class</label>
161
+ <div style="display: inline;">
162
+ <input style="width: 70%;" type="text" id="node-input-className" placeholder="Optional CSS class name(s)" style="flex-grow: 1;">
163
+ <a
164
+ data-html="true"
165
+ title="Dynamic Property: Class names can also be set by sending a message to the node with a msg.topic of 'ui-property:class' and a payload containing the class name(s) to be applied. NOTE: classes set at runtime will be applied in addition to any class(es) set in the nodes class field."
166
+ class="red-ui-button ui-node-popover-title"
167
+ 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;">
168
+ <i style="font-family: ui-serif;">fx</i>
169
+ </a>
170
+ </div>
171
+ </div>
172
+ <!--<div class="form-row">
173
+ <label for="node-input-tooltip"><i class="fa fa-info-circle"></i> Tooltip</label>
174
+ <input type="text" id="node-input-tooltip" placeholder="optional tooltip">
175
+ </div>-->
176
+ <div class="form-row node-input-option-container-row" style="margin-bottom: 0px;width: 100%">
177
+ <label for="node-input-width" style="vertical-align:top"><i class="fa fa-list-alt"></i> Options</label>
178
+ <div id="node-input-option-container-div" style="box-sizing:border-box; border-radius:5px; height:257px; padding:5px; border:1px solid var(--red-ui-form-input-border-color, #ccc); overflow-y:scroll; display:inline-block; width:calc(70% + 15px);">
179
+ <span id="valWarning" style="color: var(--red-ui-text-color-error, #910000)"><b>All Values must be unique.</b></span>
180
+ <ol id="node-input-option-container" style="list-style-type:none; margin:0;"></ol>
181
+ </div>
182
+ </div>
183
+ <div class="form-row">
184
+ <a href="#" class="editor-button editor-button-small" id="node-input-add-option" style="margin-top:4px; margin-left:103px;"><i class="fa fa-plus"></i> <span>option</span></a>
185
+ </div>
186
+ <div class="form-row">
187
+ <label style="width:auto" for="node-input-multiple"><i class="fa fa-th-list"></i> Allow multiple selections from list: </label>
188
+ <input type="checkbox" checked id="node-input-multiple" style="display: inline-block; width: auto; margin: 0px 0px 0px 4px;">
189
+ </div>
190
+ <div class="form-row">
191
+ <label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, pass through to output: </label>
192
+ <input type="checkbox" checked id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
193
+ </div>
194
+ <div class="form-row">
195
+ <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
196
+ <input type="text" id="node-input-topic" style="width:70%" placeholder="optional msg.topic">
197
+ <input type="hidden" id="node-input-topicType">
198
+ </div>
199
+ </script>
@@ -0,0 +1,19 @@
1
+ module.exports = function (RED) {
2
+ function DropdownNode (config) {
3
+ // create node in Node-RED
4
+ RED.nodes.createNode(this, config)
5
+ const node = this
6
+
7
+ // which group are we rendering this widget
8
+ const group = RED.nodes.getNode(config.group)
9
+
10
+ const evts = {
11
+ onChange: true
12
+ }
13
+
14
+ // inform the dashboard UI that we are adding this node
15
+ group.register(node, config, evts)
16
+ }
17
+
18
+ RED.nodes.registerType('ui-dropdown', DropdownNode)
19
+ }