@theotherwillembotha/node-red-plugincore 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +616 -220
  2. package/build/GenerateNodes.js +3 -6
  3. package/build/Nodes.html +3 -1504
  4. package/build/Nodes.js +5 -15
  5. package/build/Plugins.html +227 -0
  6. package/build/Plugins.js +18 -8
  7. package/build/core/NodeConstructor.d.ts +9 -16
  8. package/build/core/NodeConstructor.d.ts.map +1 -1
  9. package/build/core/NodeConstructor.js +109 -146
  10. package/build/core/NodeGenerator.d.ts.map +1 -1
  11. package/build/core/NodeGenerator.js +79 -28
  12. package/build/core/NodeManagerRuntime.d.ts +44 -0
  13. package/build/core/NodeManagerRuntime.d.ts.map +1 -0
  14. package/build/core/NodeManagerRuntime.js +140 -0
  15. package/build/core/Utils.d.ts.map +1 -1
  16. package/build/core/logger/service/LoggerService.d.ts +6 -5
  17. package/build/core/logger/service/LoggerService.d.ts.map +1 -1
  18. package/build/core/logger/service/LoggerService.js +51 -20
  19. package/build/core/logger/template/LoggerTemplate.d.ts.map +1 -1
  20. package/build/core/metrics/MetricsDecorator.d.ts +1 -2
  21. package/build/core/metrics/MetricsDecorator.d.ts.map +1 -1
  22. package/build/core/metrics/MetricsDecorator.js +89 -116
  23. package/build/core/metrics/node/CounterMetricConfigNode.d.ts.map +1 -1
  24. package/build/core/metrics/node/CounterMetricConfigNode.js +4 -5
  25. package/build/core/metrics/node/GaugeMetricConfigNode.d.ts.map +1 -1
  26. package/build/core/metrics/node/GaugeMetricConfigNode.js +5 -6
  27. package/build/core/metrics/node/MetricsConfigNode.d.ts +19 -9
  28. package/build/core/metrics/node/MetricsConfigNode.d.ts.map +1 -1
  29. package/build/core/metrics/node/MetricsConfigNode.js +14 -45
  30. package/build/core/metrics/node/TimerMetricConfigNode.d.ts.map +1 -1
  31. package/build/core/metrics/node/TimerMetricConfigNode.js +11 -17
  32. package/build/core/metrics/service/MetricsService.d.ts +110 -114
  33. package/build/core/metrics/service/MetricsService.d.ts.map +1 -1
  34. package/build/core/metrics/service/MetricsService.js +122 -339
  35. package/build/core/metrics/template/CounterMetricTemplate.d.ts.map +1 -1
  36. package/build/core/metrics/template/GaugeMetricTemplate.d.ts.map +1 -1
  37. package/build/core/metrics/template/MetricsTemplate.d.ts.map +1 -1
  38. package/build/core/metrics/template/MetricsTemplate.js +1 -2
  39. package/build/core/metrics/template/TimerMetricTemplate.d.ts.map +1 -1
  40. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts +1 -1
  41. package/build/core/other/node/DelegatedConfigReferenceNode.d.ts.map +1 -1
  42. package/build/core/other/node/DelegatedConfigReferenceNode.js +1 -1
  43. package/build/core/other/service/InputService.d.ts.map +1 -1
  44. package/build/core/other/service/SettingsService.d.ts +1 -2
  45. package/build/core/other/service/SettingsService.d.ts.map +1 -1
  46. package/build/core/other/service/SettingsService.js +19 -5
  47. package/build/core/other/template/BasicTemplate.d.ts.map +1 -1
  48. package/build/core/other/template/SettingsTemplate.d.ts.map +1 -1
  49. package/build/core/state/node/InternalStateConfigNode.d.ts +16 -0
  50. package/build/core/state/node/InternalStateConfigNode.d.ts.map +1 -0
  51. package/build/core/state/node/InternalStateConfigNode.js +89 -0
  52. package/build/core/state/node/StateConfigNode.d.ts +24 -0
  53. package/build/core/state/node/StateConfigNode.d.ts.map +1 -0
  54. package/build/core/state/node/StateConfigNode.js +22 -0
  55. package/build/core/state/service/StateService.d.ts +34 -0
  56. package/build/core/state/service/StateService.d.ts.map +1 -0
  57. package/build/core/state/service/StateService.js +201 -0
  58. package/build/core/state/template/StateTemplate.d.ts +8 -0
  59. package/build/core/state/template/StateTemplate.d.ts.map +1 -0
  60. package/build/core/state/template/StateTemplate.js +11 -0
  61. package/build/core/tagging/ServiceDescriptionDecorator.d.ts +10 -0
  62. package/build/core/tagging/ServiceDescriptionDecorator.d.ts.map +1 -0
  63. package/build/core/tagging/ServiceDescriptionDecorator.js +10 -0
  64. package/build/core/tagging/service/NodeTypeService.d.ts +1 -2
  65. package/build/core/tagging/service/NodeTypeService.d.ts.map +1 -1
  66. package/build/core/tagging/service/NodeTypeService.js +19 -5
  67. package/build/core/ui/template/ScriptEditorTemplate.d.ts.map +1 -1
  68. package/build/core/ui/template/UIHelperTemplate.d.ts.map +1 -1
  69. package/build/core/webhook/node/WebhookServerConfigNode.d.ts.map +1 -1
  70. package/build/core/webhook/service/ReverseProxyTypeService.d.ts.map +1 -1
  71. package/build/core/webhook/service/WebhookServerService.d.ts +3 -4
  72. package/build/core/webhook/service/WebhookServerService.d.ts.map +1 -1
  73. package/build/core/webhook/service/WebhookServerService.js +48 -18
  74. package/build/core/webhook/template/WebhookTemplate.d.ts.map +1 -1
  75. package/build/index.d.ts +5 -2
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +6 -2
  78. package/build/runtime/NodeManagerRuntime.js +140 -0
  79. package/documentation/CounterMetricConfigNode.png +0 -0
  80. package/documentation/GaugeMetricConfigNode.png +0 -0
  81. package/documentation/LoggerTemplate.png +0 -0
  82. package/documentation/MetricsTemplate.png +0 -0
  83. package/documentation/StateTemplate.png +0 -0
  84. package/documentation/TimerMetricConfigNode_histogram.png +0 -0
  85. package/documentation/TimerMetricConfigNode_summary.png +0 -0
  86. package/documentation/WebhookServerConfigNode.png +0 -0
  87. package/documentation/WebhookTemplate.png +0 -0
  88. package/package.json +4 -10
  89. package/src/GenerateNodes.ts +7 -11
  90. package/src/core/NodeConstructor.ts +130 -186
  91. package/src/core/NodeGenerator.ts +88 -33
  92. package/src/core/NodeManagerRuntime.ts +179 -0
  93. package/src/core/logger/service/LoggerService.ts +39 -26
  94. package/src/core/logger/template/LoggerTemplate.html +11 -6
  95. package/src/core/metrics/MetricsDecorator.ts +140 -168
  96. package/src/core/metrics/node/CounterMetricConfigNode.ts +20 -20
  97. package/src/core/metrics/node/GaugeMetricConfigNode.ts +24 -25
  98. package/src/core/metrics/node/MetricsConfigNode.ts +29 -48
  99. package/src/core/metrics/node/TimerMetricConfigNode.ts +50 -59
  100. package/src/core/metrics/service/MetricsService.ts +214 -489
  101. package/src/core/metrics/template/CounterMetricTemplate.html +0 -1
  102. package/src/core/metrics/template/GaugeMetricTemplate.html +1 -1
  103. package/src/core/metrics/template/MetricsTemplate.html +132 -13
  104. package/src/core/metrics/template/MetricsTemplate.ts +3 -4
  105. package/src/core/other/node/DelegatedConfigReferenceNode.ts +1 -1
  106. package/src/core/other/service/SettingsService.ts +6 -10
  107. package/src/core/other/template/BasicTemplate.html +1 -1
  108. package/src/core/state/node/InternalStateConfigNode.html +77 -0
  109. package/src/core/state/node/InternalStateConfigNode.ts +102 -0
  110. package/src/core/state/node/StateConfigNode.ts +29 -0
  111. package/src/core/state/service/StateService.ts +180 -0
  112. package/src/core/state/template/StateTemplate.html +244 -0
  113. package/src/core/state/template/StateTemplate.ts +17 -0
  114. package/src/core/tagging/ServiceDescriptionDecorator.ts +23 -0
  115. package/src/core/tagging/service/NodeTypeService.ts +6 -11
  116. package/src/core/ui/template/UIHelperTemplate.html +41 -26
  117. package/src/core/webhook/service/WebhookServerService.ts +39 -23
  118. package/src/core/webhook/template/WebhookTemplate.html +28 -35
  119. package/src/index.ts +7 -2
  120. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts +0 -20
  121. package/build/core/logger/node/ConsoleLoggerConfigNode.d.ts.map +0 -1
  122. package/build/core/logger/node/ConsoleLoggerConfigNode.js +0 -62
  123. package/build/core/logger/node/RestLoggerConfigNode.d.ts +0 -40
  124. package/build/core/logger/node/RestLoggerConfigNode.d.ts.map +0 -1
  125. package/build/core/logger/node/RestLoggerConfigNode.js +0 -128
  126. package/documentation/ConsoleLoggerConfigNode.png +0 -0
  127. package/documentation/MetricsConfigNode.png +0 -0
  128. package/documentation/RestLoggerConfigNode.png +0 -0
  129. package/documentation/TimerMetricConfigNode.png +0 -0
  130. package/documentation/WebhookNode.png +0 -0
  131. package/src/core/logger/node/ConsoleLoggerConfigNode.html +0 -90
  132. package/src/core/logger/node/ConsoleLoggerConfigNode.ts +0 -71
  133. package/src/core/logger/node/RestLoggerConfigNode.html +0 -183
  134. package/src/core/logger/node/RestLoggerConfigNode.ts +0 -170
