@gregoriusrippenstein/node-red-contrib-nodedev 0.1.6 → 0.1.7
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 +3 -0
- package/nodes/10-node-factory.html +24 -11
- package/nodes/10-node-factory.js +8 -81
- 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/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.
|
|
@@ -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": "{{",
|
|
@@ -430,76 +427,12 @@ module.exports = function (RED) {
|
|
|
430
427
|
} else {
|
|
431
428
|
/* assume that payload is a buffer with a .tgz if not, error out */
|
|
432
429
|
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
|
-
}
|
|
430
|
+
const onError = (err) => {
|
|
431
|
+
msg.error = err;
|
|
432
|
+
done("extraction error", msg)
|
|
433
|
+
}
|
|
502
434
|
|
|
435
|
+
const onFinish = (allFiles) => {
|
|
503
436
|
msg.payload = JSON.stringify(allFiles);
|
|
504
437
|
send(msg)
|
|
505
438
|
|
|
@@ -515,15 +448,9 @@ module.exports = function (RED) {
|
|
|
515
448
|
);
|
|
516
449
|
|
|
517
450
|
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);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
require('./lib/tarhelpers.js').convertTarFile(RED, msg.payload, onFinish, onError)
|
|
527
454
|
} catch (err) {
|
|
528
455
|
msg.error = err
|
|
529
456
|
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
|
+
}
|
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.7",
|
|
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>
|