@theotherwillembotha/node-red-plugincore 0.0.51 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @theotherwillembotha/node-red-plugincore
2
2
 
3
- A TypeScript framework for building production-grade Node-RED plugins with built-in support for structured logging, Prometheus metrics, and webhook servers.
3
+ A TypeScript framework for building production-grade Node-RED plugins with built-in support for structured logging, Prometheus metrics, webhook servers, and reusable UI components.
4
4
 
5
5
  This package has two roles:
6
6
 
7
7
  1. **Config nodes** — a set of shared configuration nodes (loggers, metric collectors, webhook server) that are installed into Node-RED and referenced by other nodes in a flow.
8
- 2. **Developer framework** — a TypeScript base library that plugin authors extend to build their own Node-RED nodes, with decorators and templates that wire in logging, metrics, and webhooks automatically.
8
+ 2. **Developer framework** — a TypeScript base library that plugin authors extend to build their own Node-RED nodes, with decorators and templates that wire in logging, metrics, webhooks, and editor UI automatically.
9
9
 
10
10
  ---
11
11
 
@@ -16,6 +16,7 @@ This framework is the foundation for a growing set of Node-RED plugins. The foll
16
16
  | Plugin | Description |
17
17
  |--------|-------------|
18
18
  | [@theotherwillembotha/node-red-telemetry](https://github.com/theotherwillembotha/nodered_telemetry) | Ready-to-use flow nodes for structured logging and Prometheus metrics — Logger, Counter, Gauge, and Timer nodes that attach to the config nodes provided by this package. |
19
+ | [@theotherwillembotha/node-red-nginxproxymanager](https://github.com/theotherwillembotha/nodered_nginxproxymanager) | Node-RED nodes for managing Nginx Proxy Manager hosts directly from your flows. Includes a config node that registers as a reverse proxy provider, an Update Host node for creating and updating proxy entries, and a Get Hosts node for retrieving the current host list. |
19
20
 
20
21
  Additional plugins will be listed here as they are published.
21
22
 
@@ -41,11 +42,19 @@ Config nodes are shared resources configured once and referenced across your flo
41
42
 
42
43
  Three logger backends are supported. All expose the same interface and are interchangeable — any node built with the `@Logger` decorator can use any of them.
43
44
 
44
- | Node | Description |
45
- |------|-------------|
46
- | **Console Logger** | Writes structured log output to stdout via Winston. Ideal for development and containerised deployments that forward stdout to a log aggregator. |
47
- | **REST Logger** | Ships log entries to a remote HTTP/HTTPS endpoint. Supports Basic and API Key authentication. |
48
- | **Loki Logger** | Pushes log entries to a Grafana Loki instance via the Loki HTTP API. Supports multi-tenant deployments. |
45
+ **Console Logger** writes structured log output to stdout via Winston. Ideal for development and containerised deployments that forward stdout to a log aggregator.
46
+
47
+ ![Console Logger Config](documentation/ConsoleLoggerConfigNode.png)
48
+
49
+ **REST Logger** ships log entries to a remote HTTP/HTTPS endpoint. Supports Basic and API Key authentication.
50
+
51
+ ![REST Logger Config](documentation/RestLoggerConfigNode.png)
52
+
53
+ **Loki Logger** — pushes log entries to a Grafana Loki instance via the Loki HTTP API. Supports multi-tenant deployments via the Tenant ID field.
54
+
55
+ ![Loki Logger Config](documentation/LokiLoggerConfigNode.png)
56
+
57
+ All three loggers share a **Level** selector (debug, info, warn, error) and a **Template** field — a Handlebars template that controls the shape of each log entry. The default `message:{{msg}}` passes the raw message through; you can customise it to include only the fields you care about.
49
58
 
50
59
  #### Metrics
51
60
 
@@ -169,9 +178,93 @@ Templates bundle reusable UI fragments that compose into any node's editor panel
169
178
  | `GaugeMetricTemplate` | Gauge config node reference |
170
179
  | `TimerMetricTemplate` | Timer config node reference |
171
180
  | `WebhookTemplate` | Webhook server reference, path, auth, and reverse proxy config |
181
+ | `UIHelperTemplate` | Global `PluginCore.dialog()` and `PluginCore.table()` UI factories (see below) |
172
182
  | `SettingsTemplate` | General settings section |
173
183
  | `BasicTemplate` | Base styles shared by all nodes |
174
184
 
185
+ ### UI helpers
186
+
187
+ Including `UIHelperTemplate` in a node's `templates` list injects two client-side factory functions into the Node-RED editor page. Both are available globally as `PluginCore.dialog(...)` and `PluginCore.table(...)` and are styled to match Node-RED's own editor aesthetic.
188
+
189
+ #### `PluginCore.dialog(options)`
190
+
191
+ Opens a modal overlay with a title bar and one or more tabs. Closes on the close button, an overlay click, or Escape.
192
+
193
+ ```javascript
194
+ PluginCore.dialog({
195
+ title: "My Plugin — Status",
196
+ tabs: [
197
+ {
198
+ label: "Proxy Hosts",
199
+ render: function($container) {
200
+ $container.append(
201
+ PluginCore.table({
202
+ columns: [
203
+ { key: "id", label: "ID" },
204
+ { key: "name", label: "Name" },
205
+ { key: "enabled", label: "Enabled",
206
+ render: function(v) {
207
+ return $("<span>")
208
+ .addClass(v ? "plugincore-status-enabled"
209
+ : "plugincore-status-disabled")
210
+ .text(v ? "✔ Enabled" : "✘ Disabled");
211
+ }}
212
+ ],
213
+ rows: data
214
+ })
215
+ );
216
+ }
217
+ }
218
+ ]
219
+ });
220
+ ```
221
+
222
+ **Options:**
223
+
224
+ | Field | Type | Description |
225
+ |-------|------|-------------|
226
+ | `title` | `string` | Heading shown in the dialog title bar |
227
+ | `tabs` | `array` | One or more tab definitions |
228
+ | `tabs[].label` | `string` | Tab heading |
229
+ | `tabs[].render` | `function($container)` | Called with a jQuery element; append content into it |
230
+
231
+ **Returns:** `{ close() }` — call `close()` to dismiss the dialog programmatically.
232
+
233
+ ---
234
+
235
+ #### `PluginCore.table(config)`
236
+
237
+ Returns a styled jQuery `<table>` element ready to append into any container.
238
+
239
+ ```javascript
240
+ var $table = PluginCore.table({
241
+ columns: [
242
+ { key: "id", label: "ID" },
243
+ { key: "domain", label: "Domain",
244
+ render: function(value, row) { return value.join(", "); } }
245
+ ],
246
+ rows: arrayOfObjects
247
+ });
248
+ $container.append($table);
249
+ ```
250
+
251
+ **Config:**
252
+
253
+ | Field | Type | Description |
254
+ |-------|------|-------------|
255
+ | `columns` | `array` | Column definitions |
256
+ | `columns[].key` | `string` | Property name on each row object |
257
+ | `columns[].label` | `string` | Column header text |
258
+ | `columns[].render` | `function(value, row)` | Optional. Return a string or jQuery element for custom cell rendering |
259
+ | `rows` | `object[]` | Data rows |
260
+
261
+ **CSS classes available for cell content:**
262
+
263
+ | Class | Colour | Intended use |
264
+ |-------|--------|-------------|
265
+ | `plugincore-status-enabled` | Green | Enabled / active state |
266
+ | `plugincore-status-disabled` | Red | Disabled / inactive state |
267
+
175
268
  ### Registering nodes for generation
176
269
 
177
270
  Create a `GenerateNodes.ts` at the root of your `src/` directory. This is the composition root — register every service, template, and node, then call `.generate()` to emit the two Node-RED entry files (`Nodes.js` and `Plugins.js`).
@@ -23,6 +23,7 @@ new index_js_1.NodeGenerator("./src/core/")
23
23
  .registerTemplate(index_js_4.GaugeMetricTemplate)
24
24
  .registerTemplate(index_js_1.TimerMetricTemplate)
25
25
  .registerTemplate(index_js_2.WebhookTemplate)
26
+ .registerTemplate(index_js_1.UIHelperTemplate)
26
27
  // nodes
27
28
  .registerNode(DelegatedConfigReferenceNode_js_1.DelegatedConfigReferenceNode)
28
29
  .registerNode(index_js_1.ConsoleLoggerConfigNode)
@@ -0,0 +1,5 @@
1
+ import { Template, TemplateDescriptor } from "../../NodeConstructor";
2
+ export declare class UIHelperTemplate extends Template {
3
+ static getTemplateDescriptor(): TemplateDescriptor;
4
+ }
5
+ //# sourceMappingURL=UIHelperTemplate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UIHelperTemplate.d.ts","sourceRoot":"","sources":["../../../../src/core/ui/template/UIHelperTemplate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAGpE,qBAAa,gBAAiB,SAAQ,QAAQ;WAE5B,qBAAqB,IAAI,kBAAkB;CAQ5D"}
@@ -0,0 +1,229 @@
1
+
2
+ <div template-section="onIncludeOnce">
3
+
4
+ <style>
5
+ /* ─── PluginCore Dialog overlay ─────────────────────────────────────────── */
6
+
7
+ .plugincore-overlay {
8
+ position: fixed;
9
+ top: 0; left: 0; right: 0; bottom: 0;
10
+ background: rgba(0, 0, 0, 0.5);
11
+ z-index: 2000;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ }
16
+
17
+ .plugincore-dialog {
18
+ background: #fff;
19
+ border: 1px solid #aaa;
20
+ border-radius: 4px;
21
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
22
+ min-width: 540px;
23
+ max-width: 85vw;
24
+ max-height: 80vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ overflow: hidden;
28
+ }
29
+
30
+ /* Title bar — matches Node-RED's main nav bar colour */
31
+ .plugincore-dialog-titlebar {
32
+ display: flex;
33
+ align-items: center;
34
+ background: #3d3d3d;
35
+ color: #fff;
36
+ padding: 6px 8px 6px 12px;
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ .plugincore-dialog-title {
41
+ flex: 1;
42
+ font-weight: 600;
43
+ font-size: 13px;
44
+ }
45
+
46
+ /* Close button — jQuery UI styling as used in Node-RED dialogs */
47
+ .plugincore-dialog-close {
48
+ color: #ccc !important;
49
+ background: transparent !important;
50
+ border-color: transparent !important;
51
+ }
52
+
53
+ .plugincore-dialog-close:hover {
54
+ color: #fff !important;
55
+ background: rgba(255, 255, 255, 0.15) !important;
56
+ border-color: rgba(255, 255, 255, 0.3) !important;
57
+ }
58
+
59
+ /* Scrollable body that contains the jQuery UI tab widget */
60
+ .plugincore-dialog-body {
61
+ flex: 1;
62
+ overflow: auto;
63
+ padding: 10px;
64
+ }
65
+
66
+ /* ─── PluginCore Table ───────────────────────────────────────────────────── */
67
+
68
+ .plugincore-table {
69
+ width: 100%;
70
+ border-collapse: collapse;
71
+ font-size: 12px;
72
+ }
73
+
74
+ .plugincore-table th {
75
+ background: #f3f3f3;
76
+ border: 1px solid #ddd;
77
+ padding: 5px 10px;
78
+ text-align: left;
79
+ font-weight: 600;
80
+ white-space: nowrap;
81
+ }
82
+
83
+ .plugincore-table td {
84
+ border: 1px solid #eee;
85
+ padding: 5px 10px;
86
+ vertical-align: middle;
87
+ }
88
+
89
+ .plugincore-table tbody tr:nth-child(even) td {
90
+ background: #fafafa;
91
+ }
92
+
93
+ .plugincore-table tbody tr:hover td {
94
+ background: #f0f6ff;
95
+ }
96
+
97
+ .plugincore-status-enabled { color: #27ae60; }
98
+ .plugincore-status-disabled { color: #c0392b; }
99
+ </style>
100
+
101
+ <script type="text/javascript">
102
+
103
+ window.PluginCore = window.PluginCore || {};
104
+
105
+ /**
106
+ * PluginCore.table(config) → jQuery <table>
107
+ *
108
+ * config: {
109
+ * columns : [{ key, label, render?(value, row) → string | jQuery }],
110
+ * rows : object[]
111
+ * }
112
+ */
113
+ PluginCore.table = function(config) {
114
+ var $table = $('<table class="plugincore-table">');
115
+
116
+ // Header
117
+ var $thead = $('<thead>');
118
+ var $hr = $('<tr>');
119
+ config.columns.forEach(function(col) {
120
+ $hr.append($('<th>').text(col.label));
121
+ });
122
+ $thead.append($hr);
123
+ $table.append($thead);
124
+
125
+ // Body
126
+ var $tbody = $('<tbody>');
127
+ (config.rows || []).forEach(function(row) {
128
+ var $tr = $('<tr>');
129
+ config.columns.forEach(function(col) {
130
+ var $td = $('<td>');
131
+ var val = row[col.key];
132
+ if (col.render) {
133
+ var rendered = col.render(val, row);
134
+ if (rendered && typeof rendered === 'object' && rendered.jquery) {
135
+ $td.append(rendered);
136
+ } else {
137
+ $td.html(rendered != null ? String(rendered) : '');
138
+ }
139
+ } else {
140
+ $td.text(val != null ? String(val) : '');
141
+ }
142
+ $tr.append($td);
143
+ });
144
+ $tbody.append($tr);
145
+ });
146
+ $table.append($tbody);
147
+
148
+ return $table;
149
+ };
150
+
151
+ /**
152
+ * PluginCore.dialog(options) → { close() }
153
+ *
154
+ * options: {
155
+ * title : string,
156
+ * tabs : [{ label: string, render($container) }]
157
+ * }
158
+ *
159
+ * Uses jQuery UI tabs for the tab widget (already bundled with Node-RED).
160
+ * Closes on: close button click, overlay click, or Escape key.
161
+ */
162
+ PluginCore.dialog = function(options) {
163
+ // Only one dialog at a time
164
+ $('.plugincore-overlay').remove();
165
+
166
+ var uid = 'plugincore-' + Date.now();
167
+
168
+ var $overlay = $('<div class="plugincore-overlay">');
169
+ var $dialog = $('<div class="plugincore-dialog">');
170
+
171
+ // ── Title bar ──────────────────────────────────────────────────────
172
+ var $titlebar = $('<div class="plugincore-dialog-titlebar">');
173
+ $titlebar.append(
174
+ $('<span class="plugincore-dialog-title">').text(options.title || '')
175
+ );
176
+ var $closeBtn = $(
177
+ '<button type="button" class="ui-button ui-corner-all ui-widget plugincore-dialog-close" title="Close">' +
178
+ '<span class="ui-icon ui-icon-closethick"></span></button>'
179
+ );
180
+ $titlebar.append($closeBtn);
181
+
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>');
186
+
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);
199
+
200
+ // ── Assemble ───────────────────────────────────────────────────────
201
+ $dialog.append($titlebar).append($body);
202
+ $overlay.append($dialog);
203
+ $('body').append($overlay);
204
+
205
+ // Initialise jQuery UI tabs (bundled with Node-RED)
206
+ $tabsEl.tabs();
207
+
208
+ // ── Close logic ───────────────────────────────────────────────────
209
+ var escNs = 'keydown.plugincore-dialog-' + uid;
210
+
211
+ function close() {
212
+ $overlay.fadeOut(150, function() { $overlay.remove(); });
213
+ $(document).off(escNs);
214
+ }
215
+
216
+ $closeBtn.on('click', close);
217
+ $overlay.on('click', function(e) {
218
+ if ($(e.target).is($overlay)) { close(); }
219
+ });
220
+ $(document).on(escNs, function(e) {
221
+ if (e.key === 'Escape') { close(); }
222
+ });
223
+
224
+ return { close: close };
225
+ };
226
+
227
+ </script>
228
+
229
+ </div>
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UIHelperTemplate = void 0;
4
+ const NodeConstructor_1 = require("../../NodeConstructor");
5
+ const NodeGenerator_1 = require("../../NodeGenerator");
6
+ class UIHelperTemplate extends NodeConstructor_1.Template {
7
+ static getTemplateDescriptor() {
8
+ return new NodeConstructor_1.TemplateDescriptor("ui-helper", UIHelperTemplate, NodeGenerator_1.SourceUtility.getSourcePath("/build/", "/src/") + "UIHelperTemplate.html", []);
9
+ }
10
+ }
11
+ exports.UIHelperTemplate = UIHelperTemplate;
package/build/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export * from "./core/metrics/node/TimerMetricConfigNode";
17
17
  export * from "./core/metrics/template/TimerMetricTemplate";
18
18
  export * from "./core/tagging/service/NodeTypeService";
19
19
  export * from "./core/tagging/NodeDescriptionDecorator";
20
+ export * from "./core/ui/template/UIHelperTemplate";
20
21
  export * from "./core/other/node/DelegatedConfigReferenceNode";
21
22
  export * from "./core/other/service/InputService";
22
23
  export * from "./core/other/service/SettingsService";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AAGxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,uCAAuC,CAAC;AACtD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAG5D,cAAc,wCAAwC,CAAC;AACvD,cAAc,yCAAyC,CAAC;AAGxD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,qCAAqC,CAAC;AACpD,cAAc,wCAAwC,CAAC;AAGvD,cAAc,yCAAyC,CAAC;AACxD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,6CAA6C,CAAC;AAG5D,cAAc,gDAAgD,CAAC;AAG/D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AAGxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,uCAAuC,CAAC;AACtD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAG5D,cAAc,wCAAwC,CAAC;AACvD,cAAc,yCAAyC,CAAC;AAGxD,cAAc,qCAAqC,CAAC;AAGpD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,qCAAqC,CAAC;AACpD,cAAc,wCAAwC,CAAC;AAGvD,cAAc,yCAAyC,CAAC;AACxD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,6CAA6C,CAAC;AAG5D,cAAc,gDAAgD,CAAC;AAG/D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC"}
package/build/index.js CHANGED
@@ -36,6 +36,8 @@ __exportStar(require("./core/metrics/template/TimerMetricTemplate"), exports);
36
36
  // TAGGING
37
37
  __exportStar(require("./core/tagging/service/NodeTypeService"), exports);
38
38
  __exportStar(require("./core/tagging/NodeDescriptionDecorator"), exports);
39
+ // UI
40
+ __exportStar(require("./core/ui/template/UIHelperTemplate"), exports);
39
41
  // OTHER
40
42
  __exportStar(require("./core/other/node/DelegatedConfigReferenceNode"), exports);
41
43
  __exportStar(require("./core/other/service/InputService"), exports);
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@theotherwillembotha/node-red-plugincore",
3
- "version": "0.0.51",
3
+ "version": "0.0.52",
4
4
  "description": "A core framework for building production-grade Node-RED plugins, with built-in support for structured logging, metrics, and webhook ingestion.",
5
5
  "author": "Willem Botha (@theotherwillembotha)",
6
6
  "license": "ISC",
7
7
  "keywords": [
8
- "node-red", "logging", "metrics", "webhook", "reverseproxy"
8
+ "node-red",
9
+ "logging",
10
+ "metrics",
11
+ "webhook",
12
+ "reverseproxy"
9
13
  ],
10
14
  "main": "./build/index.js",
11
15
  "exports": "./build/index.js",
@@ -20,13 +24,20 @@
20
24
  },
21
25
  "repository": {
22
26
  "type": "git",
23
- "url": "https://github.com/theotherwillembotha/nodered_plugincore.git"
27
+ "url": "git+https://github.com/theotherwillembotha/nodered_plugincore.git"
24
28
  },
25
29
  "bugs": {
26
30
  "url": "https://github.com/theotherwillembotha/nodered_plugincore/issues"
27
31
  },
28
32
  "types": "./build/index.d.ts",
29
- "files": ["build/", "src/", "icons/", "documentation/", "LICENSE", "README.md"],
33
+ "files": [
34
+ "build/",
35
+ "src/",
36
+ "icons/",
37
+ "documentation/",
38
+ "LICENSE",
39
+ "README.md"
40
+ ],
30
41
  "homepage": "https://github.com/theotherwillembotha/nodered_plugincore#readme",
31
42
  "engines": {
32
43
  "node": ">=18",
@@ -35,7 +46,7 @@
35
46
  "devDependencies": {
36
47
  "@types/deep-equal": "^1.0.4",
37
48
  "@types/js-beautify": "^1.14.3",
38
- "@types/jsdom": "^21.1.7",
49
+ "@types/jsdom": "^28.0.3",
39
50
  "@types/markdown-it": "^14.1.2",
40
51
  "@types/node": "^22.15.29",
41
52
  "@types/node-red": "~1.3.5",
@@ -47,12 +58,12 @@
47
58
  "express": "^5.1.0",
48
59
  "handlebars": "^4.7.8",
49
60
  "js-beautify": "^1.15.4",
50
- "jsdom": "^26.1.0",
61
+ "jsdom": "^29.1.1",
51
62
  "markdown-it": "^14.1.0",
52
63
  "network": "^0.7.0",
53
64
  "prom-client": "^15.1.3",
54
65
  "reflect-metadata": "^0.2.2",
55
- "whatwg-url": "^14.2.0",
66
+ "whatwg-url": "^16.0.1",
56
67
  "winston": "^3.17.0",
57
68
  "winston-loki": "^6.1.3"
58
69
  },
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { DelegatedConfigReferenceNode } from "./core/other/node/DelegatedConfigReferenceNode.js";
3
3
  import { NodeTypeService } from "./core/tagging/service/NodeTypeService.js";
4
- import { NodeGenerator, SettingsService, BasicTemplate, SettingsTemplate, TimerMetricTemplate, ConsoleLoggerConfigNode, RestLoggerConfigNode, LokiLoggerConfigNode } from "./index.js"
4
+ import { NodeGenerator, SettingsService, BasicTemplate, SettingsTemplate, TimerMetricTemplate, ConsoleLoggerConfigNode, RestLoggerConfigNode, LokiLoggerConfigNode, UIHelperTemplate } from "./index.js"
5
5
  import { WebhookServerConfigNode, WebhookServerService, WebhookTemplate, } from "./index.js"
6
6
 
7
7
  import {LoggerService, LoggerTemplate } from "./index.js";
@@ -26,6 +26,7 @@ new NodeGenerator("./src/core/")
26
26
  .registerTemplate(GaugeMetricTemplate)
27
27
  .registerTemplate(TimerMetricTemplate)
28
28
  .registerTemplate(WebhookTemplate)
29
+ .registerTemplate(UIHelperTemplate)
29
30
 
30
31
  // nodes
31
32
  .registerNode(DelegatedConfigReferenceNode)
@@ -0,0 +1,229 @@
1
+
2
+ <div template-section="onIncludeOnce">
3
+
4
+ <style>
5
+ /* ─── PluginCore Dialog overlay ─────────────────────────────────────────── */
6
+
7
+ .plugincore-overlay {
8
+ position: fixed;
9
+ top: 0; left: 0; right: 0; bottom: 0;
10
+ background: rgba(0, 0, 0, 0.5);
11
+ z-index: 2000;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ }
16
+
17
+ .plugincore-dialog {
18
+ background: #fff;
19
+ border: 1px solid #aaa;
20
+ border-radius: 4px;
21
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
22
+ min-width: 540px;
23
+ max-width: 85vw;
24
+ max-height: 80vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ overflow: hidden;
28
+ }
29
+
30
+ /* Title bar — matches Node-RED's main nav bar colour */
31
+ .plugincore-dialog-titlebar {
32
+ display: flex;
33
+ align-items: center;
34
+ background: #3d3d3d;
35
+ color: #fff;
36
+ padding: 6px 8px 6px 12px;
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ .plugincore-dialog-title {
41
+ flex: 1;
42
+ font-weight: 600;
43
+ font-size: 13px;
44
+ }
45
+
46
+ /* Close button — jQuery UI styling as used in Node-RED dialogs */
47
+ .plugincore-dialog-close {
48
+ color: #ccc !important;
49
+ background: transparent !important;
50
+ border-color: transparent !important;
51
+ }
52
+
53
+ .plugincore-dialog-close:hover {
54
+ color: #fff !important;
55
+ background: rgba(255, 255, 255, 0.15) !important;
56
+ border-color: rgba(255, 255, 255, 0.3) !important;
57
+ }
58
+
59
+ /* Scrollable body that contains the jQuery UI tab widget */
60
+ .plugincore-dialog-body {
61
+ flex: 1;
62
+ overflow: auto;
63
+ padding: 10px;
64
+ }
65
+
66
+ /* ─── PluginCore Table ───────────────────────────────────────────────────── */
67
+
68
+ .plugincore-table {
69
+ width: 100%;
70
+ border-collapse: collapse;
71
+ font-size: 12px;
72
+ }
73
+
74
+ .plugincore-table th {
75
+ background: #f3f3f3;
76
+ border: 1px solid #ddd;
77
+ padding: 5px 10px;
78
+ text-align: left;
79
+ font-weight: 600;
80
+ white-space: nowrap;
81
+ }
82
+
83
+ .plugincore-table td {
84
+ border: 1px solid #eee;
85
+ padding: 5px 10px;
86
+ vertical-align: middle;
87
+ }
88
+
89
+ .plugincore-table tbody tr:nth-child(even) td {
90
+ background: #fafafa;
91
+ }
92
+
93
+ .plugincore-table tbody tr:hover td {
94
+ background: #f0f6ff;
95
+ }
96
+
97
+ .plugincore-status-enabled { color: #27ae60; }
98
+ .plugincore-status-disabled { color: #c0392b; }
99
+ </style>
100
+
101
+ <script type="text/javascript">
102
+
103
+ window.PluginCore = window.PluginCore || {};
104
+
105
+ /**
106
+ * PluginCore.table(config) → jQuery <table>
107
+ *
108
+ * config: {
109
+ * columns : [{ key, label, render?(value, row) → string | jQuery }],
110
+ * rows : object[]
111
+ * }
112
+ */
113
+ PluginCore.table = function(config) {
114
+ var $table = $('<table class="plugincore-table">');
115
+
116
+ // Header
117
+ var $thead = $('<thead>');
118
+ var $hr = $('<tr>');
119
+ config.columns.forEach(function(col) {
120
+ $hr.append($('<th>').text(col.label));
121
+ });
122
+ $thead.append($hr);
123
+ $table.append($thead);
124
+
125
+ // Body
126
+ var $tbody = $('<tbody>');
127
+ (config.rows || []).forEach(function(row) {
128
+ var $tr = $('<tr>');
129
+ config.columns.forEach(function(col) {
130
+ var $td = $('<td>');
131
+ var val = row[col.key];
132
+ if (col.render) {
133
+ var rendered = col.render(val, row);
134
+ if (rendered && typeof rendered === 'object' && rendered.jquery) {
135
+ $td.append(rendered);
136
+ } else {
137
+ $td.html(rendered != null ? String(rendered) : '');
138
+ }
139
+ } else {
140
+ $td.text(val != null ? String(val) : '');
141
+ }
142
+ $tr.append($td);
143
+ });
144
+ $tbody.append($tr);
145
+ });
146
+ $table.append($tbody);
147
+
148
+ return $table;
149
+ };
150
+
151
+ /**
152
+ * PluginCore.dialog(options) → { close() }
153
+ *
154
+ * options: {
155
+ * title : string,
156
+ * tabs : [{ label: string, render($container) }]
157
+ * }
158
+ *
159
+ * Uses jQuery UI tabs for the tab widget (already bundled with Node-RED).
160
+ * Closes on: close button click, overlay click, or Escape key.
161
+ */
162
+ PluginCore.dialog = function(options) {
163
+ // Only one dialog at a time
164
+ $('.plugincore-overlay').remove();
165
+
166
+ var uid = 'plugincore-' + Date.now();
167
+
168
+ var $overlay = $('<div class="plugincore-overlay">');
169
+ var $dialog = $('<div class="plugincore-dialog">');
170
+
171
+ // ── Title bar ──────────────────────────────────────────────────────
172
+ var $titlebar = $('<div class="plugincore-dialog-titlebar">');
173
+ $titlebar.append(
174
+ $('<span class="plugincore-dialog-title">').text(options.title || '')
175
+ );
176
+ var $closeBtn = $(
177
+ '<button type="button" class="ui-button ui-corner-all ui-widget plugincore-dialog-close" title="Close">' +
178
+ '<span class="ui-icon ui-icon-closethick"></span></button>'
179
+ );
180
+ $titlebar.append($closeBtn);
181
+
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>');
186
+
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);
199
+
200
+ // ── Assemble ───────────────────────────────────────────────────────
201
+ $dialog.append($titlebar).append($body);
202
+ $overlay.append($dialog);
203
+ $('body').append($overlay);
204
+
205
+ // Initialise jQuery UI tabs (bundled with Node-RED)
206
+ $tabsEl.tabs();
207
+
208
+ // ── Close logic ───────────────────────────────────────────────────
209
+ var escNs = 'keydown.plugincore-dialog-' + uid;
210
+
211
+ function close() {
212
+ $overlay.fadeOut(150, function() { $overlay.remove(); });
213
+ $(document).off(escNs);
214
+ }
215
+
216
+ $closeBtn.on('click', close);
217
+ $overlay.on('click', function(e) {
218
+ if ($(e.target).is($overlay)) { close(); }
219
+ });
220
+ $(document).on(escNs, function(e) {
221
+ if (e.key === 'Escape') { close(); }
222
+ });
223
+
224
+ return { close: close };
225
+ };
226
+
227
+ </script>
228
+
229
+ </div>
@@ -0,0 +1,14 @@
1
+ import { Template, TemplateDescriptor } from "../../NodeConstructor"
2
+ import { SourceUtility } from "../../NodeGenerator";
3
+
4
+ export class UIHelperTemplate extends Template {
5
+
6
+ public static getTemplateDescriptor(): TemplateDescriptor {
7
+ return new TemplateDescriptor(
8
+ "ui-helper",
9
+ UIHelperTemplate,
10
+ SourceUtility.getSourcePath("/build/", "/src/") + "UIHelperTemplate.html",
11
+ []
12
+ );
13
+ }
14
+ }
package/src/index.ts CHANGED
@@ -24,6 +24,9 @@ export * from "./core/metrics/template/TimerMetricTemplate";
24
24
  export * from "./core/tagging/service/NodeTypeService";
25
25
  export * from "./core/tagging/NodeDescriptionDecorator";
26
26
 
27
+ // UI
28
+ export * from "./core/ui/template/UIHelperTemplate";
29
+
27
30
  // OTHER
28
31
  export * from "./core/other/node/DelegatedConfigReferenceNode";
29
32
  export * from "./core/other/service/InputService";