@@ -0,0 +1,244 @@
1
+ <script type="text/javascript" template-section="onCompose">
2
+ node
3
+ .addDefault("stateReference", { required: false, value: '', type: "DelegatedConfigReferenceNode" })
4
+ </script>
5
+
6
+ <script type="text/javascript" template-section="onIncludeEditPrepare">
7
+
8
+ // Read name suffix and declared states from the build-time templateConfig injected by NodeConstructor.
9
+ let _stateNameSuffix = (templateConfig && templateConfig.nameSuffix) ? templateConfig.nameSuffix : '-State';
10
+ let _stateStates = (templateConfig && templateConfig.defaultStates) ? templateConfig.defaultStates : null;
11
+
12
+ let stateSection = $("#state-config-section");
13
+ let typeSelector = stateSection.find("#state-type-selector");
14
+ let configureBtn = stateSection.find("#state-configure-btn");
15
+ let stateRefSelector = stateSection.find("#state-ref-selector");
16
+
17
+ // Seed the hidden select with the current value and record the original for cleanup.
18
+ stateRefSelector.empty();
19
+ if (node.stateReference) {
20
+ let existingNode = RED.nodes.node(node.stateReference);
21
+ let label = existingNode ? (existingNode.name || node.stateReference) : node.stateReference;
22
+ stateRefSelector.append($('<option>').val(node.stateReference).text(label));
23
+ stateRefSelector.val(node.stateReference);
24
+ }
25
+ stateRefSelector.data('originalRef', node.stateReference || '');
26
+
27
+ let updateConfigureBtn = function() {
28
+ configureBtn.toggleClass("disabled", !typeSelector.val());
29
+ };
30
+
31
+ let getStateProviderTypes = async function() {
32
+ let types = [];
33
+ await $.ajax({
34
+ url: "nodetypeservice/find?tag=StateProvider",
35
+ type: "GET",
36
+ contentType: "application/json; charset=utf-8",
37
+ success: (r) => { types = r; },
38
+ error: (jqXHR, s, err) => { console.log("StateTemplate: provider types error", jqXHR, s, err); },
39
+ });
40
+ return types;
41
+ };
42
+
43
+ // RED.editor.editConfig captures a reference to the field element before the inner config
44
+ // editor opens. When editing a ConfigNode, the outer editor's DOM is swapped out while the
45
+ // inner editor is active, so the captured reference becomes stale and editConfig's own
46
+ // callback silently fails to update our hidden select.
47
+ //
48
+ // Instead we listen for the 'nodes:add' event which fires via RED.nodes.add() whenever a
49
+ // new node (including config nodes) is added to the workspace - regardless of DOM state.
50
+ let stateNodeAddHandler = function(addedNode) {
51
+ getStateProviderTypes().then(function(types) {
52
+ let typeIds = types.map(function(t) { return t.id; });
53
+ if (typeIds.indexOf(addedNode.type) < 0) { return; }
54
+
55
+ console.log("StateTemplate: nodes:add - state provider added:", addedNode.type, addedNode.id);
56
+
57
+ let prevRef = stateRefSelector.val() || '';
58
+ let originalRef = stateRefSelector.data('originalRef') || '';
59
+
60
+ // Clean up any intermediate node created earlier this session (not the original).
61
+ if (prevRef && prevRef !== originalRef) {
62
+ let inter = RED.nodes.node(prevRef);
63
+ if (inter) { RED.nodes.remove(inter); RED.nodes.dirty(true); }
64
+ }
65
+
66
+ // Update the hidden select with the newly created node.
67
+ stateRefSelector.empty();
68
+ stateRefSelector.append($('<option>').val(addedNode.id).text(addedNode.name || addedNode.id));
69
+ stateRefSelector.val(addedNode.id);
70
+
71
+ // Sync the visible type selector.
72
+ typeSelector.val(addedNode.type);
73
+ updateConfigureBtn();
74
+ });
75
+ };
76
+
77
+ RED.events.on('nodes:add', stateNodeAddHandler);
78
+ stateSection.data('stateNodeAddHandler', stateNodeAddHandler);
79
+
80
+ // When the type dropdown changes, restore the original ref if it matches,
81
+ // or clear so the user must click Configure to create a new instance.
82
+ typeSelector.on('change', function() {
83
+ let selectedType = typeSelector.val() || '';
84
+ let originalRef = stateRefSelector.data('originalRef') || '';
85
+ let currentRef = stateRefSelector.val() || '';
86
+
87
+ if (!selectedType) {
88
+ stateRefSelector.val('');
89
+ updateConfigureBtn();
90
+ return;
91
+ }
92
+ if (originalRef) {
93
+ let originalNode = RED.nodes.node(originalRef);
94
+ if (originalNode && originalNode.type === selectedType) {
95
+ stateRefSelector.val(originalRef);
96
+ updateConfigureBtn();
97
+ return;
98
+ }
99
+ }
100
+ if (currentRef) {
101
+ let currentNode = RED.nodes.node(currentRef);
102
+ if (currentNode && currentNode.type === selectedType) {
103
+ updateConfigureBtn();
104
+ return;
105
+ }
106
+ }
107
+ stateRefSelector.val('');
108
+ updateConfigureBtn();
109
+ });
110
+
111
+ configureBtn.on('click', function(event) {
112
+ event.stopPropagation();
113
+ if (configureBtn.hasClass("disabled")) return;
114
+
115
+ let selectedType = typeSelector.val() || '';
116
+ if (!selectedType) return;
117
+
118
+ let currentRef = stateRefSelector.val() || '';
119
+ let currentNode = currentRef ? RED.nodes.node(currentRef) : null;
120
+
121
+ // Always set __stateTemplateContext so the config node editor can read defaultName and states.
122
+ window.__stateTemplateContext = {
123
+ defaultName: (node.name || node.type) + _stateNameSuffix,
124
+ states: _stateStates
125
+ };
126
+
127
+ if (currentNode && currentNode.type === selectedType) {
128
+ // Editing an existing owned node - nodes:add won't fire, select already has the value.
129
+ RED.editor.editConfig("#state-ref-selector", selectedType, currentRef);
130
+ } else {
131
+ // Creating a new instance - nodes:add handler will capture the new ID.
132
+ RED.editor.editConfig("#state-ref-selector", selectedType, "_ADD_");
133
+ }
134
+ });
135
+
136
+ // Populate the type dropdown, pre-select the current type, or auto-create a default.
137
+ getStateProviderTypes().then(function(providerTypes) {
138
+ providerTypes.forEach(function(t) {
139
+ $('<option>').val(t.id).text(t.name).appendTo(typeSelector);
140
+ });
141
+
142
+ let currentRef = stateRefSelector.val();
143
+ if (currentRef) {
144
+ // Existing reference - just pre-select its type.
145
+ let currentNode = RED.nodes.node(currentRef);
146
+ if (currentNode) typeSelector.val(currentNode.type);
147
+ updateConfigureBtn();
148
+ } else if (providerTypes.length > 0) {
149
+ // No state reference - auto-create a default InternalStateConfigNode so the
150
+ // consumer always has a valid state provider without requiring user action.
151
+ let defaultType = providerTypes.find(function(t) { return t.id === "InternalStateConfigNode"; }) || providerTypes[0];
152
+ let nodeDef = RED.nodes.getType(defaultType.id);
153
+ if (nodeDef) {
154
+ // _def and _users MUST be set explicitly when creating a node programmatically:
155
+ // - _def: RED.nodes.add() triggers updateConfigNodeUsers() which accesses
156
+ // n._def.defaults. Without it: "can't access property '_', e._def is undefined".
157
+ // - _users: When the outer config node is saved, Node-RED updates the referenced
158
+ // config node's user list. Without it: "can't access property 'indexOf',
159
+ // t.users is undefined".
160
+ let newNode = { id: RED.nodes.id(), type: defaultType.id, _def: nodeDef, _users: [], users: [] };
161
+ let defs = nodeDef.defaults || {};
162
+ for (let key in defs) {
163
+ if (defs[key].value !== undefined) { newNode[key] = defs[key].value; }
164
+ }
165
+ newNode.name = (node.name || node.type) + _stateNameSuffix;
166
+ // Pre-select the type immediately; nodes:add handler will set the ref ID.
167
+ typeSelector.val(defaultType.id);
168
+ updateConfigureBtn();
169
+ RED.nodes.add(newNode);
170
+ }
171
+ }
172
+ });
173
+ </script>
174
+
175
+ <script type="text/javascript" template-section="onIncludeEditSave">
176
+ var _stateSection = $("#state-config-section");
177
+ var _stateNodeAddHandler = _stateSection.data('stateNodeAddHandler');
178
+ if (_stateNodeAddHandler) {
179
+ RED.events.off('nodes:add', _stateNodeAddHandler);
180
+ _stateSection.data('stateNodeAddHandler', null);
181
+ }
182
+ var _stateRefSelector = _stateSection.find("#state-ref-selector");
183
+ node.stateReference = _stateRefSelector.val() || '';
184
+ console.log("StateTemplate: save - stateReference:", node.stateReference);
185
+ var _stateOriginalRef = _stateRefSelector.data('originalRef') || '';
186
+ if (_stateOriginalRef && _stateOriginalRef !== node.stateReference) {
187
+ var _stateOldNode = RED.nodes.node(_stateOriginalRef);
188
+ if (_stateOldNode) {
189
+ RED.nodes.remove(_stateOldNode);
190
+ RED.nodes.dirty(true);
191
+ }
192
+ }
193
+ </script>
194
+
195
+ <script type="text/javascript" template-section="onIncludeEditCancel">
196
+ var _stateSection = $("#state-config-section");
197
+ var _stateNodeAddHandler = _stateSection.data('stateNodeAddHandler');
198
+ if (_stateNodeAddHandler) {
199
+ RED.events.off('nodes:add', _stateNodeAddHandler);
200
+ _stateSection.data('stateNodeAddHandler', null);
201
+ }
202
+ var _stateRefSelector = _stateSection.find("#state-ref-selector");
203
+ var _stateCurrentRef = _stateRefSelector.val() || '';
204
+ var _stateOriginalRef = _stateRefSelector.data('originalRef') || '';
205
+ if (_stateCurrentRef && _stateCurrentRef !== _stateOriginalRef) {
206
+ var _stateTempNode = RED.nodes.node(_stateCurrentRef);
207
+ if (_stateTempNode) {
208
+ RED.nodes.remove(_stateTempNode);
209
+ RED.nodes.dirty(true);
210
+ }
211
+ }
212
+ </script>
213
+
214
+ <script type="text/html" template-section="onIncludeEditForm">
215
+ <div id="state-config-section">
216
+ <div class="form-row editorsectionheading">
217
+ <i class="w-16 fa fa-exchange"></i> <span>State</span>
218
+ </div>
219
+ <div class="editorgroupborder">
220
+ <div class="form-row nomargin">
221
+ <label class="towb_editorlabel" for="state-type-selector">Provider</label>
222
+ <div style="width: 70%; display: inline-flex;">
223
+ <select id="state-type-selector" style="flex-grow: 1; min-width: 0;">
224
+ </select>
225
+ <a id="state-configure-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;" title="Configure state provider">
226
+ <i class="fa fa-pencil"></i>
227
+ </a>
228
+ </div>
229
+ </div>
230
+ <!-- Hidden real select - populated by nodes:add handler when a new state provider is created. -->
231
+ <select id="state-ref-selector" style="display:none;"></select>
232
+ </div>
233
+ </div>
234
+ </script>
235
+
236
+ <script type="text/markdown" template-section="onIncludeDocumentation">
237
+ ### State
238
+
239
+ : *provider* (stateconfig) : Selects the type of state backend for this node. Click the pencil icon to create or configure the provider instance.
240
+
241
+ Available built-in provider: **Internal State Config Node** - stores state in memory with optional persistence across redeployments and restarts.
242
+
243
+ Each node owns its state provider exclusively. Switching provider types during configuration will delete the old provider instance when the editor is saved.
244
+ </script>
@@ -0,0 +1,17 @@
1
+ import { SourceUtility } from "../../NodeGenerator";
2
+ import { BaseNodeConfig, Template, TemplateDescriptor } from "../../NodeConstructor";
3
+
4
+ export interface StateTemplateConfig extends BaseNodeConfig {
5
+ stateReference: string;
6
+ }
7
+
8
+ export class StateTemplate extends Template {
9
+
10
+ public static getTemplateDescriptor(): TemplateDescriptor {
11
+ return new TemplateDescriptor(
12
+ "state",
13
+ StateTemplate,
14
+ SourceUtility.getSourcePath("/build/", "/src/") + "StateTemplate.html",
15
+ []);
16
+ }
17
+ }
@@ -0,0 +1,23 @@
1
+ import { DependencyType, ServiceDescriptor } from "../NodeConstructor";
2
+
3
+ export type ServiceDescriptionConfig = {
4
+ id: string;
5
+ sourceFile: string;
6
+ type?: string;
7
+ name?: string;
8
+ dependencies?: DependencyType[];
9
+ }
10
+
11
+ export function ServiceDescription(config: ServiceDescriptionConfig) {
12
+ return function(target: any) {
13
+ const descriptor = new ServiceDescriptor(
14
+ config.id,
15
+ config.name ?? target.name,
16
+ config.type ?? "services-plugin",
17
+ config.sourceFile,
18
+ target,
19
+ config.dependencies
20
+ );
21
+ target.getServiceDescriptor = (): ServiceDescriptor => descriptor;
22
+ };
23
+ }
@@ -1,8 +1,13 @@
1
1
  import { INodeClass } from "../../NodeGenerator";
