@theotherwillembotha/node-red-circuitbreaker 0.0.53

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,2190 @@
1
+
2
+ <!--
3
+
4
+ This file was automatically generated using the NODERED Core utility.
5
+ Any modifications to his file will be overwritten the next time the code is regenerated.
6
+
7
+ You have been warned.
8
+
9
+ -->
10
+
11
+ <!-- CircuitBreakerConfigNode -->
12
+ <style>
13
+ .theotherwillembotha-compact-table li {
14
+ padding-top: 4px;
15
+ padding-bottom: 4px;
16
+ }
17
+
18
+ </style>
19
+ <script type="text/javascript">
20
+ RED.nodes.registerType('CircuitBreakerConfigNode', {
21
+ category: 'config',
22
+ label: function() {
23
+ return this.name
24
+ },
25
+ paletteLabel: 'Circuit Breaker Config',
26
+ defaults: {
27
+ name: {
28
+ value: 'Circuit Breaker Config Example',
29
+ required: true
30
+ },
31
+ defaultState: {
32
+ value: 'Closed',
33
+ required: true
34
+ },
35
+ },
36
+ oneditprepare: function() {
37
+ // **** CircuitBreakerConfigNode **** //
38
+ {
39
+ let node = this;
40
+ $("#node-config-input-defaultState").typedInput({
41
+ type: "defaultState",
42
+ types: [{
43
+ value: "defaultState",
44
+ options: [{
45
+ value: "Closed",
46
+ label: "Closed"
47
+ }, {
48
+ value: "Open",
49
+ label: "Open"
50
+ }]
51
+ }]
52
+ });
53
+ }
54
+ },
55
+ oneditsave: function() {
56
+ // **** CircuitBreakerConfigNode **** //
57
+ {
58
+ let node = this;
59
+ }
60
+ },
61
+ oneditcancel: function() {},
62
+ oneditdelete: function() {},
63
+ });
64
+
65
+ </script>
66
+
67
+
68
+ <script type="text/html" data-template-name='CircuitBreakerConfigNode'>
69
+ <div id='section_CircuitBreakerConfigNode'>
70
+ <div class="form-row">
71
+ <label for="node-config-input-name" class="towb_editorlabel"><i class="fa fa-font"></i> Name</label>
72
+ <input type="text" id="node-config-input-name" />
73
+ </div>
74
+
75
+ <div class="form-row">
76
+ <label for="node-config-input-defaultState" class="towb_editorlabel"><i class="fa fa-power-off"></i> Default State</label>
77
+ <input type="text" id="node-config-input-defaultState" />
78
+ </div>
79
+
80
+
81
+ </div>
82
+ </script>
83
+
84
+ <script type="text/markdown" data-help-name='CircuitBreakerConfigNode'>
85
+ Shared state node for a circuit breaker instance. All Circuit Breaker nodes that should operate on the same breaker must reference the same config node.
86
+
87
+ ### Properties
88
+
89
+ : *name* (string) : A descriptive label for this circuit breaker instance.
90
+ : *default state* (select) : The state the breaker starts in when Node-RED deploys. **Closed** (default) - messages flow normally. **Open** - the breaker starts tripped.
91
+
92
+ ### Breaker context
93
+
94
+ Each config node holds a key/value context store (`BreakerContext`) that is accessible from Fault Detector and State scripts. Use `breaker.context().get(key, default)` and `breaker.context().set(key, value)` to store state between messages - for example to maintain a rolling failure window.
95
+ </script>
96
+ <!-- CircuitBreakerNode -->
97
+ <style>
98
+ .node_label_white {
99
+ fill: white
100
+ }
101
+
102
+ </style>
103
+ <!-- logger -->
104
+ <style>
105
+ .editorgroupborder {
106
+ border-width: 1px;
107
+ border-style: solid;
108
+ border-color: lightgray;
109
+ padding: 3px;
110
+ margin-top: 2px;
111
+ margin-bottom: 2px;
112
+ }
113
+
114
+ .editorsectionheading {
115
+ font-weight: 600;
116
+ margin-bottom: 1px !important;
117
+ margin-top: 6px !important;
118
+ }
119
+
120
+ .nomargin {
121
+ margin-bottom: 1px !important;
122
+ margin-top: 1px !important;
123
+ }
124
+
125
+ .slidecontainer {
126
+ width: 100%;
127
+ }
128
+
129
+ .slider {
130
+ -webkit-appearance: none;
131
+ appearance: none;
132
+ width: 100%;
133
+ height: 25px;
134
+ background: #d3d3d3;
135
+ outline: none;
136
+ opacity: 0.7;
137
+ -webkit-transition: .2s;
138
+ transition: opacity .2s;
139
+ }
140
+
141
+ .slider:hover {
142
+ opacity: 1;
143
+ }
144
+
145
+ .slider::-webkit-slider-thumb {
146
+ -webkit-appearance: none;
147
+ appearance: none;
148
+ width: 25px;
149
+ height: 25px;
150
+ background: #04AA6D;
151
+ cursor: pointer;
152
+ }
153
+
154
+ .slider::-moz-range-thumb {
155
+ width: 25px;
156
+ height: 25px;
157
+ background: #04AA6D;
158
+ cursor: pointer;
159
+ }
160
+
161
+ .towb_editorlabel {
162
+ width: 120px !important;
163
+ }
164
+
165
+ .towb_editorfield {
166
+ width: 70% !important;
167
+ }
168
+
169
+ .towb_editorfield_short {
170
+ width: 30% !important;
171
+ }
172
+
173
+ </style>
174
+ <script type="text/javascript">
175
+ // CSS
176
+ dropdownStyles = `
177
+ .loggertype-dropdown {
178
+ position: absolute;
179
+ background: white;
180
+ border: 1px solid #ccc;
181
+ border-radius: 4px;
182
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
183
+ max-height: 150px;
184
+ overflow-y: auto;
185
+ z-index: 1000;
186
+ min-width: 150px;
187
+ }
188
+ .loggertype-dropdown .dropdown-item {
189
+ padding: 6px 12px;
190
+ cursor: pointer;
191
+ }
192
+ .loggertype-dropdown .dropdown-item:hover {
193
+ background: #f0f0f0;
194
+ }
195
+ `;
196
+ $('<style>').text(dropdownStyles).appendTo('head');
197
+
198
+ </script>
199
+ <script type="text/javascript">
200
+ RED.nodes.registerType('CircuitBreakerNode', {
201
+ category: 'circuitbreaker',
202
+ icon: 'circuitrbreaker.png',
203
+ color: '#1D7874',
204
+ labelStyle: 'node_label_white',
205
+ label: function() {
206
+ return this.name
207
+ },
208
+ paletteLabel: 'Circuit Breaker',
209
+ inputs: 1,
210
+ inputLabels: (i) => 'input',
211
+ outputs: 2,
212
+ outputLabels: (i) => ['closed', 'open'][i],
213
+ defaults: {
214
+ name: {
215
+ value: 'CircuitBreaker Example',
216
+ required: true
217
+ },
218
+ circuitbreakerconfig: {
219
+ type: 'CircuitBreakerConfigNode',
220
+ required: true
221
+ },
222
+ logEnabled: {
223
+ value: false,
224
+ required: true
225
+ },
226
+ logger: {
227
+ required: false,
228
+ value: '',
229
+ type: 'DelegatedConfigReferenceNode'
230
+ },
231
+ logTemplateOverrideEnabled: {
232
+ value: false
233
+ },
234
+ logTemplateOverride: {
235
+ value: 'message:{{msg}}'
236
+ },
237
+ metricsEnabled: {
238
+ value: false,
239
+ required: true
240
+ },
241
+ metricsReference: {
242
+ type: 'MetricsConfigNode',
243
+ required: false
244
+ },
245
+ },
246
+ oneditprepare: function() {
247
+ // **** CircuitBreakerNode **** //
248
+ {
249
+ let node = this;
250
+ }
251
+ // **** logger **** //
252
+ {
253
+ let node = this;
254
+ // controls.
255
+ let logEnabled = $("#node-input-logEnabled");
256
+ let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
257
+ let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
258
+ let logEnabledSection = $("#section_logEnabled");
259
+ // functions.
260
+ let getLoggerTypes = async function() {
261
+ let types = [];
262
+ await $.ajax({
263
+ url: "nodetypeservice/find?tag=LoggerType",
264
+ type: "GET",
265
+ contentType: "application/json; charset=utf-8",
266
+ data: JSON.stringify({
267
+ id: node.id
268
+ }),
269
+ success: (response) => {
270
+ types = response;
271
+ },
272
+ error: (jqXHR, textStatus, errorThrown) => {
273
+ console.log(jqXHR, textStatus, errorThrown);
274
+ },
275
+ });
276
+ return types;
277
+ }
278
+ let getLoggers = async () => {
279
+ let loggers = [];
280
+ // get the list of types again.
281
+ let loggerTypes = (await getLoggerTypes()).map(type => type.id);
282
+ // add all of the known loggers.
283
+ RED.nodes.eachConfig(cfg => {
284
+ if (loggerTypes.includes(cfg.type)) {
285
+ loggers.push({
286
+ id: cfg.id,
287
+ name: cfg.label(),
288
+ type: cfg.type
289
+ });
290
+ }
291
+ });
292
+ return loggers;
293
+ };
294
+ let updateLoggerSelectorList = async () => {
295
+ loggerSelector.empty();
296
+ (await getLoggers()).forEach(logger => {
297
+ $('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
298
+ });
299
+ $('<option value="_ADD_">none</option>').appendTo(loggerSelector);
300
+ }
301
+ logEnabled.on('change', function() {
302
+ if (logEnabled.prop("checked")) {
303
+ logEnabledSection.show();
304
+ node._def.defaults.logger.required = true;
305
+ } else {
306
+ logEnabledSection.hide()
307
+ node._def.defaults.logger.required = false;
308
+ }
309
+ });
310
+ logTemplateEnabled.on('change', function() {
311
+ if (logTemplateEnabled.prop("checked")) {
312
+ logTemplateEnabledSection.show();
313
+ node._def.defaults.logTemplateOverride.required = true;
314
+ } else {
315
+ logTemplateEnabledSection.hide()
316
+ node._def.defaults.logTemplateOverride.required = false;
317
+ }
318
+ });
319
+ node.logTemplateOverrideEditor = RED.editor.createEditor({
320
+ id: 'node-input-logTemplateOverrideEditor',
321
+ mode: 'ace/mode/handlebars',
322
+ value: node.logTemplateOverride
323
+ });
324
+ let loggerSelector = $("#logger-selector");
325
+ let loggerEditButton = $("#logger-edit-btn");
326
+ let loggerAddButton = $("#logger-add-btn");
327
+ loggerEditButton.on('click', async function(event) {
328
+ event.stopPropagation();
329
+ if (loggerEditButton.hasClass("disabled")) {
330
+ return;
331
+ }
332
+ // figure out what type of logger this is.
333
+ let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
334
+ RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
335
+ });
336
+ loggerAddButton.on('click', function(event) {
337
+ event.stopPropagation();
338
+ let $btn = $(event.target);
339
+ // Remove any existing dropdown
340
+ $('.loggertype-dropdown').remove();
341
+ getLoggerTypes().then(loggerTypes => {
342
+ let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
343
+ loggerTypes.forEach(loggerType => {
344
+ $('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
345
+ padding: '4px 8px',
346
+ cursor: 'pointer'
347
+ }).appendTo($dropdown);
348
+ });
349
+ // Append first so outerWidth() is measurable, then position so the
350
+ // right edge of the dropdown aligns with the right edge of the button.
351
+ $('body').append($dropdown);
352
+ $dropdown.css({
353
+ top: $btn.offset().top + $btn.outerHeight(),
354
+ left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
355
+ });
356
+ $(document).on('mousedown.dropdown', function(e) {
357
+ if (!$(e.target).closest('.loggertype-dropdown').length) {
358
+ $dropdown.remove();
359
+ $(document).off('mousedown.dropdown'); // Clean up the listener
360
+ }
361
+ });
362
+ // Handle item selection
363
+ $dropdown.on('click', '.dropdown-item', function(e) {
364
+ e.stopPropagation();
365
+ const selectedValue = $(this).data('value');
366
+ const selectedText = $(this).text();
367
+ // Do whatever you need with the selection
368
+ console.log('Selected:', selectedValue, selectedText);
369
+ $dropdown.remove();
370
+ RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
371
+ });
372
+ })
373
+ });
374
+ loggerSelector.on("focus", updateLoggerSelectorList);
375
+ loggerSelector.on('change', function(event) {
376
+ if ("_ADD_" === loggerSelector.val()) {
377
+ loggerEditButton.addClass("disabled")
378
+ } else {
379
+ loggerEditButton.removeClass("disabled")
380
+ }
381
+ });
382
+ // lastly, make sure that we set the correct value on the logger selector.
383
+ updateLoggerSelectorList().then(() => {
384
+ loggerSelector.val(node.logger);
385
+ loggerSelector.trigger('change');
386
+ });
387
+ }
388
+ // **** metrics **** //
389
+ {
390
+ let node = this;
391
+ let metricsEnabled = $("#node-input-metricsEnabled");
392
+ let metricsEnabledSection = $("#section_metricsEnabled");
393
+ metricsEnabled.on('change', function() {
394
+ if (metricsEnabled.prop("checked")) {
395
+ metricsEnabledSection.show();
396
+ node._def.defaults.metricsReference.required = true;
397
+ } else {
398
+ metricsEnabledSection.hide()
399
+ node._def.defaults.metricsReference.required = false;
400
+ }
401
+ });
402
+ }
403
+ },
404
+ oneditsave: function() {
405
+ // **** CircuitBreakerNode **** //
406
+ {
407
+ let node = this;
408
+ }
409
+ // **** logger **** //
410
+ {
411
+ node = this;
412
+ var selectedLogger = $("#logger-selector").val();
413
+ node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
414
+ node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
415
+ node.logTemplateOverrideEditor.destroy();
416
+ delete node.logTemplateOverrideEditor;
417
+ }
418
+ // **** metrics **** //
419
+ {}
420
+ },
421
+ oneditcancel: function() {},
422
+ oneditdelete: function() {},
423
+ });
424
+
425
+ </script>
426
+
427
+
428
+ <script type="text/html" data-template-name='CircuitBreakerNode'>
429
+ <div id='section_CircuitBreakerNode'>
430
+ <div class="form-row">
431
+ <label for="node-input-name" class="towb_editorlabel"><i class="fa fa-font"></i> Name</label>
432
+ <input type="text" id="node-input-name" />
433
+ </div>
434
+
435
+ <div class="form-row">
436
+ <label for="node-input-circuitbreakerconfig" class="towb_editorlabel"><i class="fa fa-bolt"></i> Circuit Breaker</label>
437
+ <input type="text" id="node-input-circuitbreakerconfig" />
438
+ </div>
439
+
440
+ </div>
441
+
442
+ <div id='section_logger'>
443
+ <div id="section_loggerconfig">
444
+ <div class="form-row editorsectionheading">
445
+ <i class="w-16 fa fa-eye"></i> <span>Logging</span>
446
+ </div>
447
+ <div class="editorgroupborder">
448
+ <div class="form-row nomargin logEnableCheck">
449
+ <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
450
+ <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
451
+ </div>
452
+
453
+ <div id="section_logEnabled">
454
+ <div class="form-row nomargin">
455
+ <label for="logger-selector">Logger</label>
456
+ <div style="width: 70%; display: inline-flex;">
457
+ <select id="logger-selector" style="flex-grow: 1;" class="">
458
+ <option value="_ADD_">none</option>
459
+ </select>
460
+ <a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
461
+ <i class="fa fa-pencil"></i>
462
+ </a>
463
+ <a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
464
+ <i class="fa fa-plus"></i>
465
+ </a>
466
+ </div>
467
+ </div>
468
+
469
+ <div class="form-row nomargin">
470
+ <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
471
+ <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
472
+ </div>
473
+
474
+ <div id="section_logTemplateOverrideEnabled">
475
+ <div class="form-row">
476
+ <label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
477
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+
484
+ </div>
485
+
486
+ <div id='section_metrics'>
487
+ <div id="section_metricsconfig">
488
+ <div class="form-row editorsectionheading">
489
+ <i class="w-16 fa fa-eye"></i> <span>Metrics</span>
490
+ </div>
491
+ <div class="editorgroupborder">
492
+ <div class="form-row nomargin metricsEnableCheck">
493
+ <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
494
+ <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
495
+ </div>
496
+
497
+ <div id="section_metricsEnabled">
498
+ <div class="form-row nomargin">
499
+ <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
500
+ <input id="node-input-metricsReference" placeholder="metrics" />
501
+ </div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+
506
+ </div>
507
+ </script>
508
+
509
+ <script type="text/markdown" data-help-name='CircuitBreakerNode'>
510
+ Routes incoming messages based on the current state of the linked circuit breaker. Place this node before any integration that may fail.
511
+
512
+ ### Properties
513
+
514
+ : *name* (string) : A descriptive label for this node.
515
+ : *circuit breaker* (Circuit Breaker Config) : The circuit breaker instance to check on each message.
516
+
517
+ ### Inputs
518
+
519
+ : *msg* (object) : Any message. The content is not inspected - only the breaker state determines routing.
520
+
521
+ ### Outputs
522
+
523
+ 1. **Closed** - the breaker is closed (healthy). The message is forwarded normally for processing.
524
+ 2. **Open** - the breaker is tripped. The message is redirected so it can be buffered, dropped, or handled gracefully.
525
+
526
+ ### Status indicator
527
+
528
+ The node displays a green dot when the breaker is closed and a red dot when it is open. The status updates automatically whenever the breaker state changes.
529
+
530
+ ### Logging
531
+ : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
532
+ : *logger* (logconfig) : the endppoint that the node will log events to.
533
+ : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
534
+ : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
535
+
536
+ ### Metrics
537
+ : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
538
+ : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
539
+ </script>
540
+ <!-- CircuitBreakerFaultDetectorNode -->
541
+ <style>
542
+ .node_label_white {
543
+ fill: white
544
+ }
545
+
546
+ </style>
547
+ <!-- logger -->
548
+ <style>
549
+ .editorgroupborder {
550
+ border-width: 1px;
551
+ border-style: solid;
552
+ border-color: lightgray;
553
+ padding: 3px;
554
+ margin-top: 2px;
555
+ margin-bottom: 2px;
556
+ }
557
+
558
+ .editorsectionheading {
559
+ font-weight: 600;
560
+ margin-bottom: 1px !important;
561
+ margin-top: 6px !important;
562
+ }
563
+
564
+ .nomargin {
565
+ margin-bottom: 1px !important;
566
+ margin-top: 1px !important;
567
+ }
568
+
569
+ .slidecontainer {
570
+ width: 100%;
571
+ }
572
+
573
+ .slider {
574
+ -webkit-appearance: none;
575
+ appearance: none;
576
+ width: 100%;
577
+ height: 25px;
578
+ background: #d3d3d3;
579
+ outline: none;
580
+ opacity: 0.7;
581
+ -webkit-transition: .2s;
582
+ transition: opacity .2s;
583
+ }
584
+
585
+ .slider:hover {
586
+ opacity: 1;
587
+ }
588
+
589
+ .slider::-webkit-slider-thumb {
590
+ -webkit-appearance: none;
591
+ appearance: none;
592
+ width: 25px;
593
+ height: 25px;
594
+ background: #04AA6D;
595
+ cursor: pointer;
596
+ }
597
+
598
+ .slider::-moz-range-thumb {
599
+ width: 25px;
600
+ height: 25px;
601
+ background: #04AA6D;
602
+ cursor: pointer;
603
+ }
604
+
605
+ .towb_editorlabel {
606
+ width: 120px !important;
607
+ }
608
+
609
+ .towb_editorfield {
610
+ width: 70% !important;
611
+ }
612
+
613
+ .towb_editorfield_short {
614
+ width: 30% !important;
615
+ }
616
+
617
+ </style>
618
+ <script type="text/javascript">
619
+ // CSS
620
+ dropdownStyles = `
621
+ .loggertype-dropdown {
622
+ position: absolute;
623
+ background: white;
624
+ border: 1px solid #ccc;
625
+ border-radius: 4px;
626
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
627
+ max-height: 150px;
628
+ overflow-y: auto;
629
+ z-index: 1000;
630
+ min-width: 150px;
631
+ }
632
+ .loggertype-dropdown .dropdown-item {
633
+ padding: 6px 12px;
634
+ cursor: pointer;
635
+ }
636
+ .loggertype-dropdown .dropdown-item:hover {
637
+ background: #f0f0f0;
638
+ }
639
+ `;
640
+ $('<style>').text(dropdownStyles).appendTo('head');
641
+
642
+ </script>
643
+ <!-- script-editor -->
644
+ <script src="resources/@theotherwillembotha/node-red-plugincore/editorLanguageExtension.js"></script>
645
+ <script type="text/javascript">
646
+ window.PluginCore = window.PluginCore || {};
647
+ /**
648
+ * PluginCore.createScriptEditor(elementId, template, initialValue)
649
+ *
650
+ * Creates a Monaco editor instance with TypeScript-aware diagnostics,
651
+ * completions, and hover info scoped to the provided type template.
652
+ *
653
+ * @param {string} elementId - DOM id of the container element.
654
+ * @param {string} template - TypeScript template string. Use `${script}`
655
+ * as the placeholder for the user's code.
656
+ * @param {string} initialValue - The starting content of the editor.
657
+ * @returns {{ getValue(): string, dispose(): void \}}
658
+ */
659
+ PluginCore.createScriptEditor = function(elementId, template, initialValue) {
660
+ var editorInstance = null;
661
+ var currentValue = initialValue || '';
662
+ ScriptEditorLanguageManager.createLanguage(monaco, template).then(function(language) {
663
+ editorInstance = monaco.editor.create(document.getElementById(elementId), {
664
+ value: currentValue,
665
+ language: language.getName(),
666
+ automaticLayout: true,
667
+ wordBasedSuggestions: 'currentDocument',
668
+ });
669
+ });
670
+ return {
671
+ getValue: function() {
672
+ return editorInstance ? editorInstance.getValue() : currentValue;
673
+ },
674
+ dispose: function() {
675
+ if (editorInstance) {
676
+ editorInstance.dispose();
677
+ editorInstance = null;
678
+ }
679
+ }
680
+ };
681
+ };
682
+
683
+ </script>
684
+ <script type="text/javascript">
685
+ RED.nodes.registerType('CircuitBreakerFaultDetectorNode', {
686
+ category: 'circuitbreaker',
687
+ icon: 'circuitrbreaker.png',
688
+ color: '#1D7874',
689
+ labelStyle: 'node_label_white',
690
+ label: function() {
691
+ return this.name
692
+ },
693
+ paletteLabel: 'Circuit Breaker Fault Detector',
694
+ inputs: 1,
695
+ inputLabels: (i) => 'input',
696
+ outputs: 2,
697
+ outputLabels: (i) => ['output', 'fault'][i],
698
+ defaults: {
699
+ name: {
700
+ value: 'CircuitBreaker Fault Detector Example',
701
+ required: true
702
+ },
703
+ circuitbreakerconfig: {
704
+ type: 'CircuitBreakerConfigNode',
705
+ required: true
706
+ },
707
+ faultFunction: {
708
+ required: true
709
+ },
710
+ tripFunction: {
711
+ required: true
712
+ },
713
+ logEnabled: {
714
+ value: false,
715
+ required: true
716
+ },
717
+ logger: {
718
+ required: false,
719
+ value: '',
720
+ type: 'DelegatedConfigReferenceNode'
721
+ },
722
+ logTemplateOverrideEnabled: {
723
+ value: false
724
+ },
725
+ logTemplateOverride: {
726
+ value: 'message:{{msg}}'
727
+ },
728
+ metricsEnabled: {
729
+ value: false,
730
+ required: true
731
+ },
732
+ metricsReference: {
733
+ type: 'MetricsConfigNode',
734
+ required: false
735
+ },
736
+ },
737
+ oneditprepare: function() {
738
+ // **** CircuitBreakerFaultDetectorNode **** //
739
+ {
740
+ let node = this;
741
+ let faultTemplate = `
742
+ /**
743
+ * Represents a message object received by this node.
744
+ */
745
+ class Message {
746
+ [key: string]: any;
747
+ }
748
+
749
+ async function(msg: Message) {
750
+ \${script}
751
+ }
752
+ `;
753
+ let faultSample = normalizeIndentation(`
754
+ return
755
+ msg.errorCode &&
756
+ msg.errorCode >= 400;
757
+ `);
758
+ node.faultEditor = PluginCore.createScriptEditor('node-input-faultFunction-editor', faultTemplate, node.faultFunction !== undefined ? node.faultFunction : faultSample);
759
+ let tripTemplate = `
760
+ /**
761
+ * Represents the breaker object that this fault is linked to.
762
+ */
763
+ interface Breaker {
764
+ context():BreakerContext;
765
+ }
766
+
767
+ /**
768
+ * Represents a context on the breaker where properties can be set or removed.
769
+ */
770
+ interface BreakerContext {
771
+ /**
772
+ * Gets a particular context value from the BreakerContext.
773
+ * @param id - The id of the property to retrieve.
774
+ * @param defaultValue - The optional default for the context property if it does not exist.
775
+ * @returns a value if it exists, or undefined otherwise.
776
+ */
777
+ get<T>(id:string, defaultValue?:T): T;
778
+
779
+ /**
780
+ * Sets a particular context value in the BreakerContext.
781
+ * @param id - The id of the property to set.
782
+ * @param value - The value to assign to the context property.
783
+ * @returns the value that has just been set.
784
+ */
785
+ set<T>(id:string, value:T): T;
786
+ }
787
+
788
+ async function(breaker:Breaker) {
789
+ \${script}
790
+ }
791
+ `;
792
+ let tripSample = normalizeIndentation(`
793
+ /** @type {number[]} */
794
+ let buffer = breaker.context().get("buffer", []);
795
+ let interval = 10*1000; // 10 seconds.
796
+ let capacity = 5; // maximum number of failures.
797
+ let time = new Date().getTime();
798
+ buffer.push(time);
799
+ buffer = buffer.filter(item => item + interval >= time);
800
+ breaker.context().set("buffer", buffer);
801
+ return (buffer.length >= capacity);
802
+ `);
803
+ node.tripEditor = PluginCore.createScriptEditor('node-input-tripFunction-editor', tripTemplate, node.tripFunction !== undefined ? node.tripFunction : tripSample);
804
+
805
+ function normalizeIndentation(code) {
806
+ const lines = code.split('\n');
807
+ const nonEmptyLines = lines.filter(line => line.trim().length > 0);
808
+ if (nonEmptyLines.length === 0) return code;
809
+ const minIndent = Math.min(...nonEmptyLines.map(line => line.match(/^\s*/)[0].length));
810
+ return lines.map(line => line.slice(minIndent)).join('\n');
811
+ }
812
+ }
813
+ // **** logger **** //
814
+ {
815
+ let node = this;
816
+ // controls.
817
+ let logEnabled = $("#node-input-logEnabled");
818
+ let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
819
+ let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
820
+ let logEnabledSection = $("#section_logEnabled");
821
+ // functions.
822
+ let getLoggerTypes = async function() {
823
+ let types = [];
824
+ await $.ajax({
825
+ url: "nodetypeservice/find?tag=LoggerType",
826
+ type: "GET",
827
+ contentType: "application/json; charset=utf-8",
828
+ data: JSON.stringify({
829
+ id: node.id
830
+ }),
831
+ success: (response) => {
832
+ types = response;
833
+ },
834
+ error: (jqXHR, textStatus, errorThrown) => {
835
+ console.log(jqXHR, textStatus, errorThrown);
836
+ },
837
+ });
838
+ return types;
839
+ }
840
+ let getLoggers = async () => {
841
+ let loggers = [];
842
+ // get the list of types again.
843
+ let loggerTypes = (await getLoggerTypes()).map(type => type.id);
844
+ // add all of the known loggers.
845
+ RED.nodes.eachConfig(cfg => {
846
+ if (loggerTypes.includes(cfg.type)) {
847
+ loggers.push({
848
+ id: cfg.id,
849
+ name: cfg.label(),
850
+ type: cfg.type
851
+ });
852
+ }
853
+ });
854
+ return loggers;
855
+ };
856
+ let updateLoggerSelectorList = async () => {
857
+ loggerSelector.empty();
858
+ (await getLoggers()).forEach(logger => {
859
+ $('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
860
+ });
861
+ $('<option value="_ADD_">none</option>').appendTo(loggerSelector);
862
+ }
863
+ logEnabled.on('change', function() {
864
+ if (logEnabled.prop("checked")) {
865
+ logEnabledSection.show();
866
+ node._def.defaults.logger.required = true;
867
+ } else {
868
+ logEnabledSection.hide()
869
+ node._def.defaults.logger.required = false;
870
+ }
871
+ });
872
+ logTemplateEnabled.on('change', function() {
873
+ if (logTemplateEnabled.prop("checked")) {
874
+ logTemplateEnabledSection.show();
875
+ node._def.defaults.logTemplateOverride.required = true;
876
+ } else {
877
+ logTemplateEnabledSection.hide()
878
+ node._def.defaults.logTemplateOverride.required = false;
879
+ }
880
+ });
881
+ node.logTemplateOverrideEditor = RED.editor.createEditor({
882
+ id: 'node-input-logTemplateOverrideEditor',
883
+ mode: 'ace/mode/handlebars',
884
+ value: node.logTemplateOverride
885
+ });
886
+ let loggerSelector = $("#logger-selector");
887
+ let loggerEditButton = $("#logger-edit-btn");
888
+ let loggerAddButton = $("#logger-add-btn");
889
+ loggerEditButton.on('click', async function(event) {
890
+ event.stopPropagation();
891
+ if (loggerEditButton.hasClass("disabled")) {
892
+ return;
893
+ }
894
+ // figure out what type of logger this is.
895
+ let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
896
+ RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
897
+ });
898
+ loggerAddButton.on('click', function(event) {
899
+ event.stopPropagation();
900
+ let $btn = $(event.target);
901
+ // Remove any existing dropdown
902
+ $('.loggertype-dropdown').remove();
903
+ getLoggerTypes().then(loggerTypes => {
904
+ let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
905
+ loggerTypes.forEach(loggerType => {
906
+ $('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
907
+ padding: '4px 8px',
908
+ cursor: 'pointer'
909
+ }).appendTo($dropdown);
910
+ });
911
+ // Append first so outerWidth() is measurable, then position so the
912
+ // right edge of the dropdown aligns with the right edge of the button.
913
+ $('body').append($dropdown);
914
+ $dropdown.css({
915
+ top: $btn.offset().top + $btn.outerHeight(),
916
+ left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
917
+ });
918
+ $(document).on('mousedown.dropdown', function(e) {
919
+ if (!$(e.target).closest('.loggertype-dropdown').length) {
920
+ $dropdown.remove();
921
+ $(document).off('mousedown.dropdown'); // Clean up the listener
922
+ }
923
+ });
924
+ // Handle item selection
925
+ $dropdown.on('click', '.dropdown-item', function(e) {
926
+ e.stopPropagation();
927
+ const selectedValue = $(this).data('value');
928
+ const selectedText = $(this).text();
929
+ // Do whatever you need with the selection
930
+ console.log('Selected:', selectedValue, selectedText);
931
+ $dropdown.remove();
932
+ RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
933
+ });
934
+ })
935
+ });
936
+ loggerSelector.on("focus", updateLoggerSelectorList);
937
+ loggerSelector.on('change', function(event) {
938
+ if ("_ADD_" === loggerSelector.val()) {
939
+ loggerEditButton.addClass("disabled")
940
+ } else {
941
+ loggerEditButton.removeClass("disabled")
942
+ }
943
+ });
944
+ // lastly, make sure that we set the correct value on the logger selector.
945
+ updateLoggerSelectorList().then(() => {
946
+ loggerSelector.val(node.logger);
947
+ loggerSelector.trigger('change');
948
+ });
949
+ }
950
+ // **** metrics **** //
951
+ {
952
+ let node = this;
953
+ let metricsEnabled = $("#node-input-metricsEnabled");
954
+ let metricsEnabledSection = $("#section_metricsEnabled");
955
+ metricsEnabled.on('change', function() {
956
+ if (metricsEnabled.prop("checked")) {
957
+ metricsEnabledSection.show();
958
+ node._def.defaults.metricsReference.required = true;
959
+ } else {
960
+ metricsEnabledSection.hide()
961
+ node._def.defaults.metricsReference.required = false;
962
+ }
963
+ });
964
+ }
965
+ },
966
+ oneditsave: function() {
967
+ // **** CircuitBreakerFaultDetectorNode **** //
968
+ {
969
+ let node = this;
970
+ node.faultFunction = node.faultEditor.getValue();
971
+ delete node.faultEditor;
972
+ node.tripFunction = node.tripEditor.getValue();
973
+ delete node.tripEditor;
974
+ }
975
+ // **** logger **** //
976
+ {
977
+ node = this;
978
+ var selectedLogger = $("#logger-selector").val();
979
+ node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
980
+ node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
981
+ node.logTemplateOverrideEditor.destroy();
982
+ delete node.logTemplateOverrideEditor;
983
+ }
984
+ // **** metrics **** //
985
+ {}
986
+ },
987
+ oneditcancel: function() {},
988
+ oneditdelete: function() {},
989
+ });
990
+
991
+ </script>
992
+
993
+
994
+ <script type="text/html" data-template-name='CircuitBreakerFaultDetectorNode'>
995
+ <div id='section_CircuitBreakerFaultDetectorNode'>
996
+ <div class="form-row">
997
+ <label for="node-input-name" class="towb_editorlabel"><i class="fa fa-font"></i> Name</label>
998
+ <input type="text" id="node-input-name" />
999
+ </div>
1000
+
1001
+ <div class="form-row">
1002
+ <label for="node-input-circuitbreakerconfig" class="towb_editorlabel"><i class="fa fa-bolt"></i> Circuit Breaker</label>
1003
+ <input type="text" id="node-input-circuitbreakerconfig" />
1004
+ </div>
1005
+
1006
+ <div class="form-row">
1007
+ <label for="node-input-faultFunction-editor" class="towb_editorlabel"><i class="fa fa-bug"></i> Fault Function</label>
1008
+ <div style="height: 200px; min-height: 80px; resize: vertical; overflow: auto;" class="node-text-editor" id="node-input-faultFunction-editor"></div>
1009
+ </div>
1010
+
1011
+ <div class="form-row">
1012
+ <label for="node-input-tripFunction-editor" class="towb_editorlabel"><i class="fa fa-toggle-off"></i> Trip Function</label>
1013
+ <div style="height: 200px; min-height: 80px; resize: vertical; overflow: auto;" class="node-text-editor" id="node-input-tripFunction-editor"></div>
1014
+ </div>
1015
+
1016
+ <div id="myMonacoContainer">
1017
+ </div>
1018
+
1019
+ </div>
1020
+
1021
+ <div id='section_logger'>
1022
+ <div id="section_loggerconfig">
1023
+ <div class="form-row editorsectionheading">
1024
+ <i class="w-16 fa fa-eye"></i> <span>Logging</span>
1025
+ </div>
1026
+ <div class="editorgroupborder">
1027
+ <div class="form-row nomargin logEnableCheck">
1028
+ <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
1029
+ <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
1030
+ </div>
1031
+
1032
+ <div id="section_logEnabled">
1033
+ <div class="form-row nomargin">
1034
+ <label for="logger-selector">Logger</label>
1035
+ <div style="width: 70%; display: inline-flex;">
1036
+ <select id="logger-selector" style="flex-grow: 1;" class="">
1037
+ <option value="_ADD_">none</option>
1038
+ </select>
1039
+ <a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
1040
+ <i class="fa fa-pencil"></i>
1041
+ </a>
1042
+ <a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
1043
+ <i class="fa fa-plus"></i>
1044
+ </a>
1045
+ </div>
1046
+ </div>
1047
+
1048
+ <div class="form-row nomargin">
1049
+ <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
1050
+ <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
1051
+ </div>
1052
+
1053
+ <div id="section_logTemplateOverrideEnabled">
1054
+ <div class="form-row">
1055
+ <label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
1056
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
1057
+ </div>
1058
+ </div>
1059
+ </div>
1060
+ </div>
1061
+ </div>
1062
+
1063
+ </div>
1064
+
1065
+ <div id='section_metrics'>
1066
+ <div id="section_metricsconfig">
1067
+ <div class="form-row editorsectionheading">
1068
+ <i class="w-16 fa fa-eye"></i> <span>Metrics</span>
1069
+ </div>
1070
+ <div class="editorgroupborder">
1071
+ <div class="form-row nomargin metricsEnableCheck">
1072
+ <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
1073
+ <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
1074
+ </div>
1075
+
1076
+ <div id="section_metricsEnabled">
1077
+ <div class="form-row nomargin">
1078
+ <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
1079
+ <input id="node-input-metricsReference" placeholder="metrics" />
1080
+ </div>
1081
+ </div>
1082
+ </div>
1083
+ </div>
1084
+
1085
+ </div>
1086
+ </script>
1087
+
1088
+ <script type="text/markdown" data-help-name='CircuitBreakerFaultDetectorNode'>
1089
+ Inspects each message for faults and decides whether the linked circuit breaker should be tripped. Place this node after the integration you are protecting - typically after an HTTP request node.
1090
+
1091
+ ### Properties
1092
+
1093
+ : *name* (string) : A descriptive label for this node.
1094
+ : *circuit breaker* (Circuit Breaker Config) : The circuit breaker to trip when the threshold is reached.
1095
+ : *fault function* (script) : A function that receives the message and returns `true` if a fault is detected.
1096
+ : *trip function* (script) : A function that receives the breaker and returns `true` if the breaker should be tripped.
1097
+
1098
+ ### Fault function
1099
+
1100
+ Called on **every message** that passes through this node. It receives `msg` and must return `true` if the message represents a failure, or `false` if it is healthy. This is where you define what "failure" means for your integration - for example a bad HTTP status code, a missing property, or an error flag set by an upstream node.
1101
+
1102
+ When the fault function returns `true`, the message is routed to the **fault** output and the trip function is evaluated. When it returns `false`, the message is considered successful and routed to the normal **output**.
1103
+
1104
+ ```javascript
1105
+ // Route to fault output if the HTTP response was an error
1106
+ return msg.statusCode !== undefined && msg.statusCode >= 400;
1107
+ ```
1108
+
1109
+ ### Trip function
1110
+
1111
+ Called only when the fault function returns `true`. It receives the `breaker` object and must return `true` if the breaker should be tripped immediately, or `false` to record the fault but leave the breaker closed.
1112
+
1113
+ This is where you implement your **threshold or windowing strategy**. You rarely want to trip on a single fault - instead you accumulate faults over a rolling time window and trip only when enough failures occur within that window. The breaker's context store persists between messages, making it ideal for tracking this state.
1114
+
1115
+ The example below trips the breaker once 5 faults occur within a 10-second window. Each call adds the current timestamp to a buffer, removes entries older than the window, saves the buffer back to the context, and then checks whether the count has reached the threshold:
1116
+
1117
+ ```javascript
1118
+ let buffer = breaker.context().get("buffer", []);
1119
+ let interval = 10 * 1000; // sliding window of 10 seconds
1120
+ let capacity = 5; // trip after 5 faults within the window
1121
+ let now = new Date().getTime();
1122
+
1123
+ buffer.push(now);
1124
+ buffer = buffer.filter(t => t + interval >= now); // discard entries outside the window
1125
+ breaker.context().set("buffer", buffer);
1126
+
1127
+ return buffer.length >= capacity; // true = trip the breaker
1128
+ ```
1129
+
1130
+ ### Outputs
1131
+
1132
+ 1. **Output** - no fault detected. The message is forwarded normally.
1133
+ 2. **Fault** - a fault was detected. The message is forwarded on the fault output for error handling.
1134
+
1135
+ ### Logging
1136
+ : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
1137
+ : *logger* (logconfig) : the endppoint that the node will log events to.
1138
+ : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
1139
+ : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
1140
+
1141
+ ### Metrics
1142
+ : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
1143
+ : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
1144
+ </script>
1145
+ <!-- CircuitBreakerEventNode -->
1146
+ <style>
1147
+ .node_label_white {
1148
+ fill: white
1149
+ }
1150
+
1151
+ </style>
1152
+ <!-- logger -->
1153
+ <style>
1154
+ .editorgroupborder {
1155
+ border-width: 1px;
1156
+ border-style: solid;
1157
+ border-color: lightgray;
1158
+ padding: 3px;
1159
+ margin-top: 2px;
1160
+ margin-bottom: 2px;
1161
+ }
1162
+
1163
+ .editorsectionheading {
1164
+ font-weight: 600;
1165
+ margin-bottom: 1px !important;
1166
+ margin-top: 6px !important;
1167
+ }
1168
+
1169
+ .nomargin {
1170
+ margin-bottom: 1px !important;
1171
+ margin-top: 1px !important;
1172
+ }
1173
+
1174
+ .slidecontainer {
1175
+ width: 100%;
1176
+ }
1177
+
1178
+ .slider {
1179
+ -webkit-appearance: none;
1180
+ appearance: none;
1181
+ width: 100%;
1182
+ height: 25px;
1183
+ background: #d3d3d3;
1184
+ outline: none;
1185
+ opacity: 0.7;
1186
+ -webkit-transition: .2s;
1187
+ transition: opacity .2s;
1188
+ }
1189
+
1190
+ .slider:hover {
1191
+ opacity: 1;
1192
+ }
1193
+
1194
+ .slider::-webkit-slider-thumb {
1195
+ -webkit-appearance: none;
1196
+ appearance: none;
1197
+ width: 25px;
1198
+ height: 25px;
1199
+ background: #04AA6D;
1200
+ cursor: pointer;
1201
+ }
1202
+
1203
+ .slider::-moz-range-thumb {
1204
+ width: 25px;
1205
+ height: 25px;
1206
+ background: #04AA6D;
1207
+ cursor: pointer;
1208
+ }
1209
+
1210
+ .towb_editorlabel {
1211
+ width: 120px !important;
1212
+ }
1213
+
1214
+ .towb_editorfield {
1215
+ width: 70% !important;
1216
+ }
1217
+
1218
+ .towb_editorfield_short {
1219
+ width: 30% !important;
1220
+ }
1221
+
1222
+ </style>
1223
+ <script type="text/javascript">
1224
+ // CSS
1225
+ dropdownStyles = `
1226
+ .loggertype-dropdown {
1227
+ position: absolute;
1228
+ background: white;
1229
+ border: 1px solid #ccc;
1230
+ border-radius: 4px;
1231
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1232
+ max-height: 150px;
1233
+ overflow-y: auto;
1234
+ z-index: 1000;
1235
+ min-width: 150px;
1236
+ }
1237
+ .loggertype-dropdown .dropdown-item {
1238
+ padding: 6px 12px;
1239
+ cursor: pointer;
1240
+ }
1241
+ .loggertype-dropdown .dropdown-item:hover {
1242
+ background: #f0f0f0;
1243
+ }
1244
+ `;
1245
+ $('<style>').text(dropdownStyles).appendTo('head');
1246
+
1247
+ </script>
1248
+ <script type="text/javascript">
1249
+ RED.nodes.registerType('CircuitBreakerEventNode', {
1250
+ category: 'circuitbreaker',
1251
+ icon: 'circuitrbreaker.png',
1252
+ color: '#1D7874',
1253
+ labelStyle: 'node_label_white',
1254
+ label: function() {
1255
+ return this.name
1256
+ },
1257
+ paletteLabel: 'Circuit Breaker Event',
1258
+ inputs: 0,
1259
+ inputLabels: (i) => 'undefined',
1260
+ outputs: 2,
1261
+ outputLabels: (i) => ['trip', 'reset'][i],
1262
+ defaults: {
1263
+ name: {
1264
+ value: 'CircuitBreakerEvent Example',
1265
+ required: true
1266
+ },
1267
+ circuitbreakerconfig: {
1268
+ type: 'CircuitBreakerConfigNode',
1269
+ required: true
1270
+ },
1271
+ logEnabled: {
1272
+ value: false,
1273
+ required: true
1274
+ },
1275
+ logger: {
1276
+ required: false,
1277
+ value: '',
1278
+ type: 'DelegatedConfigReferenceNode'
1279
+ },
1280
+ logTemplateOverrideEnabled: {
1281
+ value: false
1282
+ },
1283
+ logTemplateOverride: {
1284
+ value: 'message:{{msg}}'
1285
+ },
1286
+ metricsEnabled: {
1287
+ value: false,
1288
+ required: true
1289
+ },
1290
+ metricsReference: {
1291
+ type: 'MetricsConfigNode',
1292
+ required: false
1293
+ },
1294
+ },
1295
+ oneditprepare: function() {
1296
+ // **** CircuitBreakerEventNode **** //
1297
+ {
1298
+ let node = this;
1299
+ }
1300
+ // **** logger **** //
1301
+ {
1302
+ let node = this;
1303
+ // controls.
1304
+ let logEnabled = $("#node-input-logEnabled");
1305
+ let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
1306
+ let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
1307
+ let logEnabledSection = $("#section_logEnabled");
1308
+ // functions.
1309
+ let getLoggerTypes = async function() {
1310
+ let types = [];
1311
+ await $.ajax({
1312
+ url: "nodetypeservice/find?tag=LoggerType",
1313
+ type: "GET",
1314
+ contentType: "application/json; charset=utf-8",
1315
+ data: JSON.stringify({
1316
+ id: node.id
1317
+ }),
1318
+ success: (response) => {
1319
+ types = response;
1320
+ },
1321
+ error: (jqXHR, textStatus, errorThrown) => {
1322
+ console.log(jqXHR, textStatus, errorThrown);
1323
+ },
1324
+ });
1325
+ return types;
1326
+ }
1327
+ let getLoggers = async () => {
1328
+ let loggers = [];
1329
+ // get the list of types again.
1330
+ let loggerTypes = (await getLoggerTypes()).map(type => type.id);
1331
+ // add all of the known loggers.
1332
+ RED.nodes.eachConfig(cfg => {
1333
+ if (loggerTypes.includes(cfg.type)) {
1334
+ loggers.push({
1335
+ id: cfg.id,
1336
+ name: cfg.label(),
1337
+ type: cfg.type
1338
+ });
1339
+ }
1340
+ });
1341
+ return loggers;
1342
+ };
1343
+ let updateLoggerSelectorList = async () => {
1344
+ loggerSelector.empty();
1345
+ (await getLoggers()).forEach(logger => {
1346
+ $('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
1347
+ });
1348
+ $('<option value="_ADD_">none</option>').appendTo(loggerSelector);
1349
+ }
1350
+ logEnabled.on('change', function() {
1351
+ if (logEnabled.prop("checked")) {
1352
+ logEnabledSection.show();
1353
+ node._def.defaults.logger.required = true;
1354
+ } else {
1355
+ logEnabledSection.hide()
1356
+ node._def.defaults.logger.required = false;
1357
+ }
1358
+ });
1359
+ logTemplateEnabled.on('change', function() {
1360
+ if (logTemplateEnabled.prop("checked")) {
1361
+ logTemplateEnabledSection.show();
1362
+ node._def.defaults.logTemplateOverride.required = true;
1363
+ } else {
1364
+ logTemplateEnabledSection.hide()
1365
+ node._def.defaults.logTemplateOverride.required = false;
1366
+ }
1367
+ });
1368
+ node.logTemplateOverrideEditor = RED.editor.createEditor({
1369
+ id: 'node-input-logTemplateOverrideEditor',
1370
+ mode: 'ace/mode/handlebars',
1371
+ value: node.logTemplateOverride
1372
+ });
1373
+ let loggerSelector = $("#logger-selector");
1374
+ let loggerEditButton = $("#logger-edit-btn");
1375
+ let loggerAddButton = $("#logger-add-btn");
1376
+ loggerEditButton.on('click', async function(event) {
1377
+ event.stopPropagation();
1378
+ if (loggerEditButton.hasClass("disabled")) {
1379
+ return;
1380
+ }
1381
+ // figure out what type of logger this is.
1382
+ let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
1383
+ RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
1384
+ });
1385
+ loggerAddButton.on('click', function(event) {
1386
+ event.stopPropagation();
1387
+ let $btn = $(event.target);
1388
+ // Remove any existing dropdown
1389
+ $('.loggertype-dropdown').remove();
1390
+ getLoggerTypes().then(loggerTypes => {
1391
+ let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
1392
+ loggerTypes.forEach(loggerType => {
1393
+ $('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
1394
+ padding: '4px 8px',
1395
+ cursor: 'pointer'
1396
+ }).appendTo($dropdown);
1397
+ });
1398
+ // Append first so outerWidth() is measurable, then position so the
1399
+ // right edge of the dropdown aligns with the right edge of the button.
1400
+ $('body').append($dropdown);
1401
+ $dropdown.css({
1402
+ top: $btn.offset().top + $btn.outerHeight(),
1403
+ left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
1404
+ });
1405
+ $(document).on('mousedown.dropdown', function(e) {
1406
+ if (!$(e.target).closest('.loggertype-dropdown').length) {
1407
+ $dropdown.remove();
1408
+ $(document).off('mousedown.dropdown'); // Clean up the listener
1409
+ }
1410
+ });
1411
+ // Handle item selection
1412
+ $dropdown.on('click', '.dropdown-item', function(e) {
1413
+ e.stopPropagation();
1414
+ const selectedValue = $(this).data('value');
1415
+ const selectedText = $(this).text();
1416
+ // Do whatever you need with the selection
1417
+ console.log('Selected:', selectedValue, selectedText);
1418
+ $dropdown.remove();
1419
+ RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
1420
+ });
1421
+ })
1422
+ });
1423
+ loggerSelector.on("focus", updateLoggerSelectorList);
1424
+ loggerSelector.on('change', function(event) {
1425
+ if ("_ADD_" === loggerSelector.val()) {
1426
+ loggerEditButton.addClass("disabled")
1427
+ } else {
1428
+ loggerEditButton.removeClass("disabled")
1429
+ }
1430
+ });
1431
+ // lastly, make sure that we set the correct value on the logger selector.
1432
+ updateLoggerSelectorList().then(() => {
1433
+ loggerSelector.val(node.logger);
1434
+ loggerSelector.trigger('change');
1435
+ });
1436
+ }
1437
+ // **** metrics **** //
1438
+ {
1439
+ let node = this;
1440
+ let metricsEnabled = $("#node-input-metricsEnabled");
1441
+ let metricsEnabledSection = $("#section_metricsEnabled");
1442
+ metricsEnabled.on('change', function() {
1443
+ if (metricsEnabled.prop("checked")) {
1444
+ metricsEnabledSection.show();
1445
+ node._def.defaults.metricsReference.required = true;
1446
+ } else {
1447
+ metricsEnabledSection.hide()
1448
+ node._def.defaults.metricsReference.required = false;
1449
+ }
1450
+ });
1451
+ }
1452
+ },
1453
+ oneditsave: function() {
1454
+ // **** CircuitBreakerEventNode **** //
1455
+ {
1456
+ let node = this;
1457
+ }
1458
+ // **** logger **** //
1459
+ {
1460
+ node = this;
1461
+ var selectedLogger = $("#logger-selector").val();
1462
+ node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
1463
+ node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
1464
+ node.logTemplateOverrideEditor.destroy();
1465
+ delete node.logTemplateOverrideEditor;
1466
+ }
1467
+ // **** metrics **** //
1468
+ {}
1469
+ },
1470
+ oneditcancel: function() {},
1471
+ oneditdelete: function() {},
1472
+ });
1473
+
1474
+ </script>
1475
+
1476
+
1477
+ <script type="text/html" data-template-name='CircuitBreakerEventNode'>
1478
+ <div id='section_CircuitBreakerEventNode'>
1479
+ <div class="form-row">
1480
+ <label for="node-input-name" class="towb_editorlabel"><i class="fa fa-font"></i> Name</label>
1481
+ <input type="text" id="node-input-name" />
1482
+ </div>
1483
+
1484
+ <div class="form-row">
1485
+ <label for="node-input-circuitbreakerconfig" class="towb_editorlabel"><i class="fa fa-bolt"></i> Circuit Breaker</label>
1486
+ <input type="text" id="node-input-circuitbreakerconfig" />
1487
+ </div>
1488
+
1489
+ </div>
1490
+
1491
+ <div id='section_logger'>
1492
+ <div id="section_loggerconfig">
1493
+ <div class="form-row editorsectionheading">
1494
+ <i class="w-16 fa fa-eye"></i> <span>Logging</span>
1495
+ </div>
1496
+ <div class="editorgroupborder">
1497
+ <div class="form-row nomargin logEnableCheck">
1498
+ <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
1499
+ <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
1500
+ </div>
1501
+
1502
+ <div id="section_logEnabled">
1503
+ <div class="form-row nomargin">
1504
+ <label for="logger-selector">Logger</label>
1505
+ <div style="width: 70%; display: inline-flex;">
1506
+ <select id="logger-selector" style="flex-grow: 1;" class="">
1507
+ <option value="_ADD_">none</option>
1508
+ </select>
1509
+ <a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
1510
+ <i class="fa fa-pencil"></i>
1511
+ </a>
1512
+ <a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
1513
+ <i class="fa fa-plus"></i>
1514
+ </a>
1515
+ </div>
1516
+ </div>
1517
+
1518
+ <div class="form-row nomargin">
1519
+ <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
1520
+ <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
1521
+ </div>
1522
+
1523
+ <div id="section_logTemplateOverrideEnabled">
1524
+ <div class="form-row">
1525
+ <label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
1526
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
1527
+ </div>
1528
+ </div>
1529
+ </div>
1530
+ </div>
1531
+ </div>
1532
+
1533
+ </div>
1534
+
1535
+ <div id='section_metrics'>
1536
+ <div id="section_metricsconfig">
1537
+ <div class="form-row editorsectionheading">
1538
+ <i class="w-16 fa fa-eye"></i> <span>Metrics</span>
1539
+ </div>
1540
+ <div class="editorgroupborder">
1541
+ <div class="form-row nomargin metricsEnableCheck">
1542
+ <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
1543
+ <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
1544
+ </div>
1545
+
1546
+ <div id="section_metricsEnabled">
1547
+ <div class="form-row nomargin">
1548
+ <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
1549
+ <input id="node-input-metricsReference" placeholder="metrics" />
1550
+ </div>
1551
+ </div>
1552
+ </div>
1553
+ </div>
1554
+
1555
+ </div>
1556
+ </script>
1557
+
1558
+ <script type="text/markdown" data-help-name='CircuitBreakerEventNode'>
1559
+ Fires a message whenever the linked circuit breaker changes state. Use this node to trigger notifications, update dashboards, or kick off recovery flows.
1560
+
1561
+ ### Properties
1562
+
1563
+ : *name* (string) : A descriptive label for this node.
1564
+ : *circuit breaker* (Circuit Breaker Config) : The circuit breaker instance to listen to.
1565
+
1566
+ ### Inputs
1567
+
1568
+ This node has no input; it fires automatically in response to breaker state changes.
1569
+
1570
+ ### Outputs
1571
+
1572
+ 1. **Trip** - fires when the breaker is tripped (opened). The message is `{ state: "Trip" }`.
1573
+ 2. **Reset** - fires when the breaker is reset (closed). The message is `{ state: "Reset" }`.
1574
+
1575
+ ### Logging
1576
+ : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
1577
+ : *logger* (logconfig) : the endppoint that the node will log events to.
1578
+ : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
1579
+ : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
1580
+
1581
+ ### Metrics
1582
+ : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
1583
+ : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
1584
+ </script>
1585
+ <!-- CircuitBreakerStateNode -->
1586
+ <style>
1587
+ .node_label_white {
1588
+ fill: white
1589
+ }
1590
+
1591
+ </style>
1592
+ <!-- logger -->
1593
+ <style>
1594
+ .editorgroupborder {
1595
+ border-width: 1px;
1596
+ border-style: solid;
1597
+ border-color: lightgray;
1598
+ padding: 3px;
1599
+ margin-top: 2px;
1600
+ margin-bottom: 2px;
1601
+ }
1602
+
1603
+ .editorsectionheading {
1604
+ font-weight: 600;
1605
+ margin-bottom: 1px !important;
1606
+ margin-top: 6px !important;
1607
+ }
1608
+
1609
+ .nomargin {
1610
+ margin-bottom: 1px !important;
1611
+ margin-top: 1px !important;
1612
+ }
1613
+
1614
+ .slidecontainer {
1615
+ width: 100%;
1616
+ }
1617
+
1618
+ .slider {
1619
+ -webkit-appearance: none;
1620
+ appearance: none;
1621
+ width: 100%;
1622
+ height: 25px;
1623
+ background: #d3d3d3;
1624
+ outline: none;
1625
+ opacity: 0.7;
1626
+ -webkit-transition: .2s;
1627
+ transition: opacity .2s;
1628
+ }
1629
+
1630
+ .slider:hover {
1631
+ opacity: 1;
1632
+ }
1633
+
1634
+ .slider::-webkit-slider-thumb {
1635
+ -webkit-appearance: none;
1636
+ appearance: none;
1637
+ width: 25px;
1638
+ height: 25px;
1639
+ background: #04AA6D;
1640
+ cursor: pointer;
1641
+ }
1642
+
1643
+ .slider::-moz-range-thumb {
1644
+ width: 25px;
1645
+ height: 25px;
1646
+ background: #04AA6D;
1647
+ cursor: pointer;
1648
+ }
1649
+
1650
+ .towb_editorlabel {
1651
+ width: 120px !important;
1652
+ }
1653
+
1654
+ .towb_editorfield {
1655
+ width: 70% !important;
1656
+ }
1657
+
1658
+ .towb_editorfield_short {
1659
+ width: 30% !important;
1660
+ }
1661
+
1662
+ </style>
1663
+ <script type="text/javascript">
1664
+ // CSS
1665
+ dropdownStyles = `
1666
+ .loggertype-dropdown {
1667
+ position: absolute;
1668
+ background: white;
1669
+ border: 1px solid #ccc;
1670
+ border-radius: 4px;
1671
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1672
+ max-height: 150px;
1673
+ overflow-y: auto;
1674
+ z-index: 1000;
1675
+ min-width: 150px;
1676
+ }
1677
+ .loggertype-dropdown .dropdown-item {
1678
+ padding: 6px 12px;
1679
+ cursor: pointer;
1680
+ }
1681
+ .loggertype-dropdown .dropdown-item:hover {
1682
+ background: #f0f0f0;
1683
+ }
1684
+ `;
1685
+ $('<style>').text(dropdownStyles).appendTo('head');
1686
+
1687
+ </script>
1688
+ <!-- script-editor -->
1689
+ <script src="resources/@theotherwillembotha/node-red-plugincore/editorLanguageExtension.js"></script>
1690
+ <script type="text/javascript">
1691
+ window.PluginCore = window.PluginCore || {};
1692
+ /**
1693
+ * PluginCore.createScriptEditor(elementId, template, initialValue)
1694
+ *
1695
+ * Creates a Monaco editor instance with TypeScript-aware diagnostics,
1696
+ * completions, and hover info scoped to the provided type template.
1697
+ *
1698
+ * @param {string} elementId - DOM id of the container element.
1699
+ * @param {string} template - TypeScript template string. Use `${script}`
1700
+ * as the placeholder for the user's code.
1701
+ * @param {string} initialValue - The starting content of the editor.
1702
+ * @returns {{ getValue(): string, dispose(): void \}}
1703
+ */
1704
+ PluginCore.createScriptEditor = function(elementId, template, initialValue) {
1705
+ var editorInstance = null;
1706
+ var currentValue = initialValue || '';
1707
+ ScriptEditorLanguageManager.createLanguage(monaco, template).then(function(language) {
1708
+ editorInstance = monaco.editor.create(document.getElementById(elementId), {
1709
+ value: currentValue,
1710
+ language: language.getName(),
1711
+ automaticLayout: true,
1712
+ wordBasedSuggestions: 'currentDocument',
1713
+ });
1714
+ });
1715
+ return {
1716
+ getValue: function() {
1717
+ return editorInstance ? editorInstance.getValue() : currentValue;
1718
+ },
1719
+ dispose: function() {
1720
+ if (editorInstance) {
1721
+ editorInstance.dispose();
1722
+ editorInstance = null;
1723
+ }
1724
+ }
1725
+ };
1726
+ };
1727
+
1728
+ </script>
1729
+ <script type="text/javascript">
1730
+ RED.nodes.registerType('CircuitBreakerStateNode', {
1731
+ category: 'circuitbreaker',
1732
+ icon: 'circuitrbreaker.png',
1733
+ color: '#1D7874',
1734
+ labelStyle: 'node_label_white',
1735
+ label: function() {
1736
+ return this.name
1737
+ },
1738
+ paletteLabel: 'Circuit Breaker State',
1739
+ inputs: 1,
1740
+ inputLabels: (i) => 'input',
1741
+ outputs: 1,
1742
+ outputLabels: (i) => ['output'][i],
1743
+ defaults: {
1744
+ name: {
1745
+ value: 'CircuitBreakerState Example',
1746
+ required: true
1747
+ },
1748
+ circuitbreakerconfig: {
1749
+ type: 'CircuitBreakerConfigNode',
1750
+ required: true
1751
+ },
1752
+ action: {
1753
+ value: 'Reset',
1754
+ required: true
1755
+ },
1756
+ script: {
1757
+ value: 'console.log(message, breaker)'
1758
+ },
1759
+ logEnabled: {
1760
+ value: false,
1761
+ required: true
1762
+ },
1763
+ logger: {
1764
+ required: false,
1765
+ value: '',
1766
+ type: 'DelegatedConfigReferenceNode'
1767
+ },
1768
+ logTemplateOverrideEnabled: {
1769
+ value: false
1770
+ },
1771
+ logTemplateOverride: {
1772
+ value: 'message:{{msg}}'
1773
+ },
1774
+ metricsEnabled: {
1775
+ value: false,
1776
+ required: true
1777
+ },
1778
+ metricsReference: {
1779
+ type: 'MetricsConfigNode',
1780
+ required: false
1781
+ },
1782
+ },
1783
+ oneditprepare: function() {
1784
+ // **** CircuitBreakerStateNode **** //
1785
+ {
1786
+ let node = this;
1787
+ let selectActionType = function(action) {
1788
+ if (action === "Script") {
1789
+ $("#CircuitBreakerStateScript").show();
1790
+ } else {
1791
+ $("#CircuitBreakerStateScript").hide();
1792
+ }
1793
+ }
1794
+ let nodeAction = $("#node-input-action");
1795
+ nodeAction.typedInput({
1796
+ type: "action",
1797
+ types: [{
1798
+ value: "action",
1799
+ options: [{
1800
+ value: "Reset",
1801
+ label: "Reset"
1802
+ }, {
1803
+ value: "Trip",
1804
+ label: "Trip"
1805
+ }, {
1806
+ value: "Script",
1807
+ label: "Script"
1808
+ }]
1809
+ }]
1810
+ });
1811
+ nodeAction.on('change', () => selectActionType(nodeAction.val()));
1812
+ selectActionType(nodeAction.val());
1813
+ let scriptTemplate = `
1814
+ /**
1815
+ * Represents the message object that is passed into this node.
1816
+ */
1817
+ interface Message {
1818
+ [key:string]:any;
1819
+ }
1820
+
1821
+ enum BreakerState {
1822
+ Open = "Open",
1823
+ Closed = "Closed"
1824
+ }
1825
+
1826
+ /**
1827
+ * Represents the breaker object that this fault is linked to.
1828
+ */
1829
+ interface Breaker {
1830
+ /**
1831
+ * Get the context that is attached to the breaker.
1832
+ * @returns the attached breaker context.
1833
+ */
1834
+ context():BreakerContext;
1835
+
1836
+ /**
1837
+ * Returns the state of the breaker.
1838
+ * @returns the state of the breaker.
1839
+ */
1840
+ state():BreakerState;
1841
+ isOpen():boolean;
1842
+ isClosed():boolean;
1843
+ reset():boolean;
1844
+ trip():boolean;
1845
+ }
1846
+
1847
+ /**
1848
+ * Represents a context on the breaker where properties can be set or removed.
1849
+ */
1850
+ interface BreakerContext {
1851
+ /**
1852
+ * Gets a particular context value from the BreakerContext.
1853
+ * @param id - The id of the property to retrieve.
1854
+ * @param defaultValue - The optional default for the context property if it does not exist.
1855
+ * @returns a value if it exists, or undefined otherwise.
1856
+ */
1857
+ get<T>(id:string, defaultValue?:T): T;
1858
+
1859
+ /**
1860
+ * Sets a particular context value in the BreakerContext.
1861
+ * @param id - The id of the property to set.
1862
+ * @param value - The value to assign to the context property.
1863
+ * @returns the value that has just been set.
1864
+ */
1865
+ set<T>(id:string, value:T): T;
1866
+ }
1867
+
1868
+ async function(breaker:Breaker, message:Message) {
1869
+ \${script}
1870
+ }
1871
+ `;
1872
+ node.scriptEditor = PluginCore.createScriptEditor('node-input-script-editor', scriptTemplate, node.script !== undefined ? node.script : 'console.log(message, breaker)');
1873
+ }
1874
+ // **** logger **** //
1875
+ {
1876
+ let node = this;
1877
+ // controls.
1878
+ let logEnabled = $("#node-input-logEnabled");
1879
+ let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
1880
+ let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
1881
+ let logEnabledSection = $("#section_logEnabled");
1882
+ // functions.
1883
+ let getLoggerTypes = async function() {
1884
+ let types = [];
1885
+ await $.ajax({
1886
+ url: "nodetypeservice/find?tag=LoggerType",
1887
+ type: "GET",
1888
+ contentType: "application/json; charset=utf-8",
1889
+ data: JSON.stringify({
1890
+ id: node.id
1891
+ }),
1892
+ success: (response) => {
1893
+ types = response;
1894
+ },
1895
+ error: (jqXHR, textStatus, errorThrown) => {
1896
+ console.log(jqXHR, textStatus, errorThrown);
1897
+ },
1898
+ });
1899
+ return types;
1900
+ }
1901
+ let getLoggers = async () => {
1902
+ let loggers = [];
1903
+ // get the list of types again.
1904
+ let loggerTypes = (await getLoggerTypes()).map(type => type.id);
1905
+ // add all of the known loggers.
1906
+ RED.nodes.eachConfig(cfg => {
1907
+ if (loggerTypes.includes(cfg.type)) {
1908
+ loggers.push({
1909
+ id: cfg.id,
1910
+ name: cfg.label(),
1911
+ type: cfg.type
1912
+ });
1913
+ }
1914
+ });
1915
+ return loggers;
1916
+ };
1917
+ let updateLoggerSelectorList = async () => {
1918
+ loggerSelector.empty();
1919
+ (await getLoggers()).forEach(logger => {
1920
+ $('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
1921
+ });
1922
+ $('<option value="_ADD_">none</option>').appendTo(loggerSelector);
1923
+ }
1924
+ logEnabled.on('change', function() {
1925
+ if (logEnabled.prop("checked")) {
1926
+ logEnabledSection.show();
1927
+ node._def.defaults.logger.required = true;
1928
+ } else {
1929
+ logEnabledSection.hide()
1930
+ node._def.defaults.logger.required = false;
1931
+ }
1932
+ });
1933
+ logTemplateEnabled.on('change', function() {
1934
+ if (logTemplateEnabled.prop("checked")) {
1935
+ logTemplateEnabledSection.show();
1936
+ node._def.defaults.logTemplateOverride.required = true;
1937
+ } else {
1938
+ logTemplateEnabledSection.hide()
1939
+ node._def.defaults.logTemplateOverride.required = false;
1940
+ }
1941
+ });
1942
+ node.logTemplateOverrideEditor = RED.editor.createEditor({
1943
+ id: 'node-input-logTemplateOverrideEditor',
1944
+ mode: 'ace/mode/handlebars',
1945
+ value: node.logTemplateOverride
1946
+ });
1947
+ let loggerSelector = $("#logger-selector");
1948
+ let loggerEditButton = $("#logger-edit-btn");
1949
+ let loggerAddButton = $("#logger-add-btn");
1950
+ loggerEditButton.on('click', async function(event) {
1951
+ event.stopPropagation();
1952
+ if (loggerEditButton.hasClass("disabled")) {
1953
+ return;
1954
+ }
1955
+ // figure out what type of logger this is.
1956
+ let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
1957
+ RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
1958
+ });
1959
+ loggerAddButton.on('click', function(event) {
1960
+ event.stopPropagation();
1961
+ let $btn = $(event.target);
1962
+ // Remove any existing dropdown
1963
+ $('.loggertype-dropdown').remove();
1964
+ getLoggerTypes().then(loggerTypes => {
1965
+ let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
1966
+ loggerTypes.forEach(loggerType => {
1967
+ $('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
1968
+ padding: '4px 8px',
1969
+ cursor: 'pointer'
1970
+ }).appendTo($dropdown);
1971
+ });
1972
+ // Append first so outerWidth() is measurable, then position so the
1973
+ // right edge of the dropdown aligns with the right edge of the button.
1974
+ $('body').append($dropdown);
1975
+ $dropdown.css({
1976
+ top: $btn.offset().top + $btn.outerHeight(),
1977
+ left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
1978
+ });
1979
+ $(document).on('mousedown.dropdown', function(e) {
1980
+ if (!$(e.target).closest('.loggertype-dropdown').length) {
1981
+ $dropdown.remove();
1982
+ $(document).off('mousedown.dropdown'); // Clean up the listener
1983
+ }
1984
+ });
1985
+ // Handle item selection
1986
+ $dropdown.on('click', '.dropdown-item', function(e) {
1987
+ e.stopPropagation();
1988
+ const selectedValue = $(this).data('value');
1989
+ const selectedText = $(this).text();
1990
+ // Do whatever you need with the selection
1991
+ console.log('Selected:', selectedValue, selectedText);
1992
+ $dropdown.remove();
1993
+ RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
1994
+ });
1995
+ })
1996
+ });
1997
+ loggerSelector.on("focus", updateLoggerSelectorList);
1998
+ loggerSelector.on('change', function(event) {
1999
+ if ("_ADD_" === loggerSelector.val()) {
2000
+ loggerEditButton.addClass("disabled")
2001
+ } else {
2002
+ loggerEditButton.removeClass("disabled")
2003
+ }
2004
+ });
2005
+ // lastly, make sure that we set the correct value on the logger selector.
2006
+ updateLoggerSelectorList().then(() => {
2007
+ loggerSelector.val(node.logger);
2008
+ loggerSelector.trigger('change');
2009
+ });
2010
+ }
2011
+ // **** metrics **** //
2012
+ {
2013
+ let node = this;
2014
+ let metricsEnabled = $("#node-input-metricsEnabled");
2015
+ let metricsEnabledSection = $("#section_metricsEnabled");
2016
+ metricsEnabled.on('change', function() {
2017
+ if (metricsEnabled.prop("checked")) {
2018
+ metricsEnabledSection.show();
2019
+ node._def.defaults.metricsReference.required = true;
2020
+ } else {
2021
+ metricsEnabledSection.hide()
2022
+ node._def.defaults.metricsReference.required = false;
2023
+ }
2024
+ });
2025
+ }
2026
+ },
2027
+ oneditsave: function() {
2028
+ // **** CircuitBreakerStateNode **** //
2029
+ {
2030
+ let node = this;
2031
+ node.script = node.scriptEditor.getValue();
2032
+ delete node.scriptEditor;
2033
+ }
2034
+ // **** logger **** //
2035
+ {
2036
+ node = this;
2037
+ var selectedLogger = $("#logger-selector").val();
2038
+ node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
2039
+ node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
2040
+ node.logTemplateOverrideEditor.destroy();
2041
+ delete node.logTemplateOverrideEditor;
2042
+ }
2043
+ // **** metrics **** //
2044
+ {}
2045
+ },
2046
+ oneditcancel: function() {},
2047
+ oneditdelete: function() {},
2048
+ });
2049
+
2050
+ </script>
2051
+
2052
+
2053
+ <script type="text/html" data-template-name='CircuitBreakerStateNode'>
2054
+ <div id='section_CircuitBreakerStateNode'>
2055
+ <div class="form-row">
2056
+ <label for="node-input-name" class="towb_editorlabel"><i class="fa fa-font"></i> Name</label>
2057
+ <input type="text" id="node-input-name" />
2058
+ </div>
2059
+
2060
+ <div class="form-row">
2061
+ <label for="node-input-circuitbreakerconfig" class="towb_editorlabel"><i class="fa fa-bolt"></i> Circuit Breaker</label>
2062
+ <input type="text" id="node-input-circuitbreakerconfig" />
2063
+ </div>
2064
+
2065
+ <div class="form-row">
2066
+ <label for="node-input-action" class="towb_editorlabel"><i class="fa fa-play"></i> On Message</label>
2067
+ <input type="text" id="node-input-action" />
2068
+ </div>
2069
+
2070
+ <div id="CircuitBreakerStateScript">
2071
+ <div class="form-row">
2072
+ <label for="node-input-script-editor" class="towb_editorlabel"><i class="fa fa-code"></i> Script Action</label>
2073
+ <div style="height: 200px; min-height: 80px; resize: vertical; overflow: auto;" class="node-text-editor" id="node-input-script-editor"></div>
2074
+ </div>
2075
+ </div>
2076
+
2077
+ </div>
2078
+
2079
+ <div id='section_logger'>
2080
+ <div id="section_loggerconfig">
2081
+ <div class="form-row editorsectionheading">
2082
+ <i class="w-16 fa fa-eye"></i> <span>Logging</span>
2083
+ </div>
2084
+ <div class="editorgroupborder">
2085
+ <div class="form-row nomargin logEnableCheck">
2086
+ <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
2087
+ <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
2088
+ </div>
2089
+
2090
+ <div id="section_logEnabled">
2091
+ <div class="form-row nomargin">
2092
+ <label for="logger-selector">Logger</label>
2093
+ <div style="width: 70%; display: inline-flex;">
2094
+ <select id="logger-selector" style="flex-grow: 1;" class="">
2095
+ <option value="_ADD_">none</option>
2096
+ </select>
2097
+ <a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
2098
+ <i class="fa fa-pencil"></i>
2099
+ </a>
2100
+ <a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
2101
+ <i class="fa fa-plus"></i>
2102
+ </a>
2103
+ </div>
2104
+ </div>
2105
+
2106
+ <div class="form-row nomargin">
2107
+ <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
2108
+ <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
2109
+ </div>
2110
+
2111
+ <div id="section_logTemplateOverrideEnabled">
2112
+ <div class="form-row">
2113
+ <label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
2114
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
2115
+ </div>
2116
+ </div>
2117
+ </div>
2118
+ </div>
2119
+ </div>
2120
+
2121
+ </div>
2122
+
2123
+ <div id='section_metrics'>
2124
+ <div id="section_metricsconfig">
2125
+ <div class="form-row editorsectionheading">
2126
+ <i class="w-16 fa fa-eye"></i> <span>Metrics</span>
2127
+ </div>
2128
+ <div class="editorgroupborder">
2129
+ <div class="form-row nomargin metricsEnableCheck">
2130
+ <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
2131
+ <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
2132
+ </div>
2133
+
2134
+ <div id="section_metricsEnabled">
2135
+ <div class="form-row nomargin">
2136
+ <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
2137
+ <input id="node-input-metricsReference" placeholder="metrics" />
2138
+ </div>
2139
+ </div>
2140
+ </div>
2141
+ </div>
2142
+
2143
+ </div>
2144
+ </script>
2145
+
2146
+ <script type="text/markdown" data-help-name='CircuitBreakerStateNode'>
2147
+ Manually controls the state of a circuit breaker when a message arrives. Use this to build recovery flows, health-check driven resets, or manual override controls.
2148
+
2149
+ ### Properties
2150
+
2151
+ : *name* (string) : A descriptive label for this node.
2152
+ : *circuit breaker* (Circuit Breaker Config) : The circuit breaker instance to control.
2153
+ : *on message* (action) : The action to perform when a message is received: **Reset**, **Trip**, or **Script**.
2154
+
2155
+ ### Actions
2156
+
2157
+ **Reset** - closes the breaker, allowing messages to flow through Circuit Breaker nodes again.
2158
+
2159
+ **Trip** - opens the breaker, diverting messages to the open output of Circuit Breaker nodes.
2160
+
2161
+ **Script** - runs a custom script that receives the full breaker API and the incoming message. Use this for conditional or gradual recovery logic.
2162
+
2163
+ ```javascript
2164
+ // Example: reset only if the breaker has been open for at least 30 seconds
2165
+ let trippedAt = breaker.context().get("trippedAt", 0);
2166
+ if (new Date().getTime() - trippedAt > 30000) {
2167
+ breaker.reset();
2168
+ }
2169
+ ```
2170
+
2171
+ ### Inputs
2172
+
2173
+ : *msg* (object) : Any message. Triggers the configured action on the linked breaker.
2174
+
2175
+ ### Outputs
2176
+
2177
+ : *msg* (object) : The original message, forwarded after the action has been applied.
2178
+
2179
+ ### Logging
2180
+ : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
2181
+ : *logger* (logconfig) : the endppoint that the node will log events to.
2182
+ : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
2183
+ : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
2184
+
2185
+ ### Metrics
2186
+ : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
2187
+ : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
2188
+ </script>
2189
+
2190
+