@flowfuse/node-red-dashboard 1.31.1-e3609ab-202609141637.0 → 1.32.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 (88) hide show
  1. package/dist/apple-touch-icon.png +0 -0
  2. package/dist/assets/abnfDiagram-N423BO3Z-BOzAedUe.js +1 -0
  3. package/dist/assets/architectureDiagram-T3A2C74G-zEM8gtfO.js +36 -0
  4. package/dist/assets/blockDiagram-VBNYF7ZC-eKuDT4u2.js +132 -0
  5. package/dist/assets/c4Diagram-5PPSVZJV-CRPvpwR2.js +10 -0
  6. package/dist/assets/chunk-2GRJ4B5K-BUP9p1Ar.js +1 -0
  7. package/dist/assets/chunk-2Q5K7J3B-DASHHxaC.js +1 -0
  8. package/dist/assets/chunk-5RXB4S5H-C76llYJF.js +231 -0
  9. package/dist/assets/chunk-5VM5RSS4-DnmzQEFn.js +15 -0
  10. package/dist/assets/chunk-6Q2QTUOP-DpGB6o5S.js +88 -0
  11. package/dist/assets/chunk-GF5L2VYU-Bhzwn3VL.js +206 -0
  12. package/dist/assets/chunk-JWPE2WC7-UNqcBwu_.js +1 -0
  13. package/dist/assets/chunk-KBJHAD2P-C38IW_UU.js +1 -0
  14. package/dist/assets/chunk-RYQCIY6F-COKxTTcO.js +1 -0
  15. package/dist/assets/chunk-XXDRQBXY-BS0ojEzX.js +1 -0
  16. package/dist/assets/classDiagram-JCYQIIEL-CxMqqBtQ.js +1 -0
  17. package/dist/assets/classDiagram-v2-OCEON4UE-CxMqqBtQ.js +1 -0
  18. package/dist/assets/cose-bilkent-JH36ORCC-9jRlXuji.js +1 -0
  19. package/dist/assets/cynefin-VYW2F7L2-BDPznSFQ.js +178 -0
  20. package/dist/assets/cynefinDiagram-MW4NZA55-lSqlEkCZ.js +62 -0
  21. package/dist/assets/cytoscape.esm-D_LviqZs.js +331 -0
  22. package/dist/assets/dagre-VZM6K2ZE-Dfoe_jp9.js +4 -0
  23. package/dist/assets/diagram-7IWD3JNH-Df2T__dV.js +30 -0
  24. package/dist/assets/diagram-B4RE2ZJO-DoGtC0h7.js +3 -0
  25. package/dist/assets/diagram-LBJQPF4R-cH8DVJ6C.js +24 -0
  26. package/dist/assets/diagram-Q27KOJAE-8JJAGHRF.js +24 -0
  27. package/dist/assets/diagram-UB23O5K3-BGCDqdpG.js +41 -0
  28. package/dist/assets/disconnected-BuxohaUu.png +0 -0
  29. package/dist/assets/ebnfDiagram-BXEA7PRR-r2bgA64b.js +1 -0
  30. package/dist/assets/echarts-BB8VCqSB.js +73 -0
  31. package/dist/assets/erDiagram-JOGREHBK-BExdsKTd.js +85 -0
  32. package/dist/assets/flowDiagram-UKHOOZJN-B8wwUqLz.js +156 -0
  33. package/dist/assets/ganttDiagram-PKOTCBZU-SViiTCQN.js +292 -0
  34. package/dist/assets/gitGraphDiagram-DS77QQ5N-CmktWysd.js +106 -0
  35. package/dist/assets/graph-C9eacEi8.js +1 -0
  36. package/dist/assets/index-BzBvlpBo.js +124 -0
  37. package/dist/assets/index-DHk7_39t.css +13 -0
  38. package/dist/assets/infoDiagram-6WML65LV-BwUFVmXD.js +2 -0
  39. package/dist/assets/ishikawaDiagram-WSZJBQD7-Df6P2A6T.js +70 -0
  40. package/dist/assets/journeyDiagram-NVQOT4AX-Cjk4fifu.js +139 -0
  41. package/dist/assets/kanban-definition-27J2QSJJ-BcoyAznM.js +89 -0
  42. package/dist/assets/katex-C5jXJg4s.js +257 -0
  43. package/dist/assets/layout-DEXfKzaS.js +1 -0
  44. package/dist/assets/logo-DIAzbBuw.png +0 -0
  45. package/dist/assets/map-Czzmt4hB.js +1 -0
  46. package/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
  47. package/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
  48. package/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
  49. package/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
  50. package/dist/assets/mermaid-CX4mRs5N.js +314 -0
  51. package/dist/assets/mindmap-definition-FAOFIHXS-D6njotiS.js +96 -0
  52. package/dist/assets/pegDiagram-VL7TDLO6-CaA_iXxT.js +1 -0
  53. package/dist/assets/pieDiagram-7S7Q4E2Y-aYh5694F.js +39 -0
  54. package/dist/assets/quadrantDiagram-CIZ2JOQS-CaWyb4Jw.js +7 -0
  55. package/dist/assets/railroadDiagram-AXF67PYL-Bd_vMLsE.js +1 -0
  56. package/dist/assets/requirementDiagram-LRYGKXZP-CMxISoyd.js +84 -0
  57. package/dist/assets/sankeyDiagram-W5VNT64P-DLbFAyXe.js +40 -0
  58. package/dist/assets/sequenceDiagram-SI44F4Z6-CzdhMpvP.js +162 -0
  59. package/dist/assets/sizeCapture-X5ZJPWSS-CnezfYZ6.js +1 -0
  60. package/dist/assets/stateDiagram-OKZ733FA-5R9RLDEM.js +1 -0
  61. package/dist/assets/stateDiagram-v2-UEYNNEHI-Ck4l-gcT.js +1 -0
  62. package/dist/assets/swimlanes-SLNWSIFB-CeEnGZwJ.js +2 -0
  63. package/dist/assets/swimlanesDiagram-ULZ7WXOC-DSebamlh.js +8 -0
  64. package/dist/assets/timeline-definition-Z64GVDOM-ZMWCENQJ.js +120 -0
  65. package/dist/assets/vennDiagram-T6HMQDX7-DVr1jz1I.js +34 -0
  66. package/dist/assets/vue-vendor-Bmy9n80X.js +49 -0
  67. package/dist/assets/wardleyDiagram-T6FBY63Y-DNp6yzs5.js +78 -0
  68. package/dist/assets/workbox-window.prod.es5-BBnX5xw4.js +2 -0
  69. package/dist/assets/xychartDiagram-ELKLHX3M-DC4NZzl1.js +7 -0
  70. package/dist/favicon.ico +0 -0
  71. package/dist/favicon.svg +482 -0
  72. package/dist/index.html +24 -0
  73. package/dist/logo-512x512.png +0 -0
  74. package/dist/logo.svg +482 -0
  75. package/dist/maskable-icon-512x512.png +0 -0
  76. package/dist/pwa-192x192.png +0 -0
  77. package/dist/pwa-512x512.png +0 -0
  78. package/dist/pwa-64x64.png +0 -0
  79. package/dist/sw.js +2 -0
  80. package/nodes/config/ui_base.js +12 -7
  81. package/nodes/store/clients.js +71 -0
  82. package/nodes/store/data.js +3 -1
  83. package/nodes/store/state.js +3 -1
  84. package/nodes/utils/index.js +29 -1
  85. package/nodes/widgets/locales/en-US/ui_control.json +2 -1
  86. package/nodes/widgets/ui_control.html +1 -1
  87. package/nodes/widgets/ui_control.js +28 -0
  88. package/package.json +2 -2
@@ -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 }
@@ -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._client?.socketId) {
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)
@@ -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._client?.socketId) {
23
+ if (isClientScoped(msg)) {
22
24
  // and a client has been defined
23
25
  checks.push(false)
24
26
  } else {
@@ -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
  }
@@ -7,7 +7,8 @@
7
7
  "events": {
8
8
  "all": "All Events",
9
9
  "change": "Page/Tab Change Events Only",
10
- "connect": "Connection Events Only"
10
+ "connect": "Connection Events Only",
11
+ "clients": "Client Presence Events Only"
11
12
  }
12
13
  }
13
14
  }
@@ -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.31.1-e3609ab-202609141637.0",
3
+ "version": "1.32.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": "^11.7.5",
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",