2
- import { BaseService, ConfigNode, FlowDeployment, ServiceDescriptor } from "../../NodeConstructor";
2
+ import { BaseService, ConfigNode, FlowDeployment } from "../../NodeConstructor";
3
+ import { ServiceDescription } from "../ServiceDescriptionDecorator";
3
4
  import { NodeAPI, NodeAPISettingsWithData } from "node-red";
4
5
 
5
6
 
7
+ @ServiceDescription({
8
+ id: "@theotherwillembotha/nodetypeservice",
9
+ sourceFile: "@theotherwillembotha/node-red-plugincore"
10
+ })
6
11
  export class NodeTypeService extends BaseService {
7
12
  private red!: NodeAPI<NodeAPISettingsWithData>;
8
13
  private nodeTypes:{[key:string]:INodeClass[]} = {};
@@ -54,14 +59,4 @@ export class NodeTypeService extends BaseService {
54
59
  return this.nodeTypes[tag] ? this.nodeTypes[tag] : [];
55
60
  }
56
61
 
57
- static override getServiceDescriptor():ServiceDescriptor {
58
- return new ServiceDescriptor(
59
- "@theotherwillembotha/nodetypeservice",
60
- "NodeTypeService",
61
- "services-plugin",
62
- "@theotherwillembotha/node-red-plugincore",
63
- NodeTypeService,
64
- []
65
- );
66
- }
67
62
  }
