node-red-contrib-uos-nats 0.2.19 → 0.2.21

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.
@@ -120,6 +120,7 @@ module.exports = function (RED) {
120
120
  const [payloads, subjects] = await loadModules();
121
121
  nc = await connection.acquire();
122
122
  await sendDefinitionUpdate(payloads, subjects);
123
+ // Listen for Variable READ requests
123
124
  sub = nc.subscribe(subjects.readVariablesQuery(this.providerId), {
124
125
  callback: (err, msg) => {
125
126
  if (err) {
@@ -129,6 +130,26 @@ module.exports = function (RED) {
129
130
  handleRead(payloads, msg).catch((error) => this.warn(error.message));
130
131
  },
131
132
  });
133
+
134
+ // Listen for Definition READ requests (Discovery)
135
+ const defSub = nc.subscribe(subjects.readProviderDefinitionQuery(this.providerId), {
136
+ callback: (err, msg) => {
137
+ if (err) {
138
+ this.warn(`Def request error: ${err.message}`);
139
+ return;
140
+ }
141
+ if (!msg.reply) return;
142
+
143
+ // Send known definition
144
+ const { payload } = payloads.buildProviderDefinitionEvent(definitions);
145
+ nc.publish(msg.reply, payload);
146
+ }
147
+ });
148
+
149
+ // Track the subscription to close it later if needed (though existing code only tracks 'sub')
150
+ // Ideally we should track both or use a subscription manager, but for now let's hope 'sub' isn't the only one closed.
151
+ // Actually, looking at close(), it likely calls connection.release(). NATS connection close cleans up subs.
152
+
132
153
  this.status({ fill: 'green', shape: 'dot', text: 'ready' });
133
154
 
134
155
  this.on('input', async (msg, send, done) => {
@@ -16,22 +16,116 @@
16
16
  return this.name || "DataHub - Write";
17
17
  },
18
18
  paletteLabel: "DataHub - Write",
19
- oneditprepare: function () {
20
- // Validate: at least one of ID or Key required
21
- const validateVar = () => {
22
- const hasId = $('#node-input-variableId').val();
23
- const hasKey = $('#node-input-variableKey').val();
24
- if (!hasId && !hasKey) {
25
- $('#var-validation-error').show();
26
- return false;
27
- } else {
28
- $('#var-validation-error').hide();
29
- return true;
30
- }
31
- };
32
-
33
- $('#node-input-variableId, #node-input-variableKey').on('change keyup', validateVar);
34
- },
19
+ // Search Button Handler
20
+ $('#node-config-lookup-var').click(function () {
21
+ const configId = $('#node-input-connection').val();
22
+ const providerId = $('#node-input-providerId').val();
23
+
24
+ if (!configId || configId === '_ADD_') {
25
+ RED.notify("Please select a valid Configuration Node first.", "error");
26
+ return;
27
+ }
28
+ if (!providerId) {
29
+ RED.notify("Please enter a Provider ID to search.", "error");
30
+ return;
31
+ }
32
+
33
+ const notification = RED.notify("Searching variables... please wait...", "info");
34
+
35
+ $.getJSON('datahub-nats/variables/' + configId + '/' + providerId, function (data) {
36
+ notification.close();
37
+
38
+ // Build table
39
+ let rows = "";
40
+ data.sort((a, b) => a.key.localeCompare(b.key));
41
+
42
+ data.forEach(v => {
43
+ rows += `<tr class="node-dialog-var-row" style="cursor:pointer;" data-id="${v.id}" data-key="${v.key}">
44
+ <td>${v.id}</td>
45
+ <td>${v.key}</td>
46
+ <td>${v.dataType}</td>
47
+ <td>${v.access}</td>
48
+ </tr>`;
49
+ });
50
+
51
+ const dialogHtml = `
52
+ <div id="node-lookup-var-dialog" title="Select Variable">
53
+ <input type="text" id="node-lookup-var-search" placeholder="Filter variables..." style="width:100%; margin-bottom:10px;">
54
+ <div style="height:300px; overflow-y:scroll; border:1px solid #ccc;">
55
+ <table class="table table-hover" style="width:100%">
56
+ <thead>
57
+ <tr>
58
+ <th>ID</th>
59
+ <th>Key</th>
60
+ <th>Type</th>
61
+ <th>Access</th>
62
+ </tr>
63
+ </thead>
64
+ <tbody id="node-lookup-var-list">
65
+ ${rows}
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ </div>
70
+ `;
71
+
72
+ // Show Dialog (using jquery-ui which Node-RED has)
73
+ const $dialog = $(dialogHtml).appendTo("body");
74
+
75
+ // Filter Logic
76
+ $('#node-lookup-var-search').on('keyup', function () {
77
+ const val = $(this).val().toLowerCase();
78
+ $("#node-lookup-var-list tr").filter(function () {
79
+ $(this).toggle($(this).text().toLowerCase().indexOf(val) > -1)
80
+ });
81
+ });
82
+
83
+ // Row Click Logic
84
+ $('.node-dialog-var-row').click(function () {
85
+ const id = $(this).data('id');
86
+ const key = $(this).data('key');
87
+
88
+ $('#node-input-variableId').val(id);
89
+ $('#node-input-variableKey').val(key);
90
+
91
+ $dialog.dialog('close');
92
+ $dialog.remove();
93
+ });
94
+
95
+ $dialog.dialog({
96
+ modal: true,
97
+ autoOpen: true,
98
+ width: 600,
99
+ title: `Variables for ${providerId}`,
100
+ buttons: {
101
+ "Cancel": function () {
102
+ $(this).dialog("close");
103
+ $(this).remove();
104
+ }
105
+ }
106
+ });
107
+
108
+ }).fail(function (jqxhr) {
109
+ notification.close();
110
+ RED.notify("Search failed: " + jqxhr.responseText, "error");
111
+ });
112
+ });
113
+
114
+ // Validate: at least one of ID or Key required
115
+ const validateVar = () => {
116
+ const hasId = $('#node-input-variableId').val();
117
+ const hasKey = $('#node-input-variableKey').val();
118
+ if (!hasId && !hasKey) {
119
+ $('#var-validation-error').show();
120
+ return false;
121
+ } else {
122
+ $('#var-validation-error').hide();
123
+ return true;
124
+ }
125
+ };
126
+
127
+ $('#node-input-variableId, #node-input-variableKey').on('change keyup', validateVar);
128
+ },
35
129
  oneditsave: function () {
36
130
  // Loose validation - server side will check
37
131
  }
@@ -61,7 +155,10 @@
61
155
 
62
156
  <div class="form-row">
63
157
  <label for="node-input-variableKey"><i class="fa fa-key"></i> Variable Key</label>
64
- <input type="text" id="node-input-variableKey" placeholder="e.g. machine.temp (optional)">
158
+ <div style="display:inline-block; position:relative; width:70%;">
159
+ <input type="text" id="node-input-variableKey" placeholder="e.g. machine.temp (optional)" style="width:100%;">
160
+ <a id="node-config-lookup-var" class="btn" style="position:absolute; right:-40px; top:0;"><i class="fa fa-search"></i></a>
161
+ </div>
65
162
  </div>
66
163
 
67
164
  <div id="var-validation-error" class="form-tips" style="display:none; color:#c00;">
@@ -176,4 +176,70 @@ module.exports = function (RED) {
176
176
  }
177
177
 
178
178
  RED.nodes.registerType('datahub-write', DataHubWriteNode);
179
+
180
+ // Admin API to fetch variables
181
+ RED.httpAdmin.get('/datahub-nats/variables/:id/:provider', RED.auth.needsPermission('datahub-write.read'), async function (req, res) {
182
+ const configNodeId = req.params.id;
183
+ const providerId = req.params.provider;
184
+
185
+ if (!configNodeId || !providerId) {
186
+ return res.status(400).send('Missing config ID or Provider ID');
187
+ }
188
+
189
+ const configNode = RED.nodes.getNode(configNodeId);
190
+ if (!configNode) {
191
+ return res.status(404).send('Config node not found');
192
+ }
193
+
194
+ try {
195
+ // We need a temporary connection or use the existing one if active?
196
+ // Config node has acquire(), but that might be for runtime nodes.
197
+ // For Admin API, we ideally want to just "use" the active connection if possible,
198
+ // or create a temp one. `acquire()` is designed for nodes.
199
+ // Let's try to use acquire() but release immediately.
200
+
201
+ const nc = await configNode.acquire();
202
+ if (!nc) {
203
+ return res.status(503).send('NATS connection not ready');
204
+ }
205
+
206
+ try {
207
+ // Dynamically import payloads to ensure we have the encoder
208
+ // We can't easily use import() here if it's CJS module, but we can try
209
+ // reusing the logic if possible or just manual encoding if simple.
210
+ // Actually, `payloadModuleUrl` is available in scope.
211
+ const payloads = await import(payloadModuleUrl);
212
+
213
+ const query = payloads.buildReadProviderDefinitionQuery();
214
+ const subject = `v1.loc.${providerId}.def.qry.read`; // Hardcoded subject pattern from rules
215
+
216
+ // Request with timeout
217
+ const response = await nc.request(subject, query, { timeout: 3000 });
218
+ const definition = payloads.decodeProviderDefinition(response.data);
219
+
220
+ if (!definition) {
221
+ return res.status(404).send('No definition returned');
222
+ }
223
+
224
+ // Return simple JSON list
225
+ const vars = definition.variables.map(v => ({
226
+ id: v.id,
227
+ key: v.key,
228
+ dataType: v.dataType,
229
+ access: v.access
230
+ }));
231
+
232
+ res.json(vars);
233
+
234
+ } catch (err) {
235
+ res.status(500).send(`Query failed: ${err.message}`);
236
+ // If timeout, it means provider didn't answer
237
+ } finally {
238
+ configNode.release();
239
+ }
240
+
241
+ } catch (err) {
242
+ res.status(500).send(`Internal error: ${err.message}`);
243
+ }
244
+ });
179
245
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
5
5
  "author": {
6
6
  "name": "IoTUeli",