@ralphwetzel/node-red-context-monitor 1.2.0 → 2.0.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.
package/README.md CHANGED
@@ -79,4 +79,16 @@ You may define a setup that doesn't monitor the (whole) object, but only one of
79
79
  <img alt="flow" src="https://raw.githubusercontent.com/ralphwetzel/node-red-context-monitor/main/resources/object_prop.png"
80
80
  style="min-width: 474px; width: 474px; align: center; border: 1px solid lightgray;"/>
81
81
 
82
- Such a monitor will react _only_, when this property and - if it's an object - its child properties are written to.
82
+ Such a monitor will react _only_, when this property and - if it's an object - its child properties are written to.
83
+
84
+ ### Subflow considerations
85
+ You may use this node in [subflows](https://nodered.org/docs/user-guide/editor/workspace/subflows).
86
+
87
+ Monitoring a `Node` scope context of a node within a subflow is only supported if both nodes (the context monitor and the node to be monitored) belong to the same subflow. Please select `Current Flow` when defining the monitoring context reference.
88
+
89
+ Please be aware that all instances of a subflow commonly share a single `Flow` scope context. According to the [Node-RED documentation](https://nodered.org/docs/user-guide/context#context-scopes), you can reference the context of the superior flow by prepending `$parent` to the context key.
90
+
91
+ ### Handling of context definition issues
92
+ If you edit this node's properties, it ensures that the context reference defined is always valid.
93
+ By _copy/paste_-ing a node already configured its configuration yet might become invalid. Same applies if you delete flows or nodes that were referenced earlier.
94
+ To ensure the setup, the node does a lazy validation of the context reference definition - when editing its properties and when it is (re)launched. You yet won't get a "red triangle" even if the reference became invalid - to allow a smooth flow editing experience.
@@ -1,6 +1,107 @@
1
1
  [
2
2
  {
3
- "id": "4980f94dd2c07b62",
3
+ "id": "6780aa88c7c4d336",
4
+ "type": "subflow",
5
+ "name": "context in subflow",
6
+ "info": "",
7
+ "category": "",
8
+ "in": [
9
+ {
10
+ "x": 120,
11
+ "y": 80,
12
+ "wires": [
13
+ {
14
+ "id": "a328f1e205c03a15"
15
+ }
16
+ ]
17
+ }
18
+ ],
19
+ "out": [
20
+ {
21
+ "x": 330,
22
+ "y": 160,
23
+ "wires": [
24
+ {
25
+ "id": "88f5bf36b88ae76b",
26
+ "port": 0
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ "x": 340,
32
+ "y": 240,
33
+ "wires": [
34
+ {
35
+ "id": "88f5bf36b88ae76b",
36
+ "port": 1
37
+ }
38
+ ]
39
+ }
40
+ ],
41
+ "env": [],
42
+ "meta": {},
43
+ "color": "#DDAA99",
44
+ "outputLabels": [
45
+ "context set",
46
+ "context change"
47
+ ]
48
+ },
49
+ {
50
+ "id": "a328f1e205c03a15",
51
+ "type": "function",
52
+ "z": "6780aa88c7c4d336",
53
+ "name": "set subflow & node context",
54
+ "func": "flow.set(\"subflow_flow_test\", \"flow\");\ncontext.set(\"subflow_node_test\", \"node\");\nreturn msg;",
55
+ "outputs": 1,
56
+ "timeout": 0,
57
+ "noerr": 0,
58
+ "initialize": "",
59
+ "finalize": "",
60
+ "libs": [],
61
+ "x": 300,
62
+ "y": 80,
63
+ "wires": [
64
+ []
65
+ ]
66
+ },
67
+ {
68
+ "id": "88f5bf36b88ae76b",
69
+ "type": "context-monitor",
70
+ "z": "6780aa88c7c4d336",
71
+ "name": "",
72
+ "monitoring": [
73
+ {
74
+ "scope": "global",
75
+ "key": "global_test"
76
+ },
77
+ {
78
+ "scope": "flow",
79
+ "flow": ".",
80
+ "key": "subflow_flow_test"
81
+ },
82
+ {
83
+ "scope": "flow",
84
+ "flow": ".",
85
+ "key": "$parent.flow_test"
86
+ },
87
+ {
88
+ "scope": "node",
89
+ "flow": ".",
90
+ "node": "a328f1e205c03a15",
91
+ "key": "subflow_node_test",
92
+ "node_label": "set subflow & node context"
93
+ }
94
+ ],
95
+ "tostatus": false,
96
+ "x": 160,
97
+ "y": 200,
98
+ "wires": [
99
+ [],
100
+ []
101
+ ]
102
+ },
103
+ {
104
+ "id": "841613b260a4c5ce",
4
105
  "type": "tab",
5
106
  "label": "Context Monitoring Example",
6
107
  "disabled": false,
@@ -8,9 +109,9 @@
8
109
  "env": []
9
110
  },
10
111
  {
11
- "id": "f2874060375785b7",
112
+ "id": "88cfd03e4a1d674a",
12
113
  "type": "inject",
13
- "z": "4980f94dd2c07b62",
114
+ "z": "841613b260a4c5ce",
14
115
  "name": "",
15
116
  "props": [
16
117
  {
@@ -28,34 +129,36 @@
28
129
  "topic": "",
29
130
  "payload": "",
30
131
  "payloadType": "date",
31
- "x": 140,
32
- "y": 80,
132
+ "x": 120,
133
+ "y": 120,
33
134
  "wires": [
34
135
  [
35
- "c096819863211fb9"
136
+ "ee1a6dc083b2db28",
137
+ "5384329446d6d4bd"
36
138
  ]
37
139
  ]
38
140
  },
39
141
  {
40
- "id": "db53945bf5a57413",
142
+ "id": "cc42435c8c1e18d2",
41
143
  "type": "debug",
42
- "z": "4980f94dd2c07b62",
43
- "name": "debug 88",
144
+ "z": "841613b260a4c5ce",
145
+ "name": "timestamp",
44
146
  "active": true,
45
147
  "tosidebar": true,
46
148
  "console": false,
47
149
  "tostatus": false,
48
- "complete": "false",
150
+ "complete": "payload",
151
+ "targetType": "msg",
49
152
  "statusVal": "",
50
153
  "statusType": "auto",
51
- "x": 660,
154
+ "x": 750,
52
155
  "y": 80,
53
156
  "wires": []
54
157
  },
55
158
  {
56
- "id": "c096819863211fb9",
159
+ "id": "ee1a6dc083b2db28",
57
160
  "type": "change",
58
- "z": "4980f94dd2c07b62",
161
+ "z": "841613b260a4c5ce",
59
162
  "name": "",
60
163
  "rules": [
61
164
  {
@@ -71,6 +174,20 @@
71
174
  "pt": "global",
72
175
  "to": "hello!",
73
176
  "tot": "str"
177
+ },
178
+ {
179
+ "t": "set",
180
+ "p": "obj_test",
181
+ "pt": "flow",
182
+ "to": "{\"x\": 1,\"y\": 2}",
183
+ "tot": "jsonata"
184
+ },
185
+ {
186
+ "t": "set",
187
+ "p": "obj_test.y",
188
+ "pt": "flow",
189
+ "to": "",
190
+ "tot": "str"
74
191
  }
75
192
  ],
76
193
  "action": "",
@@ -78,18 +195,18 @@
78
195
  "from": "",
79
196
  "to": "",
80
197
  "reg": false,
81
- "x": 300,
198
+ "x": 310,
82
199
  "y": 80,
83
200
  "wires": [
84
201
  [
85
- "b1d17d7418f7a28f"
202
+ "5bb28494f5abb001"
86
203
  ]
87
204
  ]
88
205
  },
89
206
  {
90
- "id": "db3afa60f9a080a8",
207
+ "id": "6af64a8365dc83ca",
91
208
  "type": "debug",
92
- "z": "4980f94dd2c07b62",
209
+ "z": "841613b260a4c5ce",
93
210
  "name": "debug on set",
94
211
  "active": true,
95
212
  "tosidebar": true,
@@ -99,41 +216,42 @@
99
216
  "targetType": "full",
100
217
  "statusVal": "",
101
218
  "statusType": "auto",
102
- "x": 330,
103
- "y": 180,
219
+ "x": 530,
220
+ "y": 240,
104
221
  "wires": []
105
222
  },
106
223
  {
107
- "id": "b1d17d7418f7a28f",
224
+ "id": "5bb28494f5abb001",
108
225
  "type": "function",
109
- "z": "4980f94dd2c07b62",
110
- "name": "set node context",
111
- "func": "context.set(\"node_test\", 15);\nreturn msg;",
226
+ "z": "841613b260a4c5ce",
227
+ "name": "set node & object context",
228
+ "func": "context.set(\"node_test\", 15);\n\nlet ot = flow.get(\"obj_test\");\not.y = \"17\";\n\nreturn msg;",
112
229
  "outputs": 1,
113
230
  "timeout": 0,
114
231
  "noerr": 0,
115
232
  "initialize": "",
116
233
  "finalize": "",
117
234
  "libs": [],
118
- "x": 490,
235
+ "x": 530,
119
236
  "y": 80,
120
237
  "wires": [
121
238
  [
122
- "db53945bf5a57413"
239
+ "cc42435c8c1e18d2"
123
240
  ]
124
241
  ]
125
242
  },
126
243
  {
127
- "id": "42634cda458394e4",
244
+ "id": "3d32ba0d798b3044",
128
245
  "type": "context-monitor",
129
- "z": "4980f94dd2c07b62",
246
+ "z": "841613b260a4c5ce",
130
247
  "name": "",
131
248
  "monitoring": [
132
249
  {
133
250
  "scope": "node",
134
- "flow": "4980f94dd2c07b62",
135
- "node": "b1d17d7418f7a28f",
136
- "key": "node_test"
251
+ "flow": ".",
252
+ "node": "5bb28494f5abb001",
253
+ "key": "node_test",
254
+ "node_label": "set node & object context"
137
255
  },
138
256
  {
139
257
  "scope": "global",
@@ -141,26 +259,31 @@
141
259
  },
142
260
  {
143
261
  "scope": "flow",
144
- "flow": "4980f94dd2c07b62",
145
- "node": "f2874060375785b7",
262
+ "flow": ".",
146
263
  "key": "flow_test"
264
+ },
265
+ {
266
+ "scope": "flow",
267
+ "key": "obj_test[\"y\"]",
268
+ "flow": "."
147
269
  }
148
270
  ],
149
- "x": 140,
150
- "y": 200,
271
+ "tostatus": false,
272
+ "x": 340,
273
+ "y": 260,
151
274
  "wires": [
152
275
  [
153
- "db3afa60f9a080a8"
276
+ "6af64a8365dc83ca"
154
277
  ],
155
278
  [
156
- "12a15905dd250321"
279
+ "73e8a36b611ff4b5"
157
280
  ]
158
281
  ]
159
282
  },
160
283
  {
161
- "id": "12a15905dd250321",
284
+ "id": "73e8a36b611ff4b5",
162
285
  "type": "debug",
163
- "z": "4980f94dd2c07b62",
286
+ "z": "841613b260a4c5ce",
164
287
  "name": "debug on change",
165
288
  "active": true,
166
289
  "tosidebar": true,
@@ -170,8 +293,76 @@
170
293
  "targetType": "full",
171
294
  "statusVal": "",
172
295
  "statusType": "auto",
173
- "x": 350,
174
- "y": 220,
296
+ "x": 550,
297
+ "y": 280,
298
+ "wires": []
299
+ },
300
+ {
301
+ "id": "5384329446d6d4bd",
302
+ "type": "subflow:6780aa88c7c4d336",
303
+ "z": "841613b260a4c5ce",
304
+ "name": "",
305
+ "x": 310,
306
+ "y": 160,
307
+ "wires": [
308
+ [
309
+ "65585d8fc2043584"
310
+ ],
311
+ [
312
+ "7740234648a5e725"
313
+ ]
314
+ ]
315
+ },
316
+ {
317
+ "id": "65585d8fc2043584",
318
+ "type": "debug",
319
+ "z": "841613b260a4c5ce",
320
+ "name": "subflow on set",
321
+ "active": true,
322
+ "tosidebar": true,
323
+ "console": false,
324
+ "tostatus": false,
325
+ "complete": "true",
326
+ "targetType": "full",
327
+ "statusVal": "",
328
+ "statusType": "auto",
329
+ "x": 540,
330
+ "y": 140,
331
+ "wires": []
332
+ },
333
+ {
334
+ "id": "7740234648a5e725",
335
+ "type": "debug",
336
+ "z": "841613b260a4c5ce",
337
+ "name": "subflow on change",
338
+ "active": true,
339
+ "tosidebar": true,
340
+ "console": false,
341
+ "tostatus": false,
342
+ "complete": "true",
343
+ "targetType": "full",
344
+ "statusVal": "",
345
+ "statusType": "auto",
346
+ "x": 550,
347
+ "y": 180,
175
348
  "wires": []
349
+ },
350
+ {
351
+ "id": "1b20ba795e278a6c",
352
+ "type": "comment",
353
+ "z": "841613b260a4c5ce",
354
+ "name": "If you got a warning that some of these nodes already exist and\\n you decided to import the flow by creating copies of these nodes,\\n the \"Node\" scope context refences might become invalid & might need to be adjusted.",
355
+ "info": "",
356
+ "x": 320,
357
+ "y": 380,
358
+ "wires": []
359
+ },
360
+ {
361
+ "id": "4569e4487b4e777f",
362
+ "type": "global-config",
363
+ "env": [],
364
+ "modules": {
365
+ "@ralphwetzel/node-red-context-monitor": "2.0.0"
366
+ }
176
367
  }
177
368
  ]
package/lib/monitor.js CHANGED
@@ -290,18 +290,12 @@ let trigger = function(root_id, context_id, key, new_value, previous_value, prop
290
290
  }
291
291
 
292
292
  switch (node.data.scope) {
293
- case "global":
294
- msg.monitoring.key = node.data.key;
295
- break;
296
- case "flow":
297
- msg.monitoring.flow = node.data.flow;
298
- msg.monitoring.key = node.data.key;
299
- break;
300
293
  case "node":
301
- msg.monitoring.flow = node.data.flow;
302
294
  msg.monitoring.node = node.data.node;
295
+ case "flow":
296
+ msg.monitoring.flow = node.data.flow;
297
+ case "global":
303
298
  msg.monitoring.key = node.data.key;
304
- break;
305
299
  }
306
300
 
307
301
  n.receive(msg);
package/monitor.html CHANGED
@@ -79,21 +79,78 @@
79
79
  function add_context(tpl) {
80
80
  let c = {
81
81
  "scope": tpl.scope ?? "global",
82
- // decode "this flow" marker
83
- "flow": ("." === tpl.flow) ? node.z : tpl.flow,
82
+
83
+ // There's a special flow marker "." that indicates "Current Flow"!
84
+ "flow": tpl.flow ?? ".",
85
+
84
86
  "node": tpl.node,
85
- "key": tpl.key ?? ""
87
+ "key": tpl.key ?? "",
88
+
89
+ // only to display invalid definitions
90
+ "flow_label": "." == tpl.flow ? "Current Flow" : (tpl.flow_label ?? "Unknown Flow"),
91
+ "node_label": tpl.node_label ?? "Unknown Node"
92
+ }
93
+
94
+ // prior 2.0, unnecessary data was stored in the config, that now might cause issues.
95
+ switch (c.scope) {
96
+ case "global":
97
+ delete c.flow;
98
+ case "flow":
99
+ delete c.node;
100
+ default:
101
+ delete c.group;
86
102
  }
103
+
87
104
  ctx.push(c);
88
105
  return c;
89
106
  }
90
107
 
108
+ function get_node_options(flow_id) {
109
+ let opts = [];
110
+
111
+ if ("." == flow_id) {
112
+ // try to get id of "Current Flow"
113
+ flow_id = RED.nodes.workspace(node.z)?.id ?? RED.nodes.subflow(node.z)?.id;
114
+ }
115
+
116
+ RED.nodes.eachNode( n => {
117
+
118
+ if (n.z == flow_id) {
119
+
120
+ // from 25-status.html
121
+ let nodeDef = RED.nodes.getType(n.type);
122
+ let label;
123
+ let sublabel;
124
+ if (nodeDef) {
125
+ let l = nodeDef.label;
126
+ label = (typeof l === "function" ? l.call(n) : l)||"";
127
+ }
128
+ if (!nodeDef || !label) {
129
+ label = n.type;
130
+ }
131
+
132
+ opts.push({
133
+ value: n.id,
134
+ label: label
135
+ });
136
+ }
137
+ });
138
+
139
+ return opts;
140
+ }
141
+
91
142
  $('#node-input-context-container').css('min-height','150px').css('min-width','450px').editableList({
92
143
  addItem: function(container, index, cfg) {
93
144
 
94
145
  let data = add_context(cfg);
95
146
  data.index = index;
96
147
 
148
+ // to manage the visual state e.g. of the error indicators
149
+ data.ok = {
150
+ flow: true,
151
+ node: true,
152
+ }
153
+
97
154
  // cfg is the data object from the node definition, stored in the 'data' store.
98
155
  // We shall keep cfg untouched to ensure automated (non-)dirty check by the runtime.
99
156
  // Thus we exchange the data stored against the 'data' object we just created.
@@ -118,11 +175,12 @@
118
175
  $(`<input type="text" id="context-scope-flows-${index}" placeholder="Flows">`).appendTo(line_flow);
119
176
  line_flow.appendTo(fragment);
120
177
 
121
- // let line_group = $('<div class="form-row" style="margin-bottom: 6px">');
122
- // // $(`<label for="context-scope-groups-${index}"" style="margin-left:10px; width: 90px"><i class="fa fa-tag"></i> Groups</label>`).appendTo(line_group);
123
- // $(`<label for="context-scope-groups-${index}"" style="margin-left:10px; width: 90px">Groups: </label>`).appendTo(line_group);
124
- // $(`<input type="text" id="context-scope-groups-${index}" placeholder="Groups">`).appendTo(line_group);
125
- // line_group.appendTo(fragment);
178
+ let line_flow_missing = $('<div class="form-row" style="margin-bottom: 6px">');
179
+ // $(`<label for="context-scope-key-${index}"" style="margin-left:10px; width: 90px"><i class="fa fa-tag"></i> Key</label>`).appendTo(line_key);
180
+ $(`<label for="context-scope-flow-error-${index}" style="margin-left:10px; width: 90px"></label>`).appendTo(line_flow_missing);
181
+ //$(`<input type="text" id="context-scope-key-${index}" placeholder="Context Variable Key">`).appendTo(line_key);
182
+ $(`<div id="context-scope-flow-error-${index}" style="display:inline"><i class="fa fa-exclamation-circle" style="color: red"></i><span style="font-size: small"> This flow does not exist.</span></div>`).appendTo(line_flow_missing);
183
+ line_flow_missing.appendTo(fragment).hide();
126
184
 
127
185
  let line_node = $('<div class="form-row" style="margin-bottom: 6px">');
128
186
  // $(`<label for="context-scope-nodes-${index}"" style="margin-left:10px; width: 90px"><i class="fa fa-tag"></i> Nodes</label>`).appendTo(line_node);
@@ -130,6 +188,13 @@
130
188
  $(`<input type="text" id="context-scope-nodes-${index}" placeholder="Nodes">`).appendTo(line_node);
131
189
  line_node.appendTo(fragment);
132
190
 
191
+ let line_node_missing = $('<div class="form-row" style="margin-bottom: 6px">');
192
+ // $(`<label for="context-scope-key-${index}"" style="margin-left:10px; width: 90px"><i class="fa fa-tag"></i> Key</label>`).appendTo(line_key);
193
+ $(`<label for="context-scope-node-error-${index}" style="margin-left:10px; width: 90px"></label>`).appendTo(line_node_missing);
194
+ //$(`<input type="text" id="context-scope-key-${index}" placeholder="Context Variable Key">`).appendTo(line_key);
195
+ $(`<div id="context-scope-node-error-${index}" style="display:inline"><i class="fa fa-exclamation-circle" style="color: red"></i><span style="font-size: small"> This node does not exist.</span></div>`).appendTo(line_node_missing);
196
+ line_node_missing.appendTo(fragment).hide();
197
+
133
198
  let line_key = $('<div class="form-row" style="margin-bottom: 6px">');
134
199
  // $(`<label for="context-scope-key-${index}"" style="margin-left:10px; width: 90px"><i class="fa fa-tag"></i> Key</label>`).appendTo(line_key);
135
200
  $(`<label for="context-scope-key-${index}" style="margin-left:10px; width: 90px">Key: </label>`).appendTo(line_key);
@@ -138,171 +203,207 @@
138
203
 
139
204
  container[0].appendChild(fragment);
140
205
 
206
+ let scope_opts = [
207
+ { value: "global", label: "Global"},
208
+ { value: "flow", label: "Flow"},
209
+ { value: "node", label: "Node"},
210
+ ];
211
+
212
+ let flow_opts = [{
213
+ value: ".", // special marker
214
+ label: "Current Flow"
215
+ }];
216
+
217
+ RED.nodes.eachWorkspace( ws => {
218
+ flow_opts.push({
219
+ value: ws.id,
220
+ label: ws.label
221
+ });
222
+ });
223
+
224
+ let node_opts = get_node_options(data.flow ?? ".");
225
+
226
+ // this function - called by TypedInput - displays the label of the node
227
+ // and (!!) does as well the error management
228
+ let node_valueLabel = function(container, value) {
229
+
230
+ // If an "invalid" value was selected, this function always receives value == ""!
231
+ // In such a case, go via data.node (!!)
232
+ // =>> line 1257 @ packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js
233
+ if ("" === value) {
234
+ value = data.node;
235
+ }
236
+
237
+ let err = false; // to draw red frame in case of anomaly
238
+ let f = node_opts?.filter( opts => {
239
+ return opts?.value == value;
240
+ });
241
+
242
+ if (node_opts.length < 1) {
243
+ container.text("No nodes in this flow.");
244
+ line_node_missing.hide();
245
+ data.ok.node = true;
246
+ err = true;
247
+ } else if (f.length > 0) {
248
+ container.text(f[0].label);
249
+ line_node_missing.hide();
250
+ data.ok.node = true;
251
+ err = false;
252
+ } else {
253
+ let label;
254
+ switch (value) {
255
+ case ".": {
256
+ label = "No node selected."
257
+ line_node_missing.hide();
258
+ data.ok.node = true;
259
+ break;
260
+ }
261
+ default: {
262
+ console.log("@default");
263
+ label = data.node_label ?? "Unknown Node";
264
+ line_node_missing.show();
265
+ data.ok.node = false;
266
+ }
267
+ }
268
+ container.text(label);
269
+ err = true;
270
+ }
271
+ container.css("color", (err == true) ? "darkgrey" : "var(--red-ui-form-text-color)");
272
+
273
+ // This draws / removes the red frame around the widget
274
+ setTimeout(() => {
275
+ $(`#context-scope-nodes-${index}`).next().toggleClass("input-error", err);
276
+ }, 150);
277
+ }
278
+
141
279
  $(`#context-scope-${index}`).typedInput({type:"scope", types:[{
142
280
  value: "scope",
143
- options: [
144
- { value: "global", label: "Global"},
145
- { value: "flow", label: "Flow"},
146
- // { value: "group", label: "Group"},
147
- { value: "node", label: "Node"},
148
- ]
281
+ options: scope_opts
149
282
  }]}).on("change", function (event, type, value) {
283
+
150
284
  switch (value) {
151
285
  case "global": {
152
286
  line_flow.hide();
153
- // line_group.hide();
287
+ line_flow_missing.toggle(false);
154
288
  line_node.hide();
289
+ line_node_missing.toggle(false);
155
290
  break;
156
291
  }
157
292
  case "flow": {
158
293
  line_flow.show();
159
- // line_group.hide();
160
- line_node.hide();
161
- break;
162
- }
163
- case "group": {
164
- line_flow.show();
165
- // line_group.show();
294
+ line_flow_missing.toggle(!data.ok.flow);
166
295
  line_node.hide();
296
+ line_node_missing.toggle(false);
297
+
298
+ // Initialize if not defined
299
+ if (!_initing) {
300
+ data.flow ??= ".";
301
+ if (data.flow !== $(`#context-scope-flows-${index}`).typedInput("value")) {
302
+ $(`#context-scope-flows-${index}`).typedInput("value", data.flow);
303
+ }
304
+ }
167
305
  break;
168
306
  }
169
307
  case "node": {
170
308
  line_flow.show();
171
- // line_group.hide();
309
+ line_flow_missing.toggle(!data.ok.flow);
172
310
  line_node.show();
173
- break;
311
+ line_node_missing.toggle(!data.ok.node);
312
+
313
+ if (!_initing) {
314
+ // Initialize if not defined
315
+ data.flow ??= ".";
316
+ if (data.flow !== $(`#context-scope-flows-${index}`).typedInput("value")) {
317
+ $(`#context-scope-flows-${index}`).typedInput("value", data.flow);
318
+ }
319
+ data.node ??= $(`#context-scope-nodes-${index}`).typedInput("value") ?? ".";
320
+ if (data.node !== $(`#context-scope-nodes-${index}`).typedInput("value")) {
321
+ $(`#context-scope-nodes-${index}`).typedInput("value", data.node);
322
+ }
323
+ }
174
324
  }
175
325
  }
176
326
  if (!_initing) {
177
327
  data.scope = value;
178
-
179
- data.flow = data.flow ?? $(`#context-scope-flows-${index}`).typedInput("value");
180
- // data.group = data.group ?? $(`#context-scope-groups-${index}`).typedInput("value");
181
- data.node = data.node ?? $(`#context-scope-nodes-${index}`).typedInput("value");
182
328
  node.dirty = true;
183
329
  }
184
-
185
- switch (data.scope) {
186
- // case "group": {
187
- // $(`#context-scope-key-${index}`).prop("disabled", data.group == "");
188
- // break;
189
- // }
190
- case "node": {
191
- $(`#context-scope-key-${index}`).prop("disabled", data.node == "");
192
- break;
193
- }
194
- }
195
-
196
- });
197
-
198
- let flow_opts = [];
199
- // let group_opts = [];
200
- let node_opts;
201
-
202
- RED.nodes.eachWorkspace( cb => {
203
- flow_opts.push({
204
- value: cb.id,
205
- label: cb.label
206
- });
207
330
  });
208
331
 
209
332
  $(`#context-scope-flows-${index}`).typedInput({type:"flow", types:[{
210
333
  value: "flow",
211
- options: flow_opts
212
- }]}).on("change", function (event, type, value) {
334
+ options: flow_opts,
335
+ valueLabel: function(container, value) {
336
+
337
+ // If an "invalid" value was selected, this function always receives value == ""!
338
+ // In such a case, go via data.node (!!)
339
+ // =>> line 1257 @ packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js
340
+ if ("" === value) {
341
+ value = data.flow;
342
+ }
213
343
 
214
- // RED.nodes.eachGroup( g => {
215
- // console.log(g)
216
- // if (g.z == value) {
217
- // group_opts.push({
218
- // value: g.id,
219
- // label: g.label ?? g.id
220
- // });
221
- // }
222
- // });
223
- // let gol = group_opts.length;
224
- // $(`#context-scope-groups-${index}`).typedInput('disable', gol < 1);
225
- // if (gol < 1) {
226
- // group_opts.push({
227
- // value: undefined,
228
- // label: "No group in this flow"
229
- // })
230
- // }
231
- // $(`#context-scope-groups-${index}`).typedInput('types', [{options: group_opts}]);
232
- // $(`#context-scope-groups-${index}`).parent().find(".red-ui-typedInput-option-label").css("color", gol < 1 ? "darkgrey" : "var(--red-ui-form-text-color)");
233
- // if ($(`#context-scope-${index}`).typedInput("value") == "group") {
234
- // $(`#context-scope-key-${index}`).prop("disabled", gol < 1);
235
- // }
236
-
237
- node_opts = [];
238
- RED.nodes.eachNode( n => {
239
-
240
- if (n.z == value) {
241
-
242
- // from 25-status.html
243
- let nodeDef = RED.nodes.getType(n.type);
244
- let label;
245
- let sublabel;
246
- if (nodeDef) {
247
- let l = nodeDef.label;
248
- label = (typeof l === "function" ? l.call(n) : l)||"";
249
- // sublabel = n.type;
250
- // if (sublabel.indexOf("subflow:") === 0) {
251
- // let subflowId = sublabel.substring(8);
252
- // let subflow = RED.nodes.subflow(subflowId);
253
- // sublabel = "subflow : "+subflow.name;
254
- // }
255
- }
256
- if (!nodeDef || !label) {
257
- label = n.type;
258
- }
344
+ let f = flow_opts?.filter( opts => {
345
+ return opts?.value == value;
346
+ });
259
347
 
260
- node_opts.push({
261
- value: n.id,
262
- label: label
263
- });
348
+ let err = false;
349
+
350
+ if (f.length > 0) {
351
+ // to display the label of the "Current Flow"
352
+ if ("." == value) {
353
+ let label = RED.nodes.workspace(node.z)?.label ?? RED.nodes.subflow(node.z)?.name ?? "";
354
+ $(`<div style="display: inline"><span>Current Flow </span><span style="font-size: x-small">>> ${label}</span></div>`).appendTo(container);
355
+ } else {
356
+ container.text(f[0].label);
357
+ }
358
+ line_flow_missing.hide();
359
+ data.ok.flow = true;
360
+ } else {
361
+ container.text(data.flow_label ?? "Unknown Flow");
362
+ line_flow_missing.show();
363
+ data.ok.flow = false;
364
+ err = true;
264
365
  }
265
- });
366
+
367
+ container.css("color", (err == true) ? "darkgrey" : "var(--red-ui-form-text-color)");
368
+ // This draws / removes the red frame around the widget
369
+ setTimeout(() => {
370
+ $(`#context-scope-flows-${index}`).next().toggleClass("input-error", err);
371
+ }, 150);
372
+ }
373
+ }]}).on("change", function (event, type, value) {
374
+
375
+ node_opts = get_node_options(value);
266
376
  let nol = node_opts.length;
267
377
  $(`#context-scope-nodes-${index}`).typedInput('disable', nol < 1);
268
- if (nol < 1) {
269
- node_opts.push({
270
- value: undefined,
271
- label: "No nodes in this flow"
272
- })
273
- }
274
- $(`#context-scope-nodes-${index}`).typedInput('types', [{options: node_opts}]);
275
- $(`#context-scope-nodes-${index}`).parent().find(".red-ui-typedInput-option-label").css("color", nol < 1 ? "darkgrey" : "var(--red-ui-form-text-color)");
276
- if ($(`#context-scope-${index}`).typedInput("value") == "node") {
277
- $(`#context-scope-key-${index}`).prop("disabled", nol < 1);
278
- }
378
+
379
+ $(`#context-scope-nodes-${index}`).typedInput('types', [{
380
+ value: "node",
381
+ options: node_opts,
382
+ valueLabel: node_valueLabel
383
+ }]);
279
384
 
280
385
  if (!_initing) {
386
+
281
387
  data.flow = value;
388
+ data.flow_label = flow_opts.find((opt) => opt.value == value)?.label;
389
+ data.node = node_opts[0]?.value ?? ".";
390
+
391
+ $(`#context-scope-nodes-${index}`).typedInput('value', data.node);
282
392
  node.dirty = true;
283
393
  }
284
394
  });
285
395
 
286
- // $(`#context-scope-groups-${index}`).typedInput({type:"group", types:[{
287
- // value: "group",
288
- // options: [
289
- // { value: "test", label: "TEST"}
290
- // ]
291
- // }]}).on("change", function (event, type, value) {
292
- // if (!_initing) {
293
- // data.group = value;
294
- // node.dirty = true;
295
- // }
296
- // });
297
-
298
396
  $(`#context-scope-nodes-${index}`).typedInput({type:"node", types:[{
299
- value: "node"
397
+ value: "node",
398
+ options: node_opts,
399
+ valueLabel: node_valueLabel
300
400
  }]}).on("change", function (event, type, value) {
401
+
301
402
  if (!_initing) {
302
403
  data.node = value;
404
+ data.node_label = node_opts.find((opt) => opt.value == value)?.label;
303
405
  node.dirty = true;
304
406
  }
305
- $(`#context-scope-key-${index}`).prop("disabled", false);
306
407
 
307
408
  });
308
409
 
@@ -318,37 +419,22 @@
318
419
  $(this).toggleClass("input-error", !validate_context_key($(this).val()));
319
420
  });
320
421
 
321
- // initialize the form
322
- $(`#context-scope-${index}`).typedInput("value", data.scope);
323
-
324
- let f = data.flow || flow_opts[0]?.value;
325
- if (f) {
326
- if (flow_opts.filter( opts => {
327
- return opts?.value == f;
328
- }).length > 0) {
329
- $(`#context-scope-flows-${index}`).typedInput("value", f);
330
- }
331
- }
422
+ // *****
423
+ // Initialize the form
424
+ //
425
+
426
+ $(`#context-scope-key-${index}`).val(data.key).prop("disabled", false);
427
+ $(`#context-scope-key-${index}`).toggleClass("input-error", !validate_context_key(data.key));
332
428
 
333
- // let g = data.group || group_opts[0]?.value;
334
- // if (g) {
335
- // if (group_opts.filter( opts => {
336
- // return opts.value == g;
337
- // }).length > 0) {
338
- // $(`#context-scope-groups-${index}`).typedInput("value", g);
339
- // }
340
- // }
341
-
342
- let n = data.node || node_opts[0]?.value;
343
- if (n) {
344
- if (node_opts.filter( opts => {
345
- return opts?.value == n;
346
- }).length > 0) {
347
- $(`#context-scope-nodes-${index}`).typedInput("value", n);
429
+ if ("global" !== data.scope) {
430
+ $(`#context-scope-flows-${index}`).typedInput("value", data.flow ?? flow_opts[0]?.value);
431
+ if ("node" == data.scope) {
432
+ $(`#context-scope-nodes-${index}`).typedInput("value", data.node);
348
433
  }
349
434
  }
350
435
 
351
- $(`#context-scope-key-${index}`).val(data.key).toggleClass("input-error", !validate_context_key(data.key));
436
+ // Once Flow & Node are set, select Scope...
437
+ $(`#context-scope-${index}`).typedInput("value", data.scope);
352
438
 
353
439
  _initing = false;
354
440
  },
@@ -383,7 +469,9 @@
383
469
  node.monitoring.forEach(data => {
384
470
  $('#node-input-context-container').editableList('addItem', data);
385
471
  });
386
- } catch {}
472
+ } catch (err) {
473
+ console.log(err);
474
+ }
387
475
 
388
476
  node._ctx = ctx;
389
477
 
@@ -402,6 +490,7 @@
402
490
  width = ti.next().outerWidth();
403
491
  }
404
492
  if (width) {
493
+ $('[id*=context-scope-nodes]').outerWidth(width);
405
494
  $('[id*=context-scope-key]').outerWidth(width);
406
495
  }
407
496
 
@@ -423,22 +512,26 @@
423
512
 
424
513
  ctx.forEach( data => {
425
514
  delete data.index;
515
+ delete data.ok;
426
516
 
427
517
  switch (data.scope) {
428
518
  case "global":
429
519
  delete data.flow;
520
+ delete data.flow_label;
430
521
  case "flow":
431
- delete data.group;
432
- // case "group":
433
- // delete data.node;
434
- // break;
522
+ delete data.node;
523
+ delete data.node_label;
435
524
  case "node":
525
+ // handle "current flow" special marker
526
+ if ("." == data.flow) {
527
+ delete data.flow_label;
528
+ }
529
+ // for a node, "." means 'no node selected'!
530
+ if ("." == data.node) {
531
+ delete data.node_label;
532
+ }
533
+ default:
436
534
  delete data.group;
437
- }
438
-
439
- if (data.flow == node.z) {
440
- // set a special marker that 'this flow' shall be referenced
441
- data.flow = ".";
442
535
  }
443
536
  })
444
537
 
package/monitor.js CHANGED
@@ -25,7 +25,6 @@ function scan_for_require_path(req_path) {
25
25
 
26
26
  if (process.env.NODE_RED_HOME) {
27
27
  found = path.join(process.env.NODE_RED_HOME, "..", req_path);
28
- console.log("@f", found);
29
28
  if (fs.existsSync(found)) {
30
29
  return found;
31
30
  }
@@ -64,7 +63,6 @@ function scan_for_require_path(req_path) {
64
63
  }
65
64
  }
66
65
 
67
- console.log(found);
68
66
  return found;
69
67
  }
70
68
 
@@ -122,14 +120,69 @@ module.exports = function(RED) {
122
120
 
123
121
  node.monitoring = [];
124
122
 
123
+ let nodes = {};
124
+ let flows = {}
125
+
126
+ // collect all known flows & nodes
127
+ RED.nodes.eachNode( n => {
128
+ if (n.id) {
129
+ nodes[n.id] ??= true;
130
+ }
131
+ if (n.z) {
132
+ flows[n.z] ??= true;
133
+ }
134
+ })
135
+
125
136
  scopes.forEach( data => {
126
137
 
127
- if (!data.key) return;
138
+ // Let's validate the given monitoring context first...
139
+ switch (data.scope) {
140
+ case "node": {
141
+ if (!data.node || "." == data.node || data.node.length < 1) {
142
+ node.warn('Scope is "Node" but node reference is missing.');
143
+ return;
144
+ }
145
+ if (!nodes[data.node]) {
146
+ node.warn('Referenced node does not exist.');
147
+ return;
148
+ }
149
+ }
150
+ case "flow": {
151
+ if (!flows[data.flow]) {
152
+ if ("." !== data.flow) {
153
+ node.warn('Referenced flow does not exist.');
154
+ return;
155
+ }
156
+ }
157
+ }
158
+ default: {
159
+ if (!data.key || data.key.length < 1) {
160
+ node.warn("Monitoring context key is missing.")
161
+ return;
162
+ }
163
+ }
164
+ }
165
+
166
+ if ("." === data.flow) {
167
+ data.flow = node.z;
168
+ }
128
169
 
129
- // support for complex keys
130
- // test["mm"].value becomes test.mm.value
131
170
  let key = data.key;
132
171
 
172
+ // resolve $parent to true flow id ... if the monitor sits in a subflow!
173
+ if ("flow" == data.scope && key.startsWith("$parent.")) {
174
+ let fl = RED.nodes.getNode(data.flow);
175
+ if ("subflow" == fl?.TYPE) {
176
+ // This might not create the expected result, if .parent is not a flow!
177
+ // ... but a group or another subflow!
178
+ // >> Fix it if someone asks for.
179
+ data.flow = fl.parent.id;
180
+ data.key = key = key.substring("$parent.".length);
181
+ }
182
+ }
183
+
184
+ // support for complex keys
185
+ // test["mm"].value becomes test.mm.value
133
186
  try {
134
187
  let key_parts = RED.util.normalisePropertyExpression(key);
135
188
  for (i=0; i<key_parts.length; i++) {
@@ -142,10 +195,6 @@ module.exports = function(RED) {
142
195
  return;
143
196
  }
144
197
 
145
- if ("." === data.flow) {
146
- data.flow = node.z;
147
- }
148
-
149
198
  let ctx = "global";
150
199
  switch (data.scope) {
151
200
  case "global":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ralphwetzel/node-red-context-monitor",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "A Node-RED node to monitor a context.",
5
5
  "main": "monitor.js",
6
6
  "scripts": {
@@ -27,15 +27,15 @@
27
27
  },
28
28
  "homepage": "https://github.com/ralphwetzel/node-red-context-monitor#readme",
29
29
  "dependencies": {
30
- "fs-extra": "^11.1.1"
30
+ "fs-extra": "^11.1.0"
31
31
  },
32
32
  "engines": {
33
33
  "node": ">=14.0.0"
34
34
  },
35
35
  "devDependencies": {
36
- "mocha": "^10.2.0",
37
- "node-red-node-test-helper": "^0.3.2",
38
- "node-red": "^3.1.0"
36
+ "mocha": "^11.7.0",
37
+ "node-red": "^3.0.0",
38
+ "node-red-node-test-helper": "^0.3.5"
39
39
  },
40
40
  "files": [
41
41
  "/examples",