@@ -27,7 +27,7 @@
27
27
  overflow: hidden;
28
28
  }
29
29
 
30
- /* Title bar matches Node-RED's main nav bar colour */
30
+ /* Title bar - matches Node-RED's main nav bar colour */
31
31
  .plugincore-dialog-titlebar {
32
32
  display: flex;
33
33
  align-items: center;
@@ -43,7 +43,7 @@
43
43
  font-size: 13px;
44
44
  }
45
45
 
46
- /* Close button jQuery UI styling as used in Node-RED dialogs */
46
+ /* Close button - jQuery UI styling as used in Node-RED dialogs */
47
47
  .plugincore-dialog-close {
48
48
  color: #ccc !important;
49
49
  background: transparent !important;
@@ -56,9 +56,9 @@
56
56
  border-color: rgba(255, 255, 255, 0.3) !important;
57
57
  }
58
58
 
59
- /* Scrollable body that contains the jQuery UI tab widget */
59
+ /* Scrollable body that contains the jQuery UI tab widget or plain content */
60
60
  .plugincore-dialog-body {
61
- flex: 1;
61
+ flex: 1 1 auto;
62
62
  overflow: auto;
63
63
  padding: 10px;
64
64
  }
@@ -148,15 +148,45 @@
148
148
  return $table;
