@flowfuse/node-red-dashboard 1.31.1-e3609ab-202609141637.0 → 1.32.1-034c8a5-202610061312.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/README.md +50 -0
- package/nodes/config/ui_base.js +12 -7
- package/nodes/store/clients.js +71 -0
- package/nodes/store/data.js +29 -1
- package/nodes/store/state.js +3 -1
- package/nodes/utils/index.js +29 -1
- package/nodes/widgets/locales/en-US/ui_control.json +2 -1
- package/nodes/widgets/ui_chart.js +2 -28
- package/nodes/widgets/ui_control.html +1 -1
- package/nodes/widgets/ui_control.js +28 -0
- package/nodes/widgets/ui_gauge.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -86,6 +86,56 @@ The original [Node-RED Dashboard](https://github.com/node-red/node-red-dashboard
|
|
|
86
86
|
|
|
87
87
|
FlowFuse Dashboard was re-built from the ground up, learning from the popular features and feedback of Node-RED Dashboard. It will carry the legacy forward, adapting to future needs while keeping the essence of open-source and community-driven development intact. The project will is licensed under Apache 2.0.
|
|
88
88
|
|
|
89
|
+
## Release process
|
|
90
|
+
|
|
91
|
+
In this project, the [Release Please](https://github.com/googleapis/release-please) is used to automatically determine the next release version based on the commit messages in the codebase.
|
|
92
|
+
|
|
93
|
+
By using the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), the project adheres to a standardized format for commit messages, which `Release Please` uses to determine whether the next release should be a major, minor, or patch release.
|
|
94
|
+
|
|
95
|
+
### Components
|
|
96
|
+
|
|
97
|
+
1. The `Prepare release` GitHub Action workflow:
|
|
98
|
+
|
|
99
|
+
* A Release Please action that analyzes commit messages to determine the type of release required (major, minor, patch) based on the Conventional Commits specification
|
|
100
|
+
* Creates a pre-release pull request with the proposed version bump and changelog
|
|
101
|
+
* Once merged, automatically updates the version number in `package.json` and creates a new release on GitHub with the appropriate changelog
|
|
102
|
+
|
|
103
|
+
2. The `Lint Pull Request Title` GitHub Action workflow:
|
|
104
|
+
|
|
105
|
+
* A workflow that runs on pull request creation and uses the `amannn/action-semantic-pull-request` action to validate that pull request titles follow the Conventional Commits format
|
|
106
|
+
* Together with adjusted default merge commit message, this ensures that all commits merged into the main branch adhere to the expected format, allowing Release Please to function correctly
|
|
107
|
+
|
|
108
|
+
3. The `Publish Release` GitHub Action workflow:
|
|
109
|
+
|
|
110
|
+
* A workflow that runs when a new git tag in `v*.*.*` format is pushed, runs the tests, builds the package and publishes the new version to the public npm registry using the `JS-DevTools/npm-publish` action
|
|
111
|
+
* Once package is published, the workflow updates the package version in the Node-RED Flow Library catalogue
|
|
112
|
+
|
|
113
|
+
### Pull Request Title Format
|
|
114
|
+
|
|
115
|
+
The Conventional Commits preset expects pull request titles to be in the following format:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
<type>(<scope>): <subject>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
* Type: Describes the category of the commit. Examples include:
|
|
122
|
+
* `feat`: A new feature (triggers a minor version bump).
|
|
123
|
+
* `fix`: A bug fix (triggers a patch version bump).
|
|
124
|
+
* `perf`: A code change that improves performance (triggers a patch version bump).
|
|
125
|
+
* `refactor`: A code change that neither fixes a bug nor adds a feature (does not trigger a release unless it's accompanied by a BREAKING CHANGE).
|
|
126
|
+
* `docs`: Documentation-only changes (does not trigger a release).
|
|
127
|
+
* `chore`: Changes to the build process or auxiliary tools and libraries (does not trigger a release).
|
|
128
|
+
* Scope: An optional part that provides additional context about what was changed (e.g., module, component).
|
|
129
|
+
* Subject: A brief description of the changes.
|
|
130
|
+
|
|
131
|
+
### Handling Breaking Changes
|
|
132
|
+
|
|
133
|
+
To indicate a breaking change, the exclamation mark `!` should be used immediately after the type/scope:
|
|
134
|
+
|
|
135
|
+
* `feat!:`
|
|
136
|
+
* `fix!:`
|
|
137
|
+
* `refactor!:`
|
|
138
|
+
|
|
89
139
|
## License
|
|
90
140
|
|
|
91
141
|
Apache License 2.0
|
package/nodes/config/ui_base.js
CHANGED
|
@@ -4,9 +4,10 @@ const path = require('path')
|
|
|
4
4
|
const axios = require('axios')
|
|
5
5
|
|
|
6
6
|
const v = require('../../package.json').version
|
|
7
|
+
const { createClientStore } = require('../store/clients.js')
|
|
7
8
|
const datastore = require('../store/data.js')
|
|
8
9
|
const statestore = require('../store/state.js')
|
|
9
|
-
const { appendTopic, addConnectionCredentials, getThirdPartyWidgets } = require('../utils/index.js')
|
|
10
|
+
const { appendTopic, addConnectionCredentials, matchesClient, normalizeClientId, getThirdPartyWidgets } = require('../utils/index.js')
|
|
10
11
|
|
|
11
12
|
// from: https://stackoverflow.com/a/28592528/3016654
|
|
12
13
|
function join (...paths) {
|
|
@@ -59,6 +60,7 @@ module.exports = function (RED) {
|
|
|
59
60
|
ioServer: null,
|
|
60
61
|
/** @type {Object.<string, Socket>} */
|
|
61
62
|
connections: {},
|
|
63
|
+
clientStore: createClientStore(),
|
|
62
64
|
settings: {},
|
|
63
65
|
contribs: {}
|
|
64
66
|
}
|
|
@@ -223,7 +225,6 @@ module.exports = function (RED) {
|
|
|
223
225
|
lang: 'en',
|
|
224
226
|
scope: './',
|
|
225
227
|
description: config.name,
|
|
226
|
-
theme_color: '#ffffff',
|
|
227
228
|
icons: [
|
|
228
229
|
{ src: hasAppIcon ? config.appIcon : 'pwa-64x64.png', sizes: '64x64', type: 'image/png' },
|
|
229
230
|
{ src: hasAppIcon ? config.appIcon : 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
|
|
@@ -413,6 +414,7 @@ module.exports = function (RED) {
|
|
|
413
414
|
socket.on('widget-action', onAction.bind(null, socket))
|
|
414
415
|
socket.on('widget-change', onChange.bind(null, socket))
|
|
415
416
|
socket.on('widget-load', onLoad.bind(null, socket))
|
|
417
|
+
socket.on('disconnect', () => uiShared.clientStore.disconnect(socket._clientId, socket.id))
|
|
416
418
|
}
|
|
417
419
|
}
|
|
418
420
|
/** @type {NodeJS.Timeout} */
|
|
@@ -467,11 +469,7 @@ module.exports = function (RED) {
|
|
|
467
469
|
}
|
|
468
470
|
}
|
|
469
471
|
// conduct the core check too
|
|
470
|
-
|
|
471
|
-
// if a particular socketid has been defined,
|
|
472
|
-
// we only send comms on the connection that matches that id
|
|
473
|
-
checks.push(msg._client?.socketId === conn.id)
|
|
474
|
-
}
|
|
472
|
+
checks.push(matchesClient(conn, msg))
|
|
475
473
|
// ensure all checks validate sending this
|
|
476
474
|
return !checks.length || !checks.includes(false)
|
|
477
475
|
}
|
|
@@ -591,6 +589,7 @@ module.exports = function (RED) {
|
|
|
591
589
|
socket.on('disconnect', reason => {
|
|
592
590
|
cleanupEventHandlers(socket)
|
|
593
591
|
delete uiShared.connections[socket.id]
|
|
592
|
+
uiShared.clientStore.disconnect(socket._clientId, socket.id)
|
|
594
593
|
node.log(`Disconnected ${socket.id} due to ${reason}`)
|
|
595
594
|
})
|
|
596
595
|
}
|
|
@@ -606,6 +605,9 @@ module.exports = function (RED) {
|
|
|
606
605
|
// node.connections[socket.id] = socket // store the connection for later use
|
|
607
606
|
uiShared.connections[socket.id] = socket // store the connection for later use
|
|
608
607
|
|
|
608
|
+
socket._clientId = normalizeClientId(socket.handshake?.query?.clientId)
|
|
609
|
+
uiShared.clientStore.connect(socket._clientId, socket.id)
|
|
610
|
+
|
|
609
611
|
emitConfig(socket)
|
|
610
612
|
|
|
611
613
|
// clean up then re-register listeners
|
|
@@ -846,6 +848,9 @@ module.exports = function (RED) {
|
|
|
846
848
|
node.on('close', (removed, done) => {
|
|
847
849
|
uiShared.ioServer?.off('connection', onConnection)
|
|
848
850
|
for (const conn of Object.values(uiShared.connections)) {
|
|
851
|
+
if (removed && conn._baseId === node.id) {
|
|
852
|
+
uiShared.clientStore.dropSocket(conn._clientId, conn.id)
|
|
853
|
+
}
|
|
849
854
|
cleanupEventHandlers(conn)
|
|
850
855
|
}
|
|
851
856
|
close(node, function (err) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const { EventEmitter } = require('events')
|
|
2
|
+
|
|
3
|
+
const DEFAULT_GRACE_MS = 20000
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks client presence across reconnects (socket.id churns every reconnect; clientId is stable).
|
|
7
|
+
* A client may hold several live sockets (one per tab); events fire on its socket count crossing
|
|
8
|
+
* zero, not per socket. Emits { event, clientId, socketId? }:
|
|
9
|
+
* connected: count 0 -> 1, no grace pending (new client)
|
|
10
|
+
* reconnected: count 0 -> 1 during the grace window (returned before being declared gone)
|
|
11
|
+
* gone: count -> 0 and the grace window elapses
|
|
12
|
+
* Extra tabs (1 -> 2, 2 -> 1) emit nothing; the client is still present.
|
|
13
|
+
*/
|
|
14
|
+
function createClientStore ({ graceMs = DEFAULT_GRACE_MS, setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout } = {}) {
|
|
15
|
+
const clients = {} // clientId -> { sockets: Set<socketId>, graceTO }
|
|
16
|
+
const events = new EventEmitter()
|
|
17
|
+
events.setMaxListeners(0)
|
|
18
|
+
|
|
19
|
+
function connect (clientId, socketId) {
|
|
20
|
+
if (!clientId) { return }
|
|
21
|
+
let entry = clients[clientId]
|
|
22
|
+
const returning = !!(entry && entry.graceTO)
|
|
23
|
+
if (!entry) {
|
|
24
|
+
entry = clients[clientId] = { sockets: new Set(), graceTO: null }
|
|
25
|
+
}
|
|
26
|
+
if (entry.graceTO) {
|
|
27
|
+
clearTimeoutFn(entry.graceTO)
|
|
28
|
+
entry.graceTO = null
|
|
29
|
+
}
|
|
30
|
+
const wasEmpty = entry.sockets.size === 0
|
|
31
|
+
entry.sockets.add(socketId)
|
|
32
|
+
if (wasEmpty) {
|
|
33
|
+
events.emit('client', { event: returning ? 'reconnected' : 'connected', clientId, socketId })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function disconnect (clientId, socketId) {
|
|
38
|
+
if (!clientId) { return }
|
|
39
|
+
const entry = clients[clientId]
|
|
40
|
+
if (!entry) { return }
|
|
41
|
+
entry.sockets.delete(socketId)
|
|
42
|
+
if (entry.sockets.size === 0 && !entry.graceTO) {
|
|
43
|
+
entry.graceTO = setTimeoutFn(() => {
|
|
44
|
+
delete clients[clientId]
|
|
45
|
+
events.emit('client', { event: 'gone', clientId })
|
|
46
|
+
}, graceMs)
|
|
47
|
+
if (typeof entry.graceTO?.unref === 'function') { entry.graceTO.unref() }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function dropSocket (clientId, socketId) {
|
|
52
|
+
if (!clientId) { return }
|
|
53
|
+
const entry = clients[clientId]
|
|
54
|
+
if (!entry) { return }
|
|
55
|
+
entry.sockets.delete(socketId)
|
|
56
|
+
if (entry.sockets.size === 0) {
|
|
57
|
+
if (entry.graceTO) { clearTimeoutFn(entry.graceTO) }
|
|
58
|
+
delete clients[clientId]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function list () {
|
|
63
|
+
return Object.entries(clients)
|
|
64
|
+
.filter(([, entry]) => entry.sockets.size > 0)
|
|
65
|
+
.map(([clientId, entry]) => ({ clientId, socketId: entry.sockets.values().next().value }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { connect, disconnect, dropSocket, list, events }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { createClientStore }
|
package/nodes/store/data.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { isClientScoped } = require('../utils/index.js')
|
|
2
|
+
|
|
1
3
|
const data = {}
|
|
2
4
|
|
|
3
5
|
const config = {
|
|
@@ -19,7 +21,7 @@ function canSaveInStore (base, node, msg) {
|
|
|
19
21
|
|
|
20
22
|
if (constrained.includes(node.type)) {
|
|
21
23
|
// core check
|
|
22
|
-
if (msg
|
|
24
|
+
if (isClientScoped(msg)) {
|
|
23
25
|
// we are in a node type that allows for definition of specific clients,
|
|
24
26
|
// and a client has been defined
|
|
25
27
|
checks.push(false)
|
|
@@ -55,6 +57,9 @@ const getters = {
|
|
|
55
57
|
// given a widget id, return the latest msg received
|
|
56
58
|
msg (id) {
|
|
57
59
|
return config.RED.util.cloneMessage(data[id])
|
|
60
|
+
},
|
|
61
|
+
has (id) {
|
|
62
|
+
return !!data[id]
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -118,15 +123,38 @@ const setters = {
|
|
|
118
123
|
data[node.id] = filteredMessages
|
|
119
124
|
}
|
|
120
125
|
}
|
|
126
|
+
},
|
|
127
|
+
keepLatestPerTopic (base, node, maxPoints) {
|
|
128
|
+
const currentData = data[node.id]
|
|
129
|
+
if (!Array.isArray(currentData)) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const counts = {}
|
|
133
|
+
const keep = []
|
|
134
|
+
let trimmed = false
|
|
135
|
+
for (let i = currentData.length - 1; i >= 0; i--) {
|
|
136
|
+
const topic = currentData[i].topic
|
|
137
|
+
counts[topic] = (counts[topic] || 0) + 1
|
|
138
|
+
if (counts[topic] <= maxPoints) {
|
|
139
|
+
keep[i] = true
|
|
140
|
+
} else {
|
|
141
|
+
trimmed = true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (trimmed) {
|
|
145
|
+
setters.filter(base, node, (m, i) => keep[i])
|
|
146
|
+
}
|
|
121
147
|
}
|
|
122
148
|
}
|
|
123
149
|
|
|
124
150
|
module.exports = {
|
|
125
151
|
get: getters.msg,
|
|
152
|
+
has: getters.has,
|
|
126
153
|
RED: getters.RED,
|
|
127
154
|
setConfig: setters.setConfig,
|
|
128
155
|
save: setters.save,
|
|
129
156
|
append: setters.append,
|
|
130
157
|
filter: setters.filter,
|
|
158
|
+
keepLatestPerTopic: setters.keepLatestPerTopic,
|
|
131
159
|
clear: setters.clear
|
|
132
160
|
}
|
package/nodes/store/state.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Store to manage any dynamic properties set
|
|
2
2
|
|
|
3
|
+
const { isClientScoped } = require('../utils/index.js')
|
|
4
|
+
|
|
3
5
|
const state = {}
|
|
4
6
|
const config = {
|
|
5
7
|
RED: null
|
|
@@ -18,7 +20,7 @@ function canSaveInStore (base, node, msg) {
|
|
|
18
20
|
// core check
|
|
19
21
|
if (base.acceptsClientConfig.includes(node.type)) {
|
|
20
22
|
// we are in a node type that allows for definition of specific clients,
|
|
21
|
-
if (msg
|
|
23
|
+
if (isClientScoped(msg)) {
|
|
22
24
|
// and a client has been defined
|
|
23
25
|
checks.push(false)
|
|
24
26
|
} else {
|
package/nodes/utils/index.js
CHANGED
|
@@ -40,6 +40,30 @@ async function getTopic (RED, config, wNode, msg) {
|
|
|
40
40
|
return topic
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function normalizeClientId (raw) {
|
|
44
|
+
if (Array.isArray(raw)) { raw = raw[0] }
|
|
45
|
+
return (raw === undefined || raw === null || raw === '') ? undefined : String(raw)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CLIENT_TARGETS = {
|
|
49
|
+
socketId: (conn) => conn.id,
|
|
50
|
+
clientId: (conn) => conn._clientId
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clientTargets (msg) {
|
|
54
|
+
return Object.entries(CLIENT_TARGETS)
|
|
55
|
+
.map(([field, connValue]) => ({ value: normalizeClientId(msg?._client?.[field]), connValue }))
|
|
56
|
+
.filter((target) => target.value !== undefined)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isClientScoped (msg) {
|
|
60
|
+
return clientTargets(msg).length > 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function matchesClient (conn, msg) {
|
|
64
|
+
return clientTargets(msg).every((target) => target.value === target.connValue(conn))
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
/**
|
|
44
68
|
* Adds socket/client data to a msg payload, if enabled
|
|
45
69
|
*
|
|
@@ -60,7 +84,8 @@ function addConnectionCredentials (RED, msg, conn, config) {
|
|
|
60
84
|
...item._client,
|
|
61
85
|
...{
|
|
62
86
|
socketId: conn.id,
|
|
63
|
-
socketIp: conn.handshake?.address
|
|
87
|
+
socketIp: conn.handshake?.address,
|
|
88
|
+
clientId: normalizeClientId(conn.handshake?.query?.clientId)
|
|
64
89
|
}
|
|
65
90
|
}
|
|
66
91
|
return item
|
|
@@ -127,5 +152,8 @@ module.exports = {
|
|
|
127
152
|
appendTopic,
|
|
128
153
|
getTopic,
|
|
129
154
|
addConnectionCredentials,
|
|
155
|
+
normalizeClientId,
|
|
156
|
+
isClientScoped,
|
|
157
|
+
matchesClient,
|
|
130
158
|
getThirdPartyWidgets
|
|
131
159
|
}
|
|
@@ -230,7 +230,7 @@ module.exports = function (RED) {
|
|
|
230
230
|
},
|
|
231
231
|
onInput: function (msg, send, done) {
|
|
232
232
|
// use our own custom onInput in order to store history of msg payloads
|
|
233
|
-
if (!datastore.
|
|
233
|
+
if (!datastore.has(node.id)) {
|
|
234
234
|
datastore.save(base, node, [])
|
|
235
235
|
}
|
|
236
236
|
// To prevent ui_update messages from deleting old data, skip this section if no msg.payload present
|
|
@@ -268,33 +268,7 @@ module.exports = function (RED) {
|
|
|
268
268
|
const maxPoints = parseInt(config.removeOlderPoints)
|
|
269
269
|
|
|
270
270
|
if (maxPoints && config.removeOlderPoints) {
|
|
271
|
-
|
|
272
|
-
// client-side does this for _each_ line
|
|
273
|
-
// remove older points using datastore.filter instead of saving the whole array
|
|
274
|
-
const lineCounts = {}
|
|
275
|
-
const _msg = datastore.get(node.id) || []
|
|
276
|
-
|
|
277
|
-
// determine which message objects to keep (latest maxPoints per label)
|
|
278
|
-
const keepIndexes = []
|
|
279
|
-
let doFiltering = false
|
|
280
|
-
for (let i = _msg.length - 1; i >= 0; i--) {
|
|
281
|
-
const m = _msg[i]
|
|
282
|
-
const label = m.topic
|
|
283
|
-
lineCounts[label] = lineCounts[label] || 0
|
|
284
|
-
if (lineCounts[label] < maxPoints) {
|
|
285
|
-
keepIndexes[i] = true
|
|
286
|
-
lineCounts[label]++
|
|
287
|
-
} else {
|
|
288
|
-
doFiltering = true
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// filter the datastore to only keep the selected messages
|
|
293
|
-
if (doFiltering) {
|
|
294
|
-
datastore.filter(base, node, (m, i) => {
|
|
295
|
-
return keepIndexes[i]
|
|
296
|
-
})
|
|
297
|
-
}
|
|
271
|
+
datastore.keepLatestPerTopic(base, node, maxPoints)
|
|
298
272
|
}
|
|
299
273
|
|
|
300
274
|
if (config.xAxisType === 'time' && config.removeOlder && config.removeOlderUnit) {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
oneditprepare: function () {
|
|
25
25
|
const node = this
|
|
26
26
|
const sel = $('#node-input-events')
|
|
27
|
-
for (const name of ['all', 'change', 'connect']) {
|
|
27
|
+
for (const name of ['all', 'change', 'connect', 'clients']) {
|
|
28
28
|
const text = c_('events.' + name)
|
|
29
29
|
$('<option/>').val(name).text(text).appendTo(sel)
|
|
30
30
|
}
|
|
@@ -411,7 +411,35 @@ module.exports = function (RED) {
|
|
|
411
411
|
node.error('No UI configured')
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
let onClientEvent = null
|
|
415
|
+
let clientEventStore = null
|
|
416
|
+
if (ui && (config.events === 'all' || config.events === 'clients')) {
|
|
417
|
+
clientEventStore = ui.uiShared?.clientStore
|
|
418
|
+
if (clientEventStore) {
|
|
419
|
+
onClientEvent = (e) => {
|
|
420
|
+
const wNode = RED.nodes.getNode(node.id)
|
|
421
|
+
if (wNode && typeof wNode.send === 'function') {
|
|
422
|
+
const client = { clientId: e.clientId }
|
|
423
|
+
if (e.socketId) { client.socketId = e.socketId }
|
|
424
|
+
let msg = { payload: 'client-' + e.event, _client: client }
|
|
425
|
+
const conn = e.socketId ? ui.uiShared?.connections?.[e.socketId] : null
|
|
426
|
+
if (conn) { msg = addConnectionCredentials(RED, msg, conn, ui) }
|
|
427
|
+
wNode.send(msg)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
clientEventStore.events.on('client', onClientEvent)
|
|
431
|
+
setImmediate(() => {
|
|
432
|
+
for (const c of clientEventStore.list()) {
|
|
433
|
+
onClientEvent({ event: 'connected', clientId: c.clientId, socketId: c.socketId })
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
414
439
|
node.on('close', function (removed, done) {
|
|
440
|
+
if (onClientEvent && clientEventStore) {
|
|
441
|
+
clientEventStore.events.off('client', onClientEvent)
|
|
442
|
+
}
|
|
415
443
|
if (removed) {
|
|
416
444
|
// handle node being removed
|
|
417
445
|
ui?.deregister(null, null, node)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowfuse/node-red-dashboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.1-034c8a5-202610061312.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"
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"marked-highlight": "^2.2.3",
|
|
99
99
|
"medium-zoom": "^1.1.0",
|
|
100
100
|
"mermaid": "^11.12.2",
|
|
101
|
-
"mocha": "^
|
|
101
|
+
"mocha": "^12.0.1",
|
|
102
102
|
"node-red": "^4.1.8",
|
|
103
103
|
"node-red-node-test-helper": "^0.3.5",
|
|
104
104
|
"npm-run-all": "^4.1.5",
|