@gregoriusrippenstein/node-red-contrib-nodedev 0.1.6 → 0.1.8
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 +8 -1
- package/nodes/10-node-factory.html +24 -11
- package/nodes/10-node-factory.js +42 -83
- package/nodes/lib/tarhelpers.js +106 -0
- package/nodes/node-factory-sidebar-cfg.html +22 -0
- package/nodes/node-factory-sidebar-cfg.js +193 -0
- package/nodes/templates/locale/en-US/tmpl.html +4 -0
- package/nodes/templates/locale/en-US/tmpl.json +24 -0
- package/nodes/templates/tmpl.html +7 -12
- package/nodes/templates/tmpl.js +3 -3
- package/package.json +15 -5
- package/plugins/node-factory-sidebar.html +188 -0
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
## Node-RED nodes for the development of Node-RED nodes in Node-RED using Node-RED nodes.
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
*** Work In Progress, not stable. ***
|
|
5
|
+
|
|
3
6
|
*What?*
|
|
4
7
|
|
|
5
8
|
The idea behind this collection of nodes to democratise the development of Node-RED nodes. Normally the development of your [own nodes](https://nodered.org/docs/creating-nodes/) will require the use of a third-party editor. Something like VSCode or Vim or Atom or god forbid, [Emacs](https://discourse.nodered.org/t/node-red-node-development-in-node-red/81525/2). This requirement makes creating nodes that much harder. So why not create nodes for Node-RED in Node-RED? After all, if you are using Node-RED, you probably understand how to use Node-RED.
|
|
@@ -59,10 +62,14 @@ Since all this happens in the browser, there is no leaving Node-RED, making it m
|
|
|
59
62
|
### Artifacts
|
|
60
63
|
|
|
61
64
|
- [Flow that maintains](https://flowhub.org/f/b92be5062203ff69) this codebase
|
|
62
|
-
- [GitHub repo](https://
|
|
65
|
+
- [GitHub repo](https://github.com/gorenje/node-red-contrib-nodedev)
|
|
63
66
|
- [NPMjs node package](https://www.npmjs.com/package/@gregoriusrippenstein/node-red-contrib-nodedev)
|
|
64
67
|
- [Node-RED node package](https://flows.nodered.org/node/@gregoriusrippenstein/node-red-contrib-nodedev)
|
|
65
68
|
|
|
69
|
+
### Similar Projects
|
|
70
|
+
|
|
71
|
+
- [node-maker](https://github.com/steveorevo/node-maker) which helps to make UI components for nodes in Node-RED.
|
|
72
|
+
|
|
66
73
|
### Outlook
|
|
67
74
|
|
|
68
75
|
Wherever the road shall lead, there it will go.
|
|
@@ -97,21 +97,34 @@
|
|
|
97
97
|
|
|
98
98
|
</script>
|
|
99
99
|
|
|
100
|
+
<script type="text/html" data-help-name="NodeFactory">
|
|
101
|
+
<p>Generate node templates providing starting points for node development</p>
|
|
102
|
+
The Node-Factory can be used to create example code for various types of Node-RED nodes.
|
|
103
|
+
</script>
|
|
104
|
+
|
|
100
105
|
<script type="text/javascript">
|
|
101
106
|
(function () {
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
RED.comms.subscribe("nodedev:perform-autoimport-nodes", (event,data) => {
|
|
109
|
+
if ( data.msg == "notify") {
|
|
110
|
+
RED.notify(data.text, {
|
|
111
|
+
type: data.type,
|
|
112
|
+
id: data.id || "NodeFactory",
|
|
113
|
+
timeout: data.timeout || 2000
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if ( data.msg == "autoimport" ) {
|
|
108
118
|
setTimeout(() => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
RED.clipboard.import();
|
|
120
|
+
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
$('#red-ui-clipboard-dialog-import-text').val(
|
|
123
|
+
data.payload
|
|
124
|
+
).trigger("paste");
|
|
125
|
+
}, 300);
|
|
126
|
+
},400)
|
|
127
|
+
}
|
|
115
128
|
});
|
|
116
129
|
|
|
117
130
|
function doSubmission(node) {
|
package/nodes/10-node-factory.js
CHANGED
|
@@ -5,9 +5,6 @@ module.exports = function (RED) {
|
|
|
5
5
|
var path = require('path');
|
|
6
6
|
|
|
7
7
|
var mustache = require("mustache");
|
|
8
|
-
var tarStream = require('tar-stream');
|
|
9
|
-
var streamx = require('streamx');
|
|
10
|
-
var pakoGzip = require('pako');
|
|
11
8
|
|
|
12
9
|
var spcRepDict = {
|
|
13
10
|
"ocb2": "{{",
|
|
@@ -229,16 +226,22 @@ module.exports = function (RED) {
|
|
|
229
226
|
function createSimpleNodeDefintions(msg,node) {
|
|
230
227
|
var htmlPath = path.join(__dirname, 'templates', 'tmpl.html');
|
|
231
228
|
var jsPath = path.join(__dirname, 'templates', 'tmpl.js');
|
|
229
|
+
var localeJsonPath = path.join(__dirname, 'templates', 'locale', 'en-US', 'tmpl.json')
|
|
230
|
+
var localeHtmlPath = path.join(__dirname, 'templates', 'locale', 'en-US', 'tmpl.html')
|
|
232
231
|
|
|
233
232
|
var content = {};
|
|
234
233
|
|
|
235
234
|
var promises = [
|
|
236
235
|
handleTemplate(msg, node, fs.readFileSync(htmlPath, 'utf8')).then((c) => { content["html"] = c }),
|
|
237
236
|
handleTemplate(msg, node, fs.readFileSync(jsPath, 'utf8')).then((c) => { content["jasc"] = c }),
|
|
237
|
+
handleTemplate(msg, node, fs.readFileSync(localeJsonPath, 'utf8')).then((c) => { content["ljsn"] = c }),
|
|
238
|
+
handleTemplate(msg, node, fs.readFileSync(localeHtmlPath, 'utf8')).then((c) => { content["lhtm"] = c }),
|
|
238
239
|
];
|
|
239
240
|
|
|
240
241
|
return Promise.all(promises).then(() => {
|
|
241
242
|
var secondId = RED.util.generateId();
|
|
243
|
+
var thirdId = RED.util.generateId();
|
|
244
|
+
var fourthId = RED.util.generateId();
|
|
242
245
|
|
|
243
246
|
return [{
|
|
244
247
|
id: RED.util.generateId(),
|
|
@@ -253,7 +256,6 @@ module.exports = function (RED) {
|
|
|
253
256
|
y: 50,
|
|
254
257
|
wires: [[secondId]]
|
|
255
258
|
},
|
|
256
|
-
|
|
257
259
|
{
|
|
258
260
|
id: secondId,
|
|
259
261
|
type: "PkgFile",
|
|
@@ -265,8 +267,35 @@ module.exports = function (RED) {
|
|
|
265
267
|
output: "str",
|
|
266
268
|
x: 100,
|
|
267
269
|
y: 100,
|
|
270
|
+
wires: [[thirdId]]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: thirdId,
|
|
274
|
+
type: "PkgFile",
|
|
275
|
+
name: "Locale: " + msg.node.name + ".json",
|
|
276
|
+
filename: "nodes/locales/en-US/" + msg.node.namelwr + ".json",
|
|
277
|
+
template: content["ljsn"],
|
|
278
|
+
syntax: "mustache",
|
|
279
|
+
format: "json",
|
|
280
|
+
output: "str",
|
|
281
|
+
x: 120,
|
|
282
|
+
y: 150,
|
|
283
|
+
wires: [[fourthId]]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: fourthId,
|
|
287
|
+
type: "PkgFile",
|
|
288
|
+
name: "Locale: " + msg.node.name + ".html",
|
|
289
|
+
filename: "nodes/locales/en-US/" + msg.node.namelwr + ".html",
|
|
290
|
+
template: content["lhtm"],
|
|
291
|
+
syntax: "mustache",
|
|
292
|
+
format: "html",
|
|
293
|
+
output: "str",
|
|
294
|
+
x: 120,
|
|
295
|
+
y: 200,
|
|
268
296
|
wires: [[]]
|
|
269
|
-
}
|
|
297
|
+
}
|
|
298
|
+
]
|
|
270
299
|
})
|
|
271
300
|
}
|
|
272
301
|
|
|
@@ -430,76 +459,12 @@ module.exports = function (RED) {
|
|
|
430
459
|
} else {
|
|
431
460
|
/* assume that payload is a buffer with a .tgz if not, error out */
|
|
432
461
|
try {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
/*
|
|
438
|
-
* there is no indication in a tar file of whether a file is binary or textual.
|
|
439
|
-
* we can only make a guess by the extension of the filename.
|
|
440
|
-
***/
|
|
441
|
-
var computeFormat = (filename) => {
|
|
442
|
-
var ext = filename.split(".").at(-1);
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
"html": "html",
|
|
446
|
-
"js": "javascript",
|
|
447
|
-
"md": "markdown",
|
|
448
|
-
"json": "json",
|
|
449
|
-
/* binary formats are encoded in base64 */
|
|
450
|
-
"png": "base64",
|
|
451
|
-
"tiff": "base64",
|
|
452
|
-
"tif": "base64",
|
|
453
|
-
"jpg": "base64",
|
|
454
|
-
"jpeg": "base64",
|
|
455
|
-
"bin": "base64",
|
|
456
|
-
"bmp": "base64",
|
|
457
|
-
"gif": "base64",
|
|
458
|
-
}[ext.toLowerCase()] || "text";
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
extract.on('entry', function (header, stream, next) {
|
|
462
|
-
// header is the tar header
|
|
463
|
-
// stream is the content body (might be an empty stream)
|
|
464
|
-
// call next when you are done with this entry
|
|
465
|
-
|
|
466
|
-
var buffer = [];
|
|
467
|
-
|
|
468
|
-
stream.on('data', function (data) {
|
|
469
|
-
buffer.push(data)
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
stream.on('end', function () {
|
|
473
|
-
var frmt = computeFormat(header.name.split("/").at(-1));
|
|
474
|
-
|
|
475
|
-
allFiles.push({
|
|
476
|
-
id: RED.util.generateId(),
|
|
477
|
-
type: "PkgFile",
|
|
478
|
-
name: header.name.split("/").at(-1),
|
|
479
|
-
filename: header.name.replace(/^package\//, ''),
|
|
480
|
-
template: Buffer.concat(buffer).toString(frmt == "base64" ? 'base64' : 'utf8'),
|
|
481
|
-
syntax: "mustache",
|
|
482
|
-
format: frmt,
|
|
483
|
-
output: "str",
|
|
484
|
-
x: 100,
|
|
485
|
-
y: 50 * (allFiles.length + 1),
|
|
486
|
-
wires: [
|
|
487
|
-
[]
|
|
488
|
-
]
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
next() // ready for next entry
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
stream.resume() // just auto drain the stream
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
extract.on('finish', function () {
|
|
498
|
-
// all entries read, wire them together
|
|
499
|
-
for (var idx = 0; idx < allFiles.length - 1; idx++) {
|
|
500
|
-
allFiles[idx].wires = [[allFiles[idx + 1].id]];
|
|
501
|
-
}
|
|
462
|
+
const onError = (err) => {
|
|
463
|
+
msg.error = err;
|
|
464
|
+
done("extraction error", msg)
|
|
465
|
+
}
|
|
502
466
|
|
|
467
|
+
const onFinish = (allFiles) => {
|
|
503
468
|
msg.payload = JSON.stringify(allFiles);
|
|
504
469
|
send(msg)
|
|
505
470
|
|
|
@@ -515,15 +480,9 @@ module.exports = function (RED) {
|
|
|
515
480
|
);
|
|
516
481
|
|
|
517
482
|
done()
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
msg.error = err;
|
|
522
|
-
done("extraction error", msg)
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
var stream = streamx.Readable.from(Buffer.from(pakoGzip.inflate(new Uint8Array(msg.payload))))
|
|
526
|
-
stream.pipe(extract);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
require('./lib/tarhelpers.js').convertTarFile(RED, msg.payload, onFinish, onError)
|
|
527
486
|
} catch (err) {
|
|
528
487
|
msg.error = err
|
|
529
488
|
done(err.message, msg)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module.exports = (function () {
|
|
2
|
+
|
|
3
|
+
var tarStream = require('tar-stream');
|
|
4
|
+
var streamx = require('streamx');
|
|
5
|
+
var pakoGzip = require('pako');
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* there is no indication in a tar file of whether a file is binary or textual.
|
|
9
|
+
* we can only make a guess by the extension of the filename.
|
|
10
|
+
***/
|
|
11
|
+
var computeFormat = (filename) => {
|
|
12
|
+
var ext = filename.split(".").at(-1);
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
"html": "html",
|
|
16
|
+
"js": "javascript",
|
|
17
|
+
"md": "markdown",
|
|
18
|
+
"json": "json",
|
|
19
|
+
"txt": "text",
|
|
20
|
+
"css": "css",
|
|
21
|
+
"yaml": "yaml",
|
|
22
|
+
"yml": "yaml",
|
|
23
|
+
|
|
24
|
+
/* binary formats are encoded in base64 */
|
|
25
|
+
"png": "base64",
|
|
26
|
+
"tiff": "base64",
|
|
27
|
+
"tif": "base64",
|
|
28
|
+
"jpg": "base64",
|
|
29
|
+
"jpeg": "base64",
|
|
30
|
+
"bin": "base64",
|
|
31
|
+
"bmp": "base64",
|
|
32
|
+
"gif": "base64",
|
|
33
|
+
"woff2": "base64",
|
|
34
|
+
"woff": "base64",
|
|
35
|
+
"ttf": "base64",
|
|
36
|
+
"mov": "base64",
|
|
37
|
+
"ico": "base64",
|
|
38
|
+
"eot": "base64",
|
|
39
|
+
}[ext.toLowerCase()] || "text";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var convertTarFile = (RED, tgzData, onFinish, onError) => {
|
|
43
|
+
const extract = tarStream.extract()
|
|
44
|
+
|
|
45
|
+
var allFiles = [];
|
|
46
|
+
|
|
47
|
+
extract.on('entry', function (header, stream, next) {
|
|
48
|
+
// header is the tar header
|
|
49
|
+
// stream is the content body (might be an empty stream)
|
|
50
|
+
// call next when you are done with this entry
|
|
51
|
+
|
|
52
|
+
var buffer = [];
|
|
53
|
+
|
|
54
|
+
stream.on('data', function (data) {
|
|
55
|
+
buffer.push(data)
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
stream.on('end', function () {
|
|
59
|
+
var frmt = computeFormat(header.name.split("/").at(-1));
|
|
60
|
+
|
|
61
|
+
allFiles.push({
|
|
62
|
+
id: RED.util.generateId(),
|
|
63
|
+
type: "PkgFile",
|
|
64
|
+
name: header.name.split("/").at(-1),
|
|
65
|
+
filename: header.name.replace(/^package\//, ''),
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
template: Buffer.concat(buffer).toString(frmt == "base64" ? 'base64' : 'utf8'),
|
|
68
|
+
syntax: "plain", // not mustache templates, these are files.
|
|
69
|
+
format: frmt,
|
|
70
|
+
output: "str",
|
|
71
|
+
x: 250 * Math.floor(allFiles.length / 40),
|
|
72
|
+
y: 50 * (allFiles.length % 40),
|
|
73
|
+
wires: [
|
|
74
|
+
[]
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
next() // ready for next entry
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
stream.resume() // just auto drain the stream
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
extract.on('finish', function () {
|
|
85
|
+
// all entries read, wire them together
|
|
86
|
+
for (var idx = 0; idx < allFiles.length - 1; idx++) {
|
|
87
|
+
allFiles[idx].wires = [[allFiles[idx + 1].id]];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onFinish(allFiles)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
extract.on('error', onError );
|
|
94
|
+
|
|
95
|
+
var stream = streamx.Readable.from(Buffer.from(pakoGzip.inflate(new Uint8Array(tgzData))))
|
|
96
|
+
stream.pipe(extract);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
let exports = {
|
|
101
|
+
computeFormat: computeFormat,
|
|
102
|
+
convertTarFile: convertTarFile
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return exports;
|
|
106
|
+
})();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function ($) {
|
|
3
|
+
RED.nodes.registerType('NodeFactorySidebarCfg', {
|
|
4
|
+
category: 'config',
|
|
5
|
+
hasUsers: false,
|
|
6
|
+
defaults: {
|
|
7
|
+
pkgname: { value: "node-red-dashboard" },
|
|
8
|
+
pkgversion: { value: "latest" }
|
|
9
|
+
},
|
|
10
|
+
paletteLabel: 'NodeFactorySidebarCfg',
|
|
11
|
+
label: function () {
|
|
12
|
+
return "NodeFactorySidebarCfg"
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
})(jQuery);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<!-- The html for the config node info panel (in right sidebar) -->
|
|
19
|
+
<script type="text/x-red" data-template-name="NodeFactorySidebarCfg">
|
|
20
|
+
<p>Generate nodes for an existing node package.</p>
|
|
21
|
+
Generate nodes for an existing node package.
|
|
22
|
+
</script>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
let pacote = require('pacote');
|
|
3
|
+
let tarHelpers = require('./lib/tarhelpers.js');
|
|
4
|
+
|
|
5
|
+
function ConfigNodeFactorySidebarFunctionality(config) {
|
|
6
|
+
RED.nodes.createNode(this, config)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
RED.nodes.registerType('NodeFactorySidebarCfg', ConfigNodeFactorySidebarFunctionality);
|
|
10
|
+
|
|
11
|
+
function createDevOpsNode(pkgname, pkgversion, x, y, nodeid) {
|
|
12
|
+
return {
|
|
13
|
+
"id": RED.util.generateId(),
|
|
14
|
+
"type": "NodeDevOps",
|
|
15
|
+
"name": "",
|
|
16
|
+
"pname": pkgname,
|
|
17
|
+
"pversion": pkgversion,
|
|
18
|
+
"pauthorname": "",
|
|
19
|
+
"pauthoremail": "",
|
|
20
|
+
"pdescription": "",
|
|
21
|
+
"noderedinstall": true,
|
|
22
|
+
"randompackagename": false,
|
|
23
|
+
"ignore_package_check": false,
|
|
24
|
+
"gitcommit": false,
|
|
25
|
+
"gitcheckforchange": false,
|
|
26
|
+
"githubowner": "",
|
|
27
|
+
"githubrepo": "",
|
|
28
|
+
"githubbranch": "main",
|
|
29
|
+
"githubauthor": "",
|
|
30
|
+
"githubauthoremail": "",
|
|
31
|
+
"githubmessage": "",
|
|
32
|
+
"npmpublish": false,
|
|
33
|
+
"npmunpublish": false,
|
|
34
|
+
"npmotp": "",
|
|
35
|
+
"x": x - 120,
|
|
36
|
+
"y": y - 50,
|
|
37
|
+
"wires": [
|
|
38
|
+
[nodeid]
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createTarballNode(x,y,nodeid) {
|
|
44
|
+
return {
|
|
45
|
+
"id": RED.util.generateId(),
|
|
46
|
+
"type": "NpmTarBall",
|
|
47
|
+
"name": "",
|
|
48
|
+
"x": x+150,
|
|
49
|
+
"y": y+50,
|
|
50
|
+
"wires": [
|
|
51
|
+
[nodeid]
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createNodeRedInstallNode(x,y) {
|
|
57
|
+
return {
|
|
58
|
+
"id": RED.util.generateId(),
|
|
59
|
+
"type": "NodeRedInstall",
|
|
60
|
+
"name": "",
|
|
61
|
+
"x": x+300,
|
|
62
|
+
"y": y+100,
|
|
63
|
+
"wires": [
|
|
64
|
+
[]
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
RED.httpAdmin.post("/NodeFactorySidebarCfg",
|
|
70
|
+
RED.auth.needsPermission("nodedev.write"),
|
|
71
|
+
(req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
if (req.body) {
|
|
74
|
+
var msg = req.body;
|
|
75
|
+
if ( msg.pkgname && msg.pkgversion ) {
|
|
76
|
+
const onFinish = (allFiles) => {
|
|
77
|
+
|
|
78
|
+
/* these are no templates, these are files - no mustache intrepretation */
|
|
79
|
+
allFiles.forEach(function(element) {
|
|
80
|
+
element.syntax = "plain"
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
var lastNode = allFiles[allFiles.length - 1];
|
|
84
|
+
|
|
85
|
+
var nodeDevOp = createDevOpsNode(msg.pkgname, msg.pkgversion, allFiles[0].x, allFiles[0].y, allFiles[0].id)
|
|
86
|
+
var nrInstallNode = createNodeRedInstallNode(lastNode.x, lastNode.y)
|
|
87
|
+
var tarballNode = createTarballNode(lastNode.x, lastNode.y, nrInstallNode.id)
|
|
88
|
+
|
|
89
|
+
let pkgjson = allFiles.filter( a => {
|
|
90
|
+
return a.filename == "package.json"
|
|
91
|
+
})[0];
|
|
92
|
+
|
|
93
|
+
if ( pkgjson) {
|
|
94
|
+
let ctnt = JSON.parse(pkgjson.template);
|
|
95
|
+
nodeDevOp.pversion = ctnt.version
|
|
96
|
+
nodeDevOp.pdescription = ctnt.description || "Empty"
|
|
97
|
+
nodeDevOp.pauthorname = ctnt.author || "Empty"
|
|
98
|
+
nodeDevOp.pauthoremail = ctnt.author || "Empty"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lastNode.wires[0].push(tarballNode.id)
|
|
102
|
+
|
|
103
|
+
allFiles.push(nodeDevOp);
|
|
104
|
+
allFiles.push(tarballNode)
|
|
105
|
+
allFiles.push(nrInstallNode)
|
|
106
|
+
|
|
107
|
+
RED.comms.publish(
|
|
108
|
+
"nodedev:perform-autoimport-nodes",
|
|
109
|
+
RED.util.encodeObject({
|
|
110
|
+
msg: "autoimport",
|
|
111
|
+
payload: JSON.stringify(allFiles),
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const onError = (err) => {
|
|
117
|
+
RED.comms.publish(
|
|
118
|
+
"nodedev:perform-autoimport-nodes",
|
|
119
|
+
RED.util.encodeObject({
|
|
120
|
+
msg: "notify",
|
|
121
|
+
text: "Failed to create nodes for " + msg.pkgname + "@" + msg.pkgversion + ": " + err,
|
|
122
|
+
type: "error",
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pacote.manifest(
|
|
128
|
+
msg.pkgname + "@" + msg.pkgversion
|
|
129
|
+
).then(manifest => {
|
|
130
|
+
RED.comms.publish(
|
|
131
|
+
"nodedev:perform-autoimport-nodes",
|
|
132
|
+
RED.util.encodeObject({
|
|
133
|
+
msg: "notify",
|
|
134
|
+
text: "Found url for " + msg.pkgname + "@" + msg.pkgversion,
|
|
135
|
+
type: "warning",
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
import('got').then((module) => {
|
|
140
|
+
module.got.get(manifest._resolved,
|
|
141
|
+
{
|
|
142
|
+
timeout: {
|
|
143
|
+
request: 25000,
|
|
144
|
+
response: 25000
|
|
145
|
+
},
|
|
146
|
+
responseType: 'buffer'
|
|
147
|
+
}).then(resp => {
|
|
148
|
+
try {
|
|
149
|
+
tarHelpers.convertTarFile(RED, resp.body, onFinish, onError);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
onError(err)
|
|
152
|
+
}
|
|
153
|
+
}).catch(err => {
|
|
154
|
+
onError(err)
|
|
155
|
+
});
|
|
156
|
+
}).catch(err => {
|
|
157
|
+
onError(err)
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
}).catch(err => {
|
|
161
|
+
RED.comms.publish(
|
|
162
|
+
"nodedev:perform-autoimport-nodes",
|
|
163
|
+
RED.util.encodeObject({
|
|
164
|
+
msg: "notify",
|
|
165
|
+
text: "Failed to retrieve tgz file for " + msg.pkgname + "@" + msg.pkgversion + ": " + err,
|
|
166
|
+
type: "error",
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
res.sendStatus(200);
|
|
172
|
+
|
|
173
|
+
RED.comms.publish(
|
|
174
|
+
"nodedev:perform-autoimport-nodes",
|
|
175
|
+
RED.util.encodeObject({
|
|
176
|
+
msg: "notify",
|
|
177
|
+
text: "Retrieving " + msg.pkgname + "@" + msg.pkgversion,
|
|
178
|
+
type: "warning",
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
res.sendStatus(404);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
res.sendStatus(405);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error("ERROR", err)
|
|
189
|
+
res.sendStatus(500);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"{{ node.name }}": {
|
|
3
|
+
{{#node.bak2frtcomm}}
|
|
4
|
+
"notify": {
|
|
5
|
+
"commfrombackend": "Backend sent something to the frontend - check browser console.",
|
|
6
|
+
"msgfrombackend": "message from backend"
|
|
7
|
+
{{ #node.frt2bakcomm }}
|
|
8
|
+
},
|
|
9
|
+
{{ /node.frt2bakcomm }}
|
|
10
|
+
{{ ^node.frt2bakcomm }}
|
|
11
|
+
}
|
|
12
|
+
{{ /node.frt2bakcomm }}
|
|
13
|
+
{{/node.bak2frtcomm}}
|
|
14
|
+
|
|
15
|
+
{{ #node.frt2bakcomm }}
|
|
16
|
+
"notifyf2b": {
|
|
17
|
+
"submissionfailed": "submission failed",
|
|
18
|
+
"buttonpressed": "button pressed on frontend",
|
|
19
|
+
"success": "Successfully sent something to the backend"
|
|
20
|
+
}
|
|
21
|
+
{{ /node.frt2bakcomm }}
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}),
|
|
16
16
|
|
|
17
17
|
success: function (resp) {
|
|
18
|
-
RED.notify("
|
|
18
|
+
RED.notify( RED._("{{ node.name}}.notifyf2b.success"), {
|
|
19
19
|
type: "warning",
|
|
20
20
|
id: "{{node.name}}",
|
|
21
21
|
timeout: 2000
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
this.messageFromBackendHandler = (topic,dataobj) => {
|
|
85
85
|
console.log( "here goes the code for handling a message from the backend", topic, dataobj);
|
|
86
86
|
|
|
87
|
-
RED.notify("
|
|
87
|
+
RED.notify(RED._("{{ node.name}}.notify.commfrombackend"), {
|
|
88
88
|
type: "success",
|
|
89
89
|
id: "{{node.name}}",
|
|
90
90
|
timeout: 2000
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
|
|
126
126
|
{{ #node.frt2bakcomm }}
|
|
127
127
|
var that = this;
|
|
128
|
-
sendToBackend(that, {"payload": "
|
|
128
|
+
sendToBackend(that, {"payload": RED._("{{ node.name}}.notifyf2b.buttonpressed")})
|
|
129
129
|
{{ /node.frt2bakcomm }}
|
|
130
130
|
|
|
131
131
|
/* here goes the button code to be executed on click */
|
|
@@ -138,13 +138,8 @@
|
|
|
138
138
|
</script>
|
|
139
139
|
|
|
140
140
|
<script type="text/html" data-template-name="{{ node.name }}">
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
</script>
|
|
146
|
-
|
|
147
|
-
<script type="text/html" data-help-name="{{ node.name }}">
|
|
148
|
-
<p>{{{ node.summary }}}</p>
|
|
149
|
-
{{{ node.description }}}
|
|
141
|
+
<div class="form-row">
|
|
142
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
|
|
143
|
+
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
|
144
|
+
</div>
|
|
150
145
|
</script>
|
package/nodes/templates/tmpl.js
CHANGED
|
@@ -14,8 +14,8 @@ module.exports = function(RED) {
|
|
|
14
14
|
{{#node.bak2frtcomm}}
|
|
15
15
|
RED.comms.publish("{{node.name}}:message-from-backend",
|
|
16
16
|
RED.util.encodeObject({
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
...msg,
|
|
18
|
+
"data": RED._("{{ node.name}}.notify.msgfrombackend"),
|
|
19
19
|
})
|
|
20
20
|
);
|
|
21
21
|
{{/node.bak2frtcomm}}
|
|
@@ -49,7 +49,7 @@ module.exports = function(RED) {
|
|
|
49
49
|
} catch (err) {
|
|
50
50
|
console.error(err);
|
|
51
51
|
res.status(500).send(err.toString());
|
|
52
|
-
node.error("{{ node.name }}:
|
|
52
|
+
node.error("{{ node.name }}: " + RED._("{{ node.name }}.notifyf2b.submissionfailed") + ": " + err.toString())
|
|
53
53
|
}
|
|
54
54
|
} else {
|
|
55
55
|
res.sendStatus(404);
|
package/package.json
CHANGED
|
@@ -1,34 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name" : "@gregoriusrippenstein/node-red-contrib-nodedev",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"pako": "latest",
|
|
6
6
|
"tar-stream": "latest",
|
|
7
7
|
"mustache": "latest",
|
|
8
8
|
"libnpmpublish": "latest",
|
|
9
|
-
"streamx": "latest"
|
|
9
|
+
"streamx": "latest",
|
|
10
|
+
"got": "latest",
|
|
11
|
+
"pacote": "latest"
|
|
10
12
|
},
|
|
11
13
|
|
|
12
14
|
"keywords": [
|
|
13
|
-
"node-red",
|
|
15
|
+
"node-red",
|
|
16
|
+
"node development",
|
|
17
|
+
"introspection",
|
|
18
|
+
"bootstrapping",
|
|
19
|
+
"recursion"
|
|
14
20
|
],
|
|
15
21
|
|
|
16
22
|
"homepage": "https://github.com/gorenje/node-red-contrib-nodedev#readme",
|
|
17
23
|
"license": "Don't do evil.",
|
|
18
|
-
"author": "Gerrit Riessen <
|
|
24
|
+
"author": "Gerrit Riessen <gerrit@openmindmap.org> (https://spread-the.love)",
|
|
19
25
|
"engines": {
|
|
20
26
|
"node": ">=8"
|
|
21
27
|
},
|
|
22
28
|
|
|
23
29
|
"node-red" : {
|
|
24
30
|
"version": ">=3.0.0",
|
|
31
|
+
"plugins": {
|
|
32
|
+
"nodefactory_sidebar": "plugins/node-factory-sidebar.html"
|
|
33
|
+
},
|
|
25
34
|
"nodes": {
|
|
26
35
|
"nodedevops": "nodes/05-node-dev-ops.js",
|
|
27
36
|
"nodefactory": "nodes/10-node-factory.js",
|
|
28
37
|
"pkgfile": "nodes/20-pkg-file.js",
|
|
29
38
|
"npmtarball": "nodes/30-npm-tarball.js",
|
|
30
39
|
"noderedinstall": "nodes/40-node-red-install.js",
|
|
31
|
-
"npmpublish": "nodes/50-npm-publish.js"
|
|
40
|
+
"npmpublish": "nodes/50-npm-publish.js",
|
|
41
|
+
"nodefactorycfg": "nodes/node-factory-sidebar-cfg.js"
|
|
32
42
|
}
|
|
33
43
|
},
|
|
34
44
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
var globalYourConfigNode = null;
|
|
4
|
+
|
|
5
|
+
function doSubmission(pkgname,pkgversion) {
|
|
6
|
+
ensureYourConfigNodeExists();
|
|
7
|
+
|
|
8
|
+
$.ajax({
|
|
9
|
+
url: "NodeFactorySidebarCfg",
|
|
10
|
+
type: "POST",
|
|
11
|
+
contentType: "application/json; charset=utf-8",
|
|
12
|
+
|
|
13
|
+
data: JSON.stringify({
|
|
14
|
+
pkgname: $("#node-input-nodefactorysidebar-package-name").val(),
|
|
15
|
+
pkgversion: $("#node-input-nodefactorysidebar-package-version").val()
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
success: function (resp) {
|
|
19
|
+
RED.notify("Submission triggered", {
|
|
20
|
+
type: "warning",
|
|
21
|
+
id: "NodeFactorySidebar",
|
|
22
|
+
timeout: 2000
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
error: function (jqXHR, textStatus, errorThrown) {
|
|
27
|
+
RED.notify("Submission failed: " + textStatus, {
|
|
28
|
+
type: "error",
|
|
29
|
+
id: "NodeFactorySidebar",
|
|
30
|
+
timeout: 2000
|
|
31
|
+
});
|
|
32
|
+
console.log(textStatus,errorThrown )
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureYourConfigNodeExists() {
|
|
38
|
+
// This function makes sure there is 1 instance of your config node is available, and that the globalYourConfigNode variable refers to it.
|
|
39
|
+
// Explained in the next step of this tutorial... --> https://discourse.nodered.org/t/tutorial-create-a-sidebar-plugin-and-persist-the-data-in-a-config-node/82020
|
|
40
|
+
|
|
41
|
+
// If we had found it previously, check if it has been deleted by the user behind our back
|
|
42
|
+
if (globalYourConfigNode !== null) {
|
|
43
|
+
var configNode = RED.nodes.node(globalYourConfigNode.id);
|
|
44
|
+
if (configNode === null) { globalYourConfigNode = null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If not found previously, let's go find it
|
|
48
|
+
if (globalYourConfigNode === null) {
|
|
49
|
+
var configNodes = [];
|
|
50
|
+
RED.nodes.eachConfig(function(configNode) {
|
|
51
|
+
if (configNode.type === 'NodeFactorySidebarCfg') {
|
|
52
|
+
configNodes.push(configNode);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Make sure we only have 1 config node
|
|
57
|
+
while (configNodes.length > 1) {
|
|
58
|
+
var configNode = configNodes.pop();
|
|
59
|
+
RED.nodes.remove(configNode.id);
|
|
60
|
+
RED.nodes.dirty(true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// When we found a config node, let's use that one
|
|
64
|
+
if (configNodes.length === 1) { globalYourConfigNode = configNodes[0]; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// When it doesn't exist yet, create it if required
|
|
68
|
+
if (globalYourConfigNode === null) {
|
|
69
|
+
// Remark: since this config node is dynamically created (and only used in this sidebar which isn't another node), the config
|
|
70
|
+
// node is in fact "unused". But since we don't want it to appear "unused" in the "config nodes" panel, we need to set hasUsers
|
|
71
|
+
// to false (see https://github.com/node-red/node-red/blob/master/CHANGELOG.md#0161-maintenance-release).
|
|
72
|
+
// The hasUsers needs also to be specified in the RED.nodes.registerType statement!
|
|
73
|
+
let typ = RED.nodes.getType("NodeFactorySidebarCfg");
|
|
74
|
+
|
|
75
|
+
globalYourConfigNode = {
|
|
76
|
+
id: RED.nodes.id(), // on the server side, this is called RED.util.generateId()
|
|
77
|
+
_def: typ,
|
|
78
|
+
type: "NodeFactorySidebarCfg",
|
|
79
|
+
hasUsers: false,
|
|
80
|
+
users: [],
|
|
81
|
+
name: "NodeFactorySidebar",
|
|
82
|
+
label: function() { return this.name || "NodeFactorySidebar"},
|
|
83
|
+
/* values and data defined by this config node */
|
|
84
|
+
pkgname: typ.defaults.pkgname.value,
|
|
85
|
+
pkgversion: typ.defaults.pkgversion.value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Add the new config node to the collection of Node-RED nodes
|
|
89
|
+
RED.nodes.add(globalYourConfigNode);
|
|
90
|
+
|
|
91
|
+
// Make sure the "Deploy" button becomes active
|
|
92
|
+
RED.nodes.dirty(true);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add your plugin as a new tabsheet in the right sidebar AFTER the flow editor is completely started
|
|
97
|
+
var initialiseConfigNodeOnce = () => {
|
|
98
|
+
RED.events.off('runtime-state', initialiseConfigNodeOnce);
|
|
99
|
+
|
|
100
|
+
// The html content of the sidebar has been specified below as a data-template, from where it can be loaded:
|
|
101
|
+
var content = $($('script[type="text/x-red"][data-template-name="NodeFactorySidebar"]').i18n().html());
|
|
102
|
+
|
|
103
|
+
// Add a "Your sidebar" tabsheet to the right sidebar panel, in which this sidebar panel can be displayed
|
|
104
|
+
// --> more details: https://nodered.org/docs/api/ui/sidebar/
|
|
105
|
+
RED.sidebar.addTab({
|
|
106
|
+
id: "NodeFactorySidebar",
|
|
107
|
+
label: "Node Factory", // short name for the tab
|
|
108
|
+
name: "Node Factory", // long name for the menu
|
|
109
|
+
content: content,
|
|
110
|
+
closeable: true,
|
|
111
|
+
// disableOnEdit: true,
|
|
112
|
+
enableOnEdit: true,
|
|
113
|
+
iconClass: "fa fa-industry" // your fontawesome icon
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
ensureYourConfigNodeExists();
|
|
117
|
+
|
|
118
|
+
var doSomething = (e) => {
|
|
119
|
+
if (e) { e.preventDefault(); }
|
|
120
|
+
doSubmission()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
$('#node-input-nodefactorysidebar-generate-btn').on('click', doSomething );
|
|
124
|
+
|
|
125
|
+
$("#node-input-nodefactorysidebar-package-version").val(globalYourConfigNode.pkgversion);
|
|
126
|
+
|
|
127
|
+
$("#node-input-nodefactorysidebar-package-version").on("change", function() {
|
|
128
|
+
ensureYourConfigNodeExists();
|
|
129
|
+
|
|
130
|
+
let data = $(this).val();
|
|
131
|
+
|
|
132
|
+
if (globalYourConfigNode.pkgversion != data) {
|
|
133
|
+
globalYourConfigNode.pkgversion = data;
|
|
134
|
+
RED.nodes.dirty(true);
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
$("#node-input-nodefactorysidebar-package-name").val(globalYourConfigNode.pkgname);
|
|
138
|
+
|
|
139
|
+
$("#node-input-nodefactorysidebar-package-name").on("change", function() {
|
|
140
|
+
ensureYourConfigNodeExists();
|
|
141
|
+
|
|
142
|
+
let data = $(this).val();
|
|
143
|
+
|
|
144
|
+
if (globalYourConfigNode.pkgname != data) {
|
|
145
|
+
globalYourConfigNode.pkgname = data;
|
|
146
|
+
RED.nodes.dirty(true);
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
RED.events.on('runtime-state', initialiseConfigNodeOnce);
|
|
153
|
+
})();
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<!-- The html for the right sidebar plugin screen -->
|
|
157
|
+
<script type="text/x-red" data-template-name="NodeFactorySidebar">
|
|
158
|
+
<div class="form-row" style="margin-left: 10px; margin-top: 30px;">
|
|
159
|
+
Enter package details here:
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="form-row" style="margin-left: 10px;">
|
|
163
|
+
<label for="node-input-nodefactorysidebar-package-name">
|
|
164
|
+
<i class="fa fa-tag"></i>
|
|
165
|
+
Name
|
|
166
|
+
</label>
|
|
167
|
+
<input type="text" id="node-input-nodefactorysidebar-package-name"
|
|
168
|
+
placeholder="node-red-dashboard">
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div class="form-row" style="margin-left: 10px;">
|
|
172
|
+
<label for="node-input-nodefactorysidebar-package-version">
|
|
173
|
+
<i class="fa fa-tag"></i>
|
|
174
|
+
Version
|
|
175
|
+
</label>
|
|
176
|
+
<input type="text" id="node-input-nodefactorysidebar-package-version"
|
|
177
|
+
placeholder="3.5.0">
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="form-row">
|
|
181
|
+
<div style="position: relative; height: 100%; margin: 15px;">
|
|
182
|
+
<button id="node-input-nodefactorysidebar-generate-btn"
|
|
183
|
+
class="red-ui-button"
|
|
184
|
+
style="width: 100%; margin-top: 30px">Generate Nodes</button>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
</script>
|