149
149
  };
150
150
 
151
+ /**
152
+ * PluginCore.tabs(tabs) → jQuery element
153
+ *
154
+ * tabs: [{ label: string, render($container) }]
155
+ *
156
+ * Builds and returns a jQuery UI tab widget element.
157
+ * Can be embedded in any container, including inside PluginCore.dialog render().
158
+ */
159
+ PluginCore.tabs = function(tabs) {
160
+ var uid = 'plugincore-tabs-' + Date.now();
161
+ var $tabsEl = $('<div>').attr('id', uid);
162
+ var $ul = $('<ul>');
163
+
164
+ (tabs || []).forEach(function(tab, i) {
165
+ var paneId = uid + '-pane-' + i;
166
+ $ul.append(
167
+ $('<li>').append($('<a>').attr('href', '#' + paneId).text(tab.label))
168
+ );
169
+ var $pane = $('<div>').attr('id', paneId);
170
+ if (tab.render) { tab.render($pane); }
171
+ $tabsEl.append($pane);
172
+ });
173
+
174
+ $tabsEl.prepend($ul);
175
+ $tabsEl.tabs();
176
+
177
+ return $tabsEl;
178
+ };
179
+
151
180
  /**
152
181
  * PluginCore.dialog(options) → { close() }
153
182
  *
154
183
  * options: {
155
- * title : string,
156
- * tabs : [{ label: string, render($container) }]
184
+ * title : string,
185
+ * render : function($container) - simple dialog; body passed directly
186
+ * tabs : [{ label: string, render($c) }] - tabbed dialog; delegates to PluginCore.tabs
157
187
  * }
158
188
  *
159
- * Uses jQuery UI tabs for the tab widget (already bundled with Node-RED).
189
+ * render and tabs can coexist: render is called first, then the tab widget is appended.
160
190
  * Closes on: close button click, overlay click, or Escape key.
161
191
  */
