@flowfuse/nr-project-nodes 0.4.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.
@@ -0,0 +1,475 @@
1
+ <style>
2
+ #dialog-form > div.form-row > .ff-project-link-input-width {
3
+ width: calc(100% - 125px);
4
+ }
5
+ #dialog-form > div.form-row > span.ff-project-link-group-option > input {
6
+ width: auto;
7
+ display: inline-block;
8
+ vertical-align: middle;
9
+ margin: 0px 2px 2px 0px;
10
+ }
11
+ #dialog-form > div.form-row > span.ff-project-link-group-option > label {
12
+ width: 250px;
13
+ display: inline-block;
14
+ }
15
+ </style>
16
+
17
+ <script type="text/html" data-template-name="project link in">
18
+ <div class="form-row">
19
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name">Name</span></label>
20
+ <input type="text" id="node-input-name" class="ff-project-link-input-width">
21
+ </div>
22
+ <div class="form-row ff-project-link-list-row">
23
+ <label><i class="fa fa-sign-in"></i> <span>Source</span></label>
24
+ <span class="ff-project-link-group-option">
25
+ <input type="radio" id="ff-project-link-radio-input-direct" name="ff-project-link-broadcast" value="false" checked />
26
+ <label for="ff-project-link-radio-input-direct">Receive messages sent to this instance</label>
27
+ </span>
28
+ </div>
29
+ <div class="form-row ff-project-link-list-row">
30
+ <label><span>&nbsp</span></label>
31
+ <span class="ff-project-link-group-option">
32
+ <input type="radio" id="ff-project-link-radio-input-broadcast" name="ff-project-link-broadcast" value="true" />
33
+ <label for="ff-project-link-radio-input-broadcast">Listen for broadcast messages from</label>
34
+ </span>
35
+ </div>
36
+ <div class="form-row ff-project-link-list-row">
37
+ <label><span>&nbsp</span></label>
38
+ <select id="node-input-projectList" class="ff-project-link-input-width" disabled>
39
+ </select>
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-input-topic"><i class="fa fa-ellipsis-h"></i> <span>Topic</span></label>
43
+ <input type="text" id="node-input-topic" class="ff-project-link-input-width">
44
+ </div>
45
+ </script>
46
+
47
+ <script type="text/html" data-template-name="project link out">
48
+ <div class="form-row">
49
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name">Name</span></label>
50
+ <input type="text" id="node-input-name" class="ff-project-link-input-width">
51
+ </div>
52
+ <div class="form-row">
53
+ <label for="node-input-mode"><i class="fa fa-cog"></i> <span>Mode</span></label>
54
+ <select id="node-input-mode" class="ff-project-link-input-width">
55
+ <option value="link" selected>Send to specified project node</option>
56
+ <option value="return">Return to project link call</option>
57
+ </select>
58
+ </div>
59
+
60
+ <div class="form-row ff-project-link-list-row">
61
+ <label><i class="fa fa-sign-out"></i> <span>Target</span></label>
62
+ <span class="ff-project-link-group-option">
63
+ <input type="radio" id="ff-project-link-radio-input-direct" name="ff-project-link-broadcast" value="false" checked />
64
+ <label for="ff-project-link-radio-input-direct">Send message to instance</label>
65
+ </span>
66
+ </div>
67
+ <div class="form-row ff-project-link-list-row">
68
+ <label><span>&nbsp</span></label>
69
+ <select id="node-input-projectList" class="ff-project-link-input-width" disabled>
70
+ </select>
71
+ </div>
72
+ <div class="form-row ff-project-link-list-row">
73
+ <label><span>&nbsp</span></label>
74
+ <span class="ff-project-link-group-option">
75
+ <input type="radio" id="ff-project-link-radio-input-broadcast" name="ff-project-link-broadcast" value="true" />
76
+ <label for="ff-project-link-radio-input-broadcast">Broadcast message to all instances</label>
77
+ </span>
78
+ </div>
79
+ <div class="form-row ff-project-link-topic-row">
80
+ <label for="node-input-topic"><i class="fa fa-ellipsis-h"></i> <span>Topic</span></label>
81
+ <input type="text" id="node-input-topic" class="ff-project-link-input-width">
82
+ </div>
83
+ </script>
84
+ <script type="text/html" data-template-name="project link call">
85
+ <div class="form-row">
86
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name">Name</span></label>
87
+ <input type="text" id="node-input-name" class="ff-project-link-input-width">
88
+ </div>
89
+ <div class="form-row">
90
+ <label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="exec.label.timeout"></span>Timeout</label>
91
+ <input type="text" id="node-input-timeout" placeholder="30" style="width: 80px; margin-right: 5px;">
92
+ <span data-i18n="inject.seconds">sec</span>
93
+ </div>
94
+ <div class="form-row">
95
+ <label for="node-input-projectList"><i class="fa fa-sign-out"></i> <span>Target</span></label>
96
+ <select id="node-input-projectList" class="ff-project-link-input-width">
97
+ </select>
98
+ </div>
99
+ <div class="form-row">
100
+ <label for="node-input-topic"><i class="fa fa-ellipsis-h"></i> <span>Topic</span></label>
101
+ <input type="text" id="node-input-topic" class="ff-project-link-input-width">
102
+ </div>
103
+ </script>
104
+
105
+ <script type="text/javascript">
106
+ /* global RED */
107
+ (function () {
108
+ /**
109
+ * Test a topic string is valid for subscription...
110
+ * * Must not contain the following characters: + # $ \
111
+ * * Must not start with a slash
112
+ * @param {string} subTopic
113
+ * @returns `true` if it is a valid sub topic
114
+ */
115
+ function isValidSubscriptionTopic (subTopic) {
116
+ return /^(?:(?:[^/$+#\b\f\n\r\t\v\0]+)(?:\/(?:[^/$+#\b\f\n\r\t\v\0]+?))*)$/.test(subTopic)
117
+ }
118
+
119
+ /**
120
+ * Test a topic string is valid for publishing...
121
+ * * Must not contain the following characters: + # $ \
122
+ * * Must not start with a slash
123
+ * @param {string} subTopic
124
+ * @returns `true` if it is a valid sub topic
125
+ */
126
+ function isValidPublishTopic (subTopic) {
127
+ return /^(?:(?:[^/$+#\b\f\n\r\t\v\0]+)(?:\/(?:[^/$+#\b\f\n\r\t\v\0]+?))*)$/.test(subTopic)
128
+ }
129
+
130
+ function onEditPrepare (node, targetType) {
131
+ let isBroadcast = node.broadcast === true
132
+ let isReturnMode = node.mode === 'return'
133
+
134
+ // Initialise UI state according to node state
135
+ loadProjectList('#node-input-projectList', node.project, targetType || node.type)
136
+ $('#node-input-name').val(node.name)
137
+ $('#node-input-topic').val(node.topic)
138
+ $('#node-input-timeout').val(node.timeout)
139
+ $('#node-input-mode').val(node.mode)
140
+ $('input:radio[name="ff-project-link-broadcast"]').val([isBroadcast.toString()])
141
+
142
+ // watch for switch between broadcast and p2p
143
+ $('input:radio[name="ff-project-link-broadcast"]').on('change', function (e) {
144
+ isBroadcast = e.target.value === 'true'
145
+ updateUI()
146
+ })
147
+
148
+ // watch for switch between out and return mode
149
+ if (node.type === 'project link out') {
150
+ $('#node-input-mode').on('change', function (e) {
151
+ isReturnMode = e.target.value === 'return'
152
+ updateUI()
153
+ })
154
+ }
155
+
156
+ updateUI()
157
+
158
+ function updateUI () {
159
+ if (node.type === 'project link out' && isReturnMode) {
160
+ $('.ff-project-link-topic-row').hide()
161
+ $('.ff-project-link-list-row').hide()
162
+ $('#node-input-projectList').prop('disabled', true)
163
+ } else if (node.type === 'project link out') {
164
+ $('.ff-project-link-topic-row').show()
165
+ $('.ff-project-link-list-row').show()
166
+ $('#node-input-projectList').prop('disabled', isBroadcast ? true : null)
167
+ } else if (node.type === 'project link in') {
168
+ $('.ff-project-link-topic-row').show()
169
+ $('.ff-project-link-list-row').show()
170
+ $('#node-input-projectList').prop('disabled', isBroadcast ? null : true)
171
+ } else {
172
+ $('.ff-project-link-topic-row').show()
173
+ $('.ff-project-link-list-row').show()
174
+ $('#node-input-projectList').prop('disabled', null)
175
+ }
176
+ }
177
+ }
178
+
179
+ function onEditSave (node) {
180
+ node.project = $('#node-input-projectList').val()
181
+ node.name = $('#node-input-name').val()
182
+ node.topic = $('#node-input-topic').val()
183
+
184
+ if (node.type === 'project link call') {
185
+ node.timeout = $('#node-input-timeout').val()
186
+ }
187
+ if (node.type === 'project link out') {
188
+ node.mode = $('#node-input-mode').val()
189
+ }
190
+ if (node.type === 'project link in' || node.type === 'project link out') {
191
+ node.broadcast = $('input:radio[name="ff-project-link-broadcast"]:checked').val() === 'true'
192
+ }
193
+ }
194
+
195
+ function loadProjectList (selector, val, nodeType) {
196
+ const el = $(selector)
197
+ if (!el || !el.length) {
198
+ return
199
+ }
200
+ el.prop('disabled', true)
201
+ val = val || el.val()
202
+ // if ajax call fails, we still want the original value set in the selector
203
+ // so that if the user clicks Done (or the workspace) the same value is
204
+ // re-entered preventing loss or change of original value
205
+ if (val) {
206
+ /** @type {HTMLOptionsCollection} */ const options = el[0].options
207
+ if (!options || !options.length || options.selectedIndex < 0 || options.item(options.selectedIndex).value !== val) {
208
+ options.selectedIndex = -1
209
+ el.append(new Option(val, val, false, true))
210
+ }
211
+ }
212
+ $.ajax({
213
+ url: 'nr-project-link/projects',
214
+ type: 'GET',
215
+ datatype: 'json'
216
+ })
217
+ .done(function (data) {
218
+ el.empty()
219
+ // broadcast not permitted in link call at this time but has been
220
+ // considered in the code base - possible future iteration
221
+ if (nodeType === 'project link in') {
222
+ el.append(new Option('all instances', 'all', false, val === 'all'))
223
+ }
224
+ const projects = (data.count ? data.projects : null) || []
225
+ for (let index = 0; index < projects.length; index++) {
226
+ const item = projects[index]
227
+ el.append(new Option(item.name, item.id, false, item.id === val))
228
+ }
229
+ $('input:radio[name="ff-project-link-broadcast"]:checked').trigger('change')
230
+ })
231
+ .fail(function (jqXHR, textStatus, errorThrown) {
232
+ $('input:radio[name="ff-project-link-broadcast"]:checked').trigger('change')
233
+ console.error(jqXHR, textStatus, errorThrown)
234
+ })
235
+ }
236
+
237
+ function onAdd () {
238
+ if (this.name === '_DEFAULT_') {
239
+ this.name = ''
240
+ RED.actions.invoke('core:generate-node-names', this, { generateHistory: false })
241
+ }
242
+ }
243
+
244
+ RED.nodes.registerType('project link in', {
245
+ category: 'common',
246
+ color: '#87D8CF',
247
+ defaults: {
248
+ name: { value: '_DEFAULT_' },
249
+ project: { value: '', required: true },
250
+ broadcast: { value: false, required: true },
251
+ topic: { value: '', required: true, validate: isValidSubscriptionTopic }
252
+ },
253
+ inputs: 0,
254
+ outputs: 1,
255
+ icon: 'ff-logo.svg',
256
+ paletteLabel: 'project in',
257
+ outputLabels: function (i) {
258
+ return this.name || 'link in'
259
+ },
260
+ // showLabel: false,
261
+ label: function () {
262
+ return this.name || this.topic || 'link in'
263
+ },
264
+ labelStyle: function () {
265
+ return this.name ? 'node_label_italic' : ''
266
+ },
267
+ oneditprepare: function () {
268
+ onEditPrepare(this, 'project link in')
269
+ },
270
+ oneditsave: function () {
271
+ onEditSave(this)
272
+ },
273
+ onadd: onAdd
274
+ })
275
+
276
+ RED.nodes.registerType('project link out', {
277
+ category: 'common',
278
+ color: '#87D8CF',
279
+ defaults: {
280
+ name: { value: '_DEFAULT_' },
281
+ mode: { value: 'link' }, // link || return
282
+ broadcast: { value: false, required: true },
283
+ project: { value: '', required: true },
284
+ topic: {
285
+ value: '',
286
+ validate: function (v) {
287
+ if (this.mode === 'return') {
288
+ return true
289
+ }
290
+ return v && isValidPublishTopic(v)
291
+ }
292
+ }
293
+ },
294
+ align: 'right',
295
+ inputs: 1,
296
+ outputs: 0,
297
+ icon: function () {
298
+ if (this.mode === 'return') {
299
+ return 'ff-logo.svg'
300
+ } else {
301
+ return 'ff-logo.svg'
302
+ }
303
+ },
304
+ paletteLabel: 'project out',
305
+ inputLabels: function (i) {
306
+ return this.name || (this.mode === 'return' ? 'link return' : 'link out')
307
+ },
308
+ // showLabel: false,
309
+ label: function () {
310
+ return this.name || (this.mode === 'return' ? 'link return' : this.topic) || 'link out'
311
+ },
312
+ labelStyle: function () {
313
+ return this.name ? 'node_label_italic' : ''
314
+ },
315
+ oneditprepare: function () {
316
+ onEditPrepare(this, 'project link out')
317
+ $('#node-input-mode').on('change', function () {
318
+ $('.node-input-link-rows').toggle(this.value === 'link')
319
+ })
320
+ if (!this.mode) {
321
+ $('#node-input-mode').val('link').trigger('change')
322
+ }
323
+ },
324
+ oneditsave: function () {
325
+ onEditSave(this)
326
+ },
327
+ onadd: onAdd
328
+ })
329
+
330
+ RED.nodes.registerType('project link call', {
331
+ category: 'common',
332
+ color: '#87D8CF',
333
+ defaults: {
334
+ name: { value: '' },
335
+ project: { value: '', required: true },
336
+ topic: { value: '', required: true, validate: isValidPublishTopic },
337
+ timeout: {
338
+ value: '30',
339
+ label: RED._('node-red:link.timeout'),
340
+ validate: RED.validators.number(true)
341
+ }
342
+ },
343
+ inputs: 1,
344
+ outputs: 1,
345
+ icon: 'ff-logo.svg',
346
+ paletteLabel: 'project call',
347
+ inputLabels: function (i) {
348
+ return this.name || 'link call'
349
+ },
350
+ label: function () {
351
+ return this.name || this.topic || 'link call'
352
+ },
353
+ labelStyle: function () {
354
+ return this.name ? 'node_label_italic' : ''
355
+ },
356
+ oneditprepare: function () {
357
+ onEditPrepare(this, 'project link call')
358
+ },
359
+ oneditsave: function () {
360
+ onEditSave(this)
361
+ }
362
+ })
363
+ })()
364
+ </script>
365
+
366
+ <script type="text/html" data-help-name="project link in">
367
+ <p>Receive messages from other Node-RED instances within your FlowFuse Team</p>
368
+ <h3>Details</h3>
369
+ <p>This node can either listen for messages broadcast by other instances,
370
+ or listen for messages sent directly to this instance.</p>
371
+ <p>The node is configured with a <code>topic</code> to listen on. This works
372
+ like an MQTT topic - allowing projects to send messages targeting different
373
+ subscribers.</p>
374
+ <p>The node does not support MQTT wildcard characters - a fully qualified topic
375
+ must be used.</p>
376
+ <h3>Output</h3>
377
+ <dl class="message-properties">
378
+ <dt><i>msg</i> <span class="property-type">object</span></dt>
379
+ <dd>
380
+ The message sent by another Node-RED instance to this node.
381
+ </dd>
382
+ <dt><i>projectLink</i> <span class="property-type">object</span></dt>
383
+ <dd>
384
+ This property contains information about the source of the message.
385
+ <ul>
386
+ <li><code>instanceId</code> - the id of the instance that sent the message</li>
387
+ <li><code>projectId</code> - <i>deprecated</i>: the id of the instance that sent the message</li>
388
+ <li><code>deviceId</code> - if present, the id of the device that sent the message</li>
389
+ <li><code>deviceName</code> - if present, the name of the device that sent the message</li>
390
+ <li><code>deviceType</code> - if present, the type of the device that sent the message</li>
391
+ <li><code>topic</code> - the topic the message was received on</li>
392
+ <li><code>callStack</code> - when using the Project Call node, this contains
393
+ information about the call stack. This property must not be modified.</li>
394
+ </ul>
395
+ </dd>
396
+ </dl>
397
+ </script>
398
+
399
+ <script type="text/html" data-help-name="project link out">
400
+ <p>Send messages to other projects within your FlowForge Team</p>
401
+ <h3>Details</h3>
402
+ <p>This node can be used to send messages to other Node-RED instances.</p>
403
+ <p>
404
+ It provides three modes of operation:
405
+ <ul>
406
+ <li>send messages to another instance</li>
407
+ <li>broadcast messages to any instance listening on the same topic</li>
408
+ <li>return the message to its sender if it originated from a Project Call node</li>
409
+ </ul>
410
+ </p>
411
+ <p>
412
+ When configured to send or broadcast messages to other instances, the node
413
+ is configured with a <code>topic</code> to send on. This works
414
+ like an MQTT topic - allowing instances to send messages targeting different
415
+ subscribers.
416
+ </p>
417
+ <p>
418
+ When configured to return the message to the previous Project Call node,
419
+ the node requires the property <code>msg.projectLink.callStack</code> to
420
+ be present. This property is set by the Project In node when it receives
421
+ messages from a Project Call node. If this property is not present, the
422
+ node will be unable to respond properly.
423
+ </p>
424
+ <h3>Input</h3>
425
+ <dl class="message-properties">
426
+ <dt><i>msg</i> <span class="property-type">object</span></dt>
427
+ <dd>
428
+ <p>The node will send the complete message object it receives.</p>\
429
+ <p>Due to the way messages are sent, not all types of property will
430
+ be included. For example, the <code>msg.req</code> and <code>msg.res</code> properties
431
+ used by the HTTP nodes will not be sent.
432
+ </p>
433
+ </dd>
434
+ </dl>
435
+ </script>
436
+
437
+ <script type="text/html" data-help-name="project link call">
438
+ <p>Send messages to other Node-RED instances within your FlowForge Team and get a response back</p>
439
+ <h3>Details</h3>
440
+ <p>
441
+ This node can be used to send messages to other instances and then wait
442
+ for a response to be sent back.
443
+ </p>
444
+ <p>
445
+ The node can be configured with a <code>timeout</code> for how long it
446
+ should wait for a response. If a response does not arrive, it will log
447
+ an error that can be caught with a Catch node
448
+ </p>
449
+ <p>
450
+ The node is configured with a <code>topic</code> to send on. This works
451
+ like an MQTT topic - allowing instances to send messages targeting different
452
+ subscribers.
453
+ </p>
454
+ <h3>Output</h3>
455
+ <dl class="message-properties">
456
+ <dt><i>msg</i> <span class="property-type">object</span></dt>
457
+ <dd>
458
+ The message sent by another instance to this node.
459
+ </dd>
460
+ <dt><i>projectLink</i> <span class="property-type">object</span></dt>
461
+ <dd>
462
+ This property contains information about the source of the message.
463
+ <ul>
464
+ <li><code>instanceId</code> - the id of the instance that sent the message</li>
465
+ <li><code>projectId</code> - <i>deprecated</i>: the id of the instance that sent the message</li>
466
+ <li><code>deviceId</code> - if present, the id of the device that sent the message</li>
467
+ <li><code>deviceName</code> - if present, the name of the device that sent the message</li>
468
+ <li><code>deviceType</code> - if present, the type of the device that sent the message</li>
469
+ <li><code>topic</code> - the topic the message was received on</li>
470
+ <li><code>callStack</code> - when using the Project Call node, this contains
471
+ information about the call stack. This property must not be modified.</li>
472
+ </ul>
473
+ </dd>
474
+ </dl>
475
+ </script>