Compare commits

...

158 Commits

Author SHA1 Message Date
Nick O'Leary
129ca0e39f bump version 2016-03-21 11:02:09 +00:00
Dave Conway-Jones
906703db5f Add timed release mode to delay node 2016-03-20 17:46:12 +00:00
Nick O'Leary
0cd4a2b4ec Add api/flow_spec tests
Part of #840
2016-03-18 21:01:21 +00:00
Nick O'Leary
aef8aaa0bd Enable link splicing for when import_dragging nodes
Closes #811
2016-03-17 11:12:45 +00:00
Nick O'Leary
428fbb8622 Fix uncaught exception on deploy whilst node sending messages 2016-03-16 15:37:44 +00:00
Nick O'Leary
b9f03e7d80 Deprecate old mqtt client and connection pool modules 2016-03-16 11:15:30 +00:00
Nick O'Leary
db686388b9 Fix registry test for Node 5 2016-03-16 11:05:10 +00:00
Nick O'Leary
626cba4002 Change node: add bool/num types to change mode
Closes #835 #835 #835
2016-03-13 23:10:10 +00:00
Nick O'Leary
37d4a6b9e2 Validate fields that are $(env-vars)
Closes #825
2016-03-13 14:29:36 +00:00
Nick O'Leary
12c4561aba Handle missing config nodes when validating node properties 2016-03-13 13:25:59 +00:00
Dave Conway-Jones
fed49e3718 pi node - don't try to send data if closing 2016-03-13 10:58:22 +00:00
Nick O'Leary
3af37d3984 Load node message catalog when added dynamically 2016-03-12 22:53:07 +00:00
Nick O'Leary
0f49a11228 Split palette labels on spaces and hypens when laying out 2016-03-12 22:41:23 +00:00
Nick O'Leary
27d3e165b0 Message catalog updates for zero-length flow file handling 2016-03-12 00:04:27 +00:00
Nick O'Leary
e941c22f6c Warn if editor routes are accessed but runtime not started
Closes #816
2016-03-12 00:03:50 +00:00
Nick O'Leary
7281e4deb6 Add zero-length flow file tests 2016-03-11 22:58:11 +00:00
Nick O'Leary
f2191e94b3 Better handling of zero-length flow files
Closes #819

If a flow file is found to be zero-bytes:
  If there is a non-empty backup, restore the backup and resolve
  If there is no backup or it is also empty, resolve empty flow
If a flow file is found to be invalid json:
  Log and resolve empty flow
2016-03-11 22:42:04 +00:00
Dave Conway-Jones
349ebfe4db remove extra brace added to library.js in error 2016-03-06 20:54:46 +00:00
Nick O'Leary
708365c4ac Allow runtime calls to RED._ to specify other namespace 2016-03-06 20:43:19 +00:00
Dave Conway-Jones
0e9ea0aff1 Replace - & _ from example node name labels with space
and align lines (ocd)
2016-03-05 17:07:39 +00:00
Dave Conway-Jones
63ba05a193 Better right alignment of numerics in delay and trigger nodes 2016-03-04 10:12:07 +00:00
Nick O'Leary
4b702815cf Strip node-red-contrib/node- prefix from Example menu labels 2016-03-03 11:02:37 +00:00
Nick O'Leary
55e66ebcac Allow node modules to include example flows 2016-03-02 23:34:24 +00:00
Nick O'Leary
dcd8b3699c Create node_modules in userDir
This ensures npm install puts modules under .node-red even if there's
already a node_modules dir in the parent directory.
2016-03-01 22:08:37 +00:00
Nick O'Leary
0e2d13172a Ensure errors in node def funcs don't break view rendering
Fixes #815

 - also fixes errors in the Catch/Status node label funcs #815
2016-03-01 21:58:57 +00:00
Nick O'Leary
2e2556fdad Merge pull request #805 from aryehof/InjectInfoUpdate
Updated Inject node info with instructions for flow and global options
2016-02-26 14:07:21 +00:00
Nick O'Leary
859a7538e1 Fix crash on repeated inject of invalid json payload 2016-02-26 10:35:15 +00:00
Dave Conway-Jones
0d1543ee8a Add tail node binary mode test 2016-02-25 08:52:43 +00:00
Dave Conway-Jones
d3a98dd355 Add binary mode to tail node 2016-02-24 23:06:27 +00:00
Dave Conway-Jones
ad10125303 revert Cheerio to somewhat smaller version 2016-02-23 09:47:49 +00:00
Dave Conway-Jones
b89e866d39 Add os/platform info to default debug 2016-02-22 17:47:16 +00:00
Dave Conway-Jones
afbaf1cfe0 make sample node info consistent <code> 2016-02-22 17:46:14 +00:00
Dave Conway-Jones
b3be8b30e7 remove direction flag from delay node 2016-02-22 17:45:28 +00:00
Aryeh Hoffman
8bfab8f73d Updated Inject node info with instructions for flow and global options 2016-02-21 18:17:05 +02:00
Nick O'Leary
c6ad2c9ad2 Don't force reconnect mqtt client if message arrives
Fixes the annoying mqtt connect/disconnect cycle
2016-02-19 22:52:43 +00:00
Nick O'Leary
3b44d9972e Bump package version 2016-02-19 21:21:22 +00:00
Nick O'Leary
af736c98f2 Add -p/--port option to override listening port 2016-02-19 21:18:50 +00:00
Nick O'Leary
f1377fa217 Invert toggle button colours so state is more obvious 2016-02-18 10:28:22 +00:00
Dave Conway-Jones
2ba146b9ff Add timeout to httprequest node
and override 2 min default in settings.js.
to Close #801
2016-02-15 09:45:58 +00:00
Dave Conway-Jones
2361607aa3 file node info to same style as others 2016-02-14 13:45:14 +00:00
Nick O'Leary
86ffc80098 Tidy up spinner css 2016-02-14 11:52:33 +00:00
Dave Conway-Jones
7f6915eb59 tcp node add reply (to all) capability
if no _session present.
2016-02-12 13:17:50 +00:00
Dave Conway-Jones
d69bcad028 hardware, logic, storage nodes info updates
(and udp)
2016-02-12 13:17:21 +00:00
Dave Conway-Jones
4cb45e2712 parser node info updates 2016-02-12 13:16:28 +00:00
Dave Conway-Jones
b7a0ad703a io and analysis nodes info updates 2016-02-12 13:15:53 +00:00
Dave Conway-Jones
7610b9a975 core nodes info updates 2016-02-12 13:15:05 +00:00
Dave Conway-Jones
7d95f621df update UDP node info to be more correct. 2016-02-11 22:21:12 +00:00
Nick O'Leary
bba210e112 Allow the template node to be treated as plain text 2016-02-11 13:16:15 +00:00
Nick O'Leary
3a97e20bde Validate MQTT In topics
Fixes #792
2016-02-10 22:38:59 +00:00
Nick O'Leary
4fe7ea00b0 httpNodeAuth should not block http options requests
Fixes #793#793#793
2016-02-10 21:57:46 +00:00
Nick O'Leary
3ec8ecd4de Disable perMessageDeflate on WS servers
Workaround for this issue: https://github.com/websockets/ws/pull/632
as it has been fixed in the 1.x release that drops support for
node 0.10...
2016-02-10 21:43:37 +00:00
Dave Conway-Jones
401e65e852 Merge pull request #799 from natcl/patch-1
Fix typo in delay node: replaced ramdom to random
Thanks @natcl
2016-02-09 23:09:10 +00:00
Nathanaël Lécaudé
e7c5b691a0 More ramdom --> random 2016-02-09 18:03:27 -05:00
Nathanaël Lécaudé
9f3ea8da67 Fix typo in delay node: replaced ramdom to random 2016-02-09 17:57:50 -05:00
Dave Conway-Jones
4d84d624b1 clear trigger status icon on re-deploy 2016-02-04 22:13:08 +00:00
Nick O'Leary
633a6a0ee6 Fix inject test to use a proper type 2016-02-04 21:52:27 +00:00
Nick O'Leary
c7bcd3f438 Don't default inject payload to blank string 2016-02-04 21:43:18 +00:00
Dave Conway-Jones
d3a29a6f16 fix trigger tests 2016-02-04 21:22:52 +00:00
Dave Conway-Jones
827711ca89 Fix util jshint as we need the behaviour. 2016-02-04 21:21:57 +00:00
Dave Conway-Jones
76e98f74fa let new typed-inputs return correctly
0 now returns correct type for boolean and number when required.
2016-02-04 21:06:20 +00:00
Dave Conway-Jones
fb09f4b22d trigger node, add configurable reset
and make it do strings when it says so, and numbers if you want.
2016-02-04 21:05:15 +00:00
Dave Conway-Jones
bb06585748 another tidy up on Pi GPIO node 2016-02-03 21:10:44 +00:00
Nick O'Leary
c76ba1dcc7 Allow function properties in settings
Fixes #790
2016-01-29 11:56:16 +00:00
Nick O'Leary
a115301b04 Fix order of config dialog calls to save/creds/validate 2016-01-29 11:56:16 +00:00
Dave Conway-Jones
72917117a9 Add debounce to Pi GPIO node 2016-01-25 09:56:35 +00:00
Nick O'Leary
6567739236 Bump version 2016-01-21 22:20:18 +00:00
Nick O'Leary
4aa6b47c0e Revert wrapping of http requestion object
Fixes #787
2016-01-21 22:15:25 +00:00
Nick O'Leary
03558b012c Bump version and dependencies 2016-01-18 11:09:52 +00:00
Nick O'Leary
3288efdad6 Remove unimplemented flow.enable/disable functions 2016-01-18 10:53:50 +00:00
Dave Conway-Jones
3902a343f3 Add ports in use warning to udp node
to close #786
Thanks @hugobox
2016-01-17 10:34:40 +00:00
Dave Conway-Jones
882b7d0391 change settings.js example to octalbonescript 2016-01-16 13:58:24 +00:00
Nick O'Leary
81f082825d Add 'previous value' option to Switch node 2016-01-15 11:35:59 +00:00
Nick O'Leary
392fd6fed3 Allow existing nodes to splice into links on drag 2016-01-14 15:59:45 +00:00
Nick O'Leary
51afed4041 Ensure config list refreshes properly on tab delete 2016-01-14 15:22:00 +00:00
Nick O'Leary
17e3b71d9c Allow update of global flow 2016-01-14 14:57:13 +00:00
Nick O'Leary
6e75089f3a CORS not properly configured on multiple http routes
Fixes #783
2016-01-13 12:54:34 +00:00
Nick O'Leary
6dc640b129 Add hidden count when config node filtered 2016-01-13 10:30:24 +00:00
Nick O'Leary
27cbaac343 Restore shift-drag to snap/unsnap to grid 2016-01-13 09:16:24 +00:00
Nick O'Leary
fa4006619e Make debug/config sidebar headers consistent 2016-01-12 23:55:18 +00:00
Nick O'Leary
cb8fe8462a Moving nodes with keyboard should flag workspace dirty 2016-01-12 23:08:13 +00:00
Nick O'Leary
abd51a5511 Notifications flagged as fixed should not be click-closable 2016-01-12 23:06:18 +00:00
Nick O'Leary
a0cc1e6b0c Add config node filter 2016-01-12 23:03:33 +00:00
Nick O'Leary
50399c6bfa Rework config sidebar and deploy warning 2016-01-12 17:54:53 +00:00
Nick O'Leary
de48c1be44 Wrap http request object to match http response object 2016-01-11 22:35:31 +00:00
Nick O'Leary
0786ec4b66 Move typedInput icons and update boolean 2016-01-11 21:24:35 +00:00
Nick O'Leary
db319e0ebc Ensure global context is seeded properly 2016-01-11 11:28:01 +00:00
Nick O'Leary
4fc568856a Clear link_splice style on drag end 2016-01-11 11:00:54 +00:00
Nick O'Leary
4c6771669b Fix palette node link splicing on Firefox 2016-01-10 22:25:20 +00:00
Nick O'Leary
9bca2a91c9 Tidy up view menu 2016-01-10 21:25:05 +00:00
Nick O'Leary
66eaaf5a48 Add 'view' menu and reorganise a few things 2016-01-09 20:39:03 +00:00
Nick O'Leary
9837f0e2e1 Highlight node port when dragging wires and undash the wires 2016-01-09 13:47:05 +00:00
Nick O'Leary
6b8ffb4c68 Fix lint issues in view 2016-01-09 00:31:05 +00:00
Nick O'Leary
f35dd34da9 Allow shift-click to detach existing wires 2016-01-09 00:29:04 +00:00
Nick O'Leary
ed19e4fa08 Splice nodes dragged from palette into links 2016-01-08 22:34:10 +00:00
Dave Conway-Jones
661e1a4f90 try to trim imported/dragged flows to [ ] 2016-01-08 19:54:16 +00:00
Nick O'Leary
5826de76ca Make dragging nodes from the palette line up better 2016-01-08 14:42:05 +00:00
Nick O'Leary
05888740e5 Update jquery version to 0.11.3 2016-01-08 14:41:37 +00:00
Nick O'Leary
41f3b0c333 Fix variable leak in theme.js 2016-01-08 13:41:33 +00:00
Nick O'Leary
70f3e72a20 Move version number as title of NR logo 2016-01-08 13:36:49 +00:00
Nick O'Leary
e873afd40b Moving nodes mark workspace as dirty 2016-01-08 11:08:48 +00:00
Nick O'Leary
2777c2937a Make version number slightly darker 2016-01-07 22:29:20 +00:00
Nick O'Leary
798903e4cc Move layout menu down one position 2016-01-07 22:28:03 +00:00
Nick O'Leary
58622ba18f Attach dialog close handlers to dialog parents 2016-01-07 20:08:31 +00:00
Nick O'Leary
c368dcd5b7 Ok/Cancel edit dialogs with Ctrl-Enter/Escape 2016-01-07 17:10:59 +00:00
Nick O'Leary
0b4c652ce7 Add version number of sidebar footer 2016-01-07 16:42:10 +00:00
Nick O'Leary
dbaacc411a Handle OSX Meta key when selecting nodes 2016-01-07 15:09:14 +00:00
Nick O'Leary
1850185d1e Add grid-alignment options 2016-01-07 14:39:01 +00:00
Nick O'Leary
2e9d445d36 Add oneditresize function definition 2016-01-06 17:01:14 +00:00
Nick O'Leary
aed89d82fb Fix template test 2016-01-06 17:01:14 +00:00
Nick O'Leary
231adac6d8 Rename typedInput.options 2016-01-06 17:01:14 +00:00
Nick O'Leary
587c4e5915 Update template node to use typedInput 2016-01-06 17:01:14 +00:00
Nick O'Leary
55f1cbf18f Ensure inject payload exists 2016-01-06 17:01:13 +00:00
Nick O'Leary
38168a545b Update Inject node to use typedInput 2016-01-06 17:01:13 +00:00
Nick O'Leary
43c6df49d7 Update typedInput nls 2016-01-06 17:01:13 +00:00
Nick O'Leary
f1c59faf72 Rename propertySelect to typedInput and add boolean opt 2016-01-06 17:01:13 +00:00
Nick O'Leary
5f7019325c Update switch/change help text to reflect updates 2016-01-06 17:01:13 +00:00
Nick O'Leary
fe4dae8518 Add propertySelect to switch node 2016-01-06 17:01:13 +00:00
Nick O'Leary
1f848b205b Add propertySelect support to Change node 2016-01-06 17:01:13 +00:00
Nick O'Leary
742c470d81 Add context/flow/global support to Function node 2016-01-06 17:01:13 +00:00
Nick O'Leary
5ead3342cc Add node context/flow/global 2016-01-06 17:01:13 +00:00
Nick O'Leary
b95dc2ecce Add propertySelect jquery widget 2016-01-06 17:01:13 +00:00
Nick O'Leary
4d0950215f Don't allow tabs or subflows to be added with new flow 2016-01-06 17:01:13 +00:00
Nick O'Leary
da0ce9fe0d Simplify flow api implementation and add logging messages 2016-01-06 17:01:13 +00:00
Nick O'Leary
ca62e720b5 Add missing spec file 2016-01-06 17:01:13 +00:00
Nick O'Leary
c4b1795396 Add add/update/delete flow apis 2016-01-06 17:01:13 +00:00
Nick O'Leary
fd2e47ed73 WIP: add flow api 2016-01-06 17:01:12 +00:00
Nick O'Leary
d5f2255a68 Handle null coreNodesPath 2016-01-06 17:01:12 +00:00
Nick O'Leary
05b58e9263 Allow core nodes dir to be provided to runtime via settings 2016-01-06 17:01:12 +00:00
Nick O'Leary
4a91c27e4b Allow server to be option on red.init 2016-01-06 17:01:12 +00:00
Nick O'Leary
3a03d46d8d Fix lint error in registry.js 2016-01-06 17:01:12 +00:00
Nick O'Leary
f03aff7006 Tidy up API passed to node modules 2016-01-06 17:01:12 +00:00
Nick O'Leary
043b8a3105 Register node message catalog directly, not via event 2016-01-06 17:01:12 +00:00
Nick O'Leary
1dd9984521 Pickup default language from i18n module 2016-01-06 17:01:12 +00:00
Nick O'Leary
d2be7f8c8f Move locale files under api/runtime components 2016-01-06 17:01:12 +00:00
Nick O'Leary
88dc202db2 Fix node test helper for api/runtime changes 2016-01-06 17:01:12 +00:00
Nick O'Leary
083d54b008 Add unit test for flow reload api 2016-01-06 17:01:11 +00:00
Nick O'Leary
87d77efa57 Add flow reload admin api 2016-01-06 17:01:11 +00:00
Nick O'Leary
35c4a41d7b Node id generation should only be done in runtime/util 2016-01-06 17:01:11 +00:00
Nick O'Leary
1ca3ca07d5 api/nodes accessing comms module incorrectly 2016-01-06 17:01:11 +00:00
Nick O'Leary
d673846e3d WIP: runtime api for node modules 2016-01-06 17:01:11 +00:00
Nick O'Leary
f62b7afede Remove all uses of fs.exists as it is deprecated
The tests still use it in places - particular localfilesystem tests,
but those tests need to be redone with sinon stubbing in place and
not rely on real fs operations.
2016-01-06 17:01:11 +00:00
Nick O'Leary
e65770a53a Add missing test resources
They were ignored as they have node_modules in the path...
2016-01-06 17:01:11 +00:00
Nick O'Leary
a92a741932 Fix incorrect async test completion 2016-01-06 17:01:11 +00:00
Nick O'Leary
45f67191ba Improve node registry test coverage 2016-01-06 17:01:11 +00:00
Nick O'Leary
93f5da325b Fix node test helper for runtime/api changes 2016-01-06 17:01:11 +00:00
Nick O'Leary
8fb955e182 Move comms from runtime to api component 2016-01-06 17:01:11 +00:00
Nick O'Leary
9f5e6a4b37 Update tests for runtime/api separation 2016-01-06 17:01:11 +00:00
Nick O'Leary
f43738446e WIP: separate runtime and api components 2016-01-06 17:01:11 +00:00
Nick O'Leary
923a46d304 Bump version 0.12.5 2016-01-06 16:55:38 +00:00
Dave Conway-Jones
b9b5eaccae better handle utf8 file output chars 2016-01-06 12:27:47 +00:00
Dave Conway-Jones
cda11491c2 bump sentiment node npm prereq 2016-01-06 12:27:47 +00:00
Nick O'Leary
98c539f662 Refresh active nodes when node properties change 2016-01-04 22:05:17 +00:00
Dave Conway-Jones
9fb958b302 close tcp node connection properly when required. 2015-12-23 20:01:05 +00:00
Dave Conway-Jones
8e25e76439 Add hint re servos to Pi GPIO node info 2015-12-23 11:50:47 +00:00
Nick O'Leary
62694da7e6 Ensure last mqtt node turns off the lights before closing
The mqtt-broker node disconnects when the last node using it
is closed. But that node-close was not waiting for the disconnect
to complete. This led to a race-condition where the using node
was recreated and started trying to use the broker node whilst it
was still disconnecting.
2015-12-22 23:31:22 +00:00
Dave Conway-Jones
86064651af Add Pi Keyboard code node 2015-12-21 10:27:58 +00:00
Dave Conway-Jones
65daaeb617 add attribute test to HTML parser node tests 2015-12-19 14:30:43 +00:00
Dave Conway-Jones
08b39f50b3 Add attribute capability to HTML parser node 2015-12-19 12:44:11 +00:00
209 changed files with 7277 additions and 3683 deletions

View File