162
192
  PluginCore.dialog = function(options) {
@@ -179,32 +209,17 @@
179
209
  );
180
210
  $titlebar.append($closeBtn);
181
211
 
182
- // ── Body + jQuery UI tabs ──────────────────────────────────────────
183
- var $body = $('<div class="plugincore-dialog-body">');
184
- var $tabsEl = $('<div>').attr('id', uid + '-tabs');
185
- var $ul = $('<ul>');
212
+ // ── Body ──────────────────────────────────────────────────────────
213
+ var $body = $('<div class="plugincore-dialog-body">');
186
214
 
187
- (options.tabs || []).forEach(function(tab, i) {
188
- var paneId = uid + '-pane-' + i;
189
- $ul.append(
190
- $('<li>').append($('<a>').attr('href', '#' + paneId).text(tab.label))
191
- );
192
- var $pane = $('<div>').attr('id', paneId);
193
- if (tab.render) { tab.render($pane); }
194
- $tabsEl.append($pane);
195
- });
196
-
197
- $tabsEl.prepend($ul);
198
- $body.append($tabsEl);
215
+ if (options.render) { options.render($body); }
216
+ if (options.tabs) { $body.append(PluginCore.tabs(options.tabs)); }
199
217
 
200
218
  // ── Assemble ───────────────────────────────────────────────────────
