@gregoriusrippenstein/node-red-contrib-nodedev 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,311 @@
1
+ <script type="text/html" data-template-name="NodeFactory">
2
+ <div class="form-row">
3
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" placeholder="Name"></div>
5
+ </div>
6
+
7
+ <div class="form-row">
8
+ <label for="node-input-autoimport">
9
+ <span>Auto Import of Nodes?</span>
10
+ </label>
11
+ <input type="checkbox" id="node-input-autoimport" style="display:inline-block; width:15px; vertical-align:baseline;">
12
+ </div>
13
+
14
+ <div class="form-row">
15
+ <label for="node-input-nodename"><i class="fa fa-tag"></i> Node Name</label>
16
+ <div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-nodename" placeholder="Node Name">
17
+ </div>
18
+ </div>
19
+
20
+ <div class="form-row">
21
+ <label for="node-input-category"><i class="fa fa-tag"></i> Node Category</label>
22
+ <div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-category" placeholder="Node Category">
23
+ </div>
24
+ </div>
25
+
26
+ <div id="node-input-row-style-colour" class="form-row">
27
+ <label>Color</label>
28
+ </div>
29
+
30
+ <div id="node-input-row-icon-picker" class="form-row">
31
+ </div>
32
+
33
+ <div class="form-row">
34
+ <label for="node-input-hasbutton">
35
+ <span>Has button?</span>
36
+ </label>
37
+ <input type="checkbox" id="node-input-hasbutton" style="display:inline-block; width:15px; vertical-align:baseline;">
38
+ </div>
39
+
40
+ <div class="form-row">
41
+ <label for="node-input-hasinput">
42
+ <span>Has input port?</span>
43
+ </label>
44
+ <input type="checkbox" id="node-input-hasinput" style="display:inline-block; width:15px; vertical-align:baseline;">
45
+ </div>
46
+
47
+ <div class="form-row">
48
+ <label for="node-input-outputcount">
49
+ <i class="fa fa-tag"></i> Output ports
50
+ </label>
51
+ <select id="node-input-outputcount"></select>
52
+ </div>
53
+
54
+ <div class="form-row">
55
+ <label for="node-input-summary">
56
+ <i class="fa fa-tag"></i>
57
+ <span>Summary</span>
58
+ </label>
59
+ <div style="height: 150px; min-height:150px; max-height: 150px;" class="node-text-editor" id="node-input-summary">
60
+ </div>
61
+ </div>
62
+
63
+ <div class="form-row">
64
+ <label for="node-input-description">
65
+ <i class="fa fa-tag"></i>
66
+ <span>Description</span>
67
+ </label>
68
+ <div style="height: 350px; min-height:350px; max-height: 350px;" class="node-text-editor" id="node-input-description">
69
+ </div>
70
+ </div>
71
+
72
+ </script>
73
+
74
+ <script type="text/javascript">
75
+ (function () {
76
+
77
+ RED.comms.subscribe("nodedev:perform-autoimport-nodes", (event,data) => {
78
+ if ( data.msg == "autoimport" ) {
79
+ RED.clipboard.import();
80
+
81
+ setTimeout(() => {
82
+ $('#red-ui-clipboard-dialog-import-text').val(
83
+ data.payload
84
+ ).trigger("paste");
85
+ }, 300);
86
+ }
87
+ });
88
+
89
+ function doSubmission(node) {
90
+
91
+ var data = {};
92
+ Object.keys(node._def.defaults).forEach( (nam) => {
93
+ data[nam] = node[nam]
94
+ })
95
+ data["name"] = node.nodename;
96
+ data["__task"] = "generate_from_templates";
97
+
98
+ $.ajax({
99
+ url: "NodeFactory/" + node.id,
100
+ type: "POST",
101
+ contentType: "application/json; charset=utf-8",
102
+
103
+ data: JSON.stringify({
104
+ node: data,
105
+ }),
106
+
107
+ success: function (resp) {
108
+ RED.notify("Data sent", {
109
+ type: "warning",
110
+ id: "FlowHubPush",
111
+ timeout: 2000
112
+ });
113
+ },
114
+
115
+ error: function (jqXHR, textStatus, errorThrown) {
116
+ if (jqXHR.status == 404) {
117
+ RED.notify(node._("common.notification.error", {
118
+ message: node._("common.notification.errors.not-deployed")
119
+ }), "error");
120
+ } else if (jqXHR.status == 405) {
121
+ RED.notify(node._("common.notification.not_allowed", {
122
+ message: node._("inject.errors.not_allowed")
123
+ }), "error");
124
+ } else if (jqXHR.status == 500) {
125
+ RED.notify(node._("common.notification.error", {
126
+ message: node._("inject.errors.failed")
127
+ }), "error");
128
+ } else if (jqXHR.status == 0) {
129
+ RED.notify(node._("common.notification.error", {
130
+ message: node._("common.notification.errors.no-response")
131
+ }), "error");
132
+ } else {
133
+ RED.notify(node._("common.notification.error", {
134
+ message: node._("common.notification.errors.unexpected", {
135
+ status: jqXHR.status, message: textStatus
136
+ })
137
+ }), "error");
138
+ }
139
+ }
140
+ });
141
+ }
142
+
143
+ RED.nodes.registerType('NodeFactory', {
144
+ color: "#e5e4ef",
145
+ category: 'nodedev',
146
+ defaults: {
147
+ name: { value: "" },
148
+ nodename: { value: "" },
149
+ color: { value: "#e5e4ef" },
150
+ hasbutton: { value: false },
151
+ hasinput: { value: true },
152
+ outputcount: { value: 1 },
153
+ autoimport: { value: true },
154
+ category: { value: "" },
155
+ summary: { value: ""},
156
+ description: { value: ""},
157
+ icon: { value: "font-awesome/fa-industry" }
158
+ },
159
+ inputs: 1,
160
+ outputs: 1,
161
+ icon: "font-awesome/fa-industry",
162
+ label: function () {
163
+ return this.name || this._def.paletteLabel;
164
+ },
165
+ labelStyle: function () {
166
+ return this.name ? "node_label_italic" : "";
167
+ },
168
+ oneditprepare: function () {
169
+ const that = this;
170
+ const stateId = RED.editor.generateViewStateId("node", this, "");
171
+
172
+ var sltObj = $('#node-input-outputcount');
173
+ sltObj.html("");
174
+
175
+ [0,1,2,3,4,5,6,7,8,9,10].forEach( function(v) {
176
+ sltObj.append($('<option></option>').val(v).html(v));
177
+ });
178
+
179
+ sltObj.val(this.outputcount || "1");
180
+
181
+ var colorPalette = [
182
+ "#DDAA99",
183
+ "#3FADB5", "#87A980", "#A6BBCF",
184
+ "#AAAA66", "#C0C0C0", "#C0DEED",
185
+ "#C7E9C0", "#D7D7A0", "#D8BFD8",
186
+ "#DAC4B4", "#DEB887", "#DEBD5C",
187
+ "#E2D96E", "#E6E0F8", "#E7E7AE",
188
+ "#E9967A", "#F3B567", "#FDD0A2",
189
+ "#FDF0C2", "#FFAAAA", "#FFCC66",
190
+ "#FFF0F0", "#FFFFFF"
191
+ ];
192
+
193
+ RED.editor.colorPicker.create({
194
+ id: "node-input-colour",
195
+ value: this.color || "#a4a4a4",
196
+ defaultValue: "#a4a4a4",
197
+ palette: colorPalette,
198
+ cellWidth: 16,
199
+ cellHeight: 16,
200
+ cellMargin: 3,
201
+ none: false,
202
+ opacity: 1.0,
203
+ sortPalette: function (a, b) { return a.l - b.l; }
204
+ }).appendTo("#node-input-row-style-colour");
205
+
206
+ $("#node-input-colour").on('change', function(ev) {
207
+ var colour = $(this).val();
208
+ var nodeDiv = $('.red-ui-search-result-node')
209
+ nodeDiv.css('backgroundColor',colour);
210
+ var borderColor = RED.utils.getDarkerColor(colour);
211
+ if (borderColor !== colour) {
212
+ nodeDiv.css('border-color',borderColor);
213
+ }
214
+ });
215
+
216
+ this.editorSummary = RED.editor.createEditor({
217
+ id: 'node-input-summary',
218
+ mode: 'ace/mode/markdown',
219
+ value: $("#node-input-summary").val()
220
+ });
221
+
222
+ this.editorDesc = RED.editor.createEditor({
223
+ id: 'node-input-description',
224
+ mode: 'ace/mode/markdown',
225
+ value: $("#node-input-description").val()
226
+ });
227
+
228
+ var node = this;
229
+ var iconRow = $('#node-input-row-icon-picker');
230
+ $('<label data-i18n="editor.settingIcon">').appendTo(iconRow);
231
+
232
+ var iconButton = $('<button type="button" class="red-ui-button red-ui-editor-node-appearance-button">').appendTo(iconRow);
233
+ $('<i class="fa fa-caret-down"></i>').appendTo(iconButton);
234
+ var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(iconButton);
235
+ var colour = this.color || RED.utils.getNodeColor(node.type, node._def);
236
+ var icon_url = RED.utils.getNodeIcon(node._def,node);
237
+ nodeDiv.css('backgroundColor',colour);
238
+ var borderColor = RED.utils.getDarkerColor(colour);
239
+ if (borderColor !== colour) {
240
+ nodeDiv.css('border-color',borderColor);
241
+ }
242
+
243
+ var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
244
+ RED.utils.createIconElement(icon_url, iconContainer, true);
245
+
246
+ iconButton.on("click", function(e) {
247
+ e.preventDefault();
248
+ var iconPath;
249
+ var icon = $("#red-ui-editor-node-icon").val()||"";
250
+ if (icon) {
251
+ iconPath = RED.utils.separateIconPath(icon);
252
+ } else {
253
+ iconPath = RED.utils.getDefaultNodeIcon(node._def, node);
254
+ }
255
+ var backgroundColor = RED.utils.getNodeColor(node.type, node._def);
256
+ if (node.type === "subflow") {
257
+ backgroundColor = $("#red-ui-editor-node-color").val();
258
+ }
259
+ RED.editor.iconPicker.show(iconButton,backgroundColor,iconPath,false,function(newIcon) {
260
+ $("#red-ui-editor-node-icon").val(newIcon||"");
261
+ var icon_url = RED.utils.getNodeIcon(node._def,{type:node.type,icon:newIcon});
262
+ RED.utils.createIconElement(icon_url, iconContainer, true);
263
+ });
264
+ });
265
+
266
+ RED.popover.tooltip(iconButton, function() {
267
+ return $("#red-ui-editor-node-icon").val() || RED._("editor.default");
268
+ });
269
+ $('<input type="hidden" id="red-ui-editor-node-icon">').val(node.icon).appendTo(iconRow);
270
+ },
271
+
272
+ oneditsave: function () {
273
+ this.color = $('#node-input-colour').val();
274
+ $("#node-input-summary").val(this.editorSummary.getValue());
275
+ $("#node-input-description").val(this.editorDesc.getValue());
276
+ this.icon = $('#red-ui-editor-node-icon').val();
277
+
278
+ this.editorSummary.destroy();
279
+ this.editorDesc.destroy();
280
+ delete this.editorSummary;
281
+ delete this.editorDesc;
282
+ },
283
+
284
+ oneditcancel: function () {
285
+ this.editorSummary.destroy();
286
+ this.editorDesc.destroy();
287
+ delete this.editorSummary;
288
+ delete this.editorDesc;
289
+ },
290
+
291
+ oneditresize: function (size) {
292
+ },
293
+
294
+ button: {
295
+ enabled: function () {
296
+ return !this.changed
297
+ },
298
+
299
+ onclick: function () {
300
+ if (this.changed) {
301
+ return RED.notify(RED._("notification.warning", {
302
+ message: RED._("notification.warnings.undeployedChanges")
303
+ }), "warning");
304
+ }
305
+
306
+ doSubmission(this)
307
+ }
308
+ },
309
+ });
310
+ })();
311
+ </script>
@@ -0,0 +1,375 @@
1
+ module.exports = function (RED) {
2
+ "use strict";
3
+
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+
7
+ var mustache = require("mustache");
8
+ var tarStream = require('tar-stream');
9
+ var streamx = require('streamx');
10
+ var pakoGzip = require('pako');
11
+
12
+ function extractTokens(tokens, set) {
13
+ set = set || new Set();
14
+ tokens.forEach(function (token) {
15
+ if (token[0] !== 'text') {
16
+ set.add(token[1]);
17
+ if (token.length > 4) {
18
+ extractTokens(token[4], set);
19
+ }
20
+ }
21
+ });
22
+ return set;
23
+ }
24
+
25
+ function parseContext(key) {
26
+ var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key);
27
+ if (match) {
28
+ var parts = {};
29
+ parts.type = match[1];
30
+ parts.store = (match[3] === '') ? "default" : match[3];
31
+ parts.field = match[4];
32
+ return parts;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ function parseEnv(key) {
38
+ var match = /^env\.(.+)/.exec(key);
39
+ if (match) {
40
+ return match[1];
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /**
46
+ * Custom Mustache Context capable to collect message property and node
47
+ * flow and global context
48
+ */
49
+
50
+ function NodeContext(msg, nodeContext, parent, escapeStrings, cachedContextTokens) {
51
+ this.msgContext = new mustache.Context(msg, parent);
52
+ this.nodeContext = nodeContext;
53
+ this.escapeStrings = escapeStrings;
54
+ this.cachedContextTokens = cachedContextTokens;
55
+ }
56
+
57
+ NodeContext.prototype = new mustache.Context();
58
+
59
+ NodeContext.prototype.lookup = function (name) {
60
+ // try message first:
61
+ try {
62
+ var value = this.msgContext.lookup(name);
63
+ if (value !== undefined) {
64
+ if (this.escapeStrings && typeof value === "string") {
65
+ value = value.replace(/\\/g, "\\\\");
66
+ value = value.replace(/\n/g, "\\n");
67
+ value = value.replace(/\t/g, "\\t");
68
+ value = value.replace(/\r/g, "\\r");
69
+ value = value.replace(/\f/g, "\\f");
70
+ value = value.replace(/[\b]/g, "\\b");
71
+ }
72
+ return value;
73
+ }
74
+
75
+ // try env
76
+ if (parseEnv(name)) {
77
+ return this.cachedContextTokens[name];
78
+ }
79
+
80
+ // try flow/global context:
81
+ var context = parseContext(name);
82
+ if (context) {
83
+ var type = context.type;
84
+ var store = context.store;
85
+ var field = context.field;
86
+ var target = this.nodeContext[type];
87
+ if (target) {
88
+ return this.cachedContextTokens[name];
89
+ }
90
+ }
91
+ return '';
92
+ }
93
+ catch (err) {
94
+ throw err;
95
+ }
96
+ }
97
+
98
+ NodeContext.prototype.push = function push(view) {
99
+ return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens);
100
+ };
101
+
102
+ function handleTemplate(msg, node, template) {
103
+ var promises = [];
104
+ var tokens = extractTokens(mustache.parse(template));
105
+ var resolvedTokens = {};
106
+
107
+ tokens.forEach(function (name) {
108
+ var env_name = parseEnv(name);
109
+ if (env_name) {
110
+ var promise = new Promise((resolve, reject) => {
111
+ var val = RED.util.evaluateNodeProperty(env_name, 'env', node)
112
+ resolvedTokens[name] = val;
113
+ resolve();
114
+ });
115
+ promises.push(promise);
116
+ return;
117
+ }
118
+
119
+ var context = parseContext(name);
120
+ if (context) {
121
+ var type = context.type;
122
+ var store = context.store;
123
+ var field = context.field;
124
+ var target = node.context()[type];
125
+ if (target) {
126
+ var promise = new Promise((resolve, reject) => {
127
+ target.get(field, store, (err, val) => {
128
+ if (err) {
129
+ reject(err);
130
+ } else {
131
+ resolvedTokens[name] = val;
132
+ resolve();
133
+ }
134
+ });
135
+ });
136
+ promises.push(promise);
137
+ return;
138
+ }
139
+ }
140
+ });
141
+
142
+ return Promise.all(promises).then(function () {
143
+ return mustache.render(template, new NodeContext(msg, node.context(), null, false, resolvedTokens));
144
+ });
145
+ }
146
+
147
+ function convertToPkgFileNodes(msg, jsCtnt, htmlCtnt) {
148
+ var allNodes = [];
149
+
150
+ var secondId = RED.util.generateId();
151
+
152
+ allNodes.push({
153
+ id: RED.util.generateId(),
154
+ type: "PkgFile",
155
+ name: msg.node.name + ".js",
156
+ filename: "nodes/" + msg.node.name.toLowerCase() + ".js",
157
+ template: jsCtnt,
158
+ syntax: "mustache",
159
+ format: "javascript",
160
+ output: "str",
161
+ x: 100,
162
+ y: 50,
163
+ wires: [
164
+ [
165
+ secondId
166
+ ]
167
+ ]
168
+ })
169
+
170
+ allNodes.push({
171
+ id: secondId,
172
+ type: "PkgFile",
173
+ name: msg.node.name + ".html",
174
+ filename: "nodes/" + msg.node.name.toLowerCase() + ".html",
175
+ template: htmlCtnt,
176
+ syntax: "mustache",
177
+ format: "html",
178
+ output: "str",
179
+ x: 100,
180
+ y: 100,
181
+ wires: [
182
+ [
183
+ ]
184
+ ]
185
+ })
186
+
187
+ return allNodes;
188
+ }
189
+
190
+ function NodeFactoryFunctionality(config) {
191
+ RED.nodes.createNode(this, config);
192
+
193
+ var node = this;
194
+ var cfg = config;
195
+
196
+ node.on('close', function () {
197
+ node.status({});
198
+ });
199
+
200
+ node.on("input", function (msg, send, done) {
201
+ if (msg.node && msg.node.__task == "generate_from_templates") {
202
+ try {
203
+ var htmlPath = path.join(__dirname, 'templates', 'tmpl.html');
204
+ var jsPath = path.join(__dirname, 'templates', 'tmpl.js');
205
+
206
+ var jsonTmpl = fs.readFileSync(jsPath, 'utf8');
207
+ var htmlTmpl = fs.readFileSync(htmlPath, 'utf8');
208
+
209
+ handleTemplate(msg, node, jsonTmpl).then(function (data) {
210
+ var jsData = data;
211
+ handleTemplate(msg, node, htmlTmpl).then(function (data) {
212
+ var nodeImpStr = JSON.stringify(convertToPkgFileNodes(msg, jsData, data));
213
+
214
+ send({ payload: nodeImpStr })
215
+
216
+ if (cfg.autoimport) {
217
+ RED.comms.publish(
218
+ "nodedev:perform-autoimport-nodes",
219
+ RED.util.encodeObject({
220
+ msg: "autoimport",
221
+ payload: nodeImpStr,
222
+ topic: msg.topic,
223
+ nodeid: node.id,
224
+ _msg: msg
225
+ })
226
+ );
227
+ }
228
+
229
+ done()
230
+ }).catch((err) => {
231
+ msg.error = err
232
+ done(err.message, msg)
233
+ })
234
+ }).catch((err) => {
235
+ msg.error = err
236
+ done(err.message, msg)
237
+ })
238
+ //send({ payload: jsonString })
239
+ } catch (err) {
240
+ msg.error = err
241
+ done(err.message, msg)
242
+ }
243
+ } else {
244
+ /* assume that payload is a buffer with a .tgz if not, error out */
245
+ try {
246
+ const extract = tarStream.extract()
247
+
248
+ var allFiles = [];
249
+
250
+ /*
251
+ * there is no indication in a tar file of whether a file is binary or textual.
252
+ * we can only make a guess by the extension of the filename.
253
+ ***/
254
+ var computeFormat = (filename) => {
255
+ var ext = filename.split(".").at(-1);
256
+
257
+ return {
258
+ "html": "html",
259
+ "js": "javascript",
260
+ "md": "markdown",
261
+ "json": "json",
262
+ /* binary formats are encoded in base64 */
263
+ "png": "base64",
264
+ "tiff": "base64",
265
+ "tif": "base64",
266
+ "jpg": "base64",
267
+ "jpeg": "base64",
268
+ "bin": "base64",
269
+ "bmp": "base64",
270
+ }[ext.toLowerCase()] || "text";
271
+ };
272
+
273
+ extract.on('entry', function (header, stream, next) {
274
+ // header is the tar header
275
+ // stream is the content body (might be an empty stream)
276
+ // call next when you are done with this entry
277
+
278
+ var buffer = [];
279
+
280
+ stream.on('data', function (data) {
281
+ buffer.push(data)
282
+ });
283
+
284
+ stream.on('end', function () {
285
+ var frmt = computeFormat(header.name.split("/").at(-1));
286
+
287
+ allFiles.push({
288
+ id: RED.util.generateId(),
289
+ type: "PkgFile",
290
+ name: header.name.split("/").at(-1),
291
+ filename: header.name.replace(/^package\//, ''),
292
+ template: Buffer.concat(buffer).toString(frmt == "base64" ? 'base64' : 'utf8'),
293
+ syntax: "mustache",
294
+ format: frmt,
295
+ output: "str",
296
+ x: 100,
297
+ y: 50 * (allFiles.length + 1),
298
+ wires: [
299
+ []
300
+ ]
301
+ })
302
+
303
+ next() // ready for next entry
304
+ })
305
+
306
+ stream.resume() // just auto drain the stream
307
+ })
308
+
309
+ extract.on('finish', function () {
310
+ // all entries read, wire them together
311
+ for (var idx = 0; idx < allFiles.length - 1; idx++) {
312
+ allFiles[idx].wires = [[allFiles[idx + 1].id]];
313
+ }
314
+
315
+ msg.payload = JSON.stringify(allFiles);
316
+ send(msg)
317
+
318
+ if (cfg.autoimport) {
319
+ RED.comms.publish(
320
+ "nodedev:perform-autoimport-nodes",
321
+ RED.util.encodeObject({
322
+ msg: "autoimport",
323
+ payload: msg.payload,
324
+ topic: msg.topic,
325
+ nodeid: node.id,
326
+ _msg: msg
327
+ })
328
+ );
329
+ }
330
+
331
+ done()
332
+ })
333
+
334
+ extract.on('error', function (err) {
335
+ msg.error = err;
336
+ done("extraction error", msg)
337
+ });
338
+
339
+ var stream = streamx.Readable.from(Buffer.from(pakoGzip.inflate(new Uint8Array(msg.payload))))
340
+ stream.pipe(extract);
341
+ } catch (err) {
342
+ msg.error = err
343
+ done(err.message, msg)
344
+ }
345
+ }
346
+ });
347
+ }
348
+
349
+ RED.nodes.registerType("NodeFactory", NodeFactoryFunctionality);
350
+
351
+ RED.httpAdmin.post("/NodeFactory/:id",
352
+ RED.auth.needsPermission("NodeFactory.write"),
353
+ (req, res) => {
354
+ var node = RED.nodes.getNode(req.params.id);
355
+ if (node != null) {
356
+ try {
357
+ if (req.body && req.params.id) {
358
+ var nde = RED.nodes.getNode(req.params.id)
359
+ if (nde && nde.type == "NodeFactory") {
360
+ node.receive(req.body);
361
+ }
362
+ } else {
363
+ res.sendStatus(404);
364
+ }
365
+ res.sendStatus(200);
366
+ } catch (err) {
367
+ res.sendStatus(500);
368
+ node.error("FlowHub: Submission failed: " +
369
+ err.toString())
370
+ }
371
+ } else {
372
+ res.sendStatus(404);
373
+ }
374
+ });
375
+ }