@flowfuse/node-red-dashboard 1.30.1-ddb5563-202512211347.0 → 1.30.2-ceb47b5-202601160854.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.
package/nodes/store/data.js
CHANGED
|
@@ -102,6 +102,22 @@ const setters = {
|
|
|
102
102
|
}
|
|
103
103
|
data[node.id].push(config.RED.util.cloneMessage(msg))
|
|
104
104
|
}
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* Fast filtering of existing array data (skips cloning and save checks for fast data cleanup)
|
|
108
|
+
* @param {*} base - the base node
|
|
109
|
+
* @param {*} node - the owner node
|
|
110
|
+
* @param {(msg) => Boolean} filterFunction
|
|
111
|
+
*/
|
|
112
|
+
filter (base, node, filterFunction) {
|
|
113
|
+
const currentData = data[node.id]
|
|
114
|
+
if (filterFunction && Array.isArray(currentData) && currentData.length) {
|
|
115
|
+
const filteredMessages = currentData.filter(filterFunction)
|
|
116
|
+
if (filteredMessages.length !== currentData.length) {
|
|
117
|
+
// no need for save operation to process messages - just apply them
|
|
118
|
+
data[node.id] = filteredMessages
|
|
119
|
+
}
|
|
120
|
+
}
|
|
105
121
|
}
|
|
106
122
|
}
|
|
107
123
|
|
|
@@ -111,5 +127,6 @@ module.exports = {
|
|
|
111
127
|
setConfig: setters.setConfig,
|
|
112
128
|
save: setters.save,
|
|
113
129
|
append: setters.append,
|
|
130
|
+
filter: setters.filter,
|
|
114
131
|
clear: setters.clear
|
|
115
132
|
}
|
|
@@ -106,6 +106,46 @@
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
</pre>
|
|
109
|
-
The options set earlier will remain in force.
|
|
109
|
+
The options set earlier will remain in force. Generally a message containing `msg.ui_update` should contain
|
|
110
|
+
no payload or other properties otherwise those will be interpreted as data to go on the chart.
|
|
111
|
+
</dd>
|
|
112
|
+
<dd>
|
|
113
|
+
The settings for each series on the chart can also be set using `msg.ui_update`. In particular this allows line
|
|
114
|
+
colours to be configured. Note that when adjusting settings
|
|
115
|
+
for a series you must include all customised settings for all series, as each update to series configuration will
|
|
116
|
+
replace any previous customisation to series.
|
|
117
|
+
For example, for a line chart with two lines 'temperature' and 'humidity', to set the colour of the temperature line
|
|
118
|
+
to red and humidity to green `msg.ui_update` could be sent containing
|
|
119
|
+
<pre>
|
|
120
|
+
{
|
|
121
|
+
"chartOptions": {
|
|
122
|
+
"series": [
|
|
123
|
+
{
|
|
124
|
+
"name": "temperature",
|
|
125
|
+
"type": "line",
|
|
126
|
+
"lineStyle": {
|
|
127
|
+
"color": "red",
|
|
128
|
+
},
|
|
129
|
+
"itemStyle": {
|
|
130
|
+
"color": "red"
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "humidity",
|
|
135
|
+
"type": "line",
|
|
136
|
+
"lineStyle": {
|
|
137
|
+
"color": "rgb(0, 255, 0)"
|
|
138
|
+
},
|
|
139
|
+
"itemStyle": {
|
|
140
|
+
"color": "rgb(0, 255, 0)"
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</pre>
|
|
147
|
+
Note that this sets the colour for the whole line. It cannot be used to change the colour for just a section of the line.
|
|
148
|
+
|
|
149
|
+
Any such message should always contain the series name and type for each series.
|
|
110
150
|
</dd>
|
|
111
151
|
</script>
|
|
@@ -14,6 +14,9 @@ module.exports = function (RED) {
|
|
|
14
14
|
const group = RED.nodes.getNode(config.group)
|
|
15
15
|
const base = group.getBase()
|
|
16
16
|
|
|
17
|
+
// add a chartOptions object into the config
|
|
18
|
+
config.chartOptions = config.chartOptions || {}
|
|
19
|
+
|
|
17
20
|
// correct typing
|
|
18
21
|
if (typeof config.xmin !== 'undefined') {
|
|
19
22
|
config.xmin = parseFloat(config.xmin)
|
|
@@ -58,19 +61,36 @@ module.exports = function (RED) {
|
|
|
58
61
|
if (removeOlder > 0) {
|
|
59
62
|
const removeOlderUnit = parseFloat(config.removeOlderUnit)
|
|
60
63
|
const ago = (removeOlder * removeOlderUnit) * 1000 // milliseconds ago
|
|
61
|
-
const
|
|
62
|
-
const
|
|
64
|
+
const cutOff = (new Date()).getTime() - ago
|
|
65
|
+
const filterFn = (msg) => {
|
|
63
66
|
let timestamp = msg._datapoint.x
|
|
64
67
|
// is x already a millisecond timestamp?
|
|
65
68
|
if (typeof (msg._datapoint.x) === 'string') {
|
|
66
69
|
timestamp = (new Date(msg._datapoint.x)).getTime()
|
|
67
70
|
}
|
|
68
|
-
return timestamp >
|
|
69
|
-
}
|
|
70
|
-
datastore.
|
|
71
|
+
return timestamp > cutOff
|
|
72
|
+
}
|
|
73
|
+
datastore.filter(base, node, filterFn)
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/**
|
|
78
|
+
* For categorical xaxis and types other than histogram then only keep the latest data point for
|
|
79
|
+
* each category in each series
|
|
80
|
+
*/
|
|
81
|
+
function clearOldCategoricalPoints () {
|
|
82
|
+
const points = datastore.get(node.id)
|
|
83
|
+
const latestSet = {}
|
|
84
|
+
for (const item of points) {
|
|
85
|
+
const { category, x } = item._datapoint
|
|
86
|
+
const key = JSON.stringify([category, x]) // a unique key for each category/series combination
|
|
87
|
+
latestSet[key] = item
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const filtered = Object.values(latestSet)
|
|
91
|
+
datastore.save(base, node, filtered)
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
// ensure sane defaults
|
|
75
95
|
if (!['msg', 'str', 'property', 'timestamp'].includes(config.xAxisPropertyType)) {
|
|
76
96
|
config.xAxisPropertyType = 'timestamp' // default to 'timestamp'
|
|
@@ -209,61 +229,78 @@ module.exports = function (RED) {
|
|
|
209
229
|
if (!datastore.get(node.id)) {
|
|
210
230
|
datastore.save(base, node, [])
|
|
211
231
|
}
|
|
212
|
-
if
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// delete old data if a replace is being performed.
|
|
217
|
-
// This is the case if msg.action is replace
|
|
218
|
-
// or the node is configured for replace and this is not being overriden by msg.action set to append
|
|
219
|
-
if (msg.action === 'replace' || (config.action === 'replace' && msg.action !== 'append')) {
|
|
220
|
-
// clear our data store as we are replacing data
|
|
232
|
+
// To prevent ui_update messages from deleting old data, skip this section if no msg.payload present
|
|
233
|
+
if (typeof msg.payload !== 'undefined') {
|
|
234
|
+
if (Array.isArray(msg.payload) && !msg.payload.length) {
|
|
235
|
+
// clear history
|
|
221
236
|
datastore.save(base, node, [])
|
|
222
|
-
}
|
|
223
|
-
if (!Array.isArray(msg.payload)) {
|
|
224
|
-
// quick clone of msg, and store in history
|
|
225
|
-
datastore.append(base, node, {
|
|
226
|
-
...msg
|
|
227
|
-
})
|
|
228
237
|
} else {
|
|
229
|
-
//
|
|
230
|
-
msg.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
// delete old data if a replace is being performed.
|
|
239
|
+
// This is the case if msg.action is replace
|
|
240
|
+
// or the node is configured for replace and this is not being overriden by msg.action set to append
|
|
241
|
+
if (msg.action === 'replace' || (config.action === 'replace' && msg.action !== 'append')) {
|
|
242
|
+
// clear our data store as we are replacing data
|
|
243
|
+
datastore.save(base, node, [])
|
|
244
|
+
}
|
|
245
|
+
if (!Array.isArray(msg.payload)) {
|
|
246
|
+
// quick clone of msg, and store in history
|
|
247
|
+
datastore.append(base, node, {
|
|
248
|
+
...msg
|
|
249
|
+
})
|
|
250
|
+
} else {
|
|
251
|
+
// we have an array in msg.payload, let's split them
|
|
252
|
+
msg.payload.forEach((p, i) => {
|
|
253
|
+
const payload = JSON.parse(JSON.stringify(p))
|
|
254
|
+
const d = msg._datapoint ? msg._datapoint[i] : null
|
|
255
|
+
const m = {
|
|
256
|
+
...msg,
|
|
257
|
+
payload,
|
|
258
|
+
_datapoint: d
|
|
259
|
+
}
|
|
260
|
+
datastore.append(base, node, m)
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const maxPoints = parseInt(config.removeOlderPoints)
|
|
265
|
+
|
|
266
|
+
if (maxPoints && config.removeOlderPoints) {
|
|
267
|
+
// account for multiple lines?
|
|
268
|
+
// client-side does this for _each_ line
|
|
269
|
+
// remove older points using datastore.filter instead of saving the whole array
|
|
270
|
+
const lineCounts = {}
|
|
271
|
+
const _msg = datastore.get(node.id) || []
|
|
272
|
+
|
|
273
|
+
// determine which message objects to keep (latest maxPoints per label)
|
|
274
|
+
const keepIndexes = []
|
|
275
|
+
let doFiltering = false
|
|
276
|
+
for (let i = _msg.length - 1; i >= 0; i--) {
|
|
277
|
+
const m = _msg[i]
|
|
278
|
+
const label = m.topic
|
|
279
|
+
lineCounts[label] = lineCounts[label] || 0
|
|
280
|
+
if (lineCounts[label] < maxPoints) {
|
|
281
|
+
keepIndexes[i] = true
|
|
282
|
+
lineCounts[label]++
|
|
283
|
+
} else {
|
|
284
|
+
doFiltering = true
|
|
285
|
+
}
|
|
237
286
|
}
|
|
238
|
-
datastore.append(base, node, m)
|
|
239
|
-
})
|
|
240
|
-
}
|
|
241
287
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// remove older points
|
|
248
|
-
const lineCounts = {}
|
|
249
|
-
const _msg = datastore.get(node.id)
|
|
250
|
-
// trawl through in reverse order, and only keep the latest points (up to maxPoints) for each label
|
|
251
|
-
for (let i = _msg.length - 1; i >= 0; i--) {
|
|
252
|
-
const msg = _msg[i]
|
|
253
|
-
const label = msg.topic
|
|
254
|
-
lineCounts[label] = lineCounts[label] || 0
|
|
255
|
-
if (lineCounts[label] >= maxPoints) {
|
|
256
|
-
_msg.splice(i, 1)
|
|
257
|
-
} else {
|
|
258
|
-
lineCounts[label]++
|
|
288
|
+
// filter the datastore to only keep the selected messages
|
|
289
|
+
if (doFiltering) {
|
|
290
|
+
datastore.filter(base, node, (m, i) => {
|
|
291
|
+
return keepIndexes[i]
|
|
292
|
+
})
|
|
259
293
|
}
|
|
260
294
|
}
|
|
261
|
-
datastore.save(base, node, _msg)
|
|
262
|
-
}
|
|
263
295
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
296
|
+
if (config.xAxisType === 'time' && config.removeOlder && config.removeOlderUnit) {
|
|
297
|
+
// remove any points older than the specified time
|
|
298
|
+
clearOldPoints()
|
|
299
|
+
} else if (config.xAxisType === 'category' && config.chartType !== 'histogram') {
|
|
300
|
+
// for categorical xaxis and types other than histogram then only keep the latest data point for
|
|
301
|
+
// each category in each series
|
|
302
|
+
clearOldCategoricalPoints()
|
|
303
|
+
}
|
|
267
304
|
}
|
|
268
305
|
}
|
|
269
306
|
|
|
@@ -77,19 +77,19 @@
|
|
|
77
77
|
</div>
|
|
78
78
|
<div class="form-row nr-db-ui-element-hide-in-subflow">
|
|
79
79
|
<label><i class="fa fa-object-group"></i> <span data-i18n="ui-base.label.size">Size</label>
|
|
80
|
-
<button class="editor-button" id="node-input-size"></button>
|
|
80
|
+
<button class="editor-button" id="node-config-input-size"></button>
|
|
81
81
|
</div>
|
|
82
82
|
<div class="form-row nr-db-ui-element-show-in-subflow">
|
|
83
83
|
<label><i class="fa fa-arrows-h"></i> <span data-i18n="ui-base.label.width">Width</label>
|
|
84
|
-
<input type="hidden" id="node-input-width">
|
|
84
|
+
<input type="hidden" id="node-config-input-width">
|
|
85
85
|
</div>
|
|
86
86
|
<div class="form-row nr-db-ui-element-show-in-subflow">
|
|
87
87
|
<label><i class="fa fa-arrows-v"></i> <span data-i18n="ui-base.label.height">Height</label>
|
|
88
|
-
<input type="hidden" id="node-input-height">
|
|
88
|
+
<input type="hidden" id="node-config-input-height">
|
|
89
89
|
</div>
|
|
90
90
|
<div class="form-row nr-db-ui-element-show-in-subflow">
|
|
91
91
|
<label><i class="fa fa-arrows"></i> <span data-i18n="ui-base.label.order">Order</label>
|
|
92
|
-
<input type="text" id="node-input-order">
|
|
92
|
+
<input type="text" id="node-input-config-order">
|
|
93
93
|
</div>
|
|
94
94
|
<div class="form-row">
|
|
95
95
|
<label for="node-config-input-className"><i class="fa fa-code"></i> <span data-i18n="ui-spacer.label.class"></span></label>
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
// The behaviour in existing nodes was to deselect all items
|
|
116
116
|
$('#node-input-deselect').prop('checked', true)
|
|
117
117
|
}
|
|
118
|
-
// if this groups parent is a subflow template, set the node-
|
|
118
|
+
// if this groups parent is a subflow template, set the node-input-width and node-input-height up
|
|
119
119
|
// as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
|
|
120
120
|
if (RED.nodes.subflow(this.z)) {
|
|
121
121
|
// change inputs from hidden to text & display them
|
|
@@ -347,8 +347,8 @@
|
|
|
347
347
|
<input type="text" id="node-input-label" data-i18n="[placeholder]ui-table.label.optionalTableTitle">
|
|
348
348
|
</div>
|
|
349
349
|
<div class="form-row" id="text-row-class">
|
|
350
|
-
<label for="node-
|
|
351
|
-
<input type="text" id="node-
|
|
350
|
+
<label for="node-input-className"><i class="fa fa-code"></i> <span data-i18n="ui-table.label.className"></span></label>
|
|
351
|
+
<input type="text" id="node-input-className" data-i18n="[placeholder]ui-table.label.classNamePlaceholder"/>
|
|
352
352
|
</div>
|
|
353
353
|
<div class="form-row">
|
|
354
354
|
<label for="node-input-maxrows"><i class="fa fa-tag"></i> <span data-i18n="ui-table.label.maxRows"></label>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowfuse/node-red-dashboard",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.2-ceb47b5-202601160854.0",
|
|
4
4
|
"description": "FlowFuse Dashboard - A collection of Node-RED nodes that provide functionality to build your own UI applications (inc. forms, buttons, charts).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red"
|