201
219
  $dialog.append($titlebar).append($body);
202
220
  $overlay.append($dialog);
203
221
  $('body').append($overlay);
204
222
 
205
- // Initialise jQuery UI tabs (bundled with Node-RED)
206
- $tabsEl.tabs();
207
-
208
223
  // ── Close logic ───────────────────────────────────────────────────
209
224
  var escNs = 'keydown.plugincore-dialog-' + uid;
210
225
 
@@ -1,13 +1,15 @@
1
1
 
2
- import {BaseService, FlowDeployment, ServiceDescriptor } from "../../NodeConstructor";
2
+ import {BaseService, FlowDeployment} from "../../NodeConstructor";
3
+ import { ServiceDescription } from "../../tagging/ServiceDescriptionDecorator";
3
4
  import { NodeAPI, NodeAPISettingsWithData } from "node-red";
4
5
  import { WebhookServerConfigNode } from "../node/WebhookServerConfigNode";
5
6
  import { WebhookTemplate } from "../template/WebhookTemplate";
6
- import express, {Express, Request, Response} from "express";
7
+ import type { Express, Request, Response } from "express";
7
8
  import { Server } from "http";
8
9
  import { ApiKeyMechanismType, RestAuthType } from "../../logger/service/LoggerService";
9
10
 
10
- var bodyParser = require('body-parser');
11
+ function getExpress(): any { return require('express'); }
12
+ function getBodyParser(): any { return require('body-parser'); }
11
13
 