@@ -124,14 +124,15 @@ module.exports = function(grunt) {
"editor/js/ui/library.js",
"editor/js/ui/notifications.js",
"editor/js/ui/subflow.js",
"editor/js/ui/touch/radialMenu.js"
"editor/js/ui/touch/radialMenu.js",
"editor/js/ui/typedInput.js"
],
dest: "public/red/red.js"
},
vendor: {
files: {
"public/vendor/vendor.js": [
"editor/vendor/jquery/js/jquery-1.11.1.min.js",
"editor/vendor/jquery/js/jquery-1.11.3.min.js",
"editor/vendor/bootstrap/js/bootstrap.min.js",
"editor/vendor/jquery/js/jquery-ui-1.10.3.custom.min.js",
"editor/vendor/jquery/js/jquery.ui.touch-punch.min.js",
@@ -174,8 +175,8 @@ module.exports = function(grunt) {
messages: {
src: [
'nodes/core/locales/en-US/messages.json',
'locales/en-US/editor.json',
'locales/en-US/runtime.json'
'red/api/locales/en-US/editor.json',
'red/runtime/locales/en-US/runtime.json'
]
}
},
@@ -223,8 +224,8 @@ module.exports = function(grunt) {
json: {
files: [
'nodes/core/locales/en-US/messages.json',
'locales/en-US/editor.json',
'locales/en-US/runtime.json'
'red/api/locales/en-US/editor.json',
'red/runtime/locales/en-US/runtime.json'
],
tasks: ['jsonlint:messages']
}
@@ -238,7 +239,7 @@ module.exports = function(grunt) {
args: nodemonArgs,
ext: 'js,html,json',
watch: [
'red','nodes','locales'
'red','nodes'
]
}
}
@@ -302,8 +303,7 @@ module.exports = function(grunt) {
'red/**',
'public/**',
'editor/templates/**',
'bin/**',
'locales/**'
'bin/**'
],
dest: path.resolve('<%= paths.dist %>/node-red-<%= pkg.version %>')
}]

View File

@@ -66,4 +66,4 @@ For more open-source projects from IBM, head over [here](http://ibm.github.io).
## Copyright and license
Copyright 2013, 2015 IBM Corp. under [the Apache 2.0 license](LICENSE).
Copyright 2013, 2016 IBM Corp. under [the Apache 2.0 license](LICENSE).

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,9 @@ RED.history = (function() {
undo_history[i].dirty = true;
}
},
list: function() {
return undo_history
},
depth: function() {
return undo_history.length;
},
@@ -80,6 +83,12 @@ RED.history = (function() {
}
}
}
if (ev.removedLinks) {
for (i=0;i<ev.removedLinks.length;i++) {
RED.nodes.addLink(ev.removedLinks[i]);
}
}
} else if (ev.t == "delete") {
if (ev.workspaces) {
for (i=0;i<ev.workspaces.length;i++) {
@@ -168,6 +177,17 @@ RED.history = (function() {
n.n.y = n.oy;
n.n.dirty = true;
}
// A move could have caused a link splice
if (ev.links) {
for (i=0;i<ev.links.length;i++) {
RED.nodes.removeLink(ev.links[i]);
}
}
if (ev.removedLinks) {
for (i=0;i<ev.removedLinks.length;i++) {
RED.nodes.addLink(ev.removedLinks[i]);
}
}
} else if (ev.t == "edit") {
for (i in ev.changes) {
if (ev.changes.hasOwnProperty(i)) {
@@ -265,7 +285,11 @@ RED.history = (function() {
RED.view.redraw(true);
RED.palette.refresh();
RED.workspaces.refresh();
RED.sidebar.config.refresh();
}
},
peek: function() {
return undo_history[undo_history.length-1];
}
}

View File

@@ -103,8 +103,10 @@ var RED = (function() {
var id = m.id;
RED.nodes.addNodeSet(m);
addedTypes = addedTypes.concat(m.types);
$.get('nodes/'+id, function(data) {
$("body").append(data);
RED.i18n.loadCatalog(id, function() {
$.get('nodes/'+id, function(data) {
$("body").append(data);
});
});
}
if (addedTypes.length) {
@@ -142,6 +144,8 @@ var RED = (function() {
RED.notify(RED._("palette.event.nodeDisabled", {count:msg.types.length})+typeList,"success");
}
}
// Refresh flow library to ensure any examples are updated
RED.library.loadFlowLibrary();
});
}
});
@@ -156,11 +160,13 @@ var RED = (function() {
function loadEditor() {
RED.menu.init({id:"btn-sidemenu",
options: [
{id:"menu-item-sidebar-menu",label:RED._("menu.label.sidebar.sidebar"),options:[
{id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:RED.sidebar.toggleSidebar, selected: true},
null
{id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[
{id:"menu-item-view-show-grid",label:RED._("menu.label.view.showGrid"),toggle:true,onselect:RED.view.toggleShowGrid},
{id:"menu-item-view-snap-grid",label:RED._("menu.label.view.snapGrid"),toggle:true,onselect:RED.view.toggleSnapGrid},
{id:"menu-item-status",label:RED._("menu.label.displayStatus"),toggle:true,onselect:toggleStatus, selected: true},
null,
{id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:RED.sidebar.toggleSidebar, selected: true}
]},
{id:"menu-item-status",label:RED._("menu.label.displayStatus"),toggle:true,onselect:toggleStatus, selected: true},
null,
{id:"menu-item-import",label:RED._("menu.label.import"),options:[
{id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:RED.clipboard.import},
@@ -171,23 +177,24 @@ var RED = (function() {
{id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:RED.library.export}
]},
null,
{id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [
{id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:RED.subflow.createSubflow},
{id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:RED.subflow.convertToSubflow},
]},
null,
{id:"menu-item-config-nodes",label:RED._("menu.label.displayConfig"),onselect:function(){}},
{id:"menu-item-workspace",label:RED._("menu.label.flows"),options:[
{id:"menu-item-workspace-add",label:RED._("menu.label.add"),onselect:RED.workspaces.add},
{id:"menu-item-workspace-edit",label:RED._("menu.label.rename"),onselect:RED.workspaces.edit},
{id:"menu-item-workspace-delete",label:RED._("menu.label.delete"),onselect:RED.workspaces.remove},
null
]},
{id:"menu-item-subflow",label:RED._("menu.label.subflows"), options: [
{id:"menu-item-subflow-create",label:RED._("menu.label.createSubflow"),onselect:RED.subflow.createSubflow},
{id:"menu-item-subflow-convert",label:RED._("menu.label.selectionToSubflow"),disabled:true,onselect:RED.subflow.convertToSubflow},
]},
null,
{id:"menu-item-keyboard-shortcuts",label:RED._("menu.label.keyboardShortcuts"),onselect:RED.keyboard.showHelp},
{id:"menu-item-help",
label: RED.settings.theme("menu.menu-item-help.label","Node-RED Website"),
href: RED.settings.theme("menu.menu-item-help.url","http://nodered.org/docs")
}
},
{id:"menu-item-node-red-version", label:"v"+RED.settings.version}
]
});

View File

@@ -16,9 +16,9 @@
RED.settings = (function () {
var loadedSettings = {};
var hasLocalStorage = function () {
try {
return 'localStorage' in window && window['localStorage'] !== null;
@@ -74,7 +74,7 @@ RED.settings = (function () {
RED.settings.set("auth-tokens",{access_token: accessToken});
window.location.search = "";
}
$.ajaxSetup({
beforeSend: function(jqXHR,settings) {
// Only attach auth header for requests to relative paths
@@ -89,7 +89,7 @@ RED.settings = (function () {
load(done);
}
var load = function(done) {
$.ajax({
headers: {
@@ -141,7 +141,7 @@ RED.settings = (function () {
set: set,
get: get,
remove: remove,
theme: theme
}
})

View File

@@ -22,7 +22,7 @@ RED.clipboard = (function() {
var exportNodesDialog;
var importNodesDialog;
function setupDialogs(){
function setupDialogs() {
dialog = $('<div id="clipboard-dialog" class="hide"><form class="dialog-form form-horizontal"></form></div>')
.appendTo("body")
.dialog({
@@ -61,7 +61,7 @@ RED.clipboard = (function() {
close: function(e) {
RED.keyboard.enable();
}
});
});
dialogContainer = dialog.children(".dialog-form");
@@ -83,9 +83,11 @@ RED.clipboard = (function() {
function validateImport() {
var importInput = $("#clipboard-import");
var v = importInput.val();
v = v.substring(v.indexOf('['),v.lastIndexOf(']')+1);
try {
JSON.parse(v);
importInput.removeClass("input-error");
importInput.val(v);
$("#clipboard-dialog-ok").button("enable");
} catch(err) {
if (v !== "") {
@@ -153,8 +155,6 @@ RED.clipboard = (function() {
RED.keyboard.add(/* e */ 69,{ctrl:true},function(){exportNodes();d3.event.preventDefault();});
RED.keyboard.add(/* i */ 73,{ctrl:true},function(){importNodes();d3.event.preventDefault();});
$('#chart').on("dragenter",function(event) {
if ($.inArray("text/plain",event.originalEvent.dataTransfer.types) != -1) {
$("#dropTarget").css({display:'table'});
@@ -173,18 +173,13 @@ RED.clipboard = (function() {
.on("drop",function(event) {
var data = event.originalEvent.dataTransfer.getData("text/plain");
hideDropTarget();
data = data.substring(data.indexOf('['),data.lastIndexOf(']')+1);
RED.view.importNodes(data);
event.preventDefault();
});
},
import: importNodes,
export: exportNodes
}
})();

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2015 IBM Corp.
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -208,13 +208,13 @@ RED.deploy = (function() {
.html("<li>"+invalidNodes.map(function(A) { return (A.tab?"["+A.tab+"] ":"")+A.label+" ("+A.type+")"}).join("</li><li>")+"</li>");
} else if (hasUnusedConfig && !ignoreDeployWarnings.unusedConfig) {
showWarning = true;
$( "#node-dialog-confirm-deploy-type" ).val("unusedConfig");
$( "#node-dialog-confirm-deploy-unused" ).show();
unusedConfigNodes.sort(sortNodeInfo);
$( "#node-dialog-confirm-deploy-unused-list" )
.html("<li>"+unusedConfigNodes.map(function(A) { return (A.tab?"["+A.tab+"] ":"")+A.label+" ("+A.type+")"}).join("</li><li>")+"</li>");
// showWarning = true;
// $( "#node-dialog-confirm-deploy-type" ).val("unusedConfig");
// $( "#node-dialog-confirm-deploy-unused" ).show();
//
// unusedConfigNodes.sort(sortNodeInfo);
// $( "#node-dialog-confirm-deploy-unused-list" )
// .html("<li>"+unusedConfigNodes.map(function(A) { return (A.tab?"["+A.tab+"] ":"")+A.label+" ("+A.type+")"}).join("</li><li>")+"</li>");
}
if (showWarning) {
$( "#node-dialog-confirm-deploy-hide" ).prop("checked",false);
@@ -241,7 +241,13 @@ RED.deploy = (function() {
"Node-RED-Deployment-Type":deploymentType
}
}).done(function(data,textStatus,xhr) {
RED.notify(RED._("deploy.successfulDeploy"),"success");
if (hasUnusedConfig) {
RED.notify(
'<p>'+RED._("deploy.successfulDeploy")+'</p>'+
'<p>'+RED._("deploy.unusedConfigNodes")+' <a href="#" onclick="RED.sidebar.config.show(true); return false;">'+RED._("deploy.unusedConfigNodesLink")+'</a></p>',"success",false,6000);
} else {
RED.notify(RED._("deploy.successfulDeploy"),"success");
}
RED.nodes.eachNode(function(node) {
if (node.changed) {
node.dirty = true;

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -116,6 +116,9 @@ RED.editor = (function() {
*/
function validateNodeProperty(node,definition,property,value) {
var valid = true;
if (/^\$\([a-zA-Z_][a-zA-Z0-9_]*\)$/.test(value)) {
return true;
}
if ("required" in definition[property] && definition[property].required) {
valid = value !== "";
}
@@ -126,8 +129,8 @@ RED.editor = (function() {
if (!value || value == "_ADD_") {
valid = definition[property].hasOwnProperty("required") && !definition[property].required;
} else {
var v = RED.nodes.node(value).valid;
valid = (v==null || v);
var configNode = RED.nodes.node(value);
valid = (configNode !== null && (configNode.valid == null || configNode.valid));
}
}
return valid;
@@ -150,9 +153,9 @@ RED.editor = (function() {
node.ports.pop();
}
RED.nodes.eachLink(function(l) {
if (l.source === node && l.sourcePort >= node.outputs) {
removedLinks.push(l);
}
if (l.source === node && l.sourcePort >= node.outputs) {
removedLinks.push(l);
}
});
} else if (node.outputs > node.ports.length) {
while (node.outputs > node.ports.length) {
@@ -305,7 +308,7 @@ RED.editor = (function() {
}
editing_node.dirty = true;
validateNode(editing_node);
RED.view.redraw();
RED.view.redraw(true);
}
$( this ).dialog( "close" );
}
@@ -348,6 +351,10 @@ RED.editor = (function() {
resize: function(e,ui) {
if (editing_node) {
$(this).dialog('option',"sizeCache-"+editing_node.type,ui.size);
if (editing_node._def.oneditresize) {
var form = $("#dialog-form");
editing_node._def.oneditresize.call(editing_node,{width:form.width(),height:form.height()});
}
}
},
open: function(e) {
@@ -364,6 +371,12 @@ RED.editor = (function() {
$(this).dialog('option','width',size.width);
$(this).dialog('option','height',size.height);
}
if (editing_node._def.oneditresize) {
setTimeout(function() {
var form = $("#dialog-form");
editing_node._def.oneditresize.call(editing_node,{width:form.width(),height:form.height()});
},0);
}
}
},
close: function(e) {
@@ -385,6 +398,12 @@ RED.editor = (function() {
}
editing_node = null;
}
}).parent().on('keydown', function(evt) {
if (evt.keyCode === $.ui.keyCode.ESCAPE && (evt.metaKey || evt.ctrlKey)) {
$("#node-dialog-cancel").click();
} else if (evt.keyCode === $.ui.keyCode.ENTER && (evt.metaKey || evt.ctrlKey)) {
$("#node-dialog-ok").click();
}
});
}
@@ -935,20 +954,20 @@ RED.editor = (function() {
RED.nodes.add(editing_config_node);
}
updateConfigNodeSelect(configProperty,configType,editing_config_node.id);
if (configTypeDef.credentials) {
updateNodeCredentials(editing_config_node,configTypeDef.credentials,"node-config-input");
}
if (configTypeDef.oneditsave) {
configTypeDef.oneditsave.call(editing_config_node);
}
if (configTypeDef.credentials) {
updateNodeCredentials(editing_config_node,configTypeDef.credentials,"node-config-input");
}
validateNode(editing_config_node);
for (var i=0;i<editing_config_node.users.length;i++) {
var user = editing_config_node.users[i];
validateNode(user);
}
updateConfigNodeSelect(configProperty,configType,editing_config_node.id);
RED.nodes.dirty(true);
RED.view.redraw(true);
$(this).dialog("close");
@@ -1008,6 +1027,12 @@ RED.editor = (function() {
cancel: '.ui-dialog-content, .ui-dialog-titlebar-close, #node-config-dialog-scope-container'
});
}
}).parent().on('keydown', function(evt) {
if (evt.keyCode === $.ui.keyCode.ESCAPE && (evt.metaKey || evt.ctrlKey)) {
$("#node-config-dialog-cancel").click();
} else if (evt.keyCode === $.ui.keyCode.ENTER && (evt.metaKey || evt.ctrlKey)) {
$("#node-config-dialog-ok").click();
}
});
}
@@ -1079,7 +1104,7 @@ RED.editor = (function() {
RED.history.push(historyEvent);
}
editing_node.dirty = true;
RED.view.redraw();
RED.view.redraw(true);
}
$( this ).dialog( "close" );
}
@@ -1129,6 +1154,12 @@ RED.editor = (function() {
$(".node-text-editor").css("height",height+"px");
subflowEditor.resize();
}
}).parent().on('keydown', function(evt) {
if (evt.keyCode === $.ui.keyCode.ESCAPE && (evt.metaKey || evt.ctrlKey)) {
$("#subflow-dialog-cancel").click();
} else if (evt.keyCode === $.ui.keyCode.ENTER && (evt.metaKey || evt.ctrlKey)) {
$("#subflow-dialog-ok").click();
}
});
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,7 +27,9 @@ RED.library = (function() {
var li;
var a;
var ul = document.createElement("ul");
ul.id = "menu-item-import-library-submenu";
if (root === "") {
ul.id = "menu-item-import-library-submenu";
}
ul.className = "dropdown-menu";
if (data.d) {
for (i in data.d) {
@@ -36,7 +38,8 @@ RED.library = (function() {
li.className = "dropdown-submenu pull-left";
a = document.createElement("a");
a.href="#";
a.innerHTML = i;
var label = i.replace(/^node-red-contrib-/,"").replace(/^node-red-node-/,"").replace(/-/," ").replace(/_/," ");
a.innerHTML = label;
li.appendChild(a);
li.appendChild(buildMenu(data.d[i],root+(root!==""?"/":"")+i));
ul.appendChild(li);
@@ -53,7 +56,7 @@ RED.library = (function() {
a.flowName = root+(root!==""?"/":"")+data.f[i];
a.onclick = function() {
$.get('library/flows/'+this.flowName, function(data) {
RED.view.importNodes(data);
RED.view.importNodes(data);
});
};
li.appendChild(a);
@@ -63,7 +66,17 @@ RED.library = (function() {
}
return ul;
};
var examples;
if (data.d && data.d._examples_) {
examples = data.d._examples_;
delete data.d._examples_;
}
var menu = buildMenu(data,"");
$("#menu-item-import-examples").remove();
if (examples) {
RED.menu.addItem("menu-item-import",{id:"menu-item-import-examples",label:RED._("menu.label.examples"),options:[]})
$("#menu-item-import-examples-submenu").replaceWith(buildMenu(examples,"_examples_"));
}
//TODO: need an api in RED.menu for this
$("#menu-item-import-library-submenu").replaceWith(menu);
});
@@ -117,7 +130,7 @@ RED.library = (function() {
$(".active",bc).removeClass("active");
bc.append(bcli);
$.getJSON("library/"+options.url+root+dirName,function(data) {
$("#node-select-library").children().first().replaceWith(buildFileList(root+dirName+"/",data));
$("#node-select-library").children().first().replaceWith(buildFileList(root+dirName+"/",data));
});
}
})();
@@ -125,20 +138,20 @@ RED.library = (function() {
ul.appendChild(li);
} else {
// file
li = buildFileListItem(v);
li.innerHTML = v.name;
li.onclick = (function() {
var item = v;
return function(e) {
$(".list-selected",ul).removeClass("list-selected");
$(this).addClass("list-selected");
$.get("library/"+options.url+root+item.fn, function(data) {
selectedLibraryItem = item;
libraryEditor.setValue(data,-1);
});
}
})();
ul.appendChild(li);
li = buildFileListItem(v);
li.innerHTML = v.name;
li.onclick = (function() {
var item = v;
return function(e) {
$(".list-selected",ul).removeClass("list-selected");
$(this).addClass("list-selected");
$.get("library/"+options.url+root+item.fn, function(data) {
selectedLibraryItem = item;
libraryEditor.setValue(data,-1);
});
}
})();
ul.appendChild(li);
}
}
return ul;
@@ -428,8 +441,8 @@ RED.library = (function() {
data: $("#node-input-library-filename").attr('nodes'),
contentType: "application/json; charset=utf-8"
}).done(function() {
RED.library.loadFlowLibrary();
RED.notify(RED._("library.savedNodes"),"success");
RED.library.loadFlowLibrary();
RED.notify(RED._("library.savedNodes"),"success");
}).fail(function(xhr,textStatus,err) {
RED.notify(RED._("library.saveFailed",{message:xhr.responseText}),"error");
});
@@ -452,7 +465,7 @@ RED.library = (function() {
close: function(e) {
RED.keyboard.enable();
}
});
});
exportToLibraryDialog.children(".dialog-form").append($(
'<div class="form-row">'+
'<label for="node-input-library-filename" data-i18n="[append]editor:library.filename"><i class="fa fa-file"></i> </label>'+

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,6 +49,13 @@ RED.notify = (function() {
};
})();
if (!fixed) {
$(n).click((function() {
var nn = n;
return function() {
nn.close();
window.clearTimeout(nn.timeoutid);
};
})());
n.timeoutid = window.setTimeout(n.close,timeout||3000);
}
currentNotifications.push(n);

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,7 +66,7 @@ RED.palette = (function() {
var lineHeight = 20;
var portHeight = 10;
var words = label.split(" ");
var words = label.split(/[ -]/);
var displayLines = [];
@@ -134,22 +134,27 @@ RED.palette = (function() {
d.id = "palette_node_"+nodeTypeId;
d.type = nt;
var label;
if (typeof def.paletteLabel === "undefined") {
label = /^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];
} else {
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
var label = /^(.*?)([ -]in|[ -]out)?$/.exec(nt)[1];
if (typeof def.paletteLabel !== "undefined") {
try {
label = (typeof def.paletteLabel === "function" ? def.paletteLabel.call(def) : def.paletteLabel)||"";
} catch(err) {
console.log("Definition error: "+nt+".paletteLabel",err);
}
}
$('<div/>',{class:"palette_label"+(def.align=="right"?" palette_label_right":"")}).appendTo(d);
d.className="palette_node";
if (def.icon) {
var icon_url = (typeof def.icon === "function" ? def.icon.call({}) : def.icon);
var icon_url = "arrow-in.png";
try {
icon_url = (typeof def.icon === "function" ? def.icon.call({}) : def.icon);
} catch(err) {
console.log("Definition error: "+nt+".icon",err);
}
var iconContainer = $('<div/>',{class:"palette_icon_container"+(def.align=="right"?" palette_icon_container_right":"")}).appendTo(d);
$('<div/>',{class:"palette_icon",style:"background-image: url(icons/"+icon_url+")"}).appendTo(iconContainer);
}
@@ -210,12 +215,83 @@ RED.palette = (function() {
var help = '<div class="node-help">'+helpText+"</div>";
RED.sidebar.info.set(help);
});
var chart = $("#chart");
var chartOffset = chart.offset();
var chartSVG = $("#chart>svg").get(0);
var activeSpliceLink;
var mouseX;
var mouseY;
var spliceTimer;
$(d).draggable({
helper: 'clone',
appendTo: 'body',
revert: true,
revertDuration: 50,
start: function() {RED.view.focus();}
start: function() {RED.view.focus();},
stop: function() { d3.select('.link_splice').classed('link_splice',false); if (spliceTimer) { clearTimeout(spliceTimer); spliceTimer = null;}},
drag: function(e,ui) {
// TODO: this is the margin-left of palette node. Hard coding
// it here makes me sad
ui.position.left += 17.5;
if (def.inputs > 0 && def.outputs > 0) {
mouseX = e.clientX - chartOffset.left+chart.scrollLeft();
mouseY = e.clientY-chartOffset.top +chart.scrollTop();
if (!spliceTimer) {
spliceTimer = setTimeout(function() {
var nodes = [];
var bestDistance = Infinity;
var bestLink = null;
if (chartSVG.getIntersectionList) {
var svgRect = chartSVG.createSVGRect();
svgRect.x = mouseX;
svgRect.y = mouseY;
svgRect.width = 1;
svgRect.height = 1;
nodes = chartSVG.getIntersectionList(svgRect,chartSVG);
mouseX /= RED.view.scale();
mouseY /= RED.view.scale();
} else {
// Firefox doesn't do getIntersectionList and that
// makes us sad
mouseX /= RED.view.scale();
mouseY /= RED.view.scale();
nodes = RED.view.getLinksAtPoint(mouseX,mouseY);
}
for (var i=0;i<nodes.length;i++) {
if (d3.select(nodes[i]).classed('link_background')) {
var length = nodes[i].getTotalLength();
for (var j=0;j<length;j+=10) {
var p = nodes[i].getPointAtLength(j);
var d2 = ((p.x-mouseX)*(p.x-mouseX))+((p.y-mouseY)*(p.y-mouseY));
if (d2 < 200 && d2 < bestDistance) {
bestDistance = d2;
bestLink = nodes[i];
}
}
}
}
if (activeSpliceLink && activeSpliceLink !== bestLink) {
d3.select(activeSpliceLink.parentNode).classed('link_splice',false);
}
if (bestLink) {
d3.select(bestLink.parentNode).classed('link_splice',true)
} else {
d3.select('.link_splice').classed('link_splice',false);
}
if (activeSpliceLink !== bestLink) {
if (bestLink) {
$(ui.helper).data('splice',d3.select(bestLink).data()[0]);
} else {
$(ui.helper).removeData('splice');
}
}
activeSpliceLink = bestLink;
spliceTimer = null;
},200);
}
}
}
});
var nodeInfo = null;
@@ -292,7 +368,7 @@ RED.palette = (function() {
}
var re = new RegExp(val,'i');
$(".palette_node").each(function(i,el) {
$("#palette-container .palette_node").each(function(i,el) {
var currentLabel = $(el).find(".palette_label").text();
if (val === "" || re.test(el.id) || re.test(currentLabel)) {
$(this).show();

View File

@@ -20,10 +20,14 @@ RED.sidebar = (function() {
id:"sidebar-tabs",
onchange:function(tab) {
$("#sidebar-content").children().hide();
$("#sidebar-footer").children().hide();
if (tab.onchange) {
tab.onchange.call(tab);
}
$(tab.content).show();
if (tab.toolbar) {
$(tab.toolbar).show();
}
},
onremove: function(tab) {
$(tab.content).hide();
@@ -58,10 +62,15 @@ RED.sidebar = (function() {
$("#sidebar-content").append(options.content);
$(options.content).hide();
if (options.toolbar) {
$("#sidebar-footer").append(options.toolbar);
$(options.toolbar).hide();
}
$(options.content).hide();
var id = options.id;
RED.menu.addItem("menu-item-sidebar-menu",{
id:"menu-item-sidebar-menu-"+options.id,
RED.menu.addItem("menu-item-view-menu",{
id:"menu-item-view-menu-"+options.id,
label:options.name,
onselect:function() {
showSidebar(options.id);
@@ -80,7 +89,7 @@ RED.sidebar = (function() {
sidebar_tabs.removeTab(id);
$(knownTabs[id].content).remove();
delete knownTabs[id];
RED.menu.removeItem("menu-item-sidebar-menu-"+id);
RED.menu.removeItem("menu-item-view-menu-"+id);
}
var sidebarSeparator = {};

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,24 +19,115 @@ RED.sidebar.config = (function() {
var content = document.createElement("div");
content.className = "sidebar-node-config"
$('<div class="palette-category">'+
'<div class="workspace-config-node-tray-header palette-header"><i class="fa fa-angle-down expanded"></i><span data-i18n="sidebar.config.local"></span></div>'+
'<ul id="workspace-config-node-tray-locals" class="palette-content config-node-list"></ul>'+
'</div>'+
'<div class="palette-category">'+
'<div class="workspace-config-node-tray-header palette-header"><i class="fa fa-angle-down expanded"></i><span data-i18n="sidebar.config.global"></span></div>'+
'<ul id="workspace-config-node-tray-globals" class="palette-content config-node-list"></ul>'+
'</div>').appendTo(content);
$('<div class="button-group sidebar-header">'+
'<a class="sidebar-header-button-toggle selected" id="workspace-config-node-filter-all" href="#"><span data-i18n="sidebar.config.filterAll"></span></a>'+
'<a class="sidebar-header-button-toggle" id="workspace-config-node-filter-unused" href="#"><span data-i18n="sidebar.config.filterUnused"></span></a> '+
'</div>'
).appendTo(content);
var toolbar = $('<div>'+
'<a class="sidebar-footer-button" id="workspace-config-node-collapse-all" href="#"><i class="fa fa-angle-double-up"></i></a> '+
'<a class="sidebar-footer-button" id="workspace-config-node-expand-all" href="#"><i class="fa fa-angle-double-down"></i></a>'+
'</div>');
var globalCategories = $("<div>").appendTo(content);
var flowCategories = $("<div>").appendTo(content);
var subflowCategories = $("<div>").appendTo(content);
var showUnusedOnly = false;
var categories = {};
function getOrCreateCategory(name,parent,label) {
name = name.replace(/\./i,"-");
if (!categories[name]) {
var container = $('<div class="palette-category workspace-config-node-category" id="workspace-config-node-category-'+name+'"></div>').appendTo(parent);
var header = $('<div class="workspace-config-node-tray-header palette-header"><i class="fa fa-angle-down expanded"></i></div>').appendTo(container);
if (label) {
$('<span class="config-node-label"/>').text(label).appendTo(header);
} else {
$('<span class="config-node-label" data-i18n="sidebar.config.'+name+'">').appendTo(header);
}
$('<span class="config-node-filter-info"></span>').appendTo(header);
category = $('<ul class="palette-content config-node-list"></ul>').appendTo(container);
container.i18n();
var icon = header.find("i");
var result = {
label: label,
list: category,
size: function() {
return result.list.find("li:not(.config_node_none)").length
},
open: function(snap) {
if (!icon.hasClass("expanded")) {
icon.addClass("expanded");
if (snap) {
result.list.show();
} else {
result.list.slideDown();
}
}
},
close: function(snap) {
if (icon.hasClass("expanded")) {
icon.removeClass("expanded");
if (snap) {
result.list.hide();
} else {
result.list.slideUp();
}
}
},
isOpen: function() {
return icon.hasClass("expanded");
}
};
header.on('click', function(e) {
if (result.isOpen()) {
result.close();
} else {
result.open();
}
});
categories[name] = result;
} else {
if (categories[name].label !== label) {
categories[name].list.parent().find('.config-node-label').text(label);
categories[name].label = label;
}
}
return categories[name];
}
function createConfigNodeList(id,nodes) {
var category = getOrCreateCategory(id.replace(/\./i,"-"))
var list = category.list;
function createConfigNodeList(nodes,list) {
nodes.sort(function(A,B) {
if (A.type < B.type) { return -1;}
if (A.type > B.type) { return 1;}
return 0;
});
if (showUnusedOnly) {
var hiddenCount = nodes.length;
nodes = nodes.filter(function(n) {
return n.users.length === 0;
})
hiddenCount = hiddenCount - nodes.length;
if (hiddenCount > 0) {
list.parent().find('.config-node-filter-info').text(RED._('sidebar.config.filtered',{count:hiddenCount})).show();
} else {
list.parent().find('.config-node-filter-info').hide();
}
} else {
list.parent().find('.config-node-filter-info').hide();
}
list.empty();
if (nodes.length === 0) {
$('<li class="config_node_none" data-i18n="sidebar.config.none">NONE</li>').i18n().appendTo(list);
category.close(true);
} else {
var currentType = "";
nodes.forEach(function(node) {
@@ -86,23 +177,46 @@ RED.sidebar.config = (function() {
RED.view.redraw();
});
});
category.open(true);
}
}
function refreshConfigNodeList() {
var validList = {"global":true};
var localConfigNodes = [];
getOrCreateCategory("global",globalCategories);
RED.nodes.eachWorkspace(function(ws) {
validList[ws.id.replace(/\./g,"-")] = true;
getOrCreateCategory(ws.id,flowCategories,ws.label);
})
RED.nodes.eachSubflow(function(sf) {
validList[sf.id.replace(/\./g,"-")] = true;
getOrCreateCategory(sf.id,subflowCategories,sf.name);
})
$(".workspace-config-node-category").each(function() {
var id = $(this).attr('id').substring("workspace-config-node-category-".length);
if (!validList[id]) {
$(this).remove();
delete categories[id];
}
})
var globalConfigNodes = [];
var configList = {};
RED.nodes.eachConfig(function(cn) {
if (cn.z == RED.workspaces.active()) {
localConfigNodes.push(cn);
if (cn.z) {//} == RED.workspaces.active()) {
configList[cn.z.replace(/\./g,"-")] = configList[cn.z.replace(/\./g,"-")]||[];
configList[cn.z.replace(/\./g,"-")].push(cn);
} else if (!cn.z) {
globalConfigNodes.push(cn);
}
});
createConfigNodeList(localConfigNodes,$("#workspace-config-node-tray-locals"));
createConfigNodeList(globalConfigNodes,$("#workspace-config-node-tray-globals"));
for (var id in validList) {
if (validList.hasOwnProperty(id)) {
createConfigNodeList(id,configList[id]||[]);
}
}
createConfigNodeList('global',globalConfigNodes);
}
function init() {
@@ -111,25 +225,63 @@ RED.sidebar.config = (function() {
label: RED._("sidebar.config.label"),
name: RED._("sidebar.config.name"),
content: content,
toolbar: toolbar,
closeable: true,
visible: false,
onchange: function() { refreshConfigNodeList(); }
});
$(".workspace-config-node-tray-header").on('click', function(e) {
var icon = $(this).find("i");
if (icon.hasClass("expanded")) {
icon.removeClass("expanded");
$(this).next().slideUp();
} else {
icon.addClass("expanded");
$(this).next().slideDown();
}
RED.menu.setAction('menu-item-config-nodes',function() {
RED.sidebar.show('config');
})
$("#workspace-config-node-collapse-all").on("click", function(e) {
e.preventDefault();
for (var cat in categories) {
if (categories.hasOwnProperty(cat)) {
categories[cat].close();
}
}
});
$("#workspace-config-node-expand-all").on("click", function(e) {
e.preventDefault();
for (var cat in categories) {
if (categories.hasOwnProperty(cat)) {
if (categories[cat].size() > 0) {
categories[cat].open();
}
}
}
});
$('#workspace-config-node-filter-all').on("click",function(e) {
e.preventDefault();
if (showUnusedOnly) {
$(this).addClass('selected');
$('#workspace-config-node-filter-unused').removeClass('selected');
showUnusedOnly = !showUnusedOnly;
refreshConfigNodeList();
}
});
$('#workspace-config-node-filter-unused').on("click",function(e) {
e.preventDefault();
if (!showUnusedOnly) {
$(this).addClass('selected');
$('#workspace-config-node-filter-all').removeClass('selected');
showUnusedOnly = !showUnusedOnly;
refreshConfigNodeList();
}
});
}
function show() {
function show(unused) {
if (unused !== undefined) {
if (unused) {
$('#workspace-config-node-filter-unused').click();
} else {
$('#workspace-config-node-filter-all').click();
}
}
refreshConfigNodeList();
RED.sidebar.show("config");
}

306
editor/js/ui/typedInput.js Normal file
View File

@@ -0,0 +1,306 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
(function($) {
var allOptions = {
msg: {value:"msg",label:"msg.",validate:/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]+)*/i},
flow: {value:"flow",label:"flow.",validate:/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]+)*/i},
global: {value:"global",label:"global.",validate:/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]+)*/i},
str: {value:"str",label:"string",icon:"red/images/typedInput/az.png"},
num: {value:"num",label:"number",icon:"red/images/typedInput/09.png",validate:/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/},
bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.png",options:["true","false"]},
json: {value:"json",label:"JSON",icon:"red/images/typedInput/json.png", validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}},
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.png"}
};
var nlsd = false;
$.widget( "nodered.typedInput", {
_create: function() {
if (!nlsd && RED && RED._) {
for (var i in allOptions) {
if (allOptions.hasOwnProperty(i)) {
allOptions[i].label = RED._("typedInput.type."+i,{defaultValue:allOptions[i].label});
}
}
}
nlsd = true;
var that = this;
this.disarmClick = false;
this.element.addClass('red-ui-typedInput');
this.uiWidth = this.element.width();
this.uiSelect = this.element
.wrap( "<div>" )
.parent();
["Right","Left"].forEach(function(d) {
var m = that.element.css("margin"+d);
that.uiSelect.css("margin"+d,m);
that.element.css("margin"+d,0);
});
this.uiSelect.addClass("red-ui-typedInput-container");
this.options.types = this.options.types||Object.keys(allOptions);
var hasSubOptions = false;
this.typeMap = {};
this.types = this.options.types.map(function(opt) {
var result;
if (typeof opt === 'string') {
result = allOptions[opt];
} else {
result = opt;
}
that.typeMap[result.value] = result;
if (result.options) {
hasSubOptions = true;
}
return result;
});
if (this.options.typeField) {
this.typeField = $(this.options.typeField).hide();
var t = this.typeField.val();
if (t && this.typeMap[t]) {
this.options.default = t;
}
} else {
this.typeField = $("<input>",{type:'hidden'}).appendTo(this.uiSelect);
}
this.selectTrigger = $('<a href="#"><i class="fa fa-sort-desc"></i></a>').prependTo(this.uiSelect);
this.selectLabel = $('<span></span>').appendTo(this.selectTrigger);
this.element.on('focus', function() {
that.uiSelect.addClass('red-ui-typedInput-focus');
});
this.element.on('blur', function() {
that.uiSelect.removeClass('red-ui-typedInput-focus');
});
this.element.on('change', function() {
that.validate();
})
this.selectTrigger.click(function(event) {
event.preventDefault();
that._showMenu(that.menu,that.selectTrigger);
});
if (hasSubOptions) {
// explicitly set optionSelectTrigger display to inline-block otherwise jQ sets it to 'inline'
this.optionSelectTrigger = $('<a href="#" class="red-ui-typedInput-option-trigger" style="display:inline-block"><i class="fa fa-sort-desc"></i></a>').appendTo(this.uiSelect);
this.optionSelectLabel = $('<span></span>').prependTo(this.optionSelectTrigger);
this.optionSelectTrigger.click(function(event) {
event.preventDefault();
if (that.optionMenu) {
that.optionMenu.css({
minWidth:that.optionSelectLabel.width()
});
that._showMenu(that.optionMenu,that.optionSelectLabel)
}
});
}
this.menu = this._createMenu(this.types, function(v) { that.type(v) });
this.type(this.options.default||this.types[0].value);
},
_hideMenu: function(menu) {
$(document).off("mousedown.close-property-select");
menu.hide();
this.element.focus();
},
_createMenu: function(opts,callback) {
var that = this;
var menu = $("<div>").addClass("red-ui-typedInput-options");
opts.forEach(function(opt) {
if (typeof opt === 'string') {
opt = {value:opt,label:opt};
}
var op = $('<a href="#">').attr("value",opt.value).appendTo(menu);
if (opt.label) {
op.text(opt.label);
}
if (opt.icon) {
$('<img>',{src:opt.icon,style:"margin-right: 4px; height: 18px;"}).prependTo(op);
} else {
op.css({paddingLeft: "18px"});
}
op.click(function(event) {
event.preventDefault();
callback(opt.value);
that._hideMenu(menu);
});
});
menu.css({
display: "none",
});
menu.appendTo(document.body);
return menu;
},
_showMenu: function(menu,relativeTo) {
if (this.disarmClick) {
this.disarmClick = false;
return
}
var that = this;
var pos = relativeTo.offset();
var height = relativeTo.height();
menu.css({
top: (height+pos.top-3)+"px",
left: (2+pos.left)+"px",
});
menu.slideDown(100);
this._delay(function() {
that.uiSelect.addClass('red-ui-typedInput-focus');
$(document).on("mousedown.close-property-select", function(event) {
if(!$(event.target).closest(menu).length) {
that._hideMenu(menu);
}
if ($(event.target).closest(relativeTo).length) {
that.disarmClick = true;
event.preventDefault();
}
})
});
},
_getLabelWidth: function(label) {
var labelWidth = label.width();
if (labelWidth === 0) {
var newTrigger = label.clone();
newTrigger.css({
position:"absolute",
top:0,
left:-1000
}).appendTo(document.body);
labelWidth = newTrigger.width()+4;
newTrigger.remove();
}
return labelWidth;
},
_resize: function() {
if (this.typeMap[this.propertyType] && this.typeMap[this.propertyType].hasValue === false) {
this.selectTrigger.width(this.uiWidth+5);
} else {
this.selectTrigger.width('auto');
var labelWidth = this._getLabelWidth(this.selectTrigger);
var newWidth = this.uiWidth-labelWidth+4;
this.element.width(newWidth);
if (this.optionSelectTrigger) {
var triggerWidth = this._getLabelWidth(this.optionSelectTrigger);
labelWidth = this._getLabelWidth(this.optionSelectLabel)-4;
this.optionSelectLabel.width(labelWidth+(newWidth-triggerWidth));
}
}
},
_destroy: function() {
this.menu.remove();
},
width: function(desiredWidth) {
this.uiWidth = desiredWidth;
this._resize();
},
value: function(value) {
if (!arguments.length) {
return this.element.val();
} else {
if (this.typeMap[this.propertyType].options) {
if (this.typeMap[this.propertyType].options.indexOf(value) === -1) {
value = "";
}
this.optionSelectLabel.text(value);
}
this.element.val(value);
this.element.trigger('change');
}
},
type: function(type) {
if (!arguments.length) {
return this.propertyType;
} else {
var opt = this.typeMap[type];
if (opt && this.propertyType !== type) {
this.propertyType = type;
this.typeField.val(type);
this.selectLabel.empty();
if (opt.icon) {
$('<img>',{src:opt.icon,style:"margin-right: 4px;height: 18px;"}).prependTo(this.selectLabel);
} else {
this.selectLabel.text(opt.label);
}
if (opt.options) {
if (this.optionSelectTrigger) {
this.optionSelectTrigger.show();
this.element.hide();
var that = this;
this.optionMenu = this._createMenu(opt.options,function(v){
that.optionSelectLabel.text(v);
that.value(v);
});
var currentVal = this.element.val();
if (opt.options.indexOf(currentVal) !== -1) {
this.optionSelectLabel.text(currentVal);
} else {
this.value(opt.options[0]);
}
}
} else {
if (this.optionMenu) {
this.optionMenu.remove();
this.optionMenu = null;
}
if (this.optionSelectTrigger) {
this.optionSelectTrigger.hide();
}
if (opt.hasValue === false) {
this.element.val("");
this.element.hide();
} else {
this.element.show();
}
this.element.trigger('change');
}
this._resize();
}
}
},
validate: function() {
var result;
var value = this.value();
var type = this.type();
if (this.typeMap[type] && this.typeMap[type].validate) {
var val = this.typeMap[type].validate;
if (typeof val === 'function') {
result = val(value);
} else {
result = val.test(value);
}
} else {
result = true;
}
if (result) {
this.uiSelect.removeClass('input-error');
} else {
this.uiSelect.addClass('input-error');
}
return result;
}
});
})(jQuery);

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,6 +31,12 @@ RED.view = (function() {
var workspaceScrollPositions = {};
var gridSize = 20;
var snapGrid = false;
var activeSpliceLink;
var spliceActive = false;
var spliceTimer;
var activeSubflow = null;
var activeNodes = [];
@@ -196,40 +202,54 @@ RED.view = (function() {
.attr('height', space_height)
.attr('fill','#fff');
//var gridScale = d3.scale.linear().range([0,2000]).domain([0,2000]);
//var grid = vis.append('g');
//
//grid.selectAll("line.horizontal").data(gridScale.ticks(100)).enter()
// .append("line")
// .attr(
// {
// "class":"horizontal",
// "x1" : 0,
// "x2" : 2000,
// "y1" : function(d){ return gridScale(d);},
// "y2" : function(d){ return gridScale(d);},
// "fill" : "none",
// "shape-rendering" : "crispEdges",
// "stroke" : "#eee",
// "stroke-width" : "1px"
// });
//grid.selectAll("line.vertical").data(gridScale.ticks(100)).enter()
// .append("line")
// .attr(
// {
// "class":"vertical",
// "y1" : 0,
// "y2" : 2000,
// "x1" : function(d){ return gridScale(d);},
// "x2" : function(d){ return gridScale(d);},
// "fill" : "none",
// "shape-rendering" : "crispEdges",
// "stroke" : "#eee",
// "stroke-width" : "1px"
// });
var gridScale = d3.scale.linear().range([0,space_width]).domain([0,space_width]);
var grid = vis.append('g');
grid.selectAll("line.horizontal").data(gridScale.ticks(space_width/gridSize)).enter()
.append("line")
.attr(
{
"class":"horizontal",
"x1" : 0,
"x2" : space_width,
"y1" : function(d){ return gridScale(d);},
"y2" : function(d){ return gridScale(d);},
"fill" : "none",
"shape-rendering" : "crispEdges",
"stroke" : "#eee",
"stroke-width" : "1px"
});
grid.selectAll("line.vertical").data(gridScale.ticks(space_width/gridSize)).enter()
.append("line")
.attr(
{
"class":"vertical",
"y1" : 0,
"y2" : space_width,
"x1" : function(d){ return gridScale(d);},
"x2" : function(d){ return gridScale(d);},
"fill" : "none",
"shape-rendering" : "crispEdges",
"stroke" : "#eee",
"stroke-width" : "1px"
});
grid.style("visibility","hidden");
var drag_line = vis.append("svg:path").attr("class", "drag_line");
var dragGroup = vis.append('g');
var drag_lines = [];
function showDragLines(nodes) {
for (var i=0;i<nodes.length;i++) {
var node = nodes[i];
node.el = dragGroup.append("svg:path").attr("class", "drag_line");
drag_lines.push(node);
}
}
function hideDragLines() {
while(drag_lines.length) {
(drag_lines.pop()).el.remove();
}
}
function updateActiveNodes() {
var activeWorkspace = RED.workspaces.active();
@@ -300,7 +320,6 @@ RED.view = (function() {
drop: function( event, ui ) {
d3.event = event;
var selected_tool = ui.draggable[0].type;
var m = /^subflow:(.+)$/.exec(selected_tool);
if (activeSubflow && m) {
@@ -313,16 +332,9 @@ RED.view = (function() {
RED.notify(RED._("notification.error",{message: RED._("notification.errors.cannotAddCircularReference")}),"error");
return;
}
}
var mousePos = d3.touches(this)[0]||d3.mouse(this);
mousePos[1] += this.scrollTop;
mousePos[0] += this.scrollLeft;
mousePos[1] /= scaleFactor;
mousePos[0] /= scaleFactor;
var nn = { id:(1+Math.random()*4294967295).toString(16),x: mousePos[0],y:mousePos[1],w:node_width,z:RED.workspaces.active()};
var nn = { id:(1+Math.random()*4294967295).toString(16),z:RED.workspaces.active()};
nn.type = selected_tool;
nn._def = RED.nodes.getType(nn.type);
@@ -338,7 +350,11 @@ RED.view = (function() {
}
if (nn._def.onadd) {
nn._def.onadd.call(nn);
try {
nn._def.onadd.call(nn);
} catch(err) {
console.log("onadd:",err);
}
}
} else {
var subflow = RED.nodes.subflow(m[1]);
@@ -347,7 +363,10 @@ RED.view = (function() {
}
nn.changed = true;
nn.w = node_width;
nn.h = Math.max(node_height,(nn.outputs||0) * 15);
var historyEvent = {
t:'add',
nodes:[nn.id],
@@ -364,6 +383,41 @@ RED.view = (function() {
}
}
var helperOffset = d3.touches(ui.helper.get(0))[0]||d3.mouse(ui.helper.get(0));
var mousePos = d3.touches(this)[0]||d3.mouse(this);
mousePos[1] += this.scrollTop + ((nn.h/2)-helperOffset[1]);
mousePos[0] += this.scrollLeft + ((nn.w/2)-helperOffset[0]);
mousePos[1] /= scaleFactor;
mousePos[0] /= scaleFactor;
if (snapGrid) {
mousePos[0] = gridSize*(Math.ceil(mousePos[0]/gridSize));
mousePos[1] = gridSize*(Math.ceil(mousePos[1]/gridSize));
}
nn.x = mousePos[0];
nn.y = mousePos[1];
var spliceLink = $(ui.helper).data('splice');
if (spliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
RED.nodes.removeLink(spliceLink);
var link1 = {
source:spliceLink.source,
sourcePort:spliceLink.sourcePort,
target: nn
};
var link2 = {
source:nn,
sourcePort:0,
target: spliceLink.target
};
RED.nodes.addLink(link1);
RED.nodes.addLink(link2);
historyEvent.links = [link1,link2];
historyEvent.removedLinks = [spliceLink];
}
RED.history.push(historyEvent);
RED.nodes.add(nn);
RED.editor.validateNode(nn);
@@ -420,6 +474,8 @@ RED.view = (function() {
}
function canvasMouseMove() {
var i;
var node;
mouse_position = d3.touches(this)[0]||d3.mouse(this);
// Prevent touch scrolling...
//if (d3.touches(this)[0]) {
@@ -466,36 +522,73 @@ RED.view = (function() {
var mousePos;
if (mouse_mode == RED.state.JOINING) {
// update drag line
drag_line.attr("class", "drag_line");
mousePos = mouse_position;
var numOutputs = (mousedown_port_type === 0)?(mousedown_node.outputs || 1):1;
var sourcePort = mousedown_port_index;
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
var sc = (mousedown_port_type === 0)?1:-1;
var dy = mousePos[1]-(mousedown_node.y+portY);
var dx = mousePos[0]-(mousedown_node.x+sc*mousedown_node.w/2);
var delta = Math.sqrt(dy*dy+dx*dx);
var scale = lineCurveScale;
var scaleY = 0;
if (delta < node_width) {
scale = 0.75-0.75*((node_width-delta)/node_width);
}
if (dx*sc < 0) {
scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
if (Math.abs(dy) < 3*node_height) {
scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
if (drag_lines.length === 0) {
if (d3.event.shiftKey) {
// Get all the wires we need to detach.
var links = [];
var filter;
if (mousedown_port_type === 0) {
filter = {
source:mousedown_node,
sourcePort: mousedown_port_index
}
} else {
filter = {
target: mousedown_node
}
}
var existingLinks = RED.nodes.filterLinks(filter);
for (i=0;i<existingLinks.length;i++) {
var link = existingLinks[i];
RED.nodes.removeLink(link);
links.push({
link:link,
node: (mousedown_port_type===0)?link.target:link.source,
port: (mousedown_port_type===0)?0:link.sourcePort,
portType: (mousedown_port_type===0)?1:0
})
}
showDragLines(links);
mouse_mode = 0;
updateActiveNodes();
redraw();
mouse_mode = RED.state.JOINING;
} else {
showDragLines([{node:mousedown_node,port:mousedown_port_index,portType:mousedown_port_type}]);
}
}
mousePos = mouse_position;
for (i=0;i<drag_lines.length;i++) {
var drag_line = drag_lines[i];
var numOutputs = (drag_line.portType === 0)?(drag_line.node.outputs || 1):1;
var sourcePort = drag_line.port;
var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
drag_line.attr("d",
"M "+(mousedown_node.x+sc*mousedown_node.w/2)+" "+(mousedown_node.y+portY)+
" C "+(mousedown_node.x+sc*(mousedown_node.w/2+node_width*scale))+" "+(mousedown_node.y+portY+scaleY*node_height)+" "+
(mousePos[0]-sc*(scale)*node_width)+" "+(mousePos[1]-scaleY*node_height)+" "+
mousePos[0]+" "+mousePos[1]
);
var sc = (drag_line.portType === 0)?1:-1;
var dy = mousePos[1]-(drag_line.node.y+portY);
var dx = mousePos[0]-(drag_line.node.x+sc*drag_line.node.w/2);
var delta = Math.sqrt(dy*dy+dx*dx);
var scale = lineCurveScale;
var scaleY = 0;
if (delta < node_width) {
scale = 0.75-0.75*((node_width-delta)/node_width);
}
if (dx*sc < 0) {
scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
if (Math.abs(dy) < 3*node_height) {
scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
}
}
drag_line.el.attr("d",
"M "+(drag_line.node.x+sc*drag_line.node.w/2)+" "+(drag_line.node.y+portY)+
" C "+(drag_line.node.x+sc*(drag_line.node.w/2+node_width*scale))+" "+(drag_line.node.y+portY+scaleY*node_height)+" "+
(mousePos[0]-sc*(scale)*node_width)+" "+(mousePos[1]-scaleY*node_height)+" "+
mousePos[0]+" "+mousePos[1]
);
}
d3.event.preventDefault();
} else if (mouse_mode == RED.state.MOVING) {
mousePos = d3.mouse(document.body);
@@ -506,11 +599,17 @@ RED.view = (function() {
if (d > 3) {
mouse_mode = RED.state.MOVING_ACTIVE;
clickElapsed = 0;
spliceActive = false;
if (moving_set.length === 1) {
node = moving_set[0];
spliceActive = node.n._def.inputs > 0 &&
node.n._def.outputs > 0 &&
RED.nodes.filterLinks({ source: node.n }).length === 0 &&
RED.nodes.filterLinks({ target: node.n }).length === 0;
}
}
} else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) {
mousePos = mouse_position;
var node;
var i;
var minX = 0;
var minY = 0;
for (var n = 0; n<moving_set.length; n++) {
@@ -532,11 +631,11 @@ RED.view = (function() {
node.n.y -= minY;
}
}
if (d3.event.shiftKey && moving_set.length > 0) {
var gridOffset = [0,0];
if (snapGrid != d3.event.shiftKey && moving_set.length > 0) {
var gridOffset = [0,0];
node = moving_set[0];
gridOffset[0] = node.n.x-(20*Math.floor((node.n.x-node.n.w/2)/20)+node.n.w/2);
gridOffset[1] = node.n.y-(20*Math.floor(node.n.y/20));
gridOffset[0] = node.n.x-(gridSize*Math.floor((node.n.x-node.n.w/2)/gridSize)+node.n.w/2);
gridOffset[1] = node.n.y-(gridSize*Math.floor(node.n.y/gridSize));
if (gridOffset[0] !== 0 || gridOffset[1] !== 0) {
for (i = 0; i<moving_set.length; i++) {
node = moving_set[i];
@@ -548,6 +647,57 @@ RED.view = (function() {
}
}
}
if ((mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) && moving_set.length === 1) {
node = moving_set[0];
if (spliceActive) {
if (!spliceTimer) {
spliceTimer = setTimeout(function() {
var nodes = [];
var bestDistance = Infinity;
var bestLink = null;
var mouseX = mousePos[0];
var mouseY = mousePos[1];
if (outer[0][0].getIntersectionList) {
var svgRect = outer[0][0].createSVGRect();
svgRect.x = mouseX;
svgRect.y = mouseY;
svgRect.width = 1;
svgRect.height = 1;
nodes = outer[0][0].getIntersectionList(svgRect, outer[0][0]);
} else {
// Firefox doesn't do getIntersectionList and that
// makes us sad
nodes = RED.view.getLinksAtPoint(mouseX,mouseY);
}
for (var i=0;i<nodes.length;i++) {
if (d3.select(nodes[i]).classed('link_background')) {
var length = nodes[i].getTotalLength();
for (var j=0;j<length;j+=10) {
var p = nodes[i].getPointAtLength(j);
var d2 = ((p.x-mouseX)*(p.x-mouseX))+((p.y-mouseY)*(p.y-mouseY));
if (d2 < 200 && d2 < bestDistance) {
bestDistance = d2;
bestLink = nodes[i];
}
}
}
}
if (activeSpliceLink && activeSpliceLink !== bestLink) {
d3.select(activeSpliceLink.parentNode).classed('link_splice',false);
}
if (bestLink) {
d3.select(bestLink.parentNode).classed('link_splice',true)
} else {
d3.select('.link_splice').classed('link_splice',false);
}
activeSpliceLink = bestLink;
spliceTimer = null;
},100);
}
}
}
}
if (mouse_mode !== 0) {
redraw();
@@ -555,8 +705,22 @@ RED.view = (function() {
}
function canvasMouseUp() {
var i;
var historyEvent;
if (mousedown_node && mouse_mode == RED.state.JOINING) {
drag_line.attr("class", "drag_line_hidden");
var removedLinks = [];
for (i=0;i<drag_lines.length;i++) {
if (drag_lines[i].link) {
removedLinks.push(drag_lines[i].link)
}
}
historyEvent = {
t:'delete',
links: removedLinks,
dirty:RED.nodes.dirty()
};
RED.history.push(historyEvent);
hideDragLines();
}
if (lasso) {
var x = parseInt(lasso.attr("x"));
@@ -594,7 +758,7 @@ RED.view = (function() {
updateSelection();
lasso.remove();
lasso = null;
} else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey ) {
} else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey&& !d3.event.metaKey ) {
clearSelection();
updateSelection();
}
@@ -604,11 +768,33 @@ RED.view = (function() {
for (var j=0;j<moving_set.length;j++) {
ns.push({n:moving_set[j].n,ox:moving_set[j].ox,oy:moving_set[j].oy});
}
RED.history.push({t:'move',nodes:ns,dirty:RED.nodes.dirty()});
historyEvent = {t:'move',nodes:ns,dirty:RED.nodes.dirty()};
if (activeSpliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
var spliceLink = d3.select(activeSpliceLink).data()[0];
RED.nodes.removeLink(spliceLink);
var link1 = {
source:spliceLink.source,
sourcePort:spliceLink.sourcePort,
target: moving_set[0].n
};
var link2 = {
source:moving_set[0].n,
sourcePort:0,
target: spliceLink.target
};
RED.nodes.addLink(link1);
RED.nodes.addLink(link2);
historyEvent.links = [link1,link2];
historyEvent.removedLinks = [spliceLink];
updateActiveNodes();
}
RED.nodes.dirty(true);
RED.history.push(historyEvent);
}
}
if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) {
for (var i=0;i<moving_set.length;i++) {
for (i=0;i<moving_set.length;i++) {
delete moving_set[i].ox;
delete moving_set[i].oy;
}
@@ -724,6 +910,7 @@ RED.view = (function() {
delete moving_set[i].oy;
}
RED.history.push({t:'move',nodes:ns,dirty:RED.nodes.dirty()});
RED.nodes.dirty(true);
}
function moveSelection(dx,dy) {
var minX = 0;
@@ -876,6 +1063,13 @@ RED.view = (function() {
mousedown_link = null;
mouse_mode = 0;
mousedown_port_type = 0;
activeSpliceLink = null;
spliceActive = false;
d3.select('.link_splice').classed('link_splice',false);
if (spliceTimer) {
clearTimeout(spliceTimer);
spliceTimer = null;
}
}
function portMouseDown(d,portType,portIndex) {
@@ -892,46 +1086,59 @@ RED.view = (function() {
}
function portMouseUp(d,portType,portIndex) {
var i;
document.body.style.cursor = "";
if (mouse_mode == RED.state.JOINING && mousedown_node) {
if (mouse_mode == RED.state.JOINING && drag_lines.length > 0) {
if (typeof TouchEvent != "undefined" && d3.event instanceof TouchEvent) {
RED.nodes.eachNode(function(n) {
if (n.z == RED.workspaces.active()) {
var hw = n.w/2;
var hh = n.h/2;
if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
mouseup_node = n;
portType = mouseup_node.inputs>0?1:0;
portIndex = 0;
}
if (n.z == RED.workspaces.active()) {
var hw = n.w/2;
var hh = n.h/2;
if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
mouseup_node = n;
portType = mouseup_node.inputs>0?1:0;
portIndex = 0;
}
}
});
} else {
mouseup_node = d;
}
if (portType == mousedown_port_type || mouseup_node === mousedown_node) {
drag_line.attr("class", "drag_line_hidden");
resetMouseVars();
return;
var addedLinks = [];
var removedLinks = [];
for (i=0;i<drag_lines.length;i++) {
if (drag_lines[i].link) {
removedLinks.push(drag_lines[i].link)
}
}
var src,dst,src_port;
if (mousedown_port_type === 0) {
src = mousedown_node;
src_port = mousedown_port_index;
dst = mouseup_node;
} else if (mousedown_port_type == 1) {
src = mouseup_node;
dst = mousedown_node;
src_port = portIndex;
for (i=0;i<drag_lines.length;i++) {
if (portType != drag_lines[i].portType && mouseup_node !== drag_lines[i].node) {
var drag_line = drag_lines[i];
var src,dst,src_port;
if (drag_line.portType === 0) {
src = drag_line.node;
src_port = drag_line.port;
dst = mouseup_node;
} else if (drag_line.portType == 1) {
src = mouseup_node;
dst = drag_line.node;
src_port = portIndex;
}
var existingLink = RED.nodes.filterLinks({source:src,target:dst,sourcePort: src_port}).length !== 0;
if (!existingLink) {
var link = {source: src, sourcePort:src_port, target: dst};
RED.nodes.addLink(link);
addedLinks.push(link);
}
}
}
var existingLink = RED.nodes.filterLinks({source:src,target:dst,sourcePort: src_port}).length !== 0;
if (!existingLink) {
var link = {source: src, sourcePort:src_port, target: dst};
RED.nodes.addLink(link);
if (addedLinks.length > 0 || removedLinks.length > 0) {
var historyEvent = {
t:'add',
links:[link],
links:addedLinks,
removedLinks: removedLinks,
dirty:RED.nodes.dirty()
};
if (activeSubflow) {
@@ -947,8 +1154,9 @@ RED.view = (function() {
RED.history.push(historyEvent);
updateActiveNodes();
RED.nodes.dirty(true);
} else {
}
resetMouseVars();
hideDragLines();
selected_link = null;
redraw();
}
@@ -977,6 +1185,29 @@ RED.view = (function() {
//RED.touch.radialMenu.show(d3.select(this),pos);
if (mouse_mode == RED.state.IMPORT_DRAGGING) {
RED.keyboard.remove(/* ESCAPE */ 27);
if (activeSpliceLink) {
// TODO: DRY - droppable/nodeMouseDown/canvasMouseUp
var spliceLink = d3.select(activeSpliceLink).data()[0];
RED.nodes.removeLink(spliceLink);
var link1 = {
source:spliceLink.source,
sourcePort:spliceLink.sourcePort,
target: moving_set[0].n
};
var link2 = {
source:moving_set[0].n,
sourcePort:0,
target: spliceLink.target
};
RED.nodes.addLink(link1);
RED.nodes.addLink(link2);
var historyEvent = RED.history.peek();
historyEvent.links = [link1,link2];
historyEvent.removedLinks = [spliceLink];
updateActiveNodes();
}
updateSelection();
RED.nodes.dirty(true);
redraw();
@@ -994,10 +1225,10 @@ RED.view = (function() {
var i;
if (d.selected && d3.event.ctrlKey) {
d.selected = false;
if (d.selected && (d3.event.ctrlKey||d3.event.metaKey)) {
mousedown_node.selected = false;
for (i=0;i<moving_set.length;i+=1) {
if (moving_set[i].n === d) {
if (moving_set[i].n === mousedown_node) {
moving_set.splice(i,1);
break;
}
@@ -1012,7 +1243,7 @@ RED.view = (function() {
moving_set.push({n:cnodes[n]});
}
} else if (!d.selected) {
if (!d3.event.ctrlKey) {
if (!d3.event.ctrlKey && !d3.event.metaKey) {
clearSelection();
}
mousedown_node.selected = true;
@@ -1049,7 +1280,11 @@ RED.view = (function() {
d.dirty = true;
}
if (d._def.button.onclick) {
d._def.button.onclick.call(d);
try {
d._def.button.onclick.call(d);
} catch(err) {
console.log("Definition error: "+d.type+".onclick",err);
}
}
if (d.dirty) {
redraw();
@@ -1123,7 +1358,7 @@ RED.view = (function() {
.on("touchstart", function(d,i){portMouseDown(d,1,0);} )
.on("mouseup", function(d,i){portMouseUp(d,1,0);})
.on("touchend",function(d,i){portMouseUp(d,1,0);} )
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 1)));})
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
outGroup.append("svg:text").attr('class','port_label').attr('x',20).attr('y',8).style("font-size","10px").text("output");
@@ -1166,7 +1401,7 @@ RED.view = (function() {
.on("touchstart", function(d,i){portMouseDown(d,0,i);} )
.on("mouseup", function(d,i){portMouseUp(d,0,i);})
.on("touchend",function(d,i){portMouseUp(d,0,i);} )
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 0) ));})
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
inGroup.append("svg:text").attr('class','port_label').attr('x',18).attr('y',20).style("font-size","10px").text("input");
@@ -1204,8 +1439,13 @@ RED.view = (function() {
var node = d3.select(this);
node.attr("id",d.id);
var l = d._def.label;
l = (typeof l === "function" ? l.call(d) : l)||"";
d.w = Math.max(node_width,calculateTextWidth(l, "node_label", 50)+(d._def.inputs>0?7:0) );
try {
l = (typeof l === "function" ? l.call(d) : l)||"";
} catch(err) {
console.log("Definition error: "+d.type+".label",err);
l = d.type;
}
d.w = Math.max(node_width,gridSize*(Math.ceil((calculateTextWidth(l, "node_label", 50)+(d._def.inputs>0?7:0))/gridSize)) );
d.h = Math.max(node_height,(d.outputs||0) * 15);
if (d._def.badge) {
@@ -1387,9 +1627,16 @@ RED.view = (function() {
//if (d.x < -50) deleteSelection(); // Delete nodes if dragged back to palette
if (d.resize) {
var l = d._def.label;
l = (typeof l === "function" ? l.call(d) : l)||"";
d.w = Math.max(node_width,calculateTextWidth(l, "node_label", 50)+(d._def.inputs>0?7:0) );
try {
l = (typeof l === "function" ? l.call(d) : l)||"";
} catch(err) {
console.log("Definition error: "+d.type+".label",err);
l = d.type;
}
var ow = d.w;
d.w = Math.max(node_width,gridSize*(Math.ceil((calculateTextWidth(l, "node_label", 50)+(d._def.inputs>0?7:0))/gridSize)) );
d.h = Math.max(node_height,(d.outputs||0) * 15);
d.x += (d.w-ow)/2;
d.resize = false;
}
var thisNode = d3.select(this);
@@ -1423,7 +1670,7 @@ RED.view = (function() {
.on("touchstart",function(d){portMouseDown(d,1,0);})
.on("mouseup",function(d){portMouseUp(d,1,0);} )
.on("touchend",function(d){portMouseUp(d,1,0);} )
.on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type != 1 ));})
.on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 1) ));})
.on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
}
@@ -1438,7 +1685,7 @@ RED.view = (function() {
.on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
.on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
.on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
.on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || (drag_lines.length > 0 && drag_lines[0].portType !== 0) ));})
.on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
d._ports.exit().remove();
@@ -1453,20 +1700,33 @@ RED.view = (function() {
});
}
thisNode.selectAll('text.node_label').text(function(d,i){
var l = "";
if (d._def.label) {
if (typeof d._def.label == "function") {
return d._def.label.call(d);
} else {
return d._def.label;
l = d._def.label;
try {
l = (typeof l === "function" ? l.call(d) : l)||"";
} catch(err) {
console.log("Definition error: "+d.type+".label",err);
l = d.type;
}
}
return "";
return l;
})
.attr('y', function(d){return (d.h/2)-1;})
.attr('class',function(d){
var s = "";
if (d._def.labelStyle) {
s = d._def.labelStyle;
try {
s = (typeof s === "function" ? s.call(d) : s)||"";
} catch(err) {
console.log("Definition error: "+d.type+".labelStyle",err);
s = "";
}
s = " "+s;
}
return 'node_label'+
(d._def.align?' node_label_'+d._def.align:'')+
(d._def.labelStyle?' '+(typeof d._def.labelStyle == "function" ? d._def.labelStyle.call(d):d._def.labelStyle):'') ;
(d._def.align?' node_label_'+d._def.align:'')+s;
});
if (d._def.icon) {
@@ -1474,7 +1734,12 @@ RED.view = (function() {
var current_url = icon.attr("xlink:href");
var icon_url;
if (typeof d._def.icon == "function") {
icon_url = d._def.icon.call(d);
try {
icon_url = d._def.icon.call(d);
} catch(err) {
console.log("icon",err);
icon_url = "arrow-in.png";
}
} else {
icon_url = d._def.icon;
}
@@ -1538,7 +1803,12 @@ RED.view = (function() {
thisNode.selectAll('text.node_badge_label').text(function(d,i) {
if (d._def.badge) {
if (typeof d._def.badge == "function") {
return d._def.badge.call(d);
try {
return d._def.badge.call(d);
} catch(err) {
console.log("Definition error: "+d.type+".badge",err);
return "";
}
} else {
return d._def.badge;
}
@@ -1618,7 +1888,7 @@ RED.view = (function() {
touchStartTime = null;
showTouchMenu(obj,pos);
},touchLongPressTimeout);
});
})
l.append("svg:path").attr("class","link_outline link_path");
l.append("svg:path").attr("class","link_line link_path")
.classed("link_subflow", function(d) { return activeSubflow && (d.source.type === "subflow" || d.target.type === "subflow") });
@@ -1746,6 +2016,12 @@ RED.view = (function() {
}
if (!touchImport) {
mouse_mode = RED.state.IMPORT_DRAGGING;
spliceActive = false;
if (new_ms.length === 1) {
node = new_ms[0];
spliceActive = node.n._def.inputs > 0 &&
node.n._def.outputs > 0;
}
}
RED.keyboard.add(/* ESCAPE */ 27,function(){
@@ -1844,6 +2120,31 @@ RED.view = (function() {
selection.link = selected_link;
}
return selection;
},
toggleShowGrid: function(state) {
if (state) {
grid.style("visibility","visible");
} else {
grid.style("visibility","hidden");
}
},
toggleSnapGrid: function(state) {
snapGrid = state;
redraw();
},
scale: function() {
return scaleFactor;
},
getLinksAtPoint: function(x,y) {
var result = [];
var links = outer.selectAll(".link_background")[0];
for (var i=0;i<links.length;i++) {
var bb = links[i].getBBox();
if (x >= bb.x && y >= bb.y && x <= bb.x+bb.width && y <= bb.y+bb.height) {
result.push(links[i])
}
}
return result;
}
};
})();

View File

@@ -28,7 +28,6 @@ RED.workspaces = (function() {
var tabId = RED.nodes.id();
do {
workspaceIndex += 1;
//TODO: nls of Sheet
} while($("#workspace-tabs a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0);
ws = {type:"tab",id:tabId,label:RED._('workspace.defaultName',{number:workspaceIndex})};
@@ -55,6 +54,7 @@ RED.workspaces = (function() {
historyEvent.workspaces = [ws];
RED.history.push(historyEvent);
RED.nodes.dirty(true);
RED.sidebar.config.refresh();
} else {
$( "#node-dialog-delete-workspace" ).dialog('option','workspace',ws);
$( "#node-dialog-delete-workspace-content" ).text(RED._("workspace.delete",{label:ws.label}));
@@ -141,8 +141,8 @@ RED.workspaces = (function() {
if (workspace.label != label) {
workspace_tabs.renameTab(workspace.id,label);
RED.nodes.dirty(true);
RED.sidebar.config.refresh();
$("#menu-item-workspace-menu-"+workspace.id.replace(".","-")).text(label);
// TODO: update entry in menu
}
$( this ).dialog( "close" );
}

View File

@@ -43,3 +43,8 @@ $workspace-button-color-disabled: #ccc;
$workspace-button-color-focus: #999;
$workspace-button-color-hover: #666;
$workspace-button-color-active: #666;
$workspace-button-color-selected: #AAA;
$typedInput-button-background: #efefef;
$typedInput-button-background-hover: #ddd;
$typedInput-button-background-active: #e3e3e3;

View File

@@ -186,7 +186,7 @@
.drag_line {
stroke: $node-selected-color;
stroke-width: 4;
stroke-width: 3;
fill: none;
pointer-events: none;
}
@@ -225,6 +225,9 @@
cursor: crosshair;
fill: none;
}
.link_splice > .link_line {
stroke-dasharray: 15,8;
}
g.link_selected path.link_line {
stroke: $node-selected-color;

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2015 IBM Corp.
* Copyright 2015, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,9 @@
font-size: 14px !important;
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif !important;
}
.ui-widget input {
box-shadow: none;
}
/* jQuery Theme overrides */
.ui-tabs .ui-tabs-panel {
@@ -73,3 +76,23 @@
.ui-dialog .ui-dialog-buttonpane {
padding: .3em 1em .5em 1em;
}
.ui-spinner {
border-radius: 4px;
padding: 0;
border: 1px solid $form-input-border-color;
}
.ui-spinner input {
margin: 0 17px 0 0;
padding: 6px;
border: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
&:focus {
outline: none;
}
}

View File

@@ -30,9 +30,9 @@
@mixin workspace-button {
@include disable-selection;
color: $workspace-button-color;
box-sizing: border-box;
display: inline-block;
color: $workspace-button-color;
background: $workspace-button-background;
border: 1px solid $secondary-border-color;
text-align: center;
@@ -57,7 +57,28 @@
background: $workspace-button-background-active;
text-decoration: none;
}
&.selected:not(.disabled) {
color: $workspace-button-color-selected;
background: $workspace-button-background-active;
cursor: default;
}
.button-group &:not(:first-child) {
border-left: none;
}
}
@mixin workspace-button-toggle {
@include workspace-button;
color: $workspace-button-color-selected;
background: $workspace-button-background-active;
&.selected:not(.disabled) {
color: $workspace-button-color;
background: $workspace-button-background;
}
}
@mixin component-footer {
border-top: 1px solid $primary-border-color;
background: #f3f3f3;

View File

@@ -33,6 +33,12 @@
border: 1px solid #325C80;
border-left-width: 16px;
}
.notification a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.notification-success {
border-color: #4B8400;

View File

@@ -114,11 +114,16 @@
}
.palette-header {
position: relative;
background: $palette-header-background;
cursor: pointer;
text-align: left;
padding: 9px;
font-weight: bold;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.palette-header i {
margin: 3px 10px 3px 3px;

View File

@@ -37,7 +37,6 @@
right: 0;
bottom: 25px;
left: 0px;
padding-top: 3px;
overflow-y: auto;
}
@@ -63,6 +62,33 @@
padding: 2px 8px;
}
.sidebar-header {
color: #666;
text-align: right;
padding: 8px 10px;
background: #f3f3f3;
border-bottom: 1px solid $secondary-border-color;
}
#sidebar-footer {
@include component-footer;
}
.sidebar-footer-button {
@include component-footer-button;
}
.sidebar-header-button {
@include workspace-button;
font-size: 13px;
line-height: 13px;
padding: 5px 8px;
}
.sidebar-header-button-toggle {
@include workspace-button-toggle;
font-size: 13px;
line-height: 13px;
padding: 5px 8px;
}
.sidebar-header-button:not(:first-child) {
border-left: none;
}

View File

@@ -41,6 +41,8 @@
@import "popover";
@import "flow";
@import "typedInput";
@import "dragdrop";
@import "keyboard";

View File

@@ -47,23 +47,35 @@
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
.config_node_type {
color: #999;
text-align: right;
padding-right: 3px;
&:not(:first-child) {
margin-top: 20px;
}
}
.config_node_none {
color: #ddd;
text-align:right;
padding-right: 3px;
}
.config_node_unused {
border-color: #aaa;
background: #f9f9f9;
border-style: dashed;
color: #aaa;
}
}
.config_node_type {
color: #999;
text-align: right;
padding-right: 3px;
&:not(:first-child) {
margin-top: 20px;
}
}
.config_node_none {
color: #ddd;
text-align:right;
padding-right: 3px;
}
.config_node_unused {
border-color: #aaa;
background: #f9f9f9;
border-style: dashed;
color: #aaa;
}
.config-node-filter-info {
position: absolute;
top: 0;
right:0;
height: 38px;
line-height: 38px;
padding: 0 8px;
background: $palette-header-background;
font-size: 0.8em;
color: #999;
font-weight: normal;
}

122
editor/sass/typedInput.scss Normal file
View File

@@ -0,0 +1,122 @@
/**
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
.red-ui-typedInput-container {
border: 1px solid $form-input-border-color;
border-radius: 4px;
height: 34px;
display: inline-block;
padding: 0;
margin: 0;
vertical-align: middle;
box-sizing: border-box;
overflow:hidden;
input {
padding: 0 0 0 1px;
margin:0;
height: 32px;
border:none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-shadow: none;
vertical-align: middle;
}
&.red-ui-typedInput-focus:not(.input-error) {
border-color: $form-input-focus-color !important;
}
a {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
padding: 0 1px 0 5px;
display:inline-block;
background: $typedInput-button-background;
height: 32px;
line-height: 32px;
vertical-align: middle;
color: #555;
i {
position:relative;
top:-3px;
margin-right:4px;
margin-top: 1px;
vertical-align: middle;
}
span {
display: inline-block;
height: 100%;
padding: 0 1px 0 5px;
}
&:hover {
text-decoration: none;
background: $typedInput-button-background-hover;
}
&:focus {
text-decoration: none;
}
&:active {
background: $typedInput-button-background-active;
text-decoration: none;
}
}
a.red-ui-typedInput-option-trigger {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
padding: 0 5px 0 0;
i {
margin-right: 0;
margin-left: 4px;
}
span {
background:#fff;
padding: 0 5px 0 5px;
}
}
}
.red-ui-typedInput-options {
@include component-shadow;
position: absolute;
border: 1px solid $primary-border-color;
background: #fff;
z-index: 2000;
a {
padding: 6px 18px 6px 6px;
display: block;
border-bottom: 1px solid $secondary-border-color;
color: #333;
&:hover {
text-decoration: none;
background: $typedInput-button-background-hover;
}
&:focus {
text-decoration: none;
}
&:active {
background: $typedInput-button-background-active;
text-decoration: none;
}
}
}

View File

@@ -35,7 +35,7 @@
</head>
<body spellcheck="false">
<div id="header">
<span class="logo">{{#header.url}}<a href="{{.}}">{{/header.url}}{{#header.image}}<img src="{{.}}">{{/header.image}} <span>{{ header.title }}</span>{{#header.url}}</a>{{/header.url}}</span>
<span class="logo">{{#header.url}}<a href="{{.}}">{{/header.url}}{{#header.image}}<img src="{{.}}" title="{{version}}">{{/header.image}} <span>{{ header.title }}</span>{{#header.url}}</a>{{/header.url}}</span>
<ul class="header-toolbar hide">
<li><a id="btn-sidemenu" class="button" data-toggle="dropdown" href="#"><i class="fa fa-bars"></i></a></li>
<ul>
@@ -104,9 +104,6 @@
<div id="node-dialog-confirm-deploy-unknown" style="text-align: left; padding-top: 10px;" data-i18n="[prepend]deploy.confirm.unknown;[append]deploy.confirm.confirm">
<ul style="font-size: 0.9em; width: 400px; margin: 10px auto; text-align: left;" id="node-dialog-confirm-deploy-unknown-list"></ul>
</div>
<div id="node-dialog-confirm-deploy-unused" style="text-align: left; padding-top: 10px;" data-i18n="[prepend]deploy.confirm.unusedConfig;[append]deploy.confirm.confirm">
<ul style="font-size: 0.9em; width: 400px; margin: 10px auto; text-align: left;" id="node-dialog-confirm-deploy-unused-list"></ul>
</div>
</form>
</div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -52,8 +52,8 @@
<!-- node in the palette. -->
<p>Simple sample input node. Just sends a single message when it starts up.
This is not very useful.</p>
<p>Outputs an object called <b>msg</b> containing <b>msg.topic</b> and
<b>msg.payload</b>. msg.payload is a String.</p>
<p>Outputs an object called <code>msg</code> containing <code>msg.topic</code> and
<code>msg.payload</code>. msg.payload is a String.</p>
</script>
<!-- Finally, the node type is registered along with all of its properties -->

View File

@@ -22,10 +22,11 @@
</script>
<script type="text/x-red" data-help-name="sentiment">
<p>Analyses the <b>msg.payload</b> and adds a <b>msg.sentiment</b> object that contains the resulting AFINN-111 sentiment score as <b>msg.sentiment.score</b>.</p>
<p>Analyses the <code>msg.payload</code> and adds a <code>msg.sentiment</code> object
that contains the resulting AFINN-111 sentiment score as <code>msg.sentiment.score</code>.</p>
<p>A score greater than zero is positive and less than zero is negative.</p>
<p>The score typically ranges from -5 to +5, but can go higher and lower.</p>
<p>An object of word score overrides can be supplied as <b>msg.overrides</b>.</p>
<p>An object of word score overrides can be supplied as <code>msg.overrides</code>.</p>
<p>See <a href="https://github.com/thisandagain/sentiment/blob/master/README.md" target="_new">the Sentiment docs here</a>.</p>
</script>

View File

@@ -15,18 +15,10 @@
-->
<script type="text/x-red" data-template-name="inject">
<div class="form-row node-input-payload">
<label for="node-input-payloadType"><i class="fa fa-envelope"></i> <span data-i18n="common.label.payload"></span></label>
<select id="node-input-payloadType" style="width:73%">
<option value="date" data-i18n="inject.timestamp"></option>
<option value="string" data-i18n="inject.string"></option>
<option value="none" data-i18n="inject.blank"></option>
</select>
</div>
<div class="form-row" id="node-input-row-payload">
<label for="node-input-payload"></label>
<input type="text" id="node-input-payload" style="width:70%">
<div class="form-row">
<label for="node-input-payload"><i class="fa fa-envelope"></i> <span data-i18n="common.label.payload"></span></label>
<input type="text" id="node-input-payload" style="width:300px">
<input type="hidden" id="node-input-payloadType">
</div>
<div class="form-row">
@@ -150,13 +142,7 @@
.inject-time-times {
width: 90px;
}
.inject-time-row > .ui-spinner {
height: 28px;
margin: 3px 0;
border-color: rgb(204, 204, 204);
}
#inject-time-time {
margin-top: 3px;
width: 75px;
}
.inject-time-count {
@@ -165,12 +151,15 @@
</style>
<script type="text/x-red" data-help-name="inject">
<p>Pressing the button on the left side of the node allows a message on a topic
to be injected into the flow. This is mainly for test purposes.</p>
to be injected into the flow.</p>
<p>The payload defaults to the current time in millisecs since 1970, but can
also be set to a String or left blank.</p>
also be set to various other javascript types.</p>
<p>The repeat function allows the payload to be sent on the required schedule.</p>
<p>The <i>Fire once at start</i> option actually waits a short interval before firing
<p>The <i>Inject once at start</i> option actually waits a short interval before firing
to give other nodes a chance to instantiate properly.</p>
<p>The <i>Flow</i> and <i>Global</i> options allow one to inject a flow or global
context value.
</p>
<p><b>Note: </b>"Interval between times" and "at a specific time" uses cron.
This means that 20 minutes will be at the next hour, 20 minutes past and
40 minutes past - not in 20 minutes time. If you want every 20 minutes
@@ -186,7 +175,22 @@
defaults: {
name: {value:""},
topic: {value:""},
payload: {value:""},
payload: {value:"", validate:function(v) {
var ptype = $("#node-input-payloadType").val() || this.payloadType;
if (ptype === 'json') {
try {
JSON.parse(v);
return true;
} catch(err) {
return false;
}
} else if (ptype === 'flow' || ptype === 'global' ) {
return /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]+)*/i.test(v);
} else if (ptype === 'num') {
return /^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v);
}
return true;
}},
payloadType: {value:"date"},
repeat: {value:""},
crontab: {value:""},
@@ -196,23 +200,51 @@
outputs:1,
icon: "inject.png",
label: function() {
if (this.payloadType === "string") {
if (this.name) {
return this.name;
} else if (this.payloadType === "string" ||
this.payloadType === "str" ||
this.payloadType === "num" ||
this.payloadType === "bool" ||
this.payloadType === "json") {
if ((this.topic !== "") && ((this.topic.length + this.payload.length) <= 32)) {
return this.name||this.topic + ":" + this.payload;
}
else if (this.payload.length < 24) {
return this.name||this.payload;
return this.topic + ":" + this.payload;
} else if (this.payload.length > 0 && this.payload.length < 24) {
return this.payload;
} else {
return this._("inject.inject");
}
} else if (this.payloadType === 'date') {
return this._("inject.timestamp")
} else if (this.payloadType === 'flow' && this.payload.length < 19) {
return 'flow.'+this.payload;
} else if (this.payloadType === 'global' && this.payload.length < 17) {
return 'global.'+this.payload;
} else {
return this._("inject.inject");
}
if ((this.topic.length < 24) && (this.topic.length > 0)) {
return this.name||this.topic;
}
else { return this.name||(this.payloadType==="date"?this._("inject.timestamp"):null)||this._("inject.inject"); }
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.payloadType == null) {
if (this.payload == "") {
this.payloadType = "date";
} else {
this.payloadType = "str";
}
} else if (this.payloadType === 'string' || this.payloadType === 'none') {
this.payloadType = "str";
}
$("#node-input-payloadType").val(this.payloadType);
$("#node-input-payload").typedInput({
default: 'str',
typeField: $("#node-input-payloadType"),
types:['flow','global','str','num','bool','json',{value:"date",label:this._("inject.timestamp"),hasValue:false}]
});
$("#inject-time-type-select").change(function() {
$("#node-input-crontab").val('');
var id = $("#inject-time-type-select option:selected").val();
@@ -373,24 +405,8 @@
$("#inject-time-type-select option").filter(function() {return $(this).val() == repeattype;}).attr('selected',true);
$("#inject-time-row-"+repeattype).show();
if (this.payloadType == null) {
if (this.payload == "") {
this.payloadType = "date";
} else {
this.payloadType = "string";
}
}
$("#node-input-payload").typedInput('type',this.payloadType);
$("#node-input-payloadType").change(function() {
var id = $("#node-input-payloadType option:selected").val();
if (id === "string") {
$("#node-input-row-payload").show();
} else {
$("#node-input-row-payload").hide();
}
});
$("#node-input-payloadType").val(this.payloadType);
$("#node-input-payloadType").change();
$("#inject-time-type-select").change();
$("#inject-time-interval-time-start").change();

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,16 +50,22 @@ module.exports = function(RED) {
}
this.on("input",function(msg) {
var msg = {topic:this.topic};
if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") {
msg.payload = Date.now();
} else if (this.payloadType == null || this.payloadType === "string") {
msg.payload = this.payload;
} else {
msg.payload = "";
try {
msg = {topic:this.topic};
if ( (this.payloadType == null && this.payload === "") || this.payloadType === "date") {
msg.payload = Date.now();
} else if (this.payloadType == null) {
msg.payload = this.payload;
} else if (this.payloadType == 'none') {
msg.payload = "";
} else {
msg.payload = RED.util.evaluateNodeProperty(this.payload,this.payloadType,this,msg);
}
this.send(msg);
msg = null;
} catch(err) {
this.error(err,msg);
}
this.send(msg);
msg = null;
});
}

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2015 IBM Corp.
Copyright 2015, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@
<option value="target" data-i18n="catch.scope.selected"></options>
</select>
</div>
<div class="form-row node-input-target-row" style="display: none;">
<div class="form-row node-input-target-row" style="display: none;min-height: 100px;">
<div id="node-input-catch-target-container-div" style="position: relative; box-sizing: border-box; border-radius: 2px; height: 180px; border: 1px solid #ccc;overflow:hidden; ">
<div style="box-sizing: border-box; line-height: 20px; font-size: 0.8em; border-bottom: 1px solid #ddd; height: 20px;">
<input type="checkbox" data-i18n="[title]catch.label.selectAll" id="node-input-target-node-checkbox-all" style="width: 30px; margin: 0 2px 1px 2px;">
@@ -106,7 +106,7 @@
outputs:1,
icon: "alert.png",
label: function() {
return this.name||this.scope?this._("catch.catchNodes",{number:this.scope.length}):this._("catch.catch");
return this.name||(this.scope?this._("catch.catchNodes",{number:this.scope.length}):this._("catch.catch"));
},
labelStyle: function() {
return this.name?"node_label_italic":"";
@@ -114,6 +114,17 @@
oneditprepare: function() {
var nodeList = $("#node-input-catch-target-container");
var node = this;
this.resize = function() {
var rows = $("#dialog-form>div:not(.node-input-target-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-target-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-catch-target-container-div").css("height",height+"px");
}
function createNodeList() {
var scope = node.scope || [];
nodeList.empty();
@@ -238,6 +249,7 @@
} else {
$(".node-input-target-row").hide();
}
node.resize();
});
if (this.scope == null) {
$("#node-input-scope-select").val("all");
@@ -245,31 +257,6 @@
$("#node-input-scope-select").val("target");
}
$("#node-input-scope-select").change();
function dialogResize() {
var rows = $("#dialog-form>div:not(.node-input-target-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-target-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-catch-target-container-div").css("height",height+"px");
};
$( "#dialog" ).on("dialogresize", dialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-catch');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
dialogResize();
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
$( "#dialog" ).off("dialogresize",dialogResize);
});
},
oneditsave: function() {
var scope = $("#node-input-scope-select").children("option:selected").val();
@@ -283,8 +270,10 @@
node.scope.push($(this).data('node-id'));
}
})
}
},
oneditresize: function(size) {
this.resize();
}
});
</script>

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2015 IBM Corp.
Copyright 2015, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@
<option value="target" data-i18n="status.scope.selected"></options>
</select>
</div>
<div class="form-row node-input-target-row" style="display: none;">
<div class="form-row node-input-target-row" style="display: none; min-height: 100px;">
<div id="node-input-status-target-container-div" style="position: relative; box-sizing: border-box; border-radius: 2px; height: 180px; border: 1px solid #ccc;overflow:hidden; ">
<div style="box-sizing: border-box; line-height: 20px; font-size: 0.8em; border-bottom: 1px solid #ddd; height: 20px;">
<input type="checkbox" data-i18n="[title]status.label.selectAll" id="node-input-target-node-checkbox-all" style="width: 30px; margin: 0 2px 1px 2px;">
@@ -97,7 +97,7 @@
outputs:1,
icon: "alert.png",
label: function() {
return this.name||this.scope?this._("status.statusNodes",{number:this.scope.length}):this._("status.status");
return this.name||(this.scope?this._("status.statusNodes",{number:this.scope.length}):this._("status.status"));
},
labelStyle: function() {
return this.name?"node_label_italic":"";
@@ -105,6 +105,17 @@
oneditprepare: function() {
var nodeList = $("#node-input-status-target-container");
var node = this;
this.resize = function() {
var rows = $("#dialog-form>div:not(.node-input-target-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-target-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-status-target-container-div").css("height",height+"px");
}
function createNodeList() {
var scope = node.scope || [];
nodeList.empty();
@@ -219,8 +230,6 @@
$(".node-input-target-node-checkbox").prop('checked',this.checked);
})
$("#node-input-scope-select").change(function(e) {
var scope = $(this).children("option:selected").val();
if (scope === "target") {
@@ -229,6 +238,7 @@
} else {
$(".node-input-target-row").hide();
}
node.resize();
});
if (this.scope == null) {
$("#node-input-scope-select").val("all");
@@ -236,31 +246,6 @@
$("#node-input-scope-select").val("target");
}
$("#node-input-scope-select").change();
function dialogResize() {
var rows = $("#dialog-form>div:not(.node-input-target-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-target-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-status-target-container-div").css("height",height+"px");
};
$( "#dialog" ).on("dialogresize", dialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-status');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
dialogResize();
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
$( "#dialog" ).off("dialogresize",dialogResize);
});
},
oneditsave: function() {
var scope = $("#node-input-scope-select").children("option:selected").val();
@@ -276,6 +261,9 @@
})
}
},
oneditresize: function(size) {
this.resize();
}
});
</script>

View File

@@ -39,13 +39,14 @@
</script>
<script type="text/x-red" data-help-name="debug">
<p>The Debug node can be connected to the output of any node. It can be used to display the output of any message property in the debug tab of the sidebar. The default is to display <b>msg.payload</b>.</p>
<p>Each message will also display the timestamp, <b>msg.topic</b> and the property chosen to output.</p>
<p>The Debug node can be connected to the output of any node. It can be used to display the output of any message
property in the debug tab of the sidebar. The default is to display <code>msg.payload</code>.</p>
<p>Each message will also display the timestamp, <code>msg.topic</code> and the type of property chosen to output.</p>
<p>The sidebar can be accessed under the options drop-down in the top right corner.</p>
<p>The button to the right of the node will toggle its output on and off so you can de-clutter the debug window.</p>
<p>If the payload is an object or buffer it will be stringified first for display and indicate that by saying "(Object)" or "(Buffer)".</p>
<p>Selecting any particular message will highlight (in red) the debug node that reported it. This is useful if you wire up multiple debug nodes.</p>
<p>Optionally can show the complete <b>msg</b> object.</p>
<p>Optionally can show the complete <code>msg</code> object, and send messages to the console log.</p>
<p>In addition any calls to node.warn or node.error will appear here.</p>
</script>
@@ -127,17 +128,10 @@
}
},
onpaletteadd: function() {
var content = document.createElement("div");
$(content).css({"position":"relative","height":"100%"});
var toolbar = document.createElement("div");
toolbar.id = "debug-toolbar";
content.appendChild(toolbar);
var content = $("<div>").css({"position":"relative","height":"100%"});
var toolbar = $('<div class="sidebar-header"><a id="debug-tab-clear" title="clear log" class="button" href="#"><i class="fa fa-trash"></i></a></div>').appendTo(content);
toolbar.innerHTML = '<div class="pull-right"><a id="debug-tab-clear" title="clear log" class="button" href="#"><i class="fa fa-trash"></i></a></div> ';
var messages = document.createElement("div");
messages.id = "debug-content";
content.appendChild(messages);
var messages = $('<div id="debug-content"/>').appendTo(content);
RED.sidebar.addTab({
id: "debug",
@@ -221,9 +215,9 @@
'</span>';
}
msg.innerHTML += '<span class="debug-message-payload">'+ payload+ '</span>';
var atBottom = (sbc.scrollHeight-messages.offsetHeight-sbc.scrollTop) < 5;
var atBottom = (sbc.scrollHeight-messages.height()-sbc.scrollTop) < 5;
messageCount++;
$(messages).append(msg);
messages.append(msg);
if (messageCount > 200) {
$("#debug-content .debug-message:first").remove();
@@ -257,17 +251,12 @@
<style>
#debug-content {
position: absolute;
top: 30px;
top: 43px;
bottom: 0px;
left:0px;
right: 0px;
overflow-y: scroll;
}
#debug-toolbar {
padding: 3px 10px;
height: 24px;
background: #f3f3f3;
}
.debug-message {
cursor: pointer;
border-bottom: 1px solid #eee;

View File

@@ -124,7 +124,6 @@ module.exports = function(RED) {
if (msg.msg.length > debuglength) {
msg.msg = msg.msg.substr(0,debuglength) +" ....";
}
RED.comms.publish("debug",msg);
}

View File

@@ -44,7 +44,7 @@
<p>Provides 3 outputs... stdout, stderr, and return code.</p>
<p>By default uses exec() which calls the command, then gets a callback on completion, returning the complete result in one message, along with any errors.</p>
<p>Optionally can use spawn() instead, which returns the output from stdout and stderr as the command runs (usually one line at a time). On completion it then returns a return code (on the 3rd output).</p>
<p>The optional append gets added to the command after the <b>msg.payload</b> - so you can do things like pipe the result to another command.</p>
<p>The optional append gets added to the command after the <code>msg.payload</code> - so you can do things like pipe the result to another command.</p>
<p>Parameters with spaces should be enclosed in quotes - <i>"This is a single parameter"</i></p>
<p>If stdout is binary a <i>buffer</i> is returned - otherwise returns a <i>string</i>.</p>
<p>The blue status icon will be visible while the node is active.</p>

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2013, 2015 IBM Corp.
Copyright 2013, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@
</div>
<div class="form-row">
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
<input id="node-input-outputs" style="width: 60px; height: 1.7em;" value="1">
<input id="node-input-outputs" style="width: 60px;" value="1">
</div>
<div class="form-tips"><span data-i18n="function.tip"></span></div>
</script>
@@ -88,31 +88,6 @@
min:1
});
function functionDialogResize() {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
that.editor.resize();
}
$( "#dialog" ).on("dialogresize", functionDialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-function');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
functionDialogResize();
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
var height = $( "#dialog" ).dialog('option','height');
$( "#dialog" ).off("dialogresize",functionDialogResize);
});
this.editor = RED.editor.createEditor({
id: 'node-input-func-editor',
mode: 'ace/mode/javascript',
@@ -141,6 +116,17 @@
}
$("#node-input-func").val(this.editor.getValue());
delete this.editor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
}
});
</script>

View File

@@ -90,7 +90,34 @@ module.exports = function(RED) {
}
},
context: {
global:RED.settings.functionGlobalContext || {}
set: function() {
node.context().set.apply(node,arguments);
},
get: function() {
return node.context().get.apply(node,arguments);
},
get global() {
return node.context().global;
},
get flow() {
return node.context().flow;
}
},
flow: {
set: function() {
node.context().flow.set.apply(node,arguments);
},
get: function() {
return node.context().flow.get.apply(node,arguments);
}
},
global: {
set: function() {
node.context().global.set.apply(node,arguments);
},
get: function() {
return node.context().global.get.apply(node,arguments);
}
},
setTimeout: function () {
var func = arguments[0];

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2013, 2015 IBM Corp.
Copyright 2013, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,29 +19,41 @@
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row" style="margin-bottom: 0px;">
<div class="form-row">
<label for="node-input-field"><i class="fa fa-edit"></i> <span data-i18n="template.label.property"></span></label>
<input type="text" id="node-input-field" placeholder="payload" style="width:250px;">
<input type="hidden" id="node-input-fieldType">
</div>
<div class="form-row" style="position: relative; margin-bottom: 0px;">
<label for="node-input-template"><i class="fa fa-file-code-o"></i> <span data-i18n="template.label.template"></span></label>
<input type="hidden" id="node-input-template" autofocus="autofocus">
<select id="node-input-format" style=" font-size: 0.8em; margin-bottom: 3px; width:110px; float:right;">
<option value="handlebars">mustache</option>
<option value="html">HTML</option>
<option value="json">JSON</option>
<option value="markdown">Markdown</option>
<option value="text">none</option>
</select>
<div style="position: absolute; right:0;display:inline-block; text-align: right; font-size: 0.8em;">
<span data-i18n="template.label.format"></span>:
<select id="node-input-format" style="width:110px; font-size: 10px !important; height: 24px; padding:0;">
<option value="handlebars">mustache</option>
<option value="html">HTML</option>
<option value="json">JSON</option>
<option value="markdown">Markdown</option>
<option value="text">none</option>
</select>
</div>
</div>
<div class="form-row node-text-editor-row">
<div style="height: 250px;" class="node-text-editor" id="node-input-template-editor" ></div>
</div>
<div class="form-row">
<label for="node-input-field"><i class="fa fa-edit"></i> <span data-i18n="template.label.property"></span></label>
msg.<input type="text" id="node-input-field" placeholder="payload" style="width:170px;">
<label for="node-input-syntax"><i class="fa fa-code"></i> <span data-i18n="template.label.syntax"></span></label>
<select id="node-input-syntax" style="width:180px;">
<option value="mustache" data-i18n="template.label.mustache"></option>
<option value="plain" data-i18n="template.label.plain"></option>
</select>
</div>
</script>
<script type="text/x-red" data-help-name="template">
<p>Creates a new message based on the provided template.</p>
<p>This uses the <i><a href="http://mustache.github.io/mustache.5.html" target="_new">mustache</a></i> format.</p>
<p>Sets a property based on the provided template.</p>
<p>By default this uses the <i><a href="http://mustache.github.io/mustache.5.html" target="_new">mustache</a></i>
format, but this can be switched off if required.</p>
<p>For example, when a template of:
<pre>Hello {{name}}. Today is {{date}}</pre>
<p>receives a message containing:
@@ -50,8 +62,10 @@
date: "Monday"
payload: ...
}</pre>
<p>The resulting payload will be:
<p>The resulting property will be:
<pre>Hello Fred. Today is Monday</pre>
<p>By default, mustache will escape any HTML entities in the values it substitutes.
To prevent this, use <code>{{{triple}}}</code> braces.
</script>
<script type="text/javascript">
@@ -61,7 +75,9 @@
defaults: {
name: {value:""},
field: {value:"payload"},
fieldType: {value:"msg"},
format: {value:"handlebars"},
syntax: {value:"mustache"},
template: {value:"This is the payload: {{payload}} !"},
},
inputs:1,
@@ -72,30 +88,19 @@
},
oneditprepare: function() {
var that = this;
function templateDialogResize() {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
that.editor.resize();
};
$( "#dialog" ).on("dialogresize", templateDialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-template');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
templateDialogResize();
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
var height = $( "#dialog" ).dialog('option','height');
$( "#dialog" ).off("dialogresize",templateDialogResize);
if (!this.fieldType) {
this.fieldType = 'msg';
}
if (!this.syntax) {
this.syntax = 'mustache';
$("#node-input-syntax").val(this.syntax);
}
$("#node-input-field").typedInput({
default: 'msg',
types: ['msg','flow','global'],
typeField: $("#node-input-fieldType")
});
this.editor = RED.editor.createEditor({
id: 'node-input-template-editor',
mode: 'ace/mode/html',
@@ -108,7 +113,7 @@
fields:['name','outputs']
});
this.editor.focus();
$("#node-input-format").change(function() {
var mod = "ace/mode/"+$("#node-input-format").val();
that.editor.getSession().setMode({
@@ -120,6 +125,17 @@
oneditsave: function() {
$("#node-input-template").val(this.editor.getValue())
delete this.editor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
}
});
</script>

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,33 +23,26 @@ module.exports = function(RED) {
this.name = n.name;
this.field = n.field || "payload";
this.template = n.template;
this.syntax = n.syntax || "mustache";
this.fieldType = n.fieldType || "msg";
var node = this;
var b = node.field.split(".");
var i = 0;
var m = null;
var rec = function(obj) {
i += 1;
if ((i < b.length) && (typeof obj[b[i-1]] === "object")) {
rec(obj[b[i-1]]); // not there yet - carry on digging
}
else {
if (i === b.length) { // we've finished so assign the value
obj[b[i-1]] = mustache.render(node.template,m);
node.send(m);
}
else {
obj[b[i-1]] = {}; // needs to be a new object so create it
rec(obj[b[i-1]]); // and carry on digging
}
}
}
node.on("input", function(msg) {
try {
m = msg;
i = 0;
rec(msg);
var value;
if (node.syntax === "mustache") {
value = mustache.render(node.template,msg);
} else {
value = node.template;
}
if (node.fieldType === 'msg') {
RED.util.setMessageProperty(msg,node.field,value);
} else if (node.fieldType === 'flow') {
node.context().flow.set(node.field,value);
} else if (node.fieldType === 'global') {
node.context().global.set(node.field,value);
}
node.send(msg);
} catch(err) {
node.error(err.message);
}

View File

@@ -14,21 +14,21 @@
limitations under the License.
-->
<!-- First, the content of the edit dialog is defined. -->
<script type="text/x-red" data-template-name="delay">
<div class="form-row">
<label for="node-input-pauseType"><i class="fa fa-tasks"></i> <span data-i18n="delay.action"></span></label>
<select id="node-input-pauseType" style="width:270px !important">
<option value="delay" data-i18n="delay.delaymsg"></option>
<option value="random" data-i18n="delay.ramdomdelay"></option>
<option value="rate" data-i18n="delay.limitrate"></option>
<option value="queue" data-i18n="delay.fairqueue"></option>
<option value="delay" data-i18n="delay.delaymsg"></option>
<option value="random" data-i18n="delay.randomdelay"></option>
<option value="rate" data-i18n="delay.limitrate"></option>
<option value="queue" data-i18n="delay.fairqueue"></option>
<option value="timed" data-i18n="delay.timedqueue"></option>
</select>
</div>
<div id="delay-details" class="form-row">
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="delay.for"></span></label>
<input type="text" id="node-input-timeout" placeholder="Time" style="direction:rtl; width:50px !important">
<input type="text" id="node-input-timeout" placeholder="Time" style="text-align:end; width:50px !important">
<select id="node-input-timeoutUnits" style="width:200px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
@@ -40,7 +40,7 @@
<div id="rate-details" class="form-row">
<label for="node-input-rate"><i class="fa fa-clock-o"></i> <span data-i18n="delay.rate"></span></label>
<input type="text" id="node-input-rate" placeholder="1" style="direction:rtl; width:30px !important">
<input type="text" id="node-input-rate" placeholder="1" style="text-align:end; width:30px !important">
<label for="node-input-rateUnits"><span data-i18n="delay.msgper"></span></label>
<select id="node-input-rateUnits" style="width:140px !important">
<option value="second" data-i18n="delay.sec"></option>
@@ -55,9 +55,9 @@
<div id="random-details" class="form-row">
<label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> <span data-i18n="delay.between"></span></label>
<input type="text" id="node-input-randomFirst" placeholder="" style="directon:rtl; width:30px !important">
<label for="node-input-randomLast" style="width:20px"> &amp; </label>
<input type="text" id="node-input-randomLast" placeholder="" style="directon:rtl; width:30px !important">
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:30px !important">
&nbsp;&nbsp;&amp;&nbsp;&nbsp;
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:30px !important">
<select id="node-input-randomUnits" style="width:140px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
@@ -74,25 +74,26 @@
</script>
<!-- Next, some simple help text is provided for the node. -->
<script type="text/x-red" data-help-name="delay">
<p>Introduces a delay into a flow or rate limits messages.</p>
<p>The default delay is 5 seconds and rate limit of 1 msg/second, but both can be configured.</p>
<p>When set to rate limit messages, they are spread across the configured time period. It can
also be set to discard any intermediate messages that arrive.</p>
<p>The "topic based fair queue" adds messages to a release queue tagged by their <b>msg.topic</b> property.
<p>The "topic based fair queue" adds messages to a release queue tagged by their <code>msg.topic</code> property.
At each "tick", derived from the rate, the next "topic" is released.
Any messages arriving on the same topic before release replace those in that position in the queue.
So each "topic" gets a turn - but the most recent value is always the one sent.</p>
<p>The "timed release queue" adds messages to an array based on their <code>msg.topic</code> property.
At each "tick", all the latest messages are released. Any new messages arriving before release will
replace those of the same topic.</p>
</script>
<!-- Finally, the node type is registered along with all of its properties -->
<script type="text/javascript">
RED.nodes.registerType('delay',{
category: 'function', // the palette category
category: 'function',
color:"#E6E0F8",
defaults: { // defines the editable properties of the node
name: {value:""}, // along with default values.
defaults: {
name: {value:""},
pauseType: {value:"delay", required:true},
timeout: {value:"5", required:true, validate:RED.validators.number()},
timeoutUnits: {value:"seconds"},
@@ -103,10 +104,10 @@
randomUnits: {value: "seconds"},
drop: {value:false}
},
inputs:1, // set the number of inputs - only 0 or 1
outputs:1, // set the number of outputs - 0 to n
icon: "timer.png", // set the icon (held in public/icons)
label: function() { // sets the default label contents
inputs:1,
outputs:1,
icon: "timer.png",
label: function() {
if (this.pauseType == "delay") {
var units = this.timeoutUnits ? this.timeoutUnits.charAt(0) : "s";
if (this.timeoutUnits == "milliseconds") { units = "ms"; }
@@ -117,12 +118,15 @@
} else if (this.pauseType == "random") {
return this.name || this._("delay.label.random");
}
else if (this.pauseType == "timed") {
return this.name || this.rate+" "+this._("delay.label.timed")+" "+this.rateUnits;
}
else {
var units = this.rateUnits ? this.rateUnits.charAt(0) : "s";
return this.name || this._("delay.label.queue")+" "+this.rate+" msg/"+units;
}
},
labelStyle: function() { // sets the class to apply to the label
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
@@ -152,6 +156,11 @@
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
} else if (this.pauseType == "timed") {
$("#delay-details").hide();
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
}
if (!this.timeoutUnits) {
@@ -187,6 +196,11 @@
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
} else if (this.value == "timed") {
$("#delay-details").hide();
$("#rate-details").show();
$("#random-details").hide();
$("#node-input-dr").hide();
}
});
}

View File

@@ -147,10 +147,17 @@ module.exports = function(RED) {
node.status({});
});
} else if (this.pauseType === "queue") {
} else if ((this.pauseType === "queue") || (this.pauseType === "timed")) {
this.intervalID = setInterval(function() {
if (node.buffer.length > 0) {
node.send(node.buffer.shift()); // send the first on the queue
if (this.pauseType === "queue") {
if (node.buffer.length > 0) {
node.send(node.buffer.shift()); // send the first on the queue
}
}
else {
while (node.buffer.length > 0) { // send the whole queue
node.send(node.buffer.shift());
}
}
node.status({text:node.buffer.length});
},node.rate);

View File

@@ -19,6 +19,7 @@
<span data-i18n="trigger.send"></span>
<select id="node-input-op1type" style="width:200px !important">
<option value="val" data-i18n="trigger.output.string"></option>
<option value="num" data-i18n="trigger.output.number"></option>
<option value="pay" data-i18n="trigger.output.existing"></option>
<option value="nul" data-i18n="trigger.output.nothing"></option>
</select>
@@ -31,7 +32,7 @@
<option value="wait" data-i18n="trigger.wait-for"></option>
</select>
<span class="node-type-wait">
<input type="text" id="node-input-duration" style="text-align:right; width:70px !important">
<input type="text" id="node-input-duration" style="text-align:end; width:70px !important">
<select id="node-input-units" style="width:140px !important">
<option value="ms" data-i18n="trigger.duration.ms"></option>
<option value="s" data-i18n="trigger.duration.s"></option>
@@ -44,35 +45,41 @@
<span data-i18n="trigger.then-send"></span>
<select id="node-input-op2type" style="width:200px !important">
<option value="val" data-i18n="trigger.output.string"></option>
<option value="num" data-i18n="trigger.output.number"></option>
<option value="pay" data-i18n="trigger.output.existing"></option>
<option value="nul" data-i18n="trigger.output.nothing"></option>
</select>
<input style="width: 145px !important" type="text" id="node-input-op2">
</div>
<div class="form-row node-type-wait">
<input type="checkbox" id="node-input-extend" style="margin-left: 5px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;" for="node-input-extend" data-i18n="trigger.extend"></label>
<input type="checkbox" id="node-input-extend" style="margin-left: 0px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;" for="node-input-extend" data-i18n="trigger.extend"></label>
</div>
<div class="form-row">
<span data-i18n="trigger.label.reset"></span><input type="text" id="node-input-reset" style="width:240px" data-i18n="[placeholder]trigger.label.resetprompt">
</div>
<br/>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></input>
</div>
<div class="form-tips" data-i18n="[html]trigger.tip"></div>
</script>
<script type="text/x-red" data-help-name="trigger">
<p>Creates two messages on the output separated by a timeout whenever ANY <b>msg</b> arrives on the input.</p>
<p>Creates two messages on the output separated by a timeout whenever <i>any</i> <code>msg</code> arrives on the input.</p>
<p>For example, this can be used to toggle a Raspberry PI GPIO pin on and off.</p>
<p>The two output states can be specified as can the duration of the timer.
Either output can be set to a value, or templated from the inbound
<b>msg</b> using mustache syntax. <pre>The payload is {{payload}}</pre></p>
<p>If the payload is an object then setting the output to <i>existing payload</i> will pass the complete payload object through.</p>
<code>msg</code> using mustache syntax. <pre>The payload is {{payload}}</pre></p>
<p>If the <code>msg.payload</code> is an object then setting the output to
<i>existing payload</i> will pass the complete payload object through.</p>
<p>Optionally the timer can be extended by being retriggered... or not.</p>
<p>By setting the first output to <i>nothing</i>, and selecting extend timer - a watchdog timer can be created.
No output will happen as long as repeated inputs occur within the timeout period.</p>
<p>Setting the timer to 0 creates an "infinite" timeout - the first output will happen but the second
never will, and neither can the first be retriggered - so a true one shot.</p>
<p>If a <b>msg.reset</b> property is present any timeout currently in progress
<p>If a <code>msg.reset</code> property is present, or the <code>msg.payload</code>
matches the optional reset value, any timeout currently in progress
will be cleared and the second output will not happen.</p>
<p>The blue status icon will be visible while the node is active.</p>
</script>
@@ -88,7 +95,8 @@
op2type: {value:"val"},
duration: {value:"250",required:true,validate:RED.validators.number()},
extend: {value:"false"},
units: {value: "ms"},
units: {value:"ms"},
reset: {value:""},
name: {value:""}
},
inputs:1,
@@ -116,14 +124,14 @@
}
});
$("#node-input-op1type").change(function() {
if ($(this).val() == "val") {
if (($(this).val() == "val")||($(this).val() == "num")) {
$("#node-input-op1").show();
} else {
$("#node-input-op1").hide();
}
});
$("#node-input-op2type").change(function() {
if ($(this).val() == "val") {
if (($(this).val() == "val")||($(this).val() == "num")) {
$("#node-input-op2").show();
} else {
$("#node-input-op2").hide();

View File

@@ -25,6 +25,7 @@ module.exports = function(RED) {
this.op2type = n.op2type || "val";
this.extend = n.extend || "false";
this.units = n.units || "ms";
this.reset = n.reset || '';
this.duration = n.duration || 250;
if (this.duration <= 0) { this.duration = 0; }
else {
@@ -34,24 +35,24 @@ module.exports = function(RED) {
}
this.op1Templated = this.op1.indexOf("{{") != -1;
this.op2Templated = this.op2.indexOf("{{") != -1;
if (!isNaN(this.op1)) { this.op1 = Number(this.op1); }
if (!isNaN(this.op2)) { this.op2 = Number(this.op2); }
if ((this.op1type === "num") && (!isNaN(this.op1))) { this.op1 = Number(this.op1); }
if ((this.op2type === "num") && (!isNaN(this.op2))) { this.op2 = Number(this.op2); }
if (this.op1 == "true") { this.op1 = true; }
if (this.op2 == "true") { this.op2 = true; }
if (this.op1 == "false") { this.op1 = false; }
if (this.op2 == "false") { this.op2 = false; }
if (this.op1 == "null") { this.op1 = null; }
if (this.op2 == "null") { this.op2 = null; }
try { this.op1 = JSON.parse(this.op1); }
catch(e) { this.op1 = this.op1; }
try { this.op2 = JSON.parse(this.op2); }
catch(e) { this.op2 = this.op2; }
//try { this.op1 = JSON.parse(this.op1); }
//catch(e) { this.op1 = this.op1; }
//try { this.op2 = JSON.parse(this.op2); }
//catch(e) { this.op2 = this.op2; }
var node = this;
var tout = null;
var m2;
this.on("input", function(msg) {
if (msg.hasOwnProperty("reset")) {
if (msg.hasOwnProperty("reset") || ((node.reset !== '')&&(msg.payload == node.reset)) ) {
clearTimeout(tout);
tout = null;
node.status({});
@@ -90,8 +91,8 @@ module.exports = function(RED) {
this.on("close", function() {
if (tout) {
clearTimeout(tout);
node.status({});
}
node.status({});
});
}
RED.nodes.registerType("trigger",TriggerNode);

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2013, 2015 IBM Corp.
Copyright 2013, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -55,33 +55,6 @@
},
oneditprepare: function() {
var that = this;
$( "#node-input-outputs" ).spinner({
min:1
});
function functionDialogResize() {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
that.editor.resize();
};
$( "#dialog" ).on("dialogresize", functionDialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-function');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
functionDialogResize();
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
var height = $( "#dialog" ).dialog('option','height');
$( "#dialog" ).off("dialogresize",functionDialogResize);
});
this.editor = RED.editor.createEditor({
id: 'node-input-info-editor',
mode: 'ace/mode/markdown',
@@ -92,6 +65,17 @@
oneditsave: function() {
$("#node-input-info").val(this.editor.getValue());
delete this.editor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$(".node-text-editor").css("height",height+"px");
this.editor.resize();
}
});
</script>

View File

@@ -41,16 +41,18 @@
</div>
<div class="form-row">
<label for="node-input-intype"><i class="fa fa-level-up"></i> <span data-i18n="rpi-gpio.label.resistor"></span></label>
<select type="text" id="node-input-intype" style="width: 150px;">
<select type="text" id="node-input-intype" style="width:100px;">
<option value="tri" data-i18n="rpi-gpio.resistor.none"></option>
<option value="up" data-i18n="rpi-gpio.resistor.pullup"></option>
<option value="down" data-i18n="rpi-gpio.resistor.pulldown"></option>
</select>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span data-i18n="rpi-gpio.label.debounce"></span>
<input type="text" id="node-input-debounce" style="width:47px; text-align:right"/>&nbsp;mS
</div>
<div class="form-row">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-read" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-read" style="width: 70%;"><span data-i18n="rpi-gpio.label.readinitial"></span></label>
<label for="node-input-read" style="width:70%;"><span data-i18n="rpi-gpio.label.readinitial"></span></label>
</div>
<br/>
<div class="form-row">
@@ -62,10 +64,11 @@
</script>
<script type="text/x-red" data-help-name="rpi-gpio in">
<p>Raspberry Pi input node. Generates a <b>msg.payload</b> with either a 0 or 1 depending on the state of the input pin. Requires the gpio command to work.</p>
<p>Raspberry Pi input node. Generates a <code>msg.payload</code> with either a
0 or 1 depending on the state of the input pin.</p>
<p>You may also enable the input pullup resistor or the pulldown resistor.</p>
<p>The <b>msg.topic</b> is set to <i>pi/{the pin number}</i></p>
<p>Requires the RPi.GPIO python library version 0.5.8 (or better) in order to work.</p>
<p>The <code>msg.topic</code> is set to <i>pi/{the pin number}</i></p>
<p>Requires the RPi.GPIO python library version 0.5.10 (or better) in order to work.</p>
<p><b>Note:</b> we are using the actual physical pin numbers on connector P1 as they are easier to locate.</p>
</script>
@@ -77,8 +80,9 @@
color:"#c6dbef",
defaults: {
name: { value:"" },
pin: { value:"",required:true,validate:RED.validators.number() },
intype: { value: "in" },
pin: { value:"tri",required:true,validate:RED.validators.number() },
intype: { value:"in" },
debounce: { value:"25" },
read: { value:false }
},
inputs:0,
@@ -104,7 +108,7 @@
var alreadyset = this._("rpi-gpio.alreadyset");
$.getJSON('rpi-gpio/'+this.id,function(data) {
$('#pitype').text(data.type);
if ((data.type === "Model B+") || (data.type === "Model A+")) {
if ((data.type !== "Model B") && (data.type !== "Model A")) {
$('#node-input-pin').append($("<option></option>").attr("value",27).text("27 - SDA0 - BCM0"));
$('#node-input-pin').append($("<option></option>").attr("value",28).text("28 - SCL0 - BCM1"));
$('#node-input-pin').append($("<option></option>").attr("value",29).text("29 - GPIO21 - BCM5"));
@@ -200,11 +204,14 @@
</script>
<script type="text/x-red" data-help-name="rpi-gpio out">
<p>Raspberry Pi output node. Expects a <b>msg.payload</b> with either a 0 or 1 (or true or false). Requires the gpio command to work.</p>
<p>Will set the selected physical pin high or low depending on the value passed in.</p>
<p>Raspberry Pi output node. Can be used in Digital, PWM or Servo modes.
<p>Digital mode expects a <code>msg.payload</code> with either a 0 or 1 (or true or false),
and will set the selected physical pin high or low depending on the value passed in.</p>
<p>The initial value of the pin at deploy time can also be set to 0 or 1.</p>
<p>When using PWM mode - expects an input value of a number 0 - 100.</p>
<p>Requires the RPi.GPIO python library version 0.5.8 (or better) in order to work.</p>
<p>When using PWM mode - expects an input value of a number 0 - 100. It can be floating point.</p>
<p>PWM mode can be used to drive a servo using input values between 10 and 20 only.
The GPIO2 pin is best for this as it uses hardware to do the PWM.</p>
<p>Requires the RPi.GPIO python library version 0.5.10 (or better) in order to work.</p>
<p><b>Note:</b> we are using the actual physical pin numbers on connector P1 as they are easier to locate.</p>
</script>
@@ -247,7 +254,7 @@
if (!$("#node-input-out").val()) { $("#node-input-out").val("out"); }
$.getJSON('rpi-gpio/'+this.id,function(data) {
$('#pitype').text(data.type);
if ((data.type === "Model B+") || (data.type === "Model A+")) {
if ((data.type !== "Model B") && (data.type !== "Model A")) {
$('#node-input-pin').append($("<option></option>").attr("value",27).text("27 - SDA0 - BCM0"));
$('#node-input-pin').append($("<option></option>").attr("value",28).text("28 - SCL0 - BCM1"));
$('#node-input-pin').append($("<option></option>").attr("value",29).text("29 - GPIO21 - BCM5"));
@@ -334,12 +341,12 @@
<script type="text/x-red" data-help-name="rpi-mouse">
<p>Raspberry Pi mouse button node. Requires a USB mouse.</p>
<p>Generates a <b>msg.payload</b> with
<p>Generates a <code>msg.payload</code> with
either a 1 or 0 when the selected mouse button is pressed and released</p>
<p>Also sets <b>msg.button</b> to the code value
<p>Also sets <code>msg.button</code> to the code value
<ul><li>1 = left</li><li>2 = right</li><li>4 = middle</li></ul>
so you can work out which button or combination was pressed.</p>
<p>And sets <b>msg.topic</b> to <i>pi/mouse</i>.</p>
<p>And sets <code>msg.topic</code> to <i>pi/mouse</i>.</p>
</script>
<script type="text/javascript">
@@ -366,3 +373,37 @@
}
});
</script>
<script type="text/x-red" data-template-name="rpi-keyboard">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/x-red" data-help-name="rpi-keyboard">
<p>Raspberry Pi keyboard handling node. Requires a USB keyboard.</p>
<p>Generates a <code>msg.payload</code> with a keycode, and <code>msg.action</code> set to
<i>up, down</i> or <i>repeat</i> whenever a key is pressed or released.</p>
<p>It also sets <code>msg.topic</code> to <i>pi/key</i>.</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('rpi-keyboard',{
category: 'Raspberry Pi',
label: 'Raspberry Pi',
color:"#c6dbef",
defaults: {
name: { value:"" }
},
inputs:0,
outputs:1,
icon: "rpi.png",
label: function() {
return this.name||this._("rpi-gpio.label.pikeyboard");;
},
labelStyle: function() {
return this.name?"node_label_italic":"";
}
});
</script>

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013,2015 IBM Corp.
* Copyright 2013,2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,21 +18,25 @@ module.exports = function(RED) {
"use strict";
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var fs = require('fs');
var fs = require('fs');
var gpioCommand = __dirname+'/nrgpio.py';
var gpioCommand = __dirname+'/nrgpio';
if (!fs.existsSync("/dev/ttyAMA0")) { // unlikely if not on a Pi
try {
fs.statSync("/dev/ttyAMA0"); // unlikely if not on a Pi
} catch(err) {
//RED.log.info(RED._("rpi-gpio.errors.ignorenode"));
throw "Info : "+RED._("rpi-gpio.errors.ignorenode");
}
if (!fs.existsSync("/usr/share/doc/python-rpi.gpio")) {
try {
fs.statSync("/usr/share/doc/python-rpi.gpio");
} catch(err) {
RED.log.warn(RED._("rpi-gpio.errors.libnotfound"));
throw "Warning : "+RED._("rpi-gpio.errors.libnotfound");
}
if ( !(1 & parseInt ((fs.statSync(gpioCommand).mode & parseInt ("777", 8)).toString (8)[0]) )) {
if ( !(1 & parseInt((fs.statSync(gpioCommand).mode & parseInt("777", 8)).toString(8)[0]) )) {
RED.log.error(RED._("rpi-gpio.errors.needtobeexecutable",{command:gpioCommand}));
throw "Error : "+RED._("rpi-gpio.errors.mustbeexecutable");
}
@@ -49,6 +53,7 @@ module.exports = function(RED) {
this.pin = n.pin;
this.intype = n.intype;
this.read = n.read || false;
this.debounce = Number(n.debounce || 25);
if (this.read) { this.buttonState = -2; }
var node = this;
if (!pinsInUse.hasOwnProperty(this.pin)) {
@@ -61,18 +66,14 @@ module.exports = function(RED) {
}
if (node.pin !== undefined) {
if (node.intype === "tri") {
node.child = spawn(gpioCommand, ["in",node.pin]);
} else {
node.child = spawn(gpioCommand, ["in",node.pin,node.intype]);
}
node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]);
node.running = true;
node.status({fill:"green",shape:"dot",text:"common.status.ok"});
node.child.stdout.on('data', function (data) {
data = data.toString().trim();
if (data.length > 0) {
if (node.buttonState !== -1) {
if (node.running && node.buttonState !== -1) {
node.send({ topic:"pi/"+node.pin, payload:Number(data) });
}
node.buttonState = data;
@@ -86,8 +87,8 @@ module.exports = function(RED) {
});
node.child.on('close', function (code) {
node.child = null;
node.running = false;
node.child = null;
if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); }
if (node.done) {
node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"});
@@ -120,7 +121,6 @@ module.exports = function(RED) {
}
RED.nodes.registerType("rpi-gpio in",GPIOInNode);
function GPIOOutNode(n) {
RED.nodes.createNode(this,n);
this.pin = n.pin;
@@ -160,7 +160,7 @@ module.exports = function(RED) {
if (node.pin !== undefined) {
if (node.set && (node.out === "out")) {
node.child = spawn(gpioCommand, [node.out,node.pin,node.level]);
} else {
} else {
node.child = spawn(gpioCommand, [node.out,node.pin]);
}
node.running = true;
@@ -210,20 +210,6 @@ module.exports = function(RED) {
});
}
var pitype = { type:"" };
exec(gpioCommand+" rev 0", function(err,stdout,stderr) {
if (err) {
RED.log.info(RED._("rpi-gpio.errors.version"));
}
else {
if (stdout.trim() == "0") { pitype = { type:"Compute" }; }
else if (stdout.trim() == "1") { pitype = { type:"A/B v1" }; }
else if (stdout.trim() == "2") { pitype = { type:"A/B v2" }; }
else if (stdout.trim() == "3") { pitype = { type:"Model B+" }; }
else { RED.log.info(RED._("rpi-gpio.errors.sawpitype"),stdout.trim()); }
}
});
RED.nodes.registerType("rpi-gpio out",GPIOOutNode);
function PiMouseNode(n) {
@@ -231,7 +217,7 @@ module.exports = function(RED) {
this.butt = n.butt || 7;
var node = this;
node.child = spawn(gpioCommand, ["mouse",node.butt]);
node.child = spawn(gpioCommand+".py", ["mouse",node.butt]);
node.status({fill:"green",shape:"dot",text:"common.status.ok"});
node.child.stdout.on('data', function (data) {
@@ -273,6 +259,70 @@ module.exports = function(RED) {
}
RED.nodes.registerType("rpi-mouse",PiMouseNode);
function PiKeyboardNode(n) {
RED.nodes.createNode(this,n);
var node = this;
node.child = spawn(gpioCommand+".py", ["kbd","0"]);
node.status({fill:"green",shape:"dot",text:"common.status.ok"});
node.child.stdout.on('data', function (data) {
var b = data.toString().trim().split(",");
var act = "up";
if (b[1] === "1") { act = "down"; }
if (b[1] === "2") { act = "repeat"; }
node.send({ topic:"pi/key", payload:Number(b[0]), action:act });
});
node.child.stderr.on('data', function (data) {
if (RED.settings.verbose) { node.log("err: "+data+" :"); }
});
node.child.on('close', function (code) {
node.running = false;
node.child = null;
if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); }
if (node.done) {
node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"});
node.done();
}
else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); }
});
node.child.on('error', function (err) {
if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); }
else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); }
else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); }
});
node.on("close", function(done) {
node.status({});
if (node.child != null) {
node.done = done;
node.child.kill('SIGINT');
node.child = null;
}
else { done(); }
});
}
RED.nodes.registerType("rpi-keyboard",PiKeyboardNode);
var pitype = { type:"" };
exec(gpioCommand+" info", function(err,stdout,stderr) {
if (err) {
RED.log.info(RED._("rpi-gpio.errors.version"));
}
else {
try {
var info = JSON.parse( stdout.trim().replace(/\'/g,"\"") );
pitype.type = info["TYPE"];
}
catch(e) {
RED.log.info(RED._("rpi-gpio.errors.sawpitype"),stdout.trim());
}
}
});
RED.httpAdmin.get('/rpi-gpio/:id', RED.auth.needsPermission('rpi-gpio.read'), function(req,res) {
res.json(pitype);
});

View File

@@ -1,6 +1,6 @@
#!/usr/bin/python
#
# Copyright 2014 IBM Corp.
# Copyright 2014,2016 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,9 +15,13 @@
# Import library functions we need
import RPi.GPIO as GPIO
import struct
import sys
import os
import subprocess
from time import sleep
bounce = 20 # bounce time in mS to apply
bounce = 25;
if sys.version_info >= (3,0):
print("Sorry - currently only configured to work with python 2.x")
@@ -92,18 +96,18 @@ if len(sys.argv) > 2:
elif cmd == "in":
#print "Initialised pin "+str(pin)+" to IN"
bounce = int(sys.argv[4])
def handle_callback(chan):
sleep(bounce/1000)
print GPIO.input(chan)
if len(sys.argv) == 4:
if sys.argv[3].lower() == "up":
GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP)
elif sys.argv[3].lower() == "down":
GPIO.setup(pin,GPIO.IN,GPIO.PUD_DOWN)
else:
GPIO.setup(pin,GPIO.IN)
if sys.argv[3].lower() == "up":
GPIO.setup(pin,GPIO.IN,GPIO.PUD_UP)
elif sys.argv[3].lower() == "down":
GPIO.setup(pin,GPIO.IN,GPIO.PUD_DOWN)
else:
GPIO.setup(pin,GPIO.IN)
print GPIO.input(pin)
GPIO.add_event_detect(pin, GPIO.BOTH, callback=handle_callback, bouncetime=bounce)
@@ -166,12 +170,6 @@ if len(sys.argv) > 2:
except:
data = 0
elif cmd == "rev":
print GPIO.RPI_REVISION
elif cmd == "ver":
print GPIO.VERSION
elif cmd == "mouse": # catch mice button events
file = open( "/dev/input/mice", "rb" )
oldbutt = 0
@@ -193,15 +191,40 @@ if len(sys.argv) > 2:
file.close()
sys.exit(0)
elif cmd == "kbd": # catch keyboard button events
try:
while not os.path.isdir("/dev/input/by-path"):
time.sleep(10)
infile = subprocess.check_output("ls /dev/input/by-path/ | grep -m 1 'kbd'", shell=True).strip()
infile_path = "/dev/input/by-path/" + infile
EVENT_SIZE = struct.calcsize('llHHI')
file = open(infile_path, "rb")
event = file.read(EVENT_SIZE)
while event:
(tv_sec, tv_usec, type, code, value) = struct.unpack('llHHI', event)
#if type != 0 or code != 0 or value != 0:
if type == 1:
# type,code,value
print("%u,%u" % (code, value))
event = file.read(EVENT_SIZE)
print "0,0"
file.close()
sys.exit(0)
except:
file.close()
sys.exit(0)
elif len(sys.argv) > 1:
cmd = sys.argv[1].lower()
if cmd == "rev":
print GPIO.RPI_REVISION
elif cmd == "ver":
print GPIO.VERSION
elif cmd == "info":
print GPIO.RPI_INFO
else:
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|ver {pin} {value|up|down}"
print " only ver (gpio version) and rev (board revision) accept no pin parameter."
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}"
print " only ver (gpio version) and info (board information) accept no pin parameter."
else:
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|ver {pin} {value|up|down}"
print "Bad parameters - in|out|pwm|buzz|byte|borg|mouse|kbd|ver|info {pin} {value|up|down}"

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2013,2015 IBM Corp.
Copyright 2013, 2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -27,9 +27,15 @@
</script>
<script type="text/x-red" data-help-name="mqtt in">
<p>MQTT input node. Connects to a broker and subscribes to the specified topic. The topic may contain MQTT wildcards.</p>
<p>Outputs an object called <b>msg</b> containing <b>msg.topic, msg.payload, msg.qos</b> and <b>msg.retain</b>.</p>
<p><b>msg.payload</b> is usually a string, but can also be a binary buffer.</p>
<p>Connects to a broker and subscribes to the specified topic.</p>
<p>Outputs a message with the properties:</p>
<ul>
<li><code>msg.topic</code></li>
<li><code>msg.payload</code></li>
<li><code>msg.qos</code></li>
<li><code>msg.retain</code></li>
</ul>
<p><code>msg.payload</code> will be a String, unless it is detected as a binary buffer.</p>
</script>
<script type="text/javascript">
@@ -37,7 +43,7 @@
category: 'input',
defaults: {
name: {value:""},
topic: {value:"",required:true},
topic: {value:"",required:true,validate: RED.validators.regex(/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/)},
broker: {type:"mqtt-broker", required:true}
},
color:"#d8bfd8",
@@ -84,9 +90,14 @@
</script>
<script type="text/x-red" data-help-name="mqtt out">
<p>Connects to a MQTT broker and publishes <b>msg.payload</b> either to the <b>msg.topic</b> or to the topic specified in the edit window. The value in the edit window has precedence.</p>
<p>Likewise QoS and/or retain values in the edit panel will overwrite any <b>msg.qos</b> and <b>msg.retain</b> properties. If nothing is set they default to <i>0</i> and <i>false</i> respectively.</p>
<p>If <b>msg.payload</b> contains an object it will be stringified before being sent.</p>
<p>Connects to a MQTT broker and publishes messages.</p>
<p>The topic used can be configured in the node or, if left blank, can be set
by <code>msg.topic</code>.</p>
<p>Likewise the QoS and retain values can be configured in the node or, if left
blank, set by <code>msg.qos</code> and <code>msg.retain</code> respectively.
By default, messages are published at QoS 0 with the retain flag set to false.</p>
<p>If <code>msg.payload</code> contains an object it will be converted to JSON
before being sent.</p>
</script>
<script type="text/javascript">
@@ -226,7 +237,7 @@
usetls: {value: false},
verifyservercert: { value: false},
compatmode: { value: true},
keepalive: {value:15,validate:RED.validators.number()},
keepalive: {value:60,validate:RED.validators.number()},
cleansession: {value: true},
willTopic: {value:""},
willQos: {value:"0"},

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013,2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,6 +45,7 @@ module.exports = function(RED) {
this.brokerurl = "";
this.connected = false;
this.connecting = false;
this.closing = false;
this.options = {};
this.queue = [];
this.subscriptions = {};
@@ -75,7 +76,7 @@ module.exports = function(RED) {
this.verifyservercert = false;
}
if (typeof this.keepalive === 'undefined'){
this.keepalive = 15;
this.keepalive = 60;
} else if (typeof this.keepalive === 'string') {
this.keepalive = Number(this.keepalive);
}
@@ -136,11 +137,17 @@ module.exports = function(RED) {
}
};
this.deregister = function(mqttNode){
this.deregister = function(mqttNode,done){
delete node.users[mqttNode.id];
if (Object.keys(node.users).length === 0) {
node.client.end();
if (node.closing) {
return done();
}
if (Object.keys(node.users).length === 0) {
if (node.client) {
return node.client.end(done);
}
}
done();
};
this.connect = function () {
@@ -177,15 +184,14 @@ module.exports = function(RED) {
if (node.birthMessage) {
node.publish(node.birthMessage);
}
// Send any queued messages
while(node.queue.length) {
var msg = node.queue.shift();
//console.log(msg);
node.publish(msg);
}
});
node.client.on("reconnect", function() {
for (var id in node.users) {
if (node.users.hasOwnProperty(id)) {
node.users[id].status({fill:"yellow",shape:"ring",text:"common.status.connecting"});
}
}
})
// Register disconnect handlers
node.client.on('close', function () {
if (node.connected) {
@@ -265,17 +271,13 @@ module.exports = function(RED) {
retain: msg.retain || false
};
node.client.publish(msg.topic, msg.payload, options, function (err){return});
} else {
if (!node.connecting) {
node.connect();
}
node.queue.push(msg);
}
};
this.on('close', function(done) {
this.closing = true;
if (this.connected) {
this.client.on('close', function() {
this.client.once('close', function() {
done();
});
this.client.end();
@@ -298,10 +300,14 @@ module.exports = function(RED) {
this.topic = n.topic;
this.broker = n.broker;
this.brokerConn = RED.nodes.getNode(this.broker);
if (!/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(this.topic)) {
return this.warn(RED._("mqtt.errors.invalid-topic"));
}
var node = this;
if (this.brokerConn) {
this.status({fill:"red",shape:"ring",text:"common.status.disconnected"});
if (this.topic) {
node.brokerConn.register(this);
this.brokerConn.subscribe(this.topic,2,function(topic,payload,packet) {
if (isUtf8(payload)) { payload = payload.toString(); }
var msg = {topic:topic,payload:payload, qos: packet.qos, retain: packet.retain};
@@ -313,15 +319,14 @@ module.exports = function(RED) {
if (this.brokerConn.connected) {
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
}
node.brokerConn.register(this);
}
else {
this.error(RED._("mqtt.errors.not-defined"));
}
this.on('close', function() {
this.on('close', function(done) {
if (node.brokerConn) {
node.brokerConn.unsubscribe(node.topic,node.id);
node.brokerConn.deregister(node);
node.brokerConn.deregister(node,done);
}
});
} else {
@@ -365,8 +370,8 @@ module.exports = function(RED) {
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
}
node.brokerConn.register(node);
this.on('close', function() {
node.brokerConn.deregister(node);
this.on('close', function(done) {
node.brokerConn.deregister(node,done);
});
} else {
this.error(RED._("mqtt.errors.missing-config"));

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013,2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -63,6 +63,8 @@ module.exports = function(RED) {
var corsSetup = false;
function createRequestWrapper(node,req) {
// This misses a bunch of properties (eg headers). Before we use this function
// need to ensure it captures everything documented by Express and HTTP modules.
var wrapper = {
_req: req
};
@@ -98,7 +100,7 @@ module.exports = function(RED) {
wrapper[f] = function() {
node.warn(RED._("httpin.errors.deprecated-call",{method:"msg.req."+f}));
var result = req[f].apply(req,arguments);
if (result === res) {
if (result === req) {
return wrapper;
} else {
return result;
@@ -154,6 +156,13 @@ module.exports = function(RED) {
return wrapper;
}
var corsHandler = function(req,res,next) { next(); }
if (RED.settings.httpNodeCors) {
corsHandler = cors(RED.settings.httpNodeCors);
RED.httpNode.options("*",corsHandler);
}
function HTTPIn(n) {
RED.nodes.createNode(this,n);
if (RED.settings.httpNodeRoot !== false) {
@@ -185,14 +194,6 @@ module.exports = function(RED) {
}
};
var corsHandler = function(req,res,next) { next(); }
if (RED.settings.httpNodeCors && !corsSetup) {
corsHandler = cors(RED.settings.httpNodeCors);
RED.httpNode.options("*",corsHandler);
corsSetup = true;
}
var httpMiddleware = function(req,res,next) { next(); }
if (RED.settings.httpNodeMiddleware) {

View File

@@ -28,6 +28,8 @@ module.exports = function(RED) {
var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
var nodeMethod = n.method || "GET";
this.ret = n.ret || "txt";
if (RED.settings.httpRequestTimeout) { this.reqTimeout = parseInt(RED.settings.httpRequestTimeout) || 120000; }
else { this.reqTimeout = 120000; }
var node = this;
var prox, noprox;
@@ -162,6 +164,13 @@ module.exports = function(RED) {
node.status({});
});
});
req.setTimeout(node.reqTimeout, function() {
node.error(RED._("common.notification.errors.no-response"),msg);
setTimeout(function() {
node.status({fill:"red",shape:"ring",text:"common.notification.errors.no-response"});
},10);
req.abort();
});
req.on('error',function(err) {
msg.payload = err.toString() + " : " + url;
msg.statusCode = err.code;

View File

@@ -38,7 +38,7 @@
<script type="text/x-red" data-help-name="websocket in">
<p>WebSocket input node.</p>
<p>By default, the data received from the WebSocket will be in <b>msg.payload</b>.
<p>By default, the data received from the WebSocket will be in <code>msg.payload</code>.
The socket can be configured to expect a properly formed JSON string, in which
case it will parse the JSON and send on the resulting object as the entire message.</p>
</script>
@@ -48,7 +48,7 @@
(function() {
function ws_oneditprepare() {
$("#websocket-client-row").hide();
$("#websocket-client-row").hide();
$("#node-input-mode").change(function(){
if( $("#node-input-mode").val() === 'client') {
$("#websocket-server-row").hide();
@@ -59,7 +59,7 @@
$("#websocket-client-row").hide();
}
});
if(this.client) {
$("#node-input-mode").val('client').change();
}
@@ -67,24 +67,24 @@
$("#node-input-mode").val('server').change();
}
}
function ws_oneditsave() {
if($("#node-input-mode").val() === 'client') {
$("#node-input-server").append('<option value="">Dummy</option>');
$("#node-input-server").val('');
$("#node-input-server").val('');
}
else {
$("#node-input-client").append('<option value="">Dummy</option>');
$("#node-input-client").val('');
$("#node-input-client").val('');
}
}
function ws_label() {
var nodeid = (this.client)?this.client:this.server;
var wsNode = RED.nodes.node(nodeid);
return this.name||(wsNode?"[ws] "+wsNode.label():"websocket");
}
function ws_validateserver() {
if($("#node-input-mode").val() === 'client' || (this.client && !this.server)) {
return true;
@@ -93,7 +93,7 @@
return RED.nodes.node(this.server) != null;
}
}
function ws_validateclient() {
if($("#node-input-mode").val() === 'client' || (this.client && !this.server)) {
return RED.nodes.node(this.client) != null;
@@ -121,7 +121,7 @@
oneditsave: ws_oneditsave,
oneditprepare: ws_oneditprepare
});
RED.nodes.registerType('websocket out',{
category: 'output',
defaults: {
@@ -141,7 +141,7 @@
oneditsave: ws_oneditsave,
oneditprepare: ws_oneditprepare
});
RED.nodes.registerType('websocket-listener',{
category: 'config',
defaults: {
@@ -152,7 +152,7 @@
outputs:0,
label: function() {
var root = RED.settings.httpNodeRoot;
if (root.slice(-1) != "/") {
if (root.slice(-1) != "/") {
root = root+"/";
}
if (this.path.charAt(0) == "/") {
@@ -164,7 +164,7 @@
},
oneditprepare: function() {
var root = RED.settings.httpNodeRoot;
if (root.slice(-1) == "/") {
if (root.slice(-1) == "/") {
root = root.slice(0,-1);
}
if (root == "") {
@@ -188,7 +188,7 @@
return this.path;
}
});
})();
</script>
@@ -217,15 +217,15 @@
<script type="text/x-red" data-help-name="websocket out">
<p>WebSocket out node.</p>
<p>By default, <b>msg.payload</b> will be sent over the WebSocket. The socket
can be configured to encode the entire message object as a JSON string and send that
<p>By default, <code>msg.payload</code> will be sent over the WebSocket. The socket
can be configured to encode the entire <code>msg</code> object as a JSON string and send that
over the WebSocket.</p>
<p>If the message arriving at this node started at a WebSocket In node, the message
will be sent back to the client that triggered the flow. Otherwise, the message
will be broadcast to all connected clients.</p>
<p>If you want to broadcast a message that started at a WebSocket In node, you
should delete the <b>msg._session</b> property within the flow</p>.
should delete the <code>msg._session</code> property within the flow.</p>
</script>
<!-- WebSocket Server configuration node -->

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013,2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -82,7 +82,15 @@ module.exports = function(RED) {
RED.server.addListener('newListener',storeListener);
// Create a WebSocket Server
node.server = new ws.Server({server:RED.server,path:path});
node.server = new ws.Server({
server:RED.server,
path:path,
// Disable the deflate option due to this issue
// https://github.com/websockets/ws/pull/632
// that is fixed in the 1.x release of the ws module
// that we cannot currently pickup as it drops node 0.10 support
perMessageDeflate: false
});
// Workaround https://github.com/einaros/ws/pull/253
// Stop listening for new listener events

View File

@@ -31,11 +31,11 @@
<p>You can enter a list of comma separated directories and/or files. You will
need to put quotes "..." around any that have spaces in.</p>
<p>On Windows you must use double back-slashes \\ in any directory names.</p>
<p>The full filename of the file that actually changed is put into <b>msg.payload</b>,
while a stringified version of the watch list is returned in <b>msg.topic</b>.</p>
<p><b>msg.file</b> contains just the short filename of the file that changed.
<b>msg.type</b> has the type of thing changed, usually <i>file</i> or <i>directory</i>,
while <b>msg.size</b> holds the file size in bytes.</p>
<p>The full filename of the file that actually changed is put into <code>msg.payload</code>,
while a stringified version of the watch list is returned in <code>msg.topic</code>.</p>
<p><code>msg.file</code> contains just the short filename of the file that changed.
<code>msg.type</code> has the type of thing changed, usually <i>file</i> or <i>directory</i>,
while <code>msg.size</code> holds the file size in bytes.</p>
<p>Of course in Linux, <i>everything</i> is a file and thus can be watched...</p>
<p><b>Note: </b>The directory or file must exist in order to be watched. If the file
or directory gets deleted it may no longer be monitored even if it gets re-created.</p>

View File

@@ -56,7 +56,7 @@
</script>
<script type="text/x-red" data-help-name="tcp in">
<p>Provides a choice of tcp inputs. Can either connect to a remote tcp port,
<p>Provides a choice of TCP inputs. Can either connect to a remote TCP port,
or accept incoming connections.</p>
</script>
@@ -147,12 +147,14 @@
</script>
<script type="text/x-red" data-help-name="tcp out">
<p>Provides a choice of tcp outputs. Can either connect to a remote tcp port,
<p>Provides a choice of TCP outputs. Can either connect to a remote TCP port,
accept incoming connections, or reply to messages received from a TCP In node.</p>
<p>Only <b>msg.payload</b> is sent.</p>
<p>If <b>msg.payload</b> is a string containing a Base64 encoding of binary
<p>Only <code>msg.payload</code> is sent.</p>
<p>If <code>msg.payload</code> is a string containing a Base64 encoding of binary
data, the Base64 decoding option will cause it to be converted back to binary
before being sent.</p>
<p>If <code>msg._session</code> is not present the payload is
sent to <b>all</b> connected clients.</p>
</script>
<script type="text/javascript">
@@ -247,12 +249,12 @@
</script>
<script type="text/x-red" data-help-name="tcp request">
<p>A simple TCP request node - sends the <b>msg.payload</b> to a server tcp port and expects a response.</p>
<p>Connects, sends the "request", reads the "response". It can either count a number of
<p>A simple TCP request node - sends the <code>msg.payload</code> to a server tcp port and expects a response.</p>
<p>Connects, sends the "request", and reads the "response". It can either count a number of
returned characters into a fixed buffer, match a specified character before returning,
wait a fixed timeout from first reply and then return, or just sit and wait for data.</p>
<p>The response will be output in <b>msg.payload</b> as a buffer, so you may want to .toString() it.</p>
<p>If you leave tcp host or port blank they must be set by using the <b>msg.host</b> and <b>msg.port</b> properties.</p>
<p>The response will be output in <code>msg.payload</code> as a buffer, so you may want to .toString() it.</p>
<p>If you leave tcp host or port blank they must be set by using the <code>msg.host</code> and <code>msg.port</code> properties.</p>
</script>
<script type="text/javascript">

View File

@@ -276,7 +276,7 @@ module.exports = function(RED) {
}
if (node.doend === true) {
end = true;
if (client) { client.end(); }
if (client) { node.status({}); client.destroy(); }
}
}
});
@@ -303,6 +303,17 @@ module.exports = function(RED) {
}
}
}
else {
for (var i in connectionPool) {
if (Buffer.isBuffer(msg.payload)) {
connectionPool[i].write(msg.payload);
} else if (typeof msg.payload === "string" && node.base64) {
connectionPool[i].write(new Buffer(msg.payload,'base64'));
} else {
connectionPool[i].write(new Buffer(""+msg.payload));
}
}
}
});
} else {
var connectedSockets = [];
@@ -438,7 +449,7 @@ module.exports = function(RED) {
msg.payload = new Buffer(i+1);
buf.copy(msg.payload,0,0,i+1);
node.send(msg);
//if (client) { client.end(); }
if (client) { node.status({}); client.destroy(); }
}, node.splitc);
i = 0;
buf[0] = data[j];
@@ -452,7 +463,7 @@ module.exports = function(RED) {
msg.payload = new Buffer(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
//if (client) { client.end(); }
if (client) { node.status({}); client.destroy(); }
i = 0;
}
}
@@ -464,7 +475,7 @@ module.exports = function(RED) {
msg.payload = new Buffer(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
//if (client) { client.end(); }
if (client) { node.status({}); client.destroy(); }
i = 0;
}
}

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2013 IBM Corp.
Copyright 2013,2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@
</div>
<div class="form-row node-input-iface">
<label for="node-input-iface"><i class="fa fa-random"></i> <span data-i18n="udp.label.interface"></span></label>
<input type="text" id="node-input-iface" data-i18n="[placeholder]udp.label.interface">
<input type="text" id="node-input-iface" data-i18n="[placeholder]udp.label.interfaceprompt">
</div>
<div class="form-row">
<label for="node-input-port"><i class="fa fa-sign-in"></i> <span data-i18n="udp.label.onport"></span></label>
@@ -52,12 +52,15 @@
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-tips"><span data-i18n="udp.tip.in"></span></div>
<div class="form-tips" id="udpporttip"><span data-i18n="[html]udp.tip.port"></span></div>
</script>
<script type="text/x-red" data-help-name="udp in">
<p>A udp input node, that produces a <b>msg.payload</b> containing a <i>BUFFER</i>, string, or base64 encoded string. Supports multicast.</p>
<p>It also provides <b>msg.ip</b> and <b>msg.port</b> to the ip address and port from which the message was received.</p>
<p>On some systems you may need to be root to use ports below 1024 and/or broadcast.</p>
<p>A UDP input node, that produces a <code>msg.payload</code> containing a
Buffer, string, or base64 encoded string. Supports multicast.</p>
<p>It also provides <code>msg.ip</code> and <code>msg.port</code> set to the
ip address and port from which the message was received.</p>
<p><b>Note</b>: On some systems you may need to be root to use ports below 1024 and/or broadcast.</p>
</script>
<script type="text/javascript">
@@ -98,6 +101,21 @@
}
});
$("#node-input-multicast").change();
var porttip = this._("udp.tip.port");
var alreadyused = this._("udp.errors.alreadyused");
var portsInUse = {};
$.getJSON('udp-ports/'+this.id,function(data) {
portsInUse = data || {};
$('#udpporttip').html(porttip + Object.keys(data||{}));
});
$("#node-input-port").change(function() {
var portnew = $("#node-input-port").val();
if (portsInUse.hasOwnProperty($("#node-input-port").val())) {
console.log($("#node-input-port").val());
RED.notify(alreadyused+" "+$("#node-input-port").val(),"warn");
}
});
}
});
</script>
@@ -147,10 +165,10 @@
</script>
<script type="text/x-red" data-help-name="udp out">
<p>This node sends <b>msg.payload</b> to the designated udp host and port. Supports multicast.</p>
<p>You may also use <b>msg.ip</b> and <b>msg.port</b> to set the destination values.<br/><b>Note</b>: the statically configured values have precedence.</p>
<p>This node sends <code>msg.payload</code> to the designated UDP host and port. Supports multicast.</p>
<p>You may also use <code>msg.ip</code> and <code>msg.port</code> to set the destination values, but the statically configured values have precedence.</p>
<p>If you select broadcast either set the address to the local broadcast ip address, or maybe try 255.255.255.255, which is the global broadcast address.</p>
<p>On some systems you may need to be root to use ports below 1024 and/or broadcast.</p>
<p><b>Note</b>: On some systems you may need to be root to use ports below 1024 and/or broadcast.</p>
</script>
<script type="text/javascript">

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013 IBM Corp.
* Copyright 2013,2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
module.exports = function(RED) {
"use strict";
var dgram = require('dgram');
var udpInputPortsInUse = {};
// The Input Node
function UDPin(n) {
@@ -28,6 +29,12 @@ module.exports = function(RED) {
this.multicast = n.multicast;
this.ipv = n.ipv || "udp4";
var node = this;
if (!udpInputPortsInUse.hasOwnProperty(this.port)) {
udpInputPortsInUse[this.port] = n.id;
}
else {
node.warn(RED._("udp.errors.alreadyused",node.port));
}
var opts = {type:node.ipv, reuseAddr:true};
if (process.version.indexOf("v0.10") === 0) { opts = node.ipv; }
@@ -76,6 +83,10 @@ module.exports = function(RED) {
});
node.on("close", function() {
console.log("ID=",node.id);
if (udpInputPortsInUse[node.port] === node.id) {
delete udpInputPortsInUse[node.port];
}
try {
server.close();
node.log(RED._("udp.status.listener-stopped"));
@@ -86,9 +97,11 @@ module.exports = function(RED) {
server.bind(node.port,node.iface);
}
RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-in.read'), function(req,res) {
res.json(udpInputPortsInUse);
});
RED.nodes.registerType("udp in",UDPin);
// The Output Node
function UDPout(n) {
RED.nodes.createNode(this,n);

View File

@@ -16,6 +16,9 @@
var util = require("util");
var mqtt = require("mqtt");
var events = require("events");
util.log("[warn] nodes/core/io/lib/mqtt.js is deprecated and will be removed in a future release of Node-RED. Please report this usage to the Node-RED mailing list.");
//var inspect = require("util").inspect;
//var Client = module.exports.Client = function(

View File

@@ -17,6 +17,8 @@ var util = require("util");
var mqtt = require("./mqtt");
var settings = require(process.env.NODE_RED_HOME+"/red/red").settings;
util.log("[warn] nodes/core/io/lib/mqttConnectionPool.js is deprecated and will be removed in a future release of Node-RED. Please report this usage to the Node-RED mailing list.");
var connections = {};
function matchTopic(ts,t) {
@@ -60,7 +62,7 @@ module.exports = {
subscribe: function(topic,qos,callback,ref) {
ref = ref||0;
subscriptions[topic] = subscriptions[topic]||{};
var sub = {
topic:topic,
qos:qos,
@@ -104,7 +106,7 @@ module.exports = {
}
},
disconnect: function(ref) {
this._instances -= 1;
if (this._instances == 0) {
client.disconnect();

View File

@@ -37,8 +37,6 @@
"repeat": "Repeat"
},
"timestamp": "timestamp",
"string": "string",
"blank": "blank",
"none": "none",
"interval": "interval",
"interval-time": "interval between times",
@@ -47,6 +45,7 @@
"minutes": "minutes",
"hours": "hours",
"between": "between",
"previous": "previous value",
"at": "at",
"and": "and",
"every": "every",
@@ -138,7 +137,11 @@
"template": {
"label": {
"template": "Template",
"property": "Property"
"property": "Set property",
"format": "Syntax Highlight",
"syntax": "Format",
"mustache": "Mustache template",
"plain": "Plain text"
},
"templatevalue": "This is the payload: {{payload}} !"
},
@@ -146,9 +149,10 @@
"action": "Action",
"for": "For",
"delaymsg": "Delay message",
"ramdomdelay": "Random delay",
"randomdelay": "Random delay",
"limitrate": "Limit rate to",
"fairqueue": "Topic based fair queue",
"timedqueue": "Timed release queue",
"milisecs": "Miliseconds",
"secs": "Seconds",
"sec": "Second",
@@ -165,8 +169,9 @@
"label": {
"delay": "delay",
"limit": "limit",
"random": "ramdom",
"queue": "queue"
"random": "random",
"queue": "queue",
"timed": "releases per"
},
"error": {
"buffer": "buffer exceeded 1000 messages"
@@ -177,8 +182,9 @@
"then": "then",
"then-send": "then send",
"output": {
"string": "the string payload",
"existing": "the existing message",
"string": "the string",
"number": "the number",
"existing": "the existing msg.payload",
"nothing": "nothing"
},
"wait-reset": "wait to be reset",
@@ -189,11 +195,13 @@
"m": "Minutes",
"h": "Hours"
},
"extend": "extend delay if new message arrives",
"tip": "The node can be reset by sending a message with the <b>msg.reset</b> property set",
"extend": " extend delay if new message arrives",
"tip": "The node can also be reset by sending a message with the <code>msg.reset</code> property set to any value.",
"label": {
"trigger": "trigger",
"trigger-block": "trigger & block"
"trigger-block": "trigger & block",
"reset": "and reset if msg.payload == ",
"resetprompt": "(optional reset value)"
}
},
"comment": {
@@ -371,7 +379,8 @@
"using": "using",
"output": "Output",
"group": "Group",
"interface": "Interface",
"interface": "Local IP",
"interfaceprompt": "(optional) local ip address to bind to",
"send": "Send a",
"toport": "to port",
"address": "Address",
@@ -398,7 +407,8 @@
},
"tip": {
"in": "Tip: Make sure your firewall will allow the data in.",
"out": "Tip: leave address and port blank if you want to set using <b>msg.ip</b> and <b>msg.port</b>."
"out": "Tip: leave address and port blank if you want to set using <code>msg.ip</code> and <code>msg.port</code>.",
"port": "Ports already in use: "
},
"status": {
"listener-at": "udp listener at __host__:__port__",
@@ -417,7 +427,8 @@
"interface": "Must be ip address of the required interface",
"ip-notset": "udp: ip address not set",
"port-notset": "udp: port not set",
"port-invalid": "udp: port number not valid"
"port-invalid": "udp: port number not valid",
"alreadyused": "udp: port already in use"
}
},
"switch": {
@@ -459,7 +470,8 @@
"replace": "Replace with"
},
"errors": {
"invalid-from": "Invalid 'from' property: __error__"
"invalid-from": "Invalid 'from' property: __error__",
"invalid-json": "Invalid 'to' JSON property"
}
},
"range": {
@@ -531,7 +543,8 @@
},
"output": {
"html": "the html content of the elements",
"text": "only the text content of the elements"
"text": "only the text content of the elements",
"attr": "an object of any attributes of the elements"
},
"format": {
"single": "as a single message containing an array",
@@ -565,8 +578,10 @@
"readinitial": "Read initial state of pin on deploy/restart?",
"type": "Type",
"initpin": "Initialise pin state?",
"debounce": "Debounce",
"button": "Button",
"pimouse": "Pi Mouse",
"pikeyboard": "Pi Keyboard",
"left": "Left",
"right": "Right",
"middle": "Middle"
@@ -624,8 +639,13 @@
"tail": {
"label": {
"filename": "Filename",
"type": "File type",
"splitlines": "Split lines on \\n?"
},
"action": {
"text": "Text - returns String",
"binary": "Binary - returns Buffer"
},
"errors": {
"windowsnotsupport": "Not currently supported on Windows."
}

View File

@@ -20,7 +20,7 @@
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row">
<span data-i18n="switch.label.property"></span> msg.<input type="text" id="node-input-property" style="width: 200px;"/>
<span data-i18n="switch.label.property"></span> <input type="text" id="node-input-property" style="width: 300px;"/>
</div>
<div class="form-row node-input-rule-container-row" style="margin-bottom: 0px;">
<div id="node-input-rule-container-div" style="box-sizing: border-box; border-radius: 5px; height: 310px; padding: 5px; border: 1px solid #ccc; overflow-y:scroll;">
@@ -39,11 +39,11 @@
</script>
<script type="text/x-red" data-help-name="switch">
<p>A simple function node to route messages based on its properties.</p>
<p>A node to route messages based on property values.</p>
<p>When a message arrives, the selected property is evaluated against each
of the defined rules. The message is then sent to the output of <i>all</i>
rules that pass.</p>
<p>Note: the <i>otherwise</i> rule applies as a "not any of" the rules preceding it.</p>
<p><b>Note</b>: the <i>otherwise</i> rule applies as a "not any of" the rules preceding it.</p>
</script>
<script type="text/javascript">
@@ -53,6 +53,7 @@
defaults: {
name: {value:""},
property: {value:"payload", required:true},
propertyType: { value:"msg" },
rules: {value:[{t:"eq", v:""}]},
checkall: {value:"true", required:true},
outputs: {value:1}
@@ -64,7 +65,10 @@
return this.name||"switch";
},
oneditprepare: function() {
var node = this;
var previousValueType = {value:"prev",label:this._("inject.previous"),hasValue:false};
$("#node-input-property").typedInput({default:this.propertyType||'msg',types:['msg','flow','global']});
var operators = [
{v:"eq",t:"=="},
{v:"neq",t:"!="},
@@ -85,7 +89,7 @@
var andLabel = this._("switch.and");
var caseLabel = this._("switch.ignorecase");
function resizeRule(rule,width) {
this.resizeRule = function(rule,newWidth) {
var selectField = rule.find("select");
var type = selectField.children("option:selected").val();
var valueField = rule.find(".node-input-rule-value");
@@ -102,14 +106,14 @@
selectField.width(selectWidth);
if (type === "btwn") {
var labelWidth = rule.find(".node-input-tule-btwn-label").width();
btwnField1.width((width-256-labelWidth)/2);
btwnField2.width((width-256-labelWidth)/2);
var labelWidth = rule.find(".node-input-rule-btwn-label").width();
btwnField1.typedInput("width",(newWidth-selectWidth-120));
btwnField2.typedInput("width",(newWidth-selectWidth-120));
} else {
if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") {
// valueField.hide();
} else {
valueField.width(width-selectWidth-120);
valueField.typedInput("width",(newWidth-selectWidth-120));
}
}
}
@@ -118,6 +122,8 @@
var container = $('<li/>',{style:"background: #fff; margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;"});
var row = $('<div/>').appendTo(container);
var row2 = $('<div/>',{style:"padding-top: 5px; padding-left: 175px;"}).appendTo(container);
var row3 = $('<div/>',{style:"padding-top: 5px; padding-left: 120px;"}).appendTo(container);
$('<i style="color: #eee; cursor: move;" class="node-input-rule-handle fa fa-bars"></i>').appendTo(row);
var selectField = $('<select/>',{style:"width:120px; margin-left: 5px; text-align: center;"}).appendTo(row);
@@ -125,11 +131,10 @@
selectField.append($("<option></option>").val(operators[d].v).text(operators[d].t));
}
var valueField = $('<input/>',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px; width: 145px;"}).appendTo(row);
var btwnField = $('<span/>').appendTo(row);
var btwnValueField = $('<input/>',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px; width: 50px;"}).appendTo(btwnField);
var btwnAndLabel = $('<span/>',{class:"node-input-tule-btwn-label"}).text(" "+andLabel+" ").appendTo(btwnField);
var btwnValue2Field = $('<input/>',{class:"node-input-rule-btwn-value2",type:"text",style:"width: 50px;margin-left:2px;"}).appendTo(btwnField);
var valueField = $('<input/>',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px; width: 145px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num',previousValueType]});
var btwnValueField = $('<input/>',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]});
var btwnAndLabel = $('<span/>',{class:"node-input-rule-btwn-label"}).text(" "+andLabel+" ").appendTo(row3);
var btwnValue2Field = $('<input/>',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num',previousValueType]});
var finalspan = $('<span/>',{style:"float: right;margin-right: 10px;"}).appendTo(row);
finalspan.append(' &#8594; <span class="node-input-rule-index">'+i+'</span> ');
@@ -141,24 +146,28 @@
$('<label/>',{for:"node-input-rule-case-"+i,style:"margin-left: 3px;"}).text(caseLabel).appendTo(row2);
selectField.change(function() {
var width = $("#node-input-rule-container").width();
resizeRule(container,width);
var type = selectField.children("option:selected").val();
node.resizeRule(container,width);
if (type === "btwn") {
valueField.hide();
btwnField.show();
valueField.parent().hide();
btwnValueField.parent().show();
} else {
btwnField.hide();
btwnValueField.parent().hide();
if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") {
valueField.hide();
valueField.parent().hide();
} else {
valueField.show();
valueField.parent().show();
}
}
if (type === "regex") {
row2.show();
row3.hide();
} else if (type === "btwn"){
row2.hide();
row3.show();
} else {
row2.hide();
row3.hide();
}
});
@@ -177,10 +186,13 @@
selectField.find("option").filter(function() {return $(this).val() == rule.t;}).attr('selected',true);
if (rule.t == "btwn") {
btwnValueField.val(rule.v);
btwnValue2Field.val(rule.v2);
btwnValueField.typedInput('value',rule.v);
btwnValueField.typedInput('type',rule.vt||'num');
btwnValue2Field.typedInput('value',rule.v2);
btwnValue2Field.typedInput('type',rule.v2t||'num');
} else if (typeof rule.v != "undefined") {
valueField.val(rule.v);
valueField.typedInput('value',rule.v);
valueField.typedInput('type',rule.vt||'str');
}
if (rule.case) {
caseSensitive.prop('checked',true);
@@ -200,23 +212,6 @@
generateRule(i+1,rule);
}
function switchDialogResize() {
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-rule-container-div").css("height",height+"px");
var rules = $("#node-input-rule-container").children();
var newWidth = $("#node-input-rule-container").width();
rules.each(function(i) {
resizeRule($(this),newWidth);
})
};
$( "#node-input-rule-container" ).sortable({
axis: "y",
update: function( event, ui ) {
@@ -229,21 +224,6 @@
cursor: "move"
});
$( "#node-input-rule-container .node-input-rule-handle" ).disableSelection();
$( "#dialog" ).on("dialogresize", switchDialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-switch');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
switchDialogResize();
} else {
setTimeout(switchDialogResize,10);
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
$( "#dialog" ).off("dialogresize",switchDialogResize);
});
},
oneditsave: function() {
var rules = $("#node-input-rule-container").children();
@@ -256,10 +236,13 @@
var r = {t:type};
if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else")) {
if (type === "btwn") {
r.v = rule.find(".node-input-rule-btwn-value").val();
r.v2 = rule.find(".node-input-rule-btwn-value2").val();
r.v = rule.find(".node-input-rule-btwn-value").typedInput('value');
r.vt = rule.find(".node-input-rule-btwn-value").typedInput('type');
r.v2 = rule.find(".node-input-rule-btwn-value2").typedInput('value');
r.v2t = rule.find(".node-input-rule-btwn-value2").typedInput('type');
} else {
r.v = rule.find(".node-input-rule-value").val();
r.v = rule.find(".node-input-rule-value").typedInput('value');
r.vt = rule.find(".node-input-rule-value").typedInput('type');
}
if (type === "regex") {
r.case = rule.find(".node-input-rule-case").prop("checked");
@@ -267,7 +250,25 @@
}
node.rules.push(r);
});
node.outputs = node.rules.length;
this.outputs = node.rules.length;
this.propertyType = $("#node-input-property").typedInput('type');
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
var height = size.height;
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-rule-container-div").css("height",height+"px");
var rules = $("#node-input-rule-container").children();
var newWidth = $("#node-input-rule-container").width();
var node = this;
rules.each(function(i) {
node.resizeRule($(this),newWidth);
})
}
});
</script>

View File

@@ -37,30 +37,55 @@ module.exports = function(RED) {
RED.nodes.createNode(this, n);
this.rules = n.rules || [];
this.property = n.property;
this.propertyType = n.propertyType || "msg";
this.checkall = n.checkall || "true";
var propertyParts = (n.property || "payload").split(".");
this.previousValue = null;
var node = this;
for (var i=0; i<this.rules.length; i+=1) {
var rule = this.rules[i];
if (!isNaN(Number(rule.v))) {
rule.v = Number(rule.v);
rule.v2 = Number(rule.v2);
if (!rule.vt) {
rule.vt = 'str';
}
if (rule.vt === 'str' || rule.vt === 'num') {
if (!isNaN(Number(rule.v))) {
rule.v = Number(rule.v);
}
}
if (typeof rule.v2 !== 'undefined') {
if (!rule.v2t) {
rule.v2t = 'str';
}
if (rule.v2t === 'str' || rule.v2t === 'num') {
if (!isNaN(Number(rule.v2))) {
rule.v2 = Number(rule.v2);
}
}
}
}
this.on('input', function (msg) {
var onward = [];
try {
var prop = propertyParts.reduce(function (obj, i) {
return obj[i]
}, msg);
var prop = RED.util.evaluateNodeProperty(node.property,node.propertyType,node,msg);
var elseflag = true;
for (var i=0; i<node.rules.length; i+=1) {
var rule = node.rules[i];
var test = prop;
var v1,v2;
if (rule.vt === 'prev') {
v1 = node.previousValue;
} else {
v1 = RED.util.evaluateNodeProperty(rule.v,rule.vt,node,msg);
}
v2 = rule.v2;
if (rule.v2t === 'prev') {
v2 = node.previousValue;
} else if (typeof v2 !== 'undefined') {
v2 = RED.util.evaluateNodeProperty(rule.v2,rule.v2t,node,msg);
}
node.previousValue = prop;
if (rule.t == "else") { test = elseflag; elseflag = true; }
if (operators[rule.t](test,rule.v, rule.v2, rule.case)) {
if (operators[rule.t](test,v1,v2,rule.case)) {
onward.push(msg);
elseflag = false;
if (node.checkall == "false") { break; }

View File

@@ -17,7 +17,7 @@
<script type="text/x-red" data-template-name="change">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
<input type="text" id="node-input-name" style="width: 370px;" data-i18n="[placeholder]common.label.name">
</div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"></span></label>
@@ -33,12 +33,12 @@
</script>
<script type="text/x-red" data-help-name="change">
<p>Set, change or delete properties of a message.</p>
<p>The node can specify multiple rules that will be applied to the message in turn.</p>
<p>Set, change or delete properties of a message, flow context or global context.</p>
<p>The node can specify multiple rules that will be applied in turn.</p>
<p>The available operations are:</p>
<ul>
<li><b>Set</b> - set a property. The <b>to</b> property can either be a string value, or reference
another message property by name, for example: <code>msg.topic</code>.</li>
<li><b>Set</b> - set a property. The value can be a variety of different types, or
can be taken from an existing message or context property.</li>
<li><b>Change</b> - search &amp; replace parts of the property. If regular expressions
are enabled, the <b>replace with</b> property can include capture groups, for example <code>$1</code></li>
<li><b>Delete</b> - delete a property.</li>
@@ -51,7 +51,7 @@
category: 'function',
defaults: {
name: {value:""},
rules:{value:[{t:"set",p:"payload",to:""}]},
rules:{value:[{t:"set",p:"payload",pt:"msg",to:"",tot:"str"}]},
// legacy
action: {value:""},
property: {value:""},
@@ -77,11 +77,11 @@
} else {
if (this.rules.length == 1) {
if (this.rules[0].t === "set") {
return this._("change.label.set",{property:"msg."+this.rules[0].p});
return this._("change.label.set",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p});
} else if (this.rules[0].t === "change") {
return this._("change.label.change",{property:"msg."+this.rules[0].p});
return this._("change.label.change",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p});
} else {
return this._("change.label.delete",{property:"msg."+this.rules[0].p});
return this._("change.label.delete",{property:(this.rules[0].pt||"msg")+"."+this.rules[0].p});
}
} else {
return this._("change.label.changeCount",{count:this.rules.length});
@@ -99,14 +99,22 @@
var search = this._("change.action.search");
var replace = this._("change.action.replace");
var regex = this._("change.label.regex");
if (this.reg === null) { $("#node-input-reg").prop('checked', true); }
$("#node-input-action").change();
function resizeRule(rule,width) {
rule.find('input[type="text"]').width(width-220);
}
function generateRule(rule) {
if (rule.t === "change" && rule.re) {
rule.fromt = 're';
delete rule.re;
}
if (rule.t === "set" && !rule.tot) {
if (rule.to.indexOf("msg.") === 0 && !rule.tot) {
rule.to = rule.to.substring(4);
rule.tot = "msg";
} else {
rule.tot = "str";
}
}
var container = $('<li/>',{style:"background: #fff; margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;"});
var row1 = $('<div/>').appendTo(container);
@@ -114,41 +122,46 @@
var row2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(container);
var row3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(container);
var selectField = $('<select/>',{class:"node-input-rule-type",style:"width: 100px"}).appendTo(row1);
var selectField = $('<select/>',{class:"node-input-rule-type",style:"width: 110px; margin-right: 10px;"}).appendTo(row1);
var selectOptions = [{v:"set",l:set},{v:"change",l:change},{v:"delete",l:del}];
for (var i=0;i<3;i++) {
selectField.append($("<option></option>").val(selectOptions[i].v).text(selectOptions[i].l));
}
$('<div/>',{style:"display:inline-block; width: 50px; text-align: right;"}).text("msg.").appendTo(row1);
var propertyName = $('<input/>',{style:"width: 220px",class:"node-input-rule-property-name",type:"text"}).appendTo(row1);
var finalspan = $('<span/>',{style:"float: right; margin-right: 10px;"}).appendTo(row1);
var deleteButton = $('<a/>',{href:"#",class:"editor-button editor-button-small", style:"margin-top: 7px; margin-left: 5px;"}).appendTo(finalspan);
var propertyName = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-name",type:"text"})
.appendTo(row1)
.typedInput({types:['msg','flow','global']});
var finalspan = $('<span/>',{style:"float: right; margin-right: 10px;"})
.appendTo(row1);
var deleteButton = $('<a/>',{href:"#",class:"editor-button editor-button-small", style:"margin-top: 7px; margin-left: 5px;"})
.appendTo(finalspan);
$('<i/>',{class:"fa fa-remove"}).appendTo(deleteButton);
$('<div/>',{style:"display: inline-block;text-align:right; width:150px;padding-right: 10px; box-sizing: border-box;"}).text(to).appendTo(row2);
var propertyValue = $('<input/>',{style:"width: 220px",class:"node-input-rule-property-value",type:"text"}).appendTo(row2);
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"})
.text(to)
.appendTo(row2);
var propertyValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-value",type:"text"})
.appendTo(row2)
.typedInput({default:'str',types:['msg','flow','global','str','num','bool','json']});
var row3_1 = $('<div/>').appendTo(row3);
$('<div/>',{style:"display: inline-block;text-align:right; width:150px;padding-right: 10px; box-sizing: border-box;"}).text(search).appendTo(row3_1);
var fromValue = $('<input/>',{style:"width: 220px",class:"node-input-rule-property-search-value",type:"text"}).appendTo(row3_1);
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"})
.text(search)
.appendTo(row3_1);
var fromValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-search-value",type:"text"})
.appendTo(row3_1)
.typedInput({default:'str',types:['msg','flow','global','str','re','num','bool']});
var row3_2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(row3);
$('<div/>',{style:"display: inline-block;text-align:right; width:150px;padding-right: 10px; box-sizing: border-box;"}).text(replace).appendTo(row3_2);
var toValue = $('<input/>',{style:"width: 220px",class:"node-input-rule-property-replace-value",type:"text"}).appendTo(row3_2);
var row3_3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(row3);
var id = "node-input-rule-property-regex-"+Math.floor(Math.random()*10000);
var useRegExp = $('<input/>',{id:id,class:"node-input-rule-property-re",type:"checkbox", style:"margin-left: 150px; margin-right: 10px; display: inline-block; width: auto; vertical-align: top;"}).appendTo(row3_3);
$('<label/>',{for:id,style:"width: auto;"}).text(regex).appendTo(row3_3);
$('<div/>',{style:"display: inline-block;text-align:right; width:120px;padding-right: 10px; box-sizing: border-box;"})
.text(replace)
.appendTo(row3_2);
var toValue = $('<input/>',{style:"width: 250px",class:"node-input-rule-property-replace-value",type:"text"})
.appendTo(row3_2)
.typedInput({default:'str',types:['msg','flow','global','str','num','bool','json']});
selectField.change(function() {
var width = $("#node-input-rule-container").width();
resizeRule(container,width);
var type = $(this).val();
if (type == "set") {
row2.show();
@@ -169,11 +182,14 @@
});
selectField.find("option").filter(function() {return $(this).val() == rule.t;}).attr('selected',true);
propertyName.val(rule.p);
propertyValue.val(rule.to);
fromValue.val(rule.from);
toValue.val(rule.to);
useRegExp.prop('checked', rule.re);
propertyName.typedInput('value',rule.p);
propertyName.typedInput('type',rule.pt)
propertyValue.typedInput('value',rule.to);
propertyValue.typedInput('type',rule.tot)
fromValue.typedInput('value',rule.from);
fromValue.typedInput('type',rule.fromt)
toValue.typedInput('value',rule.to);
toValue.typedInput('type',rule.tot)
selectField.change();
$("#node-input-rule-container").append(container);
@@ -185,7 +201,8 @@
if (!this.rules) {
var rule = {
t:(this.action=="replace"?"set":this.action),
p:this.property
p:this.property,
pt:"msg"
}
if (rule.t === "set") {
@@ -208,37 +225,6 @@
for (var i=0;i<this.rules.length;i++) {
generateRule(this.rules[i]);
}
function changeDialogResize() {
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-rule-container-div").css("height",height+"px");
var rules = $("#node-input-rule-container").children();
var newWidth = $("#node-input-rule-container").width();
rules.each(function(i) {
resizeRule($(this),newWidth);
})
};
$( "#dialog" ).on("dialogresize", changeDialogResize);
$( "#dialog" ).one("dialogopen", function(ev) {
var size = $( "#dialog" ).dialog('option','sizeCache-change');
if (size) {
$("#dialog").dialog('option','width',size.width);
$("#dialog").dialog('option','height',size.height);
changeDialogResize();
} else {
setTimeout(changeDialogResize,10);
}
});
$( "#dialog" ).one("dialogclose", function(ev,ui) {
$( "#dialog" ).off("dialogresize",changeDialogResize);
});
},
oneditsave: function() {
var rules = $("#node-input-rule-container").children();
@@ -250,17 +236,37 @@
var type = rule.find(".node-input-rule-type option:selected").val();
var r = {
t:type,
p:rule.find(".node-input-rule-property-name").val()
p:rule.find(".node-input-rule-property-name").typedInput('value'),
pt:rule.find(".node-input-rule-property-name").typedInput('type')
};
if (type === "set") {
r.to = rule.find(".node-input-rule-property-value").val();
r.to = rule.find(".node-input-rule-property-value").typedInput('value');
r.tot = rule.find(".node-input-rule-property-value").typedInput('type');
} else if (type === "change") {
r.from = rule.find(".node-input-rule-property-search-value").val();
r.to = rule.find(".node-input-rule-property-replace-value").val();
r.re = rule.find(".node-input-rule-property-re").prop('checked');
r.from = rule.find(".node-input-rule-property-search-value").typedInput('value');
r.fromt = rule.find(".node-input-rule-property-search-value").typedInput('type');
r.to = rule.find(".node-input-rule-property-replace-value").typedInput('value');
r.tot = rule.find(".node-input-rule-property-replace-value").typedInput('type');
}
node.rules.push(r);
});
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
var height = size.height;
for (var i=0;i<rows.size();i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-rule-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#node-input-rule-container-div").css("height",height+"px");
var rules = $("#node-input-rule-container").children();
var newWidth = $("#node-input-rule-container").width();
rules.each(function(i) {
$(this).find('.red-ui-typedInput').typedInput("width",newWidth-180);
})
$("#node-input-name").width(newWidth-130);
}
});
</script>

View File

@@ -19,15 +19,15 @@ module.exports = function(RED) {
function ChangeNode(n) {
RED.nodes.createNode(this, n);
this.rules = n.rules;
if (!this.rules) {
var rule = {
t:(n.action=="replace"?"set":n.action),
p:n.property||""
}
if (rule.t === "set") {
rule.to = n.to||"";
} else if (rule.t === "change") {
@@ -37,80 +37,185 @@ module.exports = function(RED) {
}
this.rules = [rule];
}
this.actions = [];
var valid = true;
for (var i=0;i<this.rules.length;i++) {
var rule = this.rules[i];
if (rule.t === "change") {
if (rule.re === false) {
rule.from = rule.from.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
// Migrate to type-aware rules
if (!rule.pt) {
rule.pt = "msg";
}
if (rule.t === "change" && rule.re) {
rule.fromt = 're';
delete rule.re;
}
if (rule.t === "set" && !rule.tot) {
if (rule.to.indexOf("msg.") === 0 && !rule.tot) {
rule.to = rule.to.substring(4);
rule.tot = "msg";
}
}
if (!rule.tot) {
rule.tot = "str";
}
if (!rule.fromt) {
rule.fromt = "str";
}
if (rule.t === "change" && rule.fromt !== 'msg' && rule.fromt !== 'flow' && rule.fromt !== 'global') {
rule.fromRE = rule.from;
if (rule.fromt !== 're') {
rule.fromRE = rule.fromRE.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
try {
rule.from = new RegExp(rule.from, "g");
rule.fromRE = new RegExp(rule.fromRE, "g");
} catch (e) {
valid = false;
this.error(RED._("change.errors.invalid-from",{error:e.message}));
}
}
if (rule.tot === 'num') {
rule.to = Number(rule.to);
} else if (rule.tot === 'json') {
try {
rule.to = JSON.parse(rule.to);
} catch(e2) {
valid = false;
this.error(RED._("change.errors.invalid-json"));
}
} else if (rule.tot === 'bool') {
rule.to = /^true$/i.test(rule.to);
}
}
function applyRule(msg,rule) {
var propertyParts;
var depth = 0;
propertyParts = rule.p.split(".");
try {
propertyParts.reduce(function(obj, i) {
var to = rule.to;
// Set msg from property to another msg property
if (rule.t === "set" && rule.to.indexOf("msg.") === 0) {
var parts = to.substring(4);
var msgPropParts = parts.split(".");
try {
msgPropParts.reduce(function(ob, j) {
to = (typeof ob[j] !== "undefined" ? ob[j] : undefined);
return to;
}, msg);
} catch (err) {}
}
var property = rule.p;
var value = rule.to;
var current;
var fromValue;
var fromType;
var fromRE;
if (rule.tot === "msg") {
value = RED.util.getMessageProperty(msg,rule.to);
} else if (rule.tot === 'flow') {
value = node.context().flow.get(rule.to);
} else if (rule.tot === 'global') {
value = node.context().global.get(rule.to);
}
if (++depth === propertyParts.length) {
if (rule.t === "change") {
if (typeof obj[i] === "string") {
obj[i] = obj[i].replace(rule.from, rule.to);
if (rule.t === 'change') {
if (rule.fromt === 'msg' || rule.fromt === 'flow' || rule.fromt === 'global') {
if (rule.fromt === "msg") {
fromValue = RED.util.getMessageProperty(msg,rule.from);
} else if (rule.tot === 'flow') {
fromValue = node.context().flow.get(rule.from);
} else if (rule.tot === 'global') {
fromValue = node.context().global.get(rule.from);
}
if (typeof fromValue === 'number' || fromValue instanceof Number) {
fromType = 'num';
} else if (typeof fromValue === 'boolean') {
fromType = 'bool'
} else if (fromValue instanceof RegExp) {
fromType = 're';
fromRE = fromValue;
} else if (typeof fromValue === 'string') {
fromType = 'str';
fromRE = fromValue.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
try {
fromRE = new RegExp(fromRE, "g");
} catch (e) {
valid = false;
node.error(RED._("change.errors.invalid-from",{error:e.message}));
return
}
} else if (rule.t === "set") {
if (typeof to === "undefined") {
delete(obj[i]);
} else {
obj[i] = to;
}
} else if (rule.t === "delete") {
delete(obj[i]);
} else {
node.error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}));
return
}
} else {
// to property doesn't exist, don't create empty object
if (typeof to === "undefined") {
return;
// setting a non-existent multilevel object, create empty parent
} else if (!obj[i]) {
obj[i] = {};
}
return obj[i];
fromType = rule.fromt;
fromValue = rule.from;
fromRE = rule.fromRE;
}
}, msg);
} catch (err) {}
}
if (rule.pt === 'msg') {
if (rule.t === 'delete') {
RED.util.setMessageProperty(msg,property,undefined);
} else if (rule.t === 'set') {
RED.util.setMessageProperty(msg,property,value);
} else if (rule.t === 'change') {
current = RED.util.getMessageProperty(msg,property);
if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
RED.util.setMessageProperty(msg,property,value);
} else {
current = current.replace(fromRE,value);
RED.util.setMessageProperty(msg,property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
RED.util.setMessageProperty(msg,property,value);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
RED.util.setMessageProperty(msg,property,value);
}
}
}
} else {
var target;
if (rule.pt === 'flow') {
target = node.context().flow;
} else if (rule.pt === 'global') {
target = node.context().global;
}
if (target) {
if (rule.t === 'delete') {
target.set(property,undefined);
} else if (rule.t === 'set') {
target.set(property,value);
} else if (rule.t === 'change') {
current = target.get(msg,property);
if (typeof current === 'string') {
if ((fromType === 'num' || fromType === 'bool') && current === fromValue) {
// str representation of exact from number/boolean
// only replace if they match exactly
target.set(property,value);
} else {
current = current.replace(fromRE,value);
target.set(property,current);
}
} else if ((typeof current === 'number' || current instanceof Number) && fromType === 'num') {
if (current == Number(fromValue)) {
target.set(property,value);
}
} else if (typeof current === 'boolean' && fromType === 'bool') {
if (current.toString() === fromValue) {
target.set(property,value);
}
}
}
}
}
} catch(err) {/*console.log(err.stack)*/}
return msg;
}
if (valid) {
var node = this;
this.on('input', function(msg) {
for (var i=0;i<this.rules.length;i++) {
msg = applyRule(msg,this.rules[i]);
if (msg === null) {
return;
}
}
node.send(msg);
});

View File

@@ -68,12 +68,12 @@
</script>
<script type="text/x-red" data-help-name="csv">
<p>A function that parses the <b>msg.payload</b> to convert CSV to/from a javascript object.
<p>A function that parses the <code>msg.payload</code> to convert CSV to/from a javascript object.
Places the result in the payload.</p>
<p>If the input is a string it tries to parse it as CSV and creates a javascript object.</p>
<p>If the input is a javascript object it tries to build a CSV string.</p>
<p>If the input is a simple array the output is just a CSV generated from that array.</p>
<p>If the input is an array of arrays or an array of objects a multiple-line CSV is created.</p>
<p>If the input is an array of arrays, or an array of objects, a multiple-line CSV is created.</p>
<p>The columns template should contain an ordered list of column headers. For CSV input these become the property names.
For CSV output these specify the properties to extract from the object and the order for the CSV.</p>
<p>If the input is an array then the columns template does not matter, but can be used to generate a row of column titles.</p>

View File

@@ -23,8 +23,8 @@
<label for="node-input-ret"><i class="fa fa-sign-out"></i> <span data-i18n="html.label.output"></span></label>
<select id="node-input-ret" style="width:76% !important">
<option value="html" data-i18n="html.output.html"></option>
<option value="text"data-i18n="html.output.text"></option>
<!-- <option value="attr">an object of any attributes</option> -->
<option value="text" data-i18n="html.output.text"></option>
<option value="attr" data-i18n="html.output.attr"></option>
<!-- <option value="val">return the value from a form element</option> -->
</select>
</div>
@@ -44,7 +44,7 @@
</script>
<script type="text/x-red" data-help-name="html">
<p>Extracts elements from an html document held in <b>msg.payload</b> using a selector.</p>
<p>Extracts elements from an html document held in <code>msg.payload</code> using a selector.</p>
<p>The selector uses <a href=="https://github.com/cheeriojs/cheerio/blob/master/Readme.md" target="_new">Cheerio</a>
which uses the <a href="https://github.com/fb55/CSSselect#user-content-supported-selectors" target="_new">CSS selector</a> syntax.</p>
<p>The result can be either a single message with a payload containing an array of the matched elements, or multiple

View File

@@ -32,9 +32,9 @@ module.exports = function(RED) {
$(node.tag).each(function() {
if (node.as === "multi") {
var pay2 = null;
if (node.ret === "html") { pay2 = $(this).html(); }
if (node.ret === "html") { pay2 = cheerio.load($(this).html().trim()).xml(); }
if (node.ret === "text") { pay2 = $(this).text(); }
//if (node.ret === "attr") { pay2 = $(this)[0]["attribs"]; }
if (node.ret === "attr") { pay2 = this.attribs; }
//if (node.ret === "val") { pay2 = $(this).val(); }
/* istanbul ignore else */
if (pay2) {
@@ -43,9 +43,9 @@ module.exports = function(RED) {
}
}
if (node.as === "single") {
if (node.ret === "html") { pay.push( $(this).html() ); }
if (node.ret === "html") { pay.push( cheerio.load($(this).html().trim()).xml() ); }
if (node.ret === "text") { pay.push( $(this).text() ); }
//if (node.ret === "attr") { pay.push( $(this)[0]["attribs"] ); }
if (node.ret === "attr") { pay.push( this.attribs ); }
//if (node.ret === "val") { pay.push( $(this).val() ); }
}
});

View File

@@ -22,7 +22,7 @@
</script>
<script type="text/x-red" data-help-name="json">
<p>A function that parses the <b>msg.payload</b> to convert a JSON string to/from a javascript object. Places the result back into the payload.</p>
<p>A function that parses the <code>msg.payload</code> to convert a JSON string to/from a javascript object. Places the result back into the payload.</p>
<p>If the input is a JSON string it tries to parse it to a javascript object.</p>
<p>If the input is a javascript object it creates a JSON string.</p>
</script>

View File

@@ -33,11 +33,11 @@
</script>
<script type="text/x-red" data-help-name="xml">
<p>A function that parses the <b>msg.payload</b> to convert xml to/from a javascript object. Places the result in the payload.</p>
<p>A function that parses the <code>msg.payload</code> to convert xml to/from a javascript object. Places the result in the payload.</p>
<p>If the input is a string it tries to parse it as XML and creates a javascript object.</p>
<p>If the input is a javascript object it tries to build an XML string.</p>
<p>You can also pass in a <b>msg.options</b> object to overide all the multitude of parameters. See
<a href="https://github.com/Leonidas-from-XIV/node-xml2js/blob/master/README.md" target="_new">the xml2js docs <i>here</i></a>
<p>You can also pass in a <code>msg.options</code> object to overide all the multitude of parameters. See
<a href="https://github.com/Leonidas-from-XIV/node-xml2js/blob/master/README.md#options" target="_new">the xml2js docs</a>
for more information.</p>
<p>If set, options in the edit dialogue override those passed in on the msg.options object.</p>
</script>

View File

@@ -1,5 +1,5 @@
<!--
Copyright 2013 IBM Corp.
Copyright 2013,2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,11 +15,18 @@
-->
<script type="text/x-red" data-template-name="tail">
<div class="form-row node-input-filename">
<div class="form-row">
<label for="node-input-filename"><i class="fa fa-file"></i> <span data-i18n="tail.label.filename"></span></label>
<input type="text" id="node-input-filename">
</div>
<div class="form-row">
<label for="node-input-filetype"><i class="fa fa-file-text-o"></i> <span data-i18n="tail.label.type"></span></label>
<select type="text" id="node-input-filetype">
<option value="text" data-i18n="tail.action.text"></option>
<option value="binary" data-i18n="tail.action.binary"></option>
</select>
</div>
<div class="form-row" id="node-tail-split">
<label>&nbsp;</label>
<input type="checkbox" id="node-input-split" placeholder="Name" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-input-split" style="width: 70%;"><span data-i18n="tail.label.splitlines"></span></label>
@@ -28,12 +35,12 @@
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
<!-- <div class="form-tips">WON'T work on Windows.</div> -->
</script>
<script type="text/x-red" data-help-name="tail">
<p>Tails (watches for things to be added) to the configured file. (Linux/Mac ONLY)</p>
<p>This won't work on Windows filesystems, as it relies on the tail -F command.</p>
<p>This will not work on Windows filesystems, as it relies on the <b>tail -F</b> command.</p>
<p>Text (UTF-8) files will be returned as strings. Binary files will be returned as a Buffer object.</p>
</script>
<script type="text/javascript">
@@ -41,6 +48,7 @@
category: 'storage-input',
defaults: {
name: {value:""},
filetype: {value:"text"},
split: {value:false},
filename: {value:"",required:true}
},
@@ -53,6 +61,12 @@
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
$("#node-input-filetype").on("change",function() {
if (this.value === "text") { $("#node-tail-split").show(); }
else { $("#node-tail-split").hide(); }
});
}
});
</script>

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2014 IBM Corp.
* Copyright 2013,2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,31 +27,36 @@ module.exports = function(RED) {
RED.nodes.createNode(this,n);
this.filename = n.filename;
this.split = n.split;
this.filetype = n.filetype || "text";
this.split = n.split || false;
var node = this;
var err = "";
// TODO: rewrite to use node-tail
var tail = spawn("tail", ["-F", "-n", "0", this.filename]);
tail.stdout.on("data", function (data) {
if (node.split) {
// TODO: allow customisation of the line break - as we do elsewhere
var strings = data.toString().split("\n");
for (var s in strings) {
//TODO: should we really filter blanks? Is that expected?
if (strings[s] !== "") {
node.send({
topic: node.filename,
payload: strings[s]
});
var msg = { topic:node.filename };
if (node.filetype === "text") {
if (node.split) {
// TODO: allow customisation of the line break - as we do elsewhere
var strings = data.toString().split("\n");
for (var s in strings) {
//TODO: should we really filter blanks? Is that expected?
if (strings[s] !== "") {
node.send({
topic: node.filename,
payload: strings[s]
});
}
}
}
else {
msg.payload = data.toString();
node.send(msg);
}
}
else {
var msg = {
topic:node.filename,
payload: data.toString()
};
msg.payload = data;
node.send(msg);
}
});

View File

@@ -44,11 +44,12 @@
</script>
<script type="text/x-red" data-help-name="file">
<p>Writes <b>msg.payload</b> to the file specified, e.g. to create a log.</p>
<p>The filename can be configured in the node, if left blank it should be set in an incoming message on <b>msg.filename</b>.</p>
<p>Writes <code>msg.payload</code> to the file specified, for example to create a log.</p>
<p>The filename can be configured in the node. If left blank it should be
set by <code>msg.filename</code> on the incoming message.</p>
<p>A newline is added to every message. But this can be turned off if required, for example, to allow binary files to be written.</p>
<p>The default behaviour is to append to the file. This can be changed to overwrite the file each time, for example if you want to output a "static" web page or report.</p>
<p>This node can also be configured to delete a file if required. <i>Note:</i> Using msg.delete is now deprecated.</p>
<p>This node can also be configured to delete a file if required.</p>
</script>
<script type="text/x-red" data-template-name="file in">
@@ -70,8 +71,10 @@
</script>
<script type="text/x-red" data-help-name="file in">
<p>Reads the specified file and sends the content as <b>msg.payload</b>, and the filename as <b>msg.filename</b>.</p>
<p>The filename can be configured in the node, if left blank it should be set in an incoming message on <b>msg.filename</b>.</p>
<p>Reads the specified file and sends the content as <code>msg.payload</code>,
and the filename as <code>msg.filename</code>.</p>
<p>The filename can be configured in the node. If left blank it should be
set by <code>msg.filename</code> on the incoming message.</p>
</script>
<script type="text/javascript">

View File

@@ -42,6 +42,7 @@ module.exports = function(RED) {
if (typeof data === "boolean") { data = data.toString(); }
if (typeof data === "number") { data = data.toString(); }
if ((this.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; }
data = new Buffer(data);
if (this.overwriteFile === "true") {
// using "binary" not {encoding:"binary"} to be 0.8 compatible for a while
fs.writeFile(filename, data, "binary", function (err) {

View File

@@ -1,6 +1,6 @@
{
"name" : "node-red",
"version" : "0.12.4",
"version" : "0.13.4",
"description" : "A visual tool for wiring the Internet of Things",
"homepage" : "http://nodered.org",
"license" : "Apache-2.0",
@@ -22,38 +22,38 @@
{"name": "Dave Conway-Jones"}
],
"keywords": [
"editor", "messaging", "iot", "m2m", "pi", "arduino", "beaglebone", "ibm", "flow"
"editor", "messaging", "iot", "ibm", "flow"
],
"dependencies": {
"basic-auth": "1.0.3",
"bcryptjs": "2.3.0",
"body-parser": "1.14.1",
"body-parser": "1.15.0",
"cheerio":"0.19.0",
"clone": "1.0.2",
"cors":"2.7.1",
"cron":"1.1.0",
"express": "4.13.3",
"express": "4.13.4",
"follow-redirects":"0.0.7",
"fs-extra": "0.26.2",
"fs-extra": "0.26.7",
"fs.notify":"0.0.4",
"i18next":"1.10.6",
"is-utf8":"0.2.0",
"is-utf8":"0.2.1",
"media-typer": "0.3.0",
"mqtt": "1.6.1",
"mustache": "2.2.0",
"mqtt": "1.7.4",
"mustache": "2.2.1",
"nopt": "3.0.6",
"oauth2orize":"1.2.0",
"oauth2orize":"1.2.2",
"on-headers":"1.0.1",
"passport":"0.3.2",
"passport-http-bearer":"1.0.1",
"passport-oauth2-client-password":"0.1.2",
"raw-body":"2.1.5",
"raw-body":"2.1.6",
"semver": "5.1.0",
"sentiment":"0.2.3",
"uglify-js":"2.6.1",
"when": "3.7.5",
"sentiment":"1.0.6",
"uglify-js":"2.6.2",
"when": "3.7.7",
"ws": "0.8.1",
"xml2js":"0.4.15",
"xml2js":"0.4.16",
"node-red-node-feedparser":"0.1.*",
"node-red-node-email":"0.1.*",
"node-red-node-twitter":"0.1.*",
@@ -67,22 +67,22 @@
"grunt": "0.4.5",
"grunt-chmod": "1.1.1",
"grunt-cli": "0.1.13",
"grunt-concurrent":"2.1.0",
"grunt-concurrent":"2.2.1",
"grunt-contrib-clean":"0.7.0",
"grunt-contrib-compress": "0.14.0",
"grunt-contrib-concat":"0.5.1",
"grunt-contrib-copy": "0.8.2",
"grunt-contrib-jshint": "0.11.3",
"grunt-contrib-uglify": "0.11.0",
"grunt-contrib-jshint": "0.12.0",
"grunt-contrib-uglify": "0.11.1",
"grunt-contrib-watch":"0.6.1",
"grunt-jsonlint":"1.0.6",
"grunt-jsonlint":"1.0.7",
"grunt-nodemon":"0.4.1",
"grunt-sass":"1.1.0",
"grunt-simple-mocha": "0.4.0",
"mocha": "2.3.4",
"grunt-simple-mocha": "0.4.1",
"mocha": "2.4.5",
"should": "6.0.3",
"sinon": "1.17.2",
"supertest": "1.1.0"
"sinon": "1.17.3",
"supertest": "1.2.0"
},
"engines": {
"node": ">=0.10"

30
red.js
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Copyright 2013, 2015 IBM Corp.
* Copyright 2013, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@ var nopt = require("nopt");
var path = require("path");
var fs = require("fs-extra");
var RED = require("./red/red.js");
var log = require("./red/log");
var server;
var app = express();
@@ -36,12 +35,14 @@ var flowFile;
var knownOpts = {
"settings":[path],
"userDir":[path],
"port": Number,
"v": Boolean,
"help": Boolean
};
var shortHands = {
"s":["--settings"],
"u":["--userDir"],
"p":["--port"],
"?":["--help"]
};
nopt.invalidHandler = function(k,v,t) {
@@ -52,11 +53,13 @@ var parsedArgs = nopt(knownOpts,shortHands,process.argv,2)
if (parsedArgs.help) {
console.log("Node-RED v"+RED.version());
console.log("Usage: node-red [-v] [-?] [--settings settings.js] [--userDir DIR] [flows.json]");
console.log("Usage: node-red [-v] [-?] [--settings settings.js] [--userDir DIR]");
console.log(" [--port PORT] [flows.json]");
console.log("");
console.log("Options:");
console.log(" -s, --settings FILE use specified settings file");
console.log(" -u, --userDir DIR use specified user directory");
console.log(" -p, --port PORT port to listen on");
console.log(" -v enable verbose output");
console.log(" -?, --help show usage");
console.log("");
@@ -154,7 +157,7 @@ if (settings.httpNodeRoot !== false) {
settings.httpNodeAuth = settings.httpNodeAuth || settings.httpAuth;
}
settings.uiPort = settings.uiPort||1880;
settings.uiPort = parsedArgs.port||settings.uiPort||1880;
settings.uiHost = settings.uiHost||"0.0.0.0";
if (flowFile) {
@@ -195,6 +198,9 @@ function basicAuthMiddleware(user,pass) {
}
return function(req,res,next) {
if (req.method === 'OPTIONS') {
return next();
}
var requestUser = basicAuth(req);
if (!requestUser || requestUser.name !== user || !checkPassword(requestUser.pass)) {
res.set('WWW-Authenticate', 'Basic realm=Authorization Required');
@@ -205,7 +211,7 @@ function basicAuthMiddleware(user,pass) {
}
if (settings.httpAdminRoot !== false && settings.httpAdminAuth) {
RED.log.warn(log._("server.httpadminauth-deprecated"));
RED.log.warn(RED.log._("server.httpadminauth-deprecated"));
app.use(settings.httpAdminRoot, basicAuthMiddleware(settings.httpAdminAuth.user,settings.httpAdminAuth.pass));
}
@@ -243,10 +249,10 @@ RED.start().then(function() {
if (settings.httpAdminRoot !== false || settings.httpNodeRoot !== false || settings.httpStatic) {
server.on('error', function(err) {
if (err.errno === "EADDRINUSE") {
RED.log.error(log._("server.unable-to-listen", {listenpath:getListenPath()}));
RED.log.error(log._("server.port-in-use"));
RED.log.error(RED.log._("server.unable-to-listen", {listenpath:getListenPath()}));
RED.log.error(RED.log._("server.port-in-use"));
} else {
RED.log.error(log._("server.uncaught-exception"));
RED.log.error(RED.log._("server.uncaught-exception"));
if (err.stack) {
RED.log.error(err.stack);
} else {
@@ -257,16 +263,16 @@ RED.start().then(function() {
});
server.listen(settings.uiPort,settings.uiHost,function() {
if (settings.httpAdminRoot === false) {
RED.log.info(log._("server.admin-ui-disabled"));
RED.log.info(RED.log._("server.admin-ui-disabled"));
}
process.title = 'node-red';
RED.log.info(log._("server.now-running", {listenpath:getListenPath()}));
RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()}));
});
} else {
RED.log.info(log._("server.headless-mode"));
RED.log.info(RED.log._("server.headless-mode"));
}
}).otherwise(function(err) {
RED.log.error(log._("server.failed-to-start"));
RED.log.error(RED.log._("server.failed-to-start"));
if (err.stack) {
RED.log.error(err.stack);
} else {

View File

@@ -25,7 +25,7 @@ var permissions = require("./permissions");
var theme = require("../theme");
var settings = null;
var log = require("../../log");
var log = null
passport.use(strategies.bearerStrategy.BearerStrategy);
@@ -36,11 +36,13 @@ var server = oauth2orize.createServer();
server.exchange(oauth2orize.exchange.password(strategies.passwordTokenExchange));
function init(_settings,storage) {
settings = _settings;
function init(runtime) {
settings = runtime.settings;
log = runtime.log;
if (settings.adminAuth) {
Users.init(settings.adminAuth);
Tokens.init(settings.adminAuth,storage);
Tokens.init(settings.adminAuth,runtime.storage);
strategies.init(runtime);
}
}

View File

@@ -26,7 +26,7 @@ var Users = require("./users");
var Clients = require("./clients");
var permissions = require("./permissions");
var log = require("../../log");
var log;
var bearerStrategy = function (accessToken, done) {
// is this a valid token?
@@ -124,6 +124,9 @@ AnonymousStrategy.prototype.authenticate = function(req) {
}
module.exports = {
init: function(runtime) {
log = runtime.log;
},
bearerStrategy: bearerStrategy,
clientPasswordStrategy: clientPasswordStrategy,
passwordTokenExchange: passwordTokenExchange,

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2014, 2015 IBM Corp.
* Copyright 2014, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
**/
var ws = require("ws");
var log = require("./log");
var log;
var server;
var settings;
@@ -29,23 +29,37 @@ var retained = {};
var heartbeatTimer;
var lastSentTime;
function init(_server,_settings) {
server = _server;
settings = _settings;
function handleStatus(event) {
publish("status/"+event.id,event.status,true);
}
function init(_server,runtime) {
server = _server;
settings = runtime.settings;
log = runtime.log;
runtime.events.removeListener("node-status",handleStatus);
runtime.events.on("node-status",handleStatus);
}
function start() {
var Tokens = require("./api/auth/tokens");
var Users = require("./api/auth/users");
var Permissions = require("./api/auth/permissions");
var Tokens = require("./auth/tokens");
var Users = require("./auth/users");
var Permissions = require("./auth/permissions");
if (!settings.disableEditor) {
Users.default().then(function(anonymousUser) {
var webSocketKeepAliveTime = settings.webSocketKeepAliveTime || 15000;
var path = settings.httpAdminRoot || "/";
path = (path.slice(0,1) != "/" ? "/":"") + path + (path.slice(-1) == "/" ? "":"/") + "comms";
wsServer = new ws.Server({server:server,path:path});
wsServer = new ws.Server({
server:server,
path:path,
// Disable the deflate option due to this issue
// https://github.com/websockets/ws/pull/632
// that is fixed in the 1.x release of the ws module
// that we cannot currently pickup as it drops node 0.10 support
perMessageDeflate: false
});
wsServer.on('connection',function(ws) {
log.audit({event: "comms.open"});
@@ -151,15 +165,17 @@ function stop() {
}
function publish(topic,data,retain) {
if (retain) {
retained[topic] = data;
} else {
delete retained[topic];
if (server) {
if (retain) {
retained[topic] = data;
} else {
delete retained[topic];
}
lastSentTime = Date.now();
activeConnections.forEach(function(conn) {
publishTo(conn,topic,data);
});
}
lastSentTime = Date.now();
activeConnections.forEach(function(conn) {
publishTo(conn,topic,data);
});
}
function publishTo(ws,topic,data) {

View File

@@ -14,10 +14,14 @@
* limitations under the License.
**/
var log = require("../log");
var api = require("../nodes");
var log;
var api;
module.exports = {
init: function(runtime) {
log = runtime.log;
api = runtime.nodes;
},
get: function (req, res) {
// TODO: It should verify the given node id is of the type specified -
// but that would add a dependency from this module to the

87
red/api/flow.js Normal file
View File

@@ -0,0 +1,87 @@
/**
* Copyright 2014, 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var log;
var redNodes;
var settings;
module.exports = {
init: function(runtime) {
settings = runtime.settings;
redNodes = runtime.nodes;
log = runtime.log;
},
get: function(req,res) {
var id = req.params.id;
var flow = redNodes.getFlow(id);
if (flow) {
log.audit({event: "flow.get",id:id},req);
res.json(flow);
} else {
log.audit({event: "flow.get",id:id,error:"not_found"},req);
res.status(404).end();
}
},
post: function(req,res) {
var flow = req.body;
redNodes.addFlow(flow).then(function(id) {
log.audit({event: "flow.add",id:id},req);
res.json({id:id});
}).otherwise(function(err) {
log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
})
},
put: function(req,res) {
var id = req.params.id;
var flow = req.body;
try {
redNodes.updateFlow(id,flow).then(function() {
log.audit({event: "flow.update",id:id},req);
res.json({id:id});
}).otherwise(function(err) {
log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
})
} catch(err) {
if (err.code === 404) {
log.audit({event: "flow.update",id:id,error:"not_found"},req);
res.status(404).end();
} else {
log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
}
}
},
delete: function(req,res) {
var id = req.params.id;
try {
redNodes.removeFlow(id).then(function() {
log.audit({event: "flow.remove",id:id},req);
res.status(204).end();
})
} catch(err) {
if (err.code === 404) {
log.audit({event: "flow.remove",id:id,error:"not_found"},req);
res.status(404).end();
} else {
log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()},req);
res.status(400).json({error:err.code||"unexpected_error", message:err.toString()});
}
}
}
}

View File

@@ -14,12 +14,16 @@
* limitations under the License.
**/
var log = require("../log");
var redNodes = require("../nodes");
var settings = require("../settings");
var log;
var redNodes;
var settings;
module.exports = {
init: function(runtime) {
settings = runtime.settings;
redNodes = runtime.nodes;
log = runtime.log;
},
get: function(req,res) {
log.audit({event: "flows.get"},req);
res.json(redNodes.getFlows());
@@ -28,12 +32,22 @@ module.exports = {
var flows = req.body;
var deploymentType = req.get("Node-RED-Deployment-Type")||"full";
log.audit({event: "flows.set",type:deploymentType},req);
redNodes.setFlows(flows,deploymentType).then(function() {
res.status(204).end();
}).otherwise(function(err) {
log.warn(log._("api.flows.error-save",{message:err.message}));
log.warn(err.stack);
res.status(500).json({error:"unexpected_error", message:err.message});
});
if (deploymentType === 'reload') {
redNodes.loadFlows().then(function() {
res.status(204).end();
}).otherwise(function(err) {
log.warn(log._("api.flows.error-reload",{message:err.message}));
log.warn(err.stack);
res.status(500).json({error:"unexpected_error", message:err.message});
});
} else {
redNodes.setFlows(flows,deploymentType).then(function() {
res.status(204).end();
}).otherwise(function(err) {
log.warn(log._("api.flows.error-save",{message:err.message}));
log.warn(err.stack);
res.status(500).json({error:"unexpected_error", message:err.message});
});
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2014 IBM Corp.
* Copyright 2014, 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,22 +19,28 @@ var bodyParser = require("body-parser");
var util = require('util');
var path = require('path');
var passport = require('passport');
var when = require('when');
var ui = require("./ui");
var nodes = require("./nodes");
var flows = require("./flows");
var flow = require("./flow");
var library = require("./library");
var info = require("./info");
var theme = require("./theme");
var locales = require("./locales");
var credentials = require("./credentials");
var log = require("../log");
var comms = require("./comms");
var auth = require("./auth");
var needsPermission = auth.needsPermission;
var settings = require("../settings");
var i18n;
var log;
var adminApp;
var nodeApp;
var server;
var runtime;
var errorHandler = function(err,req,res,next) {
if (err.message === "request entity too large") {
@@ -46,71 +52,126 @@ var errorHandler = function(err,req,res,next) {
res.status(400).json({error:"unexpected_error", message:err.toString()});
};
function init(adminApp,storage) {
auth.init(settings,storage);
// Editor
if (!settings.disableEditor) {
ui.init(settings);
var editorApp = express();
editorApp.get("/",ui.ensureSlash,ui.editor);
editorApp.get("/icons/:icon",ui.icon);
if (settings.editorTheme) {
editorApp.use("/theme",theme.init(settings));
}
editorApp.use("/",ui.editorResources);
adminApp.use(editorApp);
var ensureRuntimeStarted = function(req,res,next) {
if (!runtime.isStarted()) {
log.error("Node-RED runtime not started");
res.status(503).send("Not started");
} else {
next();
}
var maxApiRequestSize = settings.apiMaxLength || '1mb';
adminApp.use(bodyParser.json({limit:maxApiRequestSize}));
adminApp.use(bodyParser.urlencoded({limit:maxApiRequestSize,extended:true}));
adminApp.get("/auth/login",auth.login);
if (settings.adminAuth) {
//TODO: all passport references ought to be in ./auth
adminApp.use(passport.initialize());
adminApp.post("/auth/token",
auth.ensureClientSecret,
auth.authenticateClient,
auth.getToken,
auth.errorHandler
);
adminApp.post("/auth/revoke",needsPermission(""),auth.revoke);
}
// Flows
adminApp.get("/flows",needsPermission("flows.read"),flows.get);
adminApp.post("/flows",needsPermission("flows.write"),flows.post);
// Nodes
adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll);
adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post);
adminApp.get("/nodes/:mod",needsPermission("nodes.read"),nodes.getModule);
adminApp.put("/nodes/:mod",needsPermission("nodes.write"),nodes.putModule);
adminApp.delete("/nodes/:mod",needsPermission("nodes.write"),nodes.delete);
adminApp.get("/nodes/:mod/:set",needsPermission("nodes.read"),nodes.getSet);
adminApp.put("/nodes/:mod/:set",needsPermission("nodes.write"),nodes.putSet);
adminApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get);
adminApp.get(/locales\/(.+)\/?$/,locales.get);
// Library
library.init(adminApp);
adminApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post);
adminApp.get("/library/flows",needsPermission("library.read"),library.getAll);
adminApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get);
// Settings
adminApp.get("/settings",needsPermission("settings.read"),info.settings);
// Error Handler
adminApp.use(errorHandler);
}
function init(_server,_runtime) {
server = _server;
runtime = _runtime;
var settings = runtime.settings;
i18n = runtime.i18n;
log = runtime.log;
if (settings.httpNodeRoot !== false) {
nodeApp = express();
}
if (settings.httpAdminRoot !== false) {
comms.init(server,runtime);
adminApp = express();
auth.init(runtime);
credentials.init(runtime);
flows.init(runtime);
flow.init(runtime);
info.init(runtime);
library.init(adminApp,runtime);
locales.init(runtime);
nodes.init(runtime);
// Editor
if (!settings.disableEditor) {
ui.init(runtime);
var editorApp = express();
editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor);
editorApp.get("/icons/:icon",ui.icon);
theme.init(runtime);
if (settings.editorTheme) {
editorApp.use("/theme",theme.app());
}
editorApp.use("/",ui.editorResources);
adminApp.use(editorApp);
}
var maxApiRequestSize = settings.apiMaxLength || '1mb';
adminApp.use(bodyParser.json({limit:maxApiRequestSize}));
adminApp.use(bodyParser.urlencoded({limit:maxApiRequestSize,extended:true}));
adminApp.get("/auth/login",auth.login,errorHandler);
if (settings.adminAuth) {
//TODO: all passport references ought to be in ./auth
adminApp.use(passport.initialize());
adminApp.post("/auth/token",
auth.ensureClientSecret,
auth.authenticateClient,
auth.getToken,
auth.errorHandler
);
adminApp.post("/auth/revoke",needsPermission(""),auth.revoke,errorHandler);
}
// Flows
adminApp.get("/flows",needsPermission("flows.read"),flows.get,errorHandler);
adminApp.post("/flows",needsPermission("flows.write"),flows.post,errorHandler);
adminApp.get("/flow/:id",needsPermission("flows.read"),flow.get,errorHandler);
adminApp.post("/flow",needsPermission("flows.write"),flow.post,errorHandler);
adminApp.delete("/flow/:id",needsPermission("flows.write"),flow.delete,errorHandler);
adminApp.put("/flow/:id",needsPermission("flows.write"),flow.put,errorHandler);
// Nodes
adminApp.get("/nodes",needsPermission("nodes.read"),nodes.getAll,errorHandler);
adminApp.post("/nodes",needsPermission("nodes.write"),nodes.post,errorHandler);
adminApp.get("/nodes/:mod",needsPermission("nodes.read"),nodes.getModule,errorHandler);
adminApp.put("/nodes/:mod",needsPermission("nodes.write"),nodes.putModule,errorHandler);
adminApp.delete("/nodes/:mod",needsPermission("nodes.write"),nodes.delete,errorHandler);
adminApp.get("/nodes/:mod/:set",needsPermission("nodes.read"),nodes.getSet,errorHandler);
adminApp.put("/nodes/:mod/:set",needsPermission("nodes.write"),nodes.putSet,errorHandler);
adminApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,errorHandler);
adminApp.get(/locales\/(.+)\/?$/,locales.get,errorHandler);
// Library
adminApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post,errorHandler);
adminApp.get("/library/flows",needsPermission("library.read"),library.getAll,errorHandler);
adminApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get,errorHandler);
// Settings
adminApp.get("/settings",needsPermission("settings.read"),info.settings,errorHandler);
// Error Handler
//adminApp.use(errorHandler);
}
}
function start() {
return i18n.registerMessageCatalog("editor",path.resolve(path.join(__dirname,"locales")),"editor.json").then(function(){
comms.start();
});
}
function stop() {
comms.stop();
return when.resolve();
}
module.exports = {
init: init
init: init,
start: start,
stop: stop,
library: {
register: library.register
},
auth: {
needsPermission: auth.needsPermission
},
comms: {
publish: comms.publish
},
get adminApp() { return adminApp; },
get nodeApp() { return nodeApp; },
get server() { return server; }
};

View File

@@ -13,28 +13,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var settings = require('../settings');
var theme = require("./theme");
var util = require('util');
var settings;
module.exports = {
init: function(runtime) {
settings = runtime.settings;
},
settings: function(req,res) {
var safeSettings = {
httpNodeRoot: settings.httpNodeRoot,
version: settings.version,
user: req.user
}
var themeSettings = theme.settings();
if (themeSettings) {
safeSettings.editorTheme = themeSettings;
}
if (util.isArray(settings.paletteCategories)) {
safeSettings.paletteCategories = settings.paletteCategories;
}
res.json(safeSettings);
}
}

View File

@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var fs = require('fs');
var fspath = require('path');
var when = require('when');
var redApp = null;
var storage = require("../storage");
var log = require("../log");
var storage;
var log;
var needsPermission = require("./auth").needsPermission;
function createLibrary(type) {
@@ -69,38 +71,135 @@ function createLibrary(type) {
});
}
}
var exampleRoots = {};
var exampleFlows = {d:{}};
var exampleCount = 0;
function getFlowsFromPath(path) {
return when.promise(function(resolve,reject) {
var result = {};
fs.readdir(path,function(err,files) {
var promises = [];
var validFiles = [];
files.forEach(function(file) {
var fullPath = fspath.join(path,file);
var stats = fs.lstatSync(fullPath);
if (stats.isDirectory()) {
validFiles.push(file);
promises.push(getFlowsFromPath(fullPath));
} else if (/\.json$/.test(file)){
validFiles.push(file);
exampleCount++;
promises.push(when.resolve(file.split(".")[0]))
}
})
var i=0;
when.all(promises).then(function(results) {
results.forEach(function(r) {
if (typeof r === 'string') {
result.f = result.f||[];
result.f.push(r);
} else {
result.d = result.d||{};
result.d[validFiles[i]] = r;
}
i++;
})
resolve(result);
})
});
})
}
function addNodeExamplesDir(module) {
exampleRoots[module.name] = module.path;
getFlowsFromPath(module.path).then(function(result) {
exampleFlows.d[module.name] = result;
});
}
function removeNodeExamplesDir(module) {
delete exampleRoots[module];
delete exampleFlows.d[module];
}
module.exports = {
init: function(app) {
init: function(app,runtime) {
redApp = app;
log = runtime.log;
storage = runtime.storage;
// TODO: this allows init to be called multiple times without
// registering multiple instances of the listener.
// It isn't.... ideal.
runtime.events.removeListener("node-examples-dir",addNodeExamplesDir);
runtime.events.on("node-examples-dir",addNodeExamplesDir);
runtime.events.removeListener("node-module-uninstalled",removeNodeExamplesDir);
runtime.events.on("node-module-uninstalled",removeNodeExamplesDir);
},
register: createLibrary,
getAll: function(req,res) {
storage.getAllFlows().then(function(flows) {
log.audit({event: "library.get.all",type:"flow"},req);
if (exampleCount > 0) {
flows.d = flows.d||{};
flows.d._examples_ = exampleFlows;
}
res.json(flows);
});
},
get: function(req,res) {
storage.getFlow(req.params[0]).then(function(data) {
// data is already a JSON string
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
res.set('Content-Type', 'application/json');
res.send(data);
}).otherwise(function(err) {
if (err) {
log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req);
res.status(403).end();
return;
if (req.params[0].indexOf("_examples_/") === 0) {
var m = /^_examples_\/([^\/]+)\/(.*)$/.exec(req.params[0]);
if (m) {
var module = m[1];
var path = m[2]+".json";
if (exampleRoots[module]) {
var fullPath = fspath.join(exampleRoots[module],path);
try {
fs.statSync(fullPath);
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
return res.sendFile(fullPath,{
headers:{
'Content-Type': 'application/json'
}
})
} catch(err) {
console.log(err);
}
}
}
// IF we get here, we didn't find the file
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req);
res.status(404).end();
});
return res.status(404).end();
} else {
storage.getFlow(req.params[0]).then(function(data) {
// data is already a JSON string
log.audit({event: "library.get",type:"flow",path:req.params[0]},req);
res.set('Content-Type', 'application/json');
res.send(data);
}).otherwise(function(err) {
if (err) {
log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()}));
if (err.code === 'forbidden') {
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req);
res.status(403).end();
return;
}
}
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req);
res.status(404).end();
});
}
},
post: function(req,res) {
// if (req.params[0].indexOf("_examples_/") === 0) {
// log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:"forbidden"}));
// log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req);
// return res.status(403).send({error:"unexpected_error", message:"forbidden"});
// }
var flow = JSON.stringify(req.body);
storage.saveFlow(req.params[0],flow).then(function() {
log.audit({event: "library.set",type:"flow",path:req.params[0]},req);

View File

@@ -13,18 +13,52 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var i18n = require("../i18n");
var fs = require('fs');
var path = require('path');
var i18n;
var supportedLangs = [];
var apiLocalDir = path.resolve(path.join(__dirname,"locales"));
var initSupportedLangs = function() {
fs.readdir(apiLocalDir, function(err,files) {
if(!err) {
supportedLangs = files;
}
});
}
function determineLangFromHeaders(acceptedLanguages){
var lang = i18n.defaultLang;
acceptedLanguages = acceptedLanguages || [];
for (var i=0;i<acceptedLanguages.length;i++){
if (supportedLangs.indexOf(acceptedLanguages[i]) !== -1){
lang = acceptedLanguages[i];
break;
// check the language without the country code
} else if (supportedLangs.indexOf(acceptedLanguages[i].split("-")[0]) !== -1) {
lang = acceptedLanguages[i].split("-")[0];
break;
}
}
return lang;
}
module.exports = {
init: function(runtime) {
i18n = runtime.i18n;
initSupportedLangs();
},
get: function(req,res) {
var namespace = req.params[0];
namespace = namespace.replace(/\.json$/,"");
var lang = i18n.determineLangFromHeaders(req.acceptsLanguages() || []);
var lang = determineLangFromHeaders(req.acceptsLanguages() || []);
var prevLang = i18n.i.lng();
i18n.i.setLng(lang, function(){
var catalog = i18n.catalog(namespace,lang);
res.json(catalog||{});
});
i18n.i.setLng(prevLang);
}
},
determineLangFromHeaders: determineLangFromHeaders
}

View File

@@ -9,28 +9,33 @@
}
},
"workspace": {
"defaultName": "Sheet __number__",
"renameSheet": "Rename sheet",
"defaultName": "Flow __number__",
"renameSheet": "Rename flow",
"confirmDelete": "Confirm delete",
"delete": "Are you sure you want to delete '__label__'?",
"dropFlowHere": "Drop the flow here"
},
"menu": {
"label": {
"sidebar": {
"sidebar": "Sidebar",
"show": "Toggle Sidebar"
"view": {
"view": "View",
"showGrid": "Show grid",
"snapGrid": "Snap to grid"
},
"displayStatus": "Display Node Status",
"displayConfig": "Configuration Nodes",
"sidebar": {
"show": "Show sidebar"
},
"displayStatus": "Show node status",
"displayConfig": "Configuration nodes",
"import": "Import",
"export": "Export",
"clipboard": "Clipboard",
"library": "Library",
"examples": "Examples",
"subflows": "Subflows",
"createSubflow": "Create Subflow",
"selectionToSubflow": "Selection to Subflow",
"flows": "Tabs",
"flows": "Flows",
"add": "Add",
"rename": "Rename",
"delete": "Delete",
@@ -78,7 +83,9 @@
"modifiedFlowsDesc": "Only deploys flows that contain changed nodes",
"modifiedNodes": "Modified Nodes",
"modifiedNodesDesc": "Only deploys nodes that have changed",
"successfulDeploy": "Successfully Deployed",
"successfulDeploy": "Successfully deployed",
"unusedConfigNodes":"You have some unused configuration nodes.",
"unusedConfigNodesLink":"Click here to see them",
"errors": {
"noResponse": "no response from server"
},
@@ -90,7 +97,6 @@
"undeployedChanges": "You have undeployed changes.\n\nLeaving this page will lose these changes.",
"improperlyConfigured": "The workspace contains some nodes that are not properly configured:",
"unknown": "The workspace contains some unknown node types:",
"unusedConfig": "The workspace contains some unused configuration nodes:",
"confirm": "Are you sure you want to deploy?"
}
},
@@ -197,11 +203,22 @@
"config": {
"name": "Configuration nodes",
"label": "config",
"local": "on this flow",
"global": "on all flows",
"global": "Global",
"none": "none",
"subflows": "subflows",
"flows": "flows"
"flows": "flows",
"filterUnused":"unused",
"filterAll":"all",
"filtered": "__count__ hidden"
}
},
"typedInput": {
"type": {
"str": "string",
"num": "number",
"re": "regular expression",
"bool": "boolean",
"json": "JSON"
}
}
}

View File

@@ -13,22 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var redNodes = require("../nodes");
var comms = require("../comms");
var log = require("../log");
var i18n = require("../i18n");
var when = require("when");
var settings = require("../settings");
var comms = require("./comms");
var locales = require("./locales");
var redNodes;
var log;
var i18n;
var settings;
module.exports = {
init: function(runtime) {
redNodes = runtime.nodes;
log = runtime.log;
i18n = runtime.i18n;
settings = runtime.settings;
},
getAll: function(req,res) {
if (req.get("accept") == "application/json") {
log.audit({event: "nodes.list.get"},req);
res.json(redNodes.getNodeList());
} else {
var lang = i18n.determineLangFromHeaders(req.acceptsLanguages());
var lang = locales.determineLangFromHeaders(req.acceptsLanguages());
log.audit({event: "nodes.configs.get"},req);
res.send(redNodes.getNodeConfigs(lang));
}
@@ -121,7 +127,7 @@ module.exports = {
res.status(404).end();
}
} else {
var lang = i18n.determineLangFromHeaders(req.acceptsLanguages());
var lang = locales.determineLangFromHeaders(req.acceptsLanguages());
result = redNodes.getNodeConfig(id,lang);
if (result) {
log.audit({event: "nodes.config.get",id:id},req);
@@ -157,8 +163,8 @@ module.exports = {
res.status(400).json({error:"invalid_request", message:"Invalid request"});
return;
}
var id = req.params.mod + "/" + req.params.set;
try {
var id = req.params.mod + "/" + req.params.set;
var node = redNodes.getNodeInfo(id);
var info;
if (!node) {
@@ -189,8 +195,8 @@ module.exports = {
res.status(400).json({error:"invalid_request", message:"Invalid request"});
return;
}
var mod = req.params.mod;
try {
var mod = req.params.mod;
var module = redNodes.getModuleInfo(mod);
if (!module) {
log.audit({event: "nodes.module.set",module:mod,error:"not_found"},req);

View File

@@ -34,6 +34,7 @@ var defaultContext = {
}
};
var theme = null;
var themeContext = clone(defaultContext);
var themeSettings = null;
@@ -53,102 +54,107 @@ function serveFile(app,baseUrl,file) {
}
module.exports = {
init: function(settings) {
init: function(runtime) {
var settings = runtime.settings;
themeContext = clone(defaultContext);
if (runtime.version) {
themeContext.version = runtime.version();
}
themeSettings = null;
theme = settings.editorTheme;
},
app: function() {
var i;
var url;
themeContext = clone(defaultContext);
themeSettings = null;
themeSettings = {};
if (settings.editorTheme) {
var theme = settings.editorTheme;
themeSettings = {};
var themeApp = express();
var themeApp = express();
if (theme.page) {
if (theme.page.css) {
var styles = theme.page.css;
if (!util.isArray(styles)) {
styles = [styles];
}
themeContext.page.css = [];
for (i=0;i<styles.length;i++) {
url = serveFile(themeApp,"/css/",styles[i]);
if (url) {
themeContext.page.css.push(url);
}
}
if (theme.page) {
if (theme.page.css) {
var styles = theme.page.css;
if (!util.isArray(styles)) {
styles = [styles];
}
themeContext.page.css = [];
if (theme.page.favicon) {
url = serveFile(themeApp,"/favicon/",theme.page.favicon)
for (i=0;i<styles.length;i++) {
url = serveFile(themeApp,"/css/",styles[i]);
if (url) {
themeContext.page.favicon = url;
}
}
themeContext.page.title = theme.page.title || themeContext.page.title;
}
if (theme.header) {
themeContext.header.title = theme.header.title || themeContext.header.title;
if (theme.header.hasOwnProperty("url")) {
themeContext.header.url = theme.header.url;
}
if (theme.header.hasOwnProperty("image")) {
if (theme.header.image) {
url = serveFile(themeApp,"/header/",theme.header.image);
if (url) {
themeContext.header.image = url;
}
} else {
themeContext.header.image = null;
themeContext.page.css.push(url);
}
}
}
if (theme.deployButton) {
if (theme.deployButton.type == "simple") {
themeSettings.deployButton = {
type: "simple"
}
if (theme.deployButton.label) {
themeSettings.deployButton.label = theme.deployButton.label;
}
if (theme.deployButton.icon) {
url = serveFile(themeApp,"/deploy/",theme.deployButton.icon);
if (url) {
themeSettings.deployButton.icon = url;
}
}
if (theme.page.favicon) {
url = serveFile(themeApp,"/favicon/",theme.page.favicon)
if (url) {
themeContext.page.favicon = url;
}
}
if (theme.hasOwnProperty("userMenu")) {
themeSettings.userMenu = theme.userMenu;
}
if (theme.login) {
if (theme.login.image) {
url = serveFile(themeApp,"/login/",theme.login.image);
if (url) {
themeContext.login = {
image: url
}
}
}
}
if (theme.hasOwnProperty("menu")) {
themeSettings.menu = theme.menu;
}
return themeApp;
themeContext.page.title = theme.page.title || themeContext.page.title;
}
if (theme.header) {
themeContext.header.title = theme.header.title || themeContext.header.title;
if (theme.header.hasOwnProperty("url")) {
themeContext.header.url = theme.header.url;
}
if (theme.header.hasOwnProperty("image")) {
if (theme.header.image) {
url = serveFile(themeApp,"/header/",theme.header.image);
if (url) {
themeContext.header.image = url;
}
} else {
themeContext.header.image = null;
}
}
}
if (theme.deployButton) {
if (theme.deployButton.type == "simple") {
themeSettings.deployButton = {
type: "simple"
}
if (theme.deployButton.label) {
themeSettings.deployButton.label = theme.deployButton.label;
}
if (theme.deployButton.icon) {
url = serveFile(themeApp,"/deploy/",theme.deployButton.icon);
if (url) {
themeSettings.deployButton.icon = url;
}
}
}
}
if (theme.hasOwnProperty("userMenu")) {
themeSettings.userMenu = theme.userMenu;
}
if (theme.login) {
if (theme.login.image) {
url = serveFile(themeApp,"/login/",theme.login.image);
if (url) {
themeContext.login = {
image: url
}
}
}
}
if (theme.hasOwnProperty("menu")) {
themeSettings.menu = theme.menu;
}
return themeApp;
},
context: function() {
return themeContext;

View File

@@ -1,5 +1,5 @@
/**
* Copyright 2013, 2014 IBM Corp.
* Copyright 2013, 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,26 +21,26 @@ var theme = require("./theme");
var Mustache = require("mustache");
var events = require("../events");
var settings;
var icon_paths = [path.resolve(__dirname + '/../../public/icons')];
var iconCache = {};
//TODO: create a default icon
var defaultIcon = path.resolve(__dirname + '/../../public/icons/arrow-in.png');
events.on("node-icon-dir",function(dir) {
icon_paths.push(path.resolve(dir));
});
var templateDir = path.resolve(__dirname+"/../../editor/templates");
var editorTemplate;
function nodeIconDir(dir) {
icon_paths.push(path.resolve(dir));
}
module.exports = {
init: function(_settings) {
settings = _settings;
init: function(runtime) {
editorTemplate = fs.readFileSync(path.join(templateDir,"index.mst"),"utf8");
Mustache.parse(editorTemplate);
// TODO: this allows init to be called multiple times without
// registering multiple instances of the listener.
// It isn't.... ideal.
runtime.events.removeListener("node-icon-dir",nodeIconDir);
runtime.events.on("node-icon-dir",nodeIconDir);
},
ensureSlash: function(req,res,next) {
@@ -59,10 +59,13 @@ module.exports = {
} else {
for (var p=0;p<icon_paths.length;p++) {
var iconPath = path.join(icon_paths[p],req.params.icon);
if (fs.existsSync(iconPath)) {
try {
fs.statSync(iconPath);
res.sendFile(iconPath);
iconCache[req.params.icon] = iconPath;
return;
} catch(err) {
// iconPath doesn't exist
}
}
res.sendFile(defaultIcon);

View File

@@ -14,22 +14,19 @@
* limitations under the License.
**/
var server = require("./server");
var nodes = require("./nodes");
var library = require("./api/library");
var comms = require("./comms");
var log = require("./log");
var util = require("./util");
var i18n = require("./i18n");
var fs = require("fs");
var settings = require("./settings");
var credentials = require("./nodes/credentials");
var auth = require("./api/auth");
var path = require('path');
var events = require("events");
var runtime = require("./runtime");
var api = require("./api");
process.env.NODE_RED_HOME = process.env.NODE_RED_HOME || path.resolve(__dirname+"/..");
var nodeApp = null;
var adminApp = null;
var server = null;
var apiEnabled = false;
function checkBuild() {
var editorFile = path.resolve(path.join(__dirname,"..","public","red","red.min.js"));
try {
@@ -41,42 +38,60 @@ function checkBuild() {
}
}
var RED = {
module.exports = {
init: function(httpServer,userSettings) {
if (!userSettings) {
userSettings = httpServer;
httpServer = null;
}
if (!userSettings.SKIP_BUILD_CHECK) {
checkBuild();
}
userSettings.version = this.version();
log.init(userSettings);
settings.init(userSettings);
server.init(httpServer,settings);
return server.app;
},
start: server.start,
stop: server.stop,
nodes: nodes,
library: { register: library.register },
credentials: credentials,
events: events,
log: log,
comms: comms,
settings:settings,
util: util,
auth: {
needsPermission: auth.needsPermission
},
version: function () {
var p = require(path.join(process.env.NODE_RED_HOME,"package.json")).version;
/* istanbul ignore else */
if (fs.existsSync(path.join(process.env.NODE_RED_HOME,".git"))) {
p += "-git";
}
return p;
},
get app() { console.log("Deprecated use of RED.app - use RED.httpAdmin instead"); return server.app },
get httpAdmin() { return server.app },
get httpNode() { return server.nodeApp },
get server() { return server.server }
};
module.exports = RED;
if (!userSettings.coreNodesDir) {
userSettings.coreNodesDir = path.resolve(path.join(__dirname,"..","nodes"));
}
if (userSettings.httpAdminRoot !== false || userSettings.httpNodeRoot !== false) {
runtime.init(userSettings,api);
api.init(httpServer,runtime);
apiEnabled = true;
} else {
runtime.init(userSettings);
apiEnabled = false;
}
adminApp = runtime.adminApi.adminApp;
nodeApp = runtime.adminApi.nodeApp;
server = runtime.adminApi.server;
return;
},
start: function() {
return runtime.start().then(function() {
if (apiEnabled) {
return api.start();
}
});
},
stop: function() {
return runtime.stop().then(function() {
if (apiEnabled) {
return api.stop();
}
})
},
nodes: runtime.nodes,
log: runtime.log,
settings:runtime.settings,
util: runtime.util,
version: runtime.version,
comms: api.comms,
library: api.library,
auth: api.auth,
get app() { console.log("Deprecated use of RED.app - use RED.httpAdmin instead"); return runtime.app },
get httpAdmin() { return adminApp },
get httpNode() { return nodeApp },
get server() { return server }
};

Some files were not shown because too many files have changed in this diff Show More