12
14
  export enum EndpointMethodType {
13
15
  GET = "GET",
@@ -46,8 +48,8 @@ export class WebhookServer {
46
48
  public constructor(config:WebhookServerConfig){
47
49
  this._config = config;
48
50
 
49
- this._app = express();
50
- this._app.use(bodyParser.json({type: "application/json", limit: '50mb'}));
51
+ this._app = getExpress()();
52
+ this._app.use(getBodyParser().json({type: "application/json", limit: '50mb'}));
51
53
  this._listener = this._app.listen(this._config.port, '0.0.0.0');
52
54
  }
53
55
 
@@ -117,6 +119,10 @@ export class WebhookServer {
117
119
 
118
120
  }
119
121
 
122
+ public close(): void {
123
+ this._listener.close();
124
+ }
125
+
120
126
  public config():WebhookServerConfig {
121
127
  return this._config;
122
128
  }
@@ -129,9 +135,19 @@ export type WebhookServerConfig = {
129
135
  externalPort:number;
130
136
  }
131
137
 
132
- export class WebhookServerService extends BaseService {
138
+ // Stored on global so all bundled copies share the same server registry.
139
+ const _GLOBAL_WEBHOOKSERVERS_KEY = '__plugincore_webhookservers__';
140
+ function getWebhookServersStore(): {[key:string]:WebhookServer} {
141
+ if (!(global as any)[_GLOBAL_WEBHOOKSERVERS_KEY]) (global as any)[_GLOBAL_WEBHOOKSERVERS_KEY] = {};
142
+ return (global as any)[_GLOBAL_WEBHOOKSERVERS_KEY];
143
+ }
133
144
 
134
- private static webhookServers:{[key:string]:WebhookServer} = {};
145
+ @ServiceDescription({
146
+ id: "@theotherwillembotha/webhookserverservice",
147
+ sourceFile: "@theotherwillembotha/node-red-plugincore",
148
+ dependencies: [WebhookServerConfigNode, WebhookTemplate]
149
+ })
150
+ export class WebhookServerService extends BaseService {
135
151
 
136
152
  constructor(){
137
153
  super("wehbookserver")
@@ -151,8 +167,10 @@ export class WebhookServerService extends BaseService {
151
167
 
152
168
  let webhookServerNodes = flowDeployment.flows.filter(node => node.type === "WebhookServerConfigNode") as any as WebhookServerConfig[];
153
169
 
170
+ const store = getWebhookServersStore();
171
+
154
172
  // find all the nodes that have been removed or changed.
155
- Object.entries(WebhookServerService.webhookServers).forEach(([key, serverService]) => {
173
+ Object.entries(store).forEach(([key, serverService]) => {
156
174
  let webhookServerNode = webhookServerNodes.find(node => node.id === key);
157
175
  let serverServiceConfig = serverService.config();
158
176
  // compare the properties.
@@ -177,30 +195,28 @@ export class WebhookServerService extends BaseService {
177
195
 
178
196
  // find all the servers that have been added.
179
197
  webhookServerNodes
180
- .filter(node => !WebhookServerService.webhookServers[node.id])
198
+ .filter(node => !store[node.id])
181
199
  .forEach(node => addedServers.push(node));
182
200
 
183
- //console.log("REMOVED SERVERS:", removedServers);
184
- //console.log("ADDED SERVERS:", addedServers);
201
+ // Close and remove servers that have been deleted or whose config changed.
202
+ removedServers.forEach(removedConfig => {
203
+ const server = store[removedConfig.id];
204
+ if (server) {
205
+ server.close();
206
+ delete store[removedConfig.id];
207
+ console.log(`Closed webhook server on port ${removedConfig.port}`);
208
+ }
209
+ });
185
210
  }
186
211
 
187
212
  public static get (config:WebhookServerConfig): WebhookServer{
188
- let server = (this.webhookServers[config.id]);
213
+ const store = getWebhookServersStore();
214
+ let server = store[config.id];
189
215
  if(!server){
190
- this.webhookServers[config.id] = server = new WebhookServer(config);
216
+ store[config.id] = server = new WebhookServer(config);
191
217
  console.log(`Creating webhook server on port ${config.port}`);
192
218
  }
193
219
  return server;
194
220
  }
195
221
 
196
- static override getServiceDescriptor():ServiceDescriptor {
197
- return new ServiceDescriptor(
198
- "@theotherwillembotha/webhookserverservice",
199
- "WebhookServerService",
200
- "services-plugin",
201
- "@theotherwillembotha/node-red-plugincore",
202
- WebhookServerService,
203
- [WebhookServerConfigNode, WebhookTemplate]
204
- );
205
- }
206
222
  }