Compare commits

...
Author SHA1 Message Date
dependabot-preview[bot] a81745d4bd [Security] Bump socket.io-parser from 3.3.0 to 3.3.2 in /app
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 3.3.0 to 3.3.2. **This update includes a security fix.**
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/3.3.2/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/3.3.0...3.3.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-06-30 17:02:46 +00:00
Thomas Nordquist 9cdfa2de7b Prepare app for notarization 2020-04-28 23:58:26 +02:00
Thomas Nordquist 355e9177fc Update electron 2020-04-27 18:45:52 +02:00
Thomas Nordquist dbb6ead7ba Fix builds and snap upload 2020-04-25 14:04:46 +02:00
Thomas Nordquist ad888f6f9a Update version 2020-04-24 15:37:56 +02:00
Thomas Nordquist 07458cd712 Expose message Ids to the user 2020-04-20 18:27:51 +02:00
Thomas Nordquist 6fc0d3f28d Fix publish button 2020-04-20 14:03:03 +02:00
Thomas Nordquist 7a91e4dee6 Fix setting environmental for mocha tests in windows builds 2020-04-20 13:21:16 +02:00
Thomas Nordquist 3c40be97b7 Fix demoVideo 2020-04-20 13:09:37 +02:00
Thomas Nordquist ff00f7a99e Add app tests to yarn test 2020-04-20 12:31:57 +02:00
Thomas Nordquist b72fc48bdb Add quality of service option to subscriptions
Fixes #323, #14, #334
Fixes #132
2020-04-20 12:24:23 +02:00
Thomas Nordquist e87a7115c5 Revert "Only produce artifacts on appveyor when a release was tagged"
This reverts commit 487eee93cd.
2020-04-19 13:13:16 +02:00
Thomas Nordquist 14dac22732 Fix password input
Fixes #292
2020-04-19 12:51:29 +02:00
Thomas Nordquist ccdaaa6ce2 Fix codestyle 2020-04-16 12:19:25 +02:00
Thomas Nordquist 487eee93cd Only produce artifacts on appveyor when a release was tagged 2020-04-16 12:17:45 +02:00
Thomas Nordquist 59737edfb4 Always focus editor after pressing buttons in Publish sidebar component 2020-04-16 11:51:18 +02:00
Thomas Nordquist 30af13f793 Update linter 2020-04-16 11:00:34 +02:00
Thomas Nordquist 19e8bfdb37 Fix chart where properties contain periods
Fixes #281
2020-04-16 10:53:53 +02:00
Thomas Nordquist a89d7cf62e Improve json editor 2020-04-16 10:50:43 +02:00
Thomas Nordquist 65e9f2e074 Update dependecies 2020-04-16 10:43:41 +02:00
Thomas Nordquist ff66d10aaa Fix property name 2020-04-16 10:30:20 +02:00
Thomas Nordquist 5203d40a24 Restore original focus after hitting publish button 2020-04-16 00:40:22 +02:00
Thomas Nordquist 237c718a0a Update prettier 2020-04-15 23:54:30 +02:00
Thomas Nordquist fbfbe94d19 Prevent focussing filter when meta or ctrl key is pressed
Fixes copy/paste problem
2020-04-15 22:49:12 +02:00
Thomas Nordquist e24e505cc0 Use selected topic when clearing publish topic, also focus input element 2020-04-15 22:23:18 +02:00
Thomas Nordquist 28743ba646 Update dependecies 2020-04-15 21:26:27 +02:00
Thomas Nordquist f63e73ee0a Add documentation for how to update http://mqtt-explorer.com 2020-01-17 12:52:34 +01:00
Thomas Nordquist fbad7afa79 Update travis ubuntu dist 2020-01-17 11:50:55 +01:00
Thomas Nordquist 34cc38ab3c Fix build 2020-01-17 09:37:47 +01:00
Thomas Nordquist 8645789550 Fix object mutability 2019-11-13 17:27:02 +01:00
Thomas Nordquist 4db6e7b1d7 Update electron to 7.1.1 2019-11-13 17:26:27 +01:00
Thomas Nordquist 1fae61b1fa Update react-transision-groups 2019-11-13 15:35:27 +01:00
Thomas Nordquist b26eac3edb Update react-transition-group 2019-11-13 15:33:16 +01:00
Thomas Nordquist 6d1354bf07 Update React & Reace-ace 2019-11-13 15:31:24 +01:00
Thomas Nordquist 7148e302f6 Add mocha as dev dependency 2019-11-13 10:47:18 +01:00
Thomas Nordquist 763a1aea69 Update dependecies 2019-11-13 10:46:22 +01:00
Thomas Nordquist 94a92e9b29 Reset empty username/password to undefined
Fixes #229
2019-11-13 09:11:02 +01:00
Thomas Nordquist 922f90c4eb Fix memoization of chart y-range parameters 2019-11-13 09:06:20 +01:00
Thomas Nordquist ff11973a9a Extract chart range definition 2019-11-12 14:38:01 +01:00
Thomas Nordquist 29fc2eea4d Update app dependencies 2019-11-12 11:16:31 +01:00
Thomas Nordquist 577e1e6b10 Fix implicit peer dependency 2019-11-12 07:30:48 +01:00
Thomas Nordquist d113fab279 Fix plot range selection
Fixes #176
2019-11-12 07:26:05 +01:00
Thomas Nordquist a6c401ab6a Fix publish sidebar width
Fixes #227
2019-11-11 17:30:18 +01:00
Thomas Nordquist 6cc8c8810b Merge branch 'master' of github.com:thomasnordquist/MQTT-Explorer 2019-10-12 09:37:54 +02:00
dependabot-preview[bot] 040a29da30 Bump @types/react from 16.8.23 to 16.9.4 in /app (#215)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 16.8.23 to 16.9.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:15:19 +02:00
dependabot-preview[bot] 45f31ad931 Bump cspell from 4.0.23 to 4.0.28 (#199)
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 4.0.23 to 4.0.28.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/cspell@4.0.23...cspell@4.0.28)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:14:34 +02:00
dependabot-preview[bot] 39ddb984ab Bump @types/mocha from 5.2.6 to 5.2.7 (#201)
Bumps [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha) from 5.2.6 to 5.2.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mocha)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:14:19 +02:00
dependabot-preview[bot] 197788eb79 Bump file-loader from 4.0.0 to 4.2.0 in /app (#213)
Bumps [file-loader](https://github.com/webpack-contrib/file-loader) from 4.0.0 to 4.2.0.
- [Release notes](https://github.com/webpack-contrib/file-loader/releases)
- [Changelog](https://github.com/webpack-contrib/file-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/file-loader/compare/v4.0.0...v4.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:13:57 +02:00
dependabot-preview[bot] c85bc0323c Bump typescript from 3.5.2 to 3.6.3 in /app (#216)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.5.2 to 3.6.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v3.5.2...v3.6.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:13:04 +02:00
dependabot-preview[bot] 65c2a6eff1 Bump @types/node from 12.6.8 to 12.7.8 in /app (#218)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.6.8 to 12.7.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:12:47 +02:00
dependabot-preview[bot] 5a4f3a0388 Bump mime from 2.4.3 to 2.4.4 (#164)
Bumps [mime](https://github.com/broofa/node-mime) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/broofa/node-mime/releases)
- [Changelog](https://github.com/broofa/node-mime/blob/master/CHANGELOG.md)
- [Commits](https://github.com/broofa/node-mime/compare/v2.4.3...v2.4.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:10:12 +02:00
dependabot-preview[bot] 03f9c6a26e Bump d3 from 5.9.2 to 5.9.7 in /app (#169)
Bumps [d3](https://github.com/d3/d3) from 5.9.2 to 5.9.7.
- [Release notes](https://github.com/d3/d3/releases)
- [Changelog](https://github.com/d3/d3/blob/master/CHANGES.md)
- [Commits](https://github.com/d3/d3/compare/v5.9.2...v5.9.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:09:58 +02:00
dependabot-preview[bot] ae7ecd4bc9 Bump webpack-cli from 3.3.4 to 3.3.6 in /app (#170)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 3.3.4 to 3.3.6.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/v3.3.6/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/v3.3.4...v3.3.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:09:08 +02:00
dependabot-preview[bot] 25469936fa Bump @types/node from 12.0.8 to 12.6.8 in /app (#167)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.0.8 to 12.6.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:08:25 +02:00
dependabot-preview[bot] bc2d066bb9 Bump @types/react from 16.8.20 to 16.8.23 in /app (#168)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 16.8.20 to 16.8.23.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:07:58 +02:00
dependabot-preview[bot] 71e376e0e4 Bump compare-versions from 3.4.0 to 3.5.0 in /app (#165)
Bumps [compare-versions](https://github.com/omichelsen/compare-versions) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/omichelsen/compare-versions/releases)
- [Changelog](https://github.com/omichelsen/compare-versions/blob/master/CHANGELOG.md)
- [Commits](https://github.com/omichelsen/compare-versions/compare/v3.4.0...v3.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:07:35 +02:00
dependabot-preview[bot] 354b014245 Bump @types/node from 12.0.8 to 12.6.8 (#163)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.0.8 to 12.6.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:06:25 +02:00
dependabot-preview[bot] d69779c497 Bump tslint from 5.16.0 to 5.18.0 (#162)
Bumps [tslint](https://github.com/palantir/tslint) from 5.16.0 to 5.18.0.
- [Release notes](https://github.com/palantir/tslint/releases)
- [Changelog](https://github.com/palantir/tslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/palantir/tslint/compare/5.16.0...5.18.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:06:17 +02:00
dependabot-preview[bot] af1219cda8 Bump mocha from 6.1.4 to 6.2.0 (#161)
Bumps [mocha](https://github.com/mochajs/mocha) from 6.1.4 to 6.2.0.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v6.1.4...v6.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:06:07 +02:00
Thomas Nordquist c7ea6790c8 Update electron-builder 2019-07-23 01:07:50 +02:00
Thomas Nordquist 779f1e9769 Prepare pre-release 2019-07-23 00:52:59 +02:00
Thomas Nordquist f690b3f5a7 Add recusrive delete warning 2019-07-23 00:51:26 +02:00
Thomas Nordquist 39d87d5a8e Remove recursive topic removal limit 2019-07-23 00:36:15 +02:00
Thomas Nordquist 6781170b85 Fix crash reporter url 2019-07-20 15:51:41 +02:00
Thomas Nordquist 7829a27c69 Bump version to v0.3.5 2019-07-18 00:06:35 +02:00
Thomas Nordquist 285001d184 Bump version 2019-07-17 23:33:41 +02:00
Thomas Nordquist d5a4d1e4d3 Fix dialog button margin 2019-07-17 23:32:52 +02:00
Thomas Nordquist c59c64b65f Fix typing 2019-07-17 20:07:25 +02:00
Thomas Nordquist 5badd19015 Add syntax highlighting to raw / json values 2019-07-17 19:49:10 +02:00
Thomas Nordquist a7eacfac46 Upgrade electron 2019-07-17 19:14:00 +02:00
Thomas Nordquist 20dae2ea2b Fix false dev dependency 2019-07-17 19:07:33 +02:00
Thomas Nordquist dc50fd40a3 Upgrade electron-updater 2019-07-17 17:46:48 +02:00
Thomas Nordquist 3df4d18a71 Update OSX image 2019-07-17 17:45:57 +02:00
Thomas Nordquist 3aeee31193 Update electron-builder 2019-07-17 17:44:51 +02:00
Thomas Nordquist af4cfec451 Auto-open publish history drawer 2019-07-17 17:25:12 +02:00
Thomas Nordquist ae915a158b Add memoization 2019-07-17 16:44:37 +02:00
Thomas Nordquist eac522fe63 Improve publish button layout 2019-07-17 16:35:47 +02:00
Thomas Nordquist a2bdf71422 Fix syntax error 2019-07-17 15:58:21 +02:00
Thomas Nordquist f2d632959b Fix broker statistic updates 2019-07-17 15:56:38 +02:00
Thomas Nordquist b33412504d Unset CSC_LINK for windows builds 2019-07-17 15:44:41 +02:00
dependabot[bot] c458f6c64d Bump lodash from 4.17.11 to 4.17.14 in /app (#158)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-17 15:20:51 +02:00
Thomas Nordquist d6489563b2 Increase max title length 2019-07-17 15:20:16 +02:00
Thomas Nordquist d651fc7a6c Refactor value renderer 2019-07-17 15:18:27 +02:00
Thomas Nordquist c86659f61c Fix case-sensitivity error 2019-07-17 14:29:34 +02:00
Thomas Nordquist 8959bbdee7 Always show chart button in history 2019-07-17 14:14:04 +02:00
Thomas Nordquist 9a8474e3d8 Make previous messages more accessible 2019-07-17 14:02:08 +02:00
Thomas Nordquist c5e0e652f3 Improve demo video 2019-07-17 13:33:34 +02:00
Thomas Nordquist f047f97e44 Fix tests 2019-07-17 13:25:44 +02:00
Thomas Nordquist cda8e928ef Bump version to v0.3.3 2019-07-17 13:19:15 +02:00
Thomas Nordquist ccbea4acc5 Fix test 2019-07-17 13:17:41 +02:00
Thomas Nordquist 8ae1528064 Add clear chart button and improve chart menu look&feel 2019-07-17 12:59:25 +02:00
Thomas Nordquist 1c52aced63 Fix button layout 2019-07-17 11:16:03 +02:00
Thomas Nordquist 72020b02b8 Update sidebar when topic is deleted 2019-07-17 10:05:05 +02:00
Thomas Nordquist aa340b2158 Fix topic deletion child topic count 2019-07-17 10:04:20 +02:00
Thomas Nordquist faf909654b Update electron 2019-07-17 09:21:13 +02:00
Thomas Nordquist 33f3458e38 Fix treeNode enhancer 2019-07-17 09:18:06 +02:00
Thomas Nordquist 3737410563 Refactor TreeNode model 2019-07-17 09:17:46 +02:00
Thomas Nordquist 28afc439ba Enable topic deletion with delete key 2019-07-17 09:16:24 +02:00
Thomas Nordquist 04ba067775 Refactor TreeNode 2019-07-17 09:15:25 +02:00
Thomas Nordquist 0a5d010827 Fix layout issue 2019-07-17 09:14:32 +02:00
Thomas Nordquist 1cef529ac7 Add confirmation dialog 2019-07-17 09:14:10 +02:00
Thomas Nordquist 094831f037 Fix highlight color in dark mode 2019-07-17 09:12:09 +02:00
dependabot[bot] 5148e92651 Bump lodash from 4.17.11 to 4.17.14 (#154)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-13 16:21:49 +02:00
dependabot[bot] 13be7a6697 Bump lodash.merge from 4.6.1 to 4.6.2 (#151)
Bumps [lodash.merge](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-13 16:19:43 +02:00
dependabot[bot] 1bef73f356 Bump lodash-es from 4.17.11 to 4.17.14 in /app (#152)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-13 16:19:27 +02:00
Thomas Nordquist 88cc57b8b1 Add .github/Funding 2019-07-13 16:15:09 +02:00
Thomas Nordquist b5d9ff3067 Refactor chart 2019-07-13 15:57:30 +02:00
Thomas Nordquist 7c63e871f0 Fix disconnect button label 2019-07-13 14:33:12 +02:00
Thomas Nordquist fd2c78eea0 Fix sidebar layout 2019-07-13 14:22:42 +02:00
Thomas Nordquist e8e8926757 Fix chart tooltip 2019-07-13 14:22:33 +02:00
Thomas Nordquist 0b6a1e42d6 Improve payload truncation in tree 2019-07-13 12:40:47 +02:00
Thomas Nordquist ff148a8e3c Improve tooltip behavior of charts 2019-07-12 17:05:35 +02:00
Thomas Nordquist ecdecd8626 Update gitignore 2019-07-11 22:09:16 +02:00
Thomas Nordquist 843f2377f0 Package mac dmg first 2019-07-11 22:08:15 +02:00
Thomas Nordquist a9a5fdf9dd Fix typo 2019-07-11 22:07:56 +02:00
Thomas Nordquist a9742ebf81 Replace PureComponent performance wrapper 2019-07-11 22:06:07 +02:00
Thomas Nordquist df3e0fc047 Refactor charts 2019-07-11 22:05:02 +02:00
Thomas Nordquist 05867dab48 Fix plot pause function 2019-07-11 18:11:26 +02:00
Thomas Nordquist 4f3150d0f3 Add RingBuffer compactation 2019-07-11 18:10:42 +02:00
Thomas Nordquist 175aeea468 Propagate settings in the tree 2019-07-11 17:14:43 +02:00
Thomas Nordquist 167f0b39e0 Do not indicate topic update on selection 2019-07-11 17:09:37 +02:00
Thomas Nordquist 7a9b7d7bf6 Fix test 2019-07-11 16:34:39 +02:00
Thomas Nordquist 1a1596ad3d Fix tree-rendering performance 2019-07-11 16:14:46 +02:00
Thomas Nordquist 8e49d19fbe Refactor 2019-07-11 15:33:42 +02:00
Thomas Nordquist df42d75651 Refactor 2019-07-11 13:54:53 +02:00
Thomas Nordquist c1e2a4c625 Improve render speed 2019-07-08 17:17:46 +02:00
150 changed files with 7058 additions and 4325 deletions
+5 -1
View File
@@ -1,6 +1,9 @@
{
"language": "en",
"words": [
"Bbreak",
"nodered",
"goog",
"thomasnordquist",
"nowrap",
"subheader",
@@ -43,6 +46,7 @@
"DEVTOOLS",
"mixins",
"Explorerdmg",
"heapsnapshot"
"heapsnapshot",
"noconflict"
]
}
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: thomasnordquist
custom: https://paypal.me/ThomasNordquist
+2
View File
@@ -7,3 +7,5 @@ build
test.dot
test.png
.awcache
.scannerwork
screen*.png
+11 -3
View File
@@ -17,7 +17,9 @@ os:
- linux
- osx
dist: xenial
osx_image: xcode10.2
dist: bionic
services:
- docker
@@ -33,6 +35,12 @@ script:
- export TRAVIS_BUILD_NUMBER="" # Override travis build number since it is uses for tagging the binary version https://github.com/electron-userland/electron-builder/issues/3730
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app thomasnordquist/ui-test-recording-env sh -c "cd app && docker/testMounted.sh"; fi
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package linux; fi
- |
if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then
openssl aes-256-cbc -d -in res/snapstore-credentials.enc -out credentials -k $SNAPSTORE_CREDENTIALS_DECRYPTION_KEY;
snapcraft login --with credentials;
rm credentials;
yarn run package linux;
fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then CSC_LINK="" yarn run package -- win; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then unset CSC_LINK; yarn run package -- win; fi
+32 -3
View File
@@ -20,6 +20,7 @@ Pull-Requests and error reports are welcome.
## Run from sources
```bash
npm install -g yarn
yarn
yarn build
yarn start
@@ -29,6 +30,7 @@ yarn start
Launch Application
```bash
npm install -g yarn
yarn
yarn dev
```
@@ -39,25 +41,52 @@ The `app` directory contains all the rendering logic, the `backend` directory cu
To achieve a reliable product automated tests run regularly on travis.
- Data model
- MQTT integration
- UI-Tests (The demo is a recorded ui test)
- Data model
- MQTT integration
- UI-Tests (The demo is a recorded ui test)
## Run UI-tests
A [mosquitto](https://mosquitto.org/) MQTT broker is required to run the ui-tests.
Run tests with
```bash
# Run chromedriver in a separate terminal session
./node_modules/.bin/chromedriver --url-base=wd/hub --port=9515 --verbose
```
Compile and execute tests
```bash
npm run build
node dist/src/spec/webdriverio.js
```
## Write docs
```
git clone https://github.com/thomasnordquist/MQTT-Explorer.git mqtt-explorer-pages
cd mqtt-explorer-pages
git checkout gh-pages
bundle install
bundle exec jekyll serve --incremental
```
Readme file: `Readme.tpl.md`
Preview is available at
http://localhost:4000/Readme.tpl
## Update docs
```
npm install
./updateReadme.ts
```
The readme will be generated from the docs.
## License
![CC-BY-ND 4.0](https://img.shields.io/badge/License-CC%20BY--ND%204.0-blue.svg)
+6 -7
View File
@@ -22,10 +22,10 @@
background-color: none;
}
25% {
background-color: #4f5781;
background-color: #595585;
}
50% {
background-color: #4f5781;
background-color: #595585;
}
100% {
background-color: none;
@@ -39,11 +39,9 @@
}
25% {
background-color: #c0c8c0;
color: #000;
}
50% {
background-color: #c0c8c0;
color: #000;
}
100% {
background-color: none;
@@ -162,7 +160,7 @@
</style>
</head>
<body>
<div id="app" style="font:-webkit-control"></div>
<div id="app" style="font: -webkit-control;"></div>
<script>
function loadScript(path) {
var script = document.createElement('script')
@@ -176,7 +174,8 @@
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script
><% }); %>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>
<script src="<%- file %>"></script>
<% }); %>
</body>
</html>
+23 -17
View File
@@ -6,7 +6,9 @@
"scripts": {
"build": "yarn rebuild && webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=5.0.4 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=7.1.1 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -",
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
"mochatest": "mocha --require ts-node/register src/**/*.spec.ts"
},
"author": "",
"license": "CC-BY-ND-4.0",
@@ -15,19 +17,20 @@
"@material-ui/icons": "^4",
"@material-ui/lab": "^4.0.0-alpha",
"@material-ui/styles": "^4",
"@types/react-transition-group": "^2.9.2",
"@types/react-transition-group": "^4",
"ace-builds": "^1.4.11",
"axios": "^0.19.0",
"brace": "^0.11.1",
"compare-versions": "^3.4.0",
"compare-versions": "^3.5.0",
"copy-text-to-clipboard": "^2.1.0",
"d3": "^5.9.2",
"d3": "^5.9.7",
"d3-shape": "^1.3.5",
"diff": "^4.0.1",
"dot-prop": "^5.0.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"file-loader": "^4.0.0",
"file-loader": "6",
"get-value": "^3.0.1",
"immutable": "^4.0.0-rc.12",
"in-viewport": "^3.6.0",
"js-base64": "^2.5.1",
"json-to-ast": "^2.1.0",
"lodash.debounce": "^4.0.8",
@@ -37,47 +40,50 @@
"number-abbreviate": "^2.0.0",
"parse-duration": "^0.1.1",
"prismjs": "^1.15.0",
"react": "16.8",
"react-ace": "^7.0.1",
"react": "^16.11",
"react-ace": "^8",
"react-dom": "^16.7.0",
"react-redux": "^7.0.3",
"react-resize-detector": "^4.1.4",
"react-split-pane": "^0.1.85",
"react-transition-group": "^4.1.1",
"react-transition-group": "^4",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "^0.4.1",
"redux-batched-actions": "0.5",
"redux-thunk": "^2.3.0",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
"uuid": "^3.3.2"
"uuid": "7"
},
"devDependencies": {
"@types/d3": "^5.7.2",
"@types/diff": "^4.0.1",
"@types/get-value": "^3.0.1",
"@types/node": "^12.0.4",
"@types/node": "^12.7.8",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.7.18",
"@types/react": "^16.9.4",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^7.0.9",
"@types/react-resize-detector": "^4.0.1",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^1.4.32",
"@types/uuid": "^3.4.4",
"@types/uuid": "^7.0.2",
"@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1",
"chai": "^4.2.0",
"cross-env": "^7.0.2",
"css-loader": "^3.0.0",
"hard-source-webpack-plugin": "^0.13.1",
"heapdump": "^0.3.12",
"html-webpack-plugin": "^4.0.0-beta.5",
"mocha": "^7.1.1",
"node-loader": "^0.6.0",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"typescript": "^3.2.2",
"style-loader": "^1",
"typescript": "^3.6.3",
"webpack": "^4.28.2",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.1.14"
},
"peerDependencies": {
+1 -3
View File
@@ -45,9 +45,7 @@ export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () =>
return
}
const charts = getState()
.charts.get('charts')
.toArray()
const charts = getState().charts.get('charts').toArray()
let viewStates: ConnectionViewStateDictionary | undefined
try {
+30 -60
View File
@@ -10,12 +10,13 @@ import { default as persistentStorage, StorageIdentifier } from '../utils/Persis
import { Dispatch } from 'redux'
import { showError } from './Global'
import { remote } from 'electron'
import * as fs from 'fs'
import { promises as fsPromise } from 'fs'
import * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
import { connectionsMigrator } from './migrations/Connection'
interface ConnectionDictionary {
export interface ConnectionDictionary {
[s: string]: ConnectionOptions
}
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
@@ -27,6 +28,12 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
try {
await ensureConnectionsHaveBeenInitialized()
connections = await persistentStorage.load(storedConnectionsIdentifier)
// Apply migrations
if (connections && connectionsMigrator.isMigrationNecessary(connections)) {
connections = connectionsMigrator.applyMigrations(connections)
await persistentStorage.store(storedConnectionsIdentifier, connections)
}
} catch (error) {
dispatch(showError(error))
}
@@ -65,35 +72,25 @@ async function openCertificate(): Promise<CertificateParameters> {
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
}
return new Promise((resolve, reject) => {
remote.dialog.showOpenDialog(
{ properties: ['openFile'], securityScopedBookmarks: true },
(filePaths?: Array<string>) => {
const selectedFile = filePaths && filePaths[0]
if (!selectedFile) {
reject(rejectReasons.noCertificateSelected)
return
}
fs.readFile(selectedFile, (error, data) => {
if (error) {
reject(error)
return
}
if (data.length > 16_384 || data.length < 128) {
reject(rejectReasons.certificateSizeDoesNotMatch)
return
}
resolve({
data: data.toString('base64'),
name: path.basename(selectedFile),
})
})
}
)
const openDialogReturnValue = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
properties: ['openFile'],
securityScopedBookmarks: true,
})
const selectedFile = openDialogReturnValue.filePaths && openDialogReturnValue.filePaths[0]
if (!selectedFile) {
throw rejectReasons.noCertificateSelected
}
const data = await fsPromise.readFile(selectedFile)
if (data.length > 16_384 || data.length < 128) {
throw rejectReasons.certificateSizeDoesNotMatch
}
return {
data: data.toString('base64'),
name: path.basename(selectedFile),
}
}
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
@@ -111,13 +108,13 @@ export const updateConnection = (connectionId: string, changeSet: Partial<Connec
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,
})
export const addSubscription = (subscription: string, connectionId: string): Action => ({
export const addSubscription = (subscription: Subscription, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION,
})
export const deleteSubscription = (subscription: string, connectionId: string): Action => ({
export const deleteSubscription = (subscription: Subscription, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION,
@@ -184,31 +181,4 @@ async function ensureConnectionsHaveBeenInitialized() {
clearLegacyConnectionOptions()
}
// Migrate connections, rewrite dictionary to "keep" it "ordered" (dictionaries do not have a guaranteed order)
const mayNeedMigrations = connections && connections['iot.eclipse.org']
if (connections && mayNeedMigrations) {
let newConnections = {}
for (const connection of Object.values(connections)) {
addMigratedConnection(newConnections, connection)
}
await persistentStorage.store(storedConnectionsIdentifier, newConnections)
}
}
function addMigratedConnection(newConnections: { [key: string]: ConnectionOptions }, connection: ConnectionOptions) {
// The host has been renamed, only change the host if it has not been changed
// Also check for ssl since SSL is not yet working
if (
connection.id === 'iot.eclipse.org' &&
connection.host === 'iot.eclipse.org' &&
connection.port === 1883 &&
!connection.encryption
) {
connection.id = 'mqtt.eclipse.org'
connection.host = 'mqtt.eclipse.org'
connection.name = 'mqtt.eclipse.org'
}
newConnections[connection.id] = connection
}
+28 -1
View File
@@ -1,4 +1,4 @@
import { ActionTypes } from '../reducers/Global'
import { ActionTypes, ConfirmationRequest } from '../reducers/Global'
import { Dispatch } from 'redux'
export const showError = (error?: string) => ({
@@ -20,3 +20,30 @@ export const toggleSettingsVisibility = () => (dispatch: Dispatch<any>) => {
type: ActionTypes.toggleSettingsVisibility,
})
}
export const requestConfirmation = (title: string, inquiry: string) => (dispatch: Dispatch<any>) => {
return new Promise(resolve => {
const confirmationRequest = {
title,
inquiry,
callback: (confirmed: boolean) => {
resolve(confirmed)
dispatch(removeConfirmationRequest(confirmationRequest))
},
}
dispatch({
confirmationRequest,
type: ActionTypes.requestConfirmation,
})
})
}
export const removeConfirmationRequest = (confirmationRequest: ConfirmationRequest) => (dispatch: Dispatch<any>) => {
return new Promise((resolve, reject) => {
dispatch({
confirmationRequest,
type: ActionTypes.removeConfirmationRequest,
})
})
}
+1 -1
View File
@@ -34,7 +34,7 @@ export const setEditorMode = (editorMode: string): Action => {
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const state = getState()
const topic = state.publish.topic
const topic = state.publish.manualTopic ?? state.tree.get('selectedTopic')?.path()
if (!topic) {
return
+3 -9
View File
@@ -105,21 +105,15 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
const topicFilter = filterStr.toLowerCase()
const nodeFilter = (node: q.TreeNode<TopicViewModel>): boolean => {
const topicMatches =
node
.path()
.toLowerCase()
.indexOf(topicFilter) !== -1
const topicMatches = node.path().toLowerCase().indexOf(topicFilter) !== -1
if (topicMatches) {
return true
}
const messageMatches =
node.message &&
node.message.value &&
Base64Message.toUnicodeString(node.message.value)
.toLowerCase()
.indexOf(filterStr) !== -1
node.message.payload &&
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1
return Boolean(messageMatches)
}
+6 -3
View File
@@ -29,9 +29,9 @@ const debouncedSelectTopic = debounce(
// Update publish topic
let setTopicDispatch: any | undefined
if (!getState().publish.topic) {
if (!getState().publish.manualTopic) {
setTopicDispatch = setTopic(topic.path())
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.topic) {
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.manualTopic) {
setTopicDispatch = setTopic(topic.path())
}
@@ -99,13 +99,16 @@ export const togglePause = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatc
const tree = getState().tree.get('tree')
const changes = tree ? tree.unmergedChanges().length : 0
if (tree) {
paused ? tree.resume() : tree.pause()
}
if (paused && changes > 0) {
dispatch(globalActions.showNotification('Applying recorded changes.'))
}
// Allow for notification to be displayed
setTimeout(() => {
tree && tree.applyUnmergedChanges()
if (paused && changes > 0) {
dispatch(globalActions.showNotification(`Successfully applied ${changes} changes.`))
}
+39 -24
View File
@@ -3,11 +3,34 @@ import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events'
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
import { globalActions } from '.'
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => (
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
const topicsForPurging = recursive ? [topic, ...topic.childTopics()] : [topic]
if (recursive) {
const topicCount = topic.childTopicCount()
const topicDelta = topic.hasMessage() ? -1 : 0
const childTopicsMessage =
topicCount + topicDelta > 0
? ` and ${topicCount + topicDelta} child ${topicCount + topicDelta === 1 ? 'topic' : 'topics'}`
: ''
const confirmed = await dispatch(
globalActions.requestConfirmation(
'Confirm delete',
`Do you want to clear "${topic.path()}"${childTopicsMessage}?\n\nThis function will send an empty payload (QoS 0, retain) to this and every subtopic, clearing retained topics in the process. Only use this function if you know what you are doing.`
)
)
if (!confirmed) {
return
}
}
dispatch(moveSelectionUpOrDownwards('next'))
const { connectionId } = getState().connection
@@ -15,27 +38,19 @@ export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicC
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
if (recursive) {
topic
.childTopics()
.filter(topic => Boolean(topic.message && topic.message.value))
.slice(0, subtopicClearLimit)
.forEach((topic, idx) => {
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
// Rate limit deletion
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
})
}
topicsForPurging
.filter(t => t.path() !== '' && t.hasMessage())
.map(t => t.path())
.forEach((path, idx) => {
const mqttMessage = {
topic: path,
payload: null,
retain: true,
qos: 0 as 0,
messageId: undefined,
}
// Rate limit deletion
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
})
}
+94
View File
@@ -0,0 +1,94 @@
import { ConfigMigrator, Migration } from '../../utils/ConfigMigrator'
import { ConnectionDictionary } from '../ConnectionManager'
import { ConnectionOptions } from '../../model/ConnectionOptions'
export interface ConnectionOptionsV0 {
type: 'mqtt'
id: string
host: string
protocol: 'mqtt' | 'ws'
basePath?: string
port: number
name: string
username?: string
password?: string
encryption: boolean
certValidation: boolean
// selfSignedCertificate?: CertificateParameters
// clientCertificate?: CertificateParameters
// clientKey?: CertificateParameters
clientId?: string
subscriptions: Array<string>
}
let migrations: Migration[] = [
// iot.eclipse.org ha moved to mqtt.eclipse.org
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
if (connection.id == 'iot.eclipse.org' && connection.host == 'iot.eclipse.org' && connection.port == 1883) {
return {
...connection,
id: 'mqtt.eclipse.org',
host: 'mqtt.eclipse.org',
name: 'mqtt.eclipse.org',
}
}
return {
...connection,
}
},
},
// Remove stored clientId if it is the default generated client id. This allows to connect multiple instances of mqtt explorer to the same broker.
// A randomly generated clientId will be used if no clientId is set.
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
if (connection.clientId && /mqtt-explorer-[0-9a-f]{8}/.test(connection.clientId)) {
return {
...connection,
clientId: undefined,
}
}
return {
...connection,
}
},
},
// Added QoS level to subscription options
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptions => {
return {
...connection,
configVersion: 1,
subscriptions: connection.subscriptions.map(topic => ({ topic, qos: 0 })),
}
},
},
]
const connectionMigrator = new ConfigMigrator(migrations)
function isMigrationNecessary(connections: ConnectionDictionary): boolean {
return Object.values(connections)
.map(connection => connectionMigrator.isMigrationNecessary(connection))
.reduce((a, b) => a || b, false)
}
function applyMigrations(connections: ConnectionDictionary): ConnectionDictionary {
let newConnectionDictionary: ConnectionDictionary = {}
Object.keys(connections).forEach(key => {
let newConnection = connectionMigrator.applyMigrations(connections[key]) as any
newConnectionDictionary[newConnection.id] = newConnection
})
return newConnectionDictionary
}
export const connectionsMigrator = {
isMigrationNecessary,
applyMigrations,
}
+7 -7
View File
@@ -1,12 +1,14 @@
import * as React from 'react'
import ConfirmationDialog from './ConfirmationDialog'
import ConnectionSetup from './ConnectionSetup/ConnectionSetup'
import CssBaseline from '@material-ui/core/CssBaseline'
import ErrorBoundary from './ErrorBoundary'
import Notification from './Layout/Notification'
import React from 'react'
import TitleBar from './Layout/TitleBar'
import UpdateNotifier from './UpdateNotifier'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { ConfirmationRequest } from '../reducers/Global'
import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../actions'
import { Theme, withStyles } from '@material-ui/core/styles'
@@ -23,6 +25,7 @@ interface Props {
actions: typeof globalActions
settingsActions: typeof settingsActions
launching: boolean
confirmationRequests: Array<ConfirmationRequest>
}
class App extends React.PureComponent<Props, {}> {
@@ -67,6 +70,7 @@ class App extends React.PureComponent<Props, {}> {
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
{this.renderNotification()}
<React.Suspense fallback={<div></div>}>
<Settings />
@@ -149,12 +153,8 @@ const mapStateToProps = (state: AppState) => {
notification: state.globalState.get('notification'),
highlightTopicUpdates: state.settings.get('highlightTopicUpdates'),
launching: state.globalState.get('launching'),
confirmationRequests: state.globalState.get('confirmationRequests'),
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(App)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
+105
View File
@@ -0,0 +1,105 @@
import DateFormatter from '../helper/DateFormatter'
import NoData from './NoData'
import NumberFormatter from '../helper/NumberFormatter'
import React, { memo, useCallback } from 'react'
import TooltipComponent from './TooltipComponent'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { emphasize } from '@material-ui/core/styles'
import { mapCurveType } from './mapCurveType'
import { PlotCurveTypes } from '../../reducers/Charts'
import { Point, Tooltip } from './Model'
import { Theme, withTheme } from '@material-ui/core'
import { useCustomXDomain } from './effects/useCustomXDomain'
import { useCustomYDomain } from './effects/useCustomYDomain'
import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, YAxis, HorizontalGridLines, Hint } = require('react-vis')
const abbreviate = require('number-abbreviate')
export interface Props {
data: Array<{ x: number; y: number }>
theme: Theme
interpolation?: PlotCurveTypes
range?: [number?, number?]
timeRangeStart?: number
color?: string
}
export default withTheme(
memo((props: Props) => {
const [width, setWidth] = React.useState(300)
const [tooltip, setTooltip] = React.useState<Tooltip | undefined>()
const detectResize = React.useCallback(newWidth => setWidth(newWidth), [])
const hintFormatter = React.useCallback(
(point: any) => [
{ title: <b>Time</b>, value: <DateFormatter timeFirst={true} date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: <NumberFormatter value={point.y} /> },
{ title: <b>Raw</b>, value: <span>{point.y}</span> },
],
[]
)
const onMouseLeave = React.useCallback(() => {
setTooltip(undefined)
}, [])
const showTooltip = React.useCallback((point: Point, something: { event: MouseEvent }) => {
if (!something) {
return
}
setTooltip({ point, value: hintFormatter(point), element: something.event.target as any })
}, [])
const paletteColor =
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
const color = props.color ? props.color : paletteColor
const highlightSelectedPoint = useCallback(
(point: Point) => {
const highlight = tooltip && tooltip.point.x === point.x && tooltip.point.y === point.y
return highlight ? emphasize(color, 0.8) : color
},
[tooltip, color]
)
const formatYAxis = useCallback((num: number) => abbreviate(num), [])
const xDomain = useCustomXDomain(props)
const yDomain = useCustomYDomain(props)
const data = props.data
const hasData = data.length > 0
const dummyDomain = [-1, 1]
const dummyData = [{ x: -2, y: -2 }]
return (
<div>
<div style={{ height: '150px', width: '100%', position: 'relative' }}>
{data.length === 0 ? <NoData /> : null}
<XYPlot
width={width}
height={180}
yDomain={hasData ? yDomain : dummyDomain}
xDomain={hasData ? xDomain : dummyDomain}
onMouseLeave={onMouseLeave}
>
<HorizontalGridLines />
<YAxis width={45} tickFormat={formatYAxis} />
<LineMarkSeries
color={color}
colorType="literal"
getColor={highlightSelectedPoint}
onValueMouseOver={showTooltip}
size={3}
data={hasData ? data : dummyData}
curve={mapCurveType(props.interpolation)}
/>
<Hint value={{ x: 0, y: 0 }} style={{ pointerEvents: 'none' }}>
<TooltipComponent tooltip={tooltip} theme={props.theme} />
</Hint>
</XYPlot>
<ReactResizeDetector handleWidth={true} onResize={detectResize} />
</div>
</div>
)
})
)
+15
View File
@@ -0,0 +1,15 @@
export interface Point {
x: number
y: number
}
export interface Tooltip {
value: Array<TooltipRows>
point: Point
element: Element | null
}
export interface TooltipRows {
title: React.ReactElement
value: React.ReactElement
}
+27
View File
@@ -0,0 +1,27 @@
import React, { memo } from 'react'
import { Typography } from '@material-ui/core'
function NoData() {
return (
<div
style={{
height: '100%',
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
color: '#ccc',
verticalAlign: 'middle',
paddingLeft: '30px',
zIndex: 10,
}}
>
<Typography style={{ fontWeight: 'bold' }} variant="h5">
No Data
</Typography>
</div>
)
}
export default memo(NoData)
@@ -0,0 +1,59 @@
import React, { memo } from 'react'
import { fade } from '@material-ui/core/styles'
import { Fade, Grow, Paper, Popper, Theme, Typography, withTheme } from '@material-ui/core'
import { Tooltip } from './Model'
function TooltipComponent(props: { tooltip?: Tooltip; theme: Theme }) {
const { tooltip } = props
return (
<Popper
style={Boolean(tooltip) ? { transition: 'all 0.1s ease-out' } : undefined}
open={Boolean(tooltip)}
transition={true}
placement="top"
anchorEl={tooltip && tooltip.element}
>
<div
style={{
paddingBottom: '8px',
transition: 'all 0.5s ease',
}}
>
<Fade in={Boolean(tooltip)} timeout={300}>
<Grow in={Boolean(tooltip)} timeout={300}>
<Paper
style={{
padding: '4px',
marginTop: '-12px',
backgroundColor: fade(
props.theme.palette.type === 'light'
? props.theme.palette.background.paper
: props.theme.palette.background.default,
0.7
),
}}
>
<table style={{ lineHeight: '1.25em' }}>
<tbody>
{tooltip &&
tooltip.value.map((v: any, idx: number) => (
<tr key={idx}>
<td>
<Typography style={{ lineHeight: '1.2' }}>{v.title}</Typography>
</td>
<td>
<Typography style={{ lineHeight: '1.2' }}>{v.value}</Typography>
</td>
</tr>
))}
</tbody>
</table>
</Paper>
</Grow>
</Fade>
</div>
</Popper>
)
}
export default withTheme(memo(TooltipComponent))
@@ -0,0 +1,10 @@
import { useMemo } from 'react'
import { Props } from '../Chart'
export function useCustomXDomain(props: Props): [number, number] | undefined {
return useMemo(() => {
const lastDataPoint = [...props.data].sort((a, b) => b.x - a.x)[0]
const lastDataDate = lastDataPoint ? lastDataPoint.x : Date.now()
return props.timeRangeStart ? [Date.now() - props.timeRangeStart, lastDataDate] : undefined
}, [props.data, props.timeRangeStart])
}
@@ -0,0 +1,45 @@
import { Props } from '../Chart'
import { useMemo } from 'react'
import { Point } from '../Model'
function defaultFor(a: number | undefined, b: number) {
return a === undefined ? b : a
}
export function useCustomYDomain(props: Props) {
return useMemo(() => {
const data = props.data
const calculatedDomain = domainForData(data)
const yDomain: [number, number] = props.range
? [defaultFor(props.range[0], calculatedDomain[0]), defaultFor(props.range[1], calculatedDomain[1])]
: calculatedDomain
return yDomain
}, [props.data, props.range])
}
function domainForData(data: Array<Point>): [number, number] {
if (!data[0]) {
const defaultDomain: [number, number] = [-1, 1]
return defaultDomain
}
let max = data[0].y
let min = data[0].y
data.forEach(d => {
if (max < d.y) {
max = d.y
}
if (min > d.y) {
min = d.y
}
})
if ((max === 1 || max === 0) && (min === 1 || min === 0)) {
return [0, 1]
}
if (min === max) {
return [min - 0.5 * min, min + 0.5 * min]
}
return [min, max]
}
+17
View File
@@ -0,0 +1,17 @@
import { PlotCurveTypes } from '../../reducers/Charts'
export function mapCurveType(type: PlotCurveTypes | undefined) {
switch (type) {
case 'curve':
return 'curveMonotoneX'
case 'linear':
return 'curveLinear'
case 'cubic_basis_spline':
return 'curveBasis'
case 'step_after':
return 'curveStepAfter'
case 'step_before':
return 'curveStepBefore'
default:
return 'curveMonotoneX'
}
}
+10 -2
View File
@@ -1,4 +1,4 @@
import * as React from 'react'
import React, { useRef } from 'react'
import Play from '@material-ui/icons/PlayArrow'
import Pause from '@material-ui/icons/PauseCircleFilled'
import Clear from '@material-ui/icons/Clear'
@@ -11,16 +11,24 @@ export function ChartActions(props: {
togglePause: () => void
parameters: ChartParameters
onRemove: () => void
resetDataAction: () => void
}) {
const menuAnchor = useRef()
return (
<div style={{ display: 'flex' }}>
<CustomIconButton tooltip={props.paused ? 'Resume chart' : 'Pause chart'} onClick={props.togglePause}>
{props.paused ? <Play /> : <Pause />}
</CustomIconButton>
<SettingsButton parameters={props.parameters} />
<SettingsButton menuAnchor={menuAnchor} parameters={props.parameters} resetDataAction={props.resetDataAction} />
<CustomIconButton tooltip="Remove chart" onClick={props.onRemove}>
<Clear data-test-type="RemoveChart" data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`} />
</CustomIconButton>
<div style={{ width: 0, overflow: 'hidden' }}>
{/* Helper element to provide an anchor element for the menu,
* so the menu prefers not to overlap with the chart
*/}
<div style={{ marginLeft: '11px' }} ref={menuAnchor as any} />
</div>
</div>
)
}
@@ -1,4 +1,4 @@
import * as React from 'react'
import React, { memo } from 'react'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
@@ -65,7 +65,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(ColorSettings)
export default connect(undefined, mapDispatchToProps)(memo(ColorSettings))
@@ -4,7 +4,7 @@ import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters, PlotCurveTypes } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { Menu, MenuItem } from '@material-ui/core'
import { Menu, MenuItem, Typography } from '@material-ui/core'
function chartParametersForAction(chart: ChartParameters, action: string) {
return {
@@ -40,7 +40,7 @@ function InterpolationSettings(props: {
const menuItems = React.useMemo(() => {
return curves.map(curve => (
<MenuItem key={curve} onClick={callbacks[curve]} selected={props.chart.interpolation === curve}>
{curve.replace(/_/g, ' ')}
<Typography variant="inherit">{curve.replace(/_/g, ' ')}</Typography>
</MenuItem>
))
}, [curves, props.chart])
@@ -60,7 +60,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(InterpolationSettings)
export default connect(undefined, mapDispatchToProps)(InterpolationSettings)
@@ -1,9 +1,10 @@
import * as React from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
import { connect } from 'react-redux'
import ArrowUpward from '@material-ui/icons/ArrowUpward'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { MenuItem, Typography, ListItemIcon } from '@material-ui/core'
function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartParameters; close: () => void }) {
const moveUp = React.useCallback(() => {
@@ -16,7 +17,10 @@ function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartPa
return (
<MenuItem key="size" onClick={moveUp}>
Move up
<ListItemIcon>
<ArrowUpward />
</ListItemIcon>
<Typography variant="inherit">Move up</Typography>
</MenuItem>
)
}
@@ -29,7 +33,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(MoveUp)
export default connect(undefined, mapDispatchToProps)(MoveUp)
@@ -56,8 +56,8 @@ function RangeSettings(props: Props) {
onClose={props.onClose}
onKeyDownCapture={handleKeyEvents}
>
<Typography>Define custom ranges for the Y-Axis</Typography>
<div style={{ padding: '0 16px' }}>
<div style={{ padding: '0 16px', width: '275px' }}>
<Typography>Define custom ranges for the Y-Axis</Typography>
<TextField
inputProps={{
ref: rangeFromRef,
@@ -95,10 +95,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(RangeSettings)
export default connect(undefined, mapDispatchToProps)(RangeSettings)
function useRangeStateToFireUpdateAction(
rangeFrom: string | number | undefined,
@@ -4,9 +4,12 @@ import CustomIconButton from '../../helper/CustomIconButton'
import MoreVertIcon from '@material-ui/icons/Settings'
import { ChartParameters } from '../../../reducers/Charts'
export function SettingsButton(props: { parameters: ChartParameters }) {
export function SettingsButton(props: {
parameters: ChartParameters
resetDataAction: () => void
menuAnchor: React.MutableRefObject<undefined>
}) {
const [visible, setVisible] = React.useState(false)
const settingsRef = React.useRef()
const toggleSettings = React.useCallback(() => {
setVisible(!visible)
}, [visible])
@@ -17,10 +20,15 @@ export function SettingsButton(props: { parameters: ChartParameters }) {
return (
<span>
<ChartSettings open={visible} close={close} anchorEl={settingsRef} chart={props.parameters} />
<ChartSettings
open={visible}
close={close}
anchorEl={props.menuAnchor}
chart={props.parameters}
resetDataAction={props.resetDataAction}
/>
<CustomIconButton tooltip="Chart settings" onClick={toggleSettings}>
<MoreVertIcon
ref={settingsRef as any}
data-test-type="ChartSettings"
data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`}
/>
@@ -1,4 +1,4 @@
import * as React from 'react'
import React, { memo } from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
import { connect } from 'react-redux'
@@ -24,16 +24,16 @@ function Size(props: {
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
<MenuItem selected={props.chart.width === undefined} onClick={setChartWidth()}>
auto
<Typography variant="inherit">auto</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'big'} onClick={setChartWidth('big')}>
100% width
<Typography variant="inherit">100% width</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'medium'} onClick={setChartWidth('medium')}>
50% width
<Typography variant="inherit">50% width</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'small'} onClick={setChartWidth('small')}>
33% width
<Typography variant="inherit">33% width</Typography>
</MenuItem>
</Menu>
)
@@ -47,7 +47,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(Size)
export default connect(undefined, mapDispatchToProps)(memo(Size))
@@ -96,7 +96,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(TimeRangeSettings)
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
@@ -1,16 +1,22 @@
import * as React from 'react'
import BarChart from '@material-ui/icons/BarChart'
import Clear from '@material-ui/icons/Refresh'
import ColorLens from '@material-ui/icons/ColorLens'
import ColorSettings from './ColorSettings'
import InterpolationSettings from './InterpolationSettings'
import MoveUp from './MoveUp'
import MultilineChart from '@material-ui/icons/MultilineChart'
import RangeSettings from './RangeSettings'
import React, { memo } from 'react'
import Size from './Size'
import Sort from '@material-ui/icons/Sort'
import TimeRangeSettings from './TimeRangeSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem } from '@material-ui/core'
import { Menu, MenuItem, ListItemIcon, Typography } from '@material-ui/core'
function ChartSettings(props: {
open: boolean
close: () => void
resetDataAction: () => void
chart: ChartParameters
anchorEl: React.MutableRefObject<undefined>
}) {
@@ -19,6 +25,7 @@ function ChartSettings(props: {
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
const [sizeVisible, setSizeVisible] = React.useState(false)
const [colorVisible, setColorVisible] = React.useState(false)
const open = props.open
const toggleRange = React.useCallback(() => {
if (open) {
@@ -59,19 +66,40 @@ function ChartSettings(props: {
<span>
<Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}>
<MenuItem key="range" onClick={toggleRange}>
Set range
<ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">Y-Axis range (Values)</Typography>
</MenuItem>
<MenuItem key="timeRange" onClick={toggleTimeRange}>
Time range
<ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">X-Axis range (Time)</Typography>
</MenuItem>
<MenuItem key="interpolation" onClick={toggleInterpolation}>
Curve interpolation
<ListItemIcon>
<MultilineChart />
</ListItemIcon>
<Typography variant="inherit">Curve interpolation</Typography>
</MenuItem>
<MenuItem key="size" onClick={toggleSize}>
Size
<ListItemIcon>
<Sort />
</ListItemIcon>
<Typography variant="inherit">Size</Typography>
</MenuItem>
<MenuItem key="color" onClick={toggleColor}>
Color
<ListItemIcon>
<ColorLens />
</ListItemIcon>
<Typography variant="inherit">Color</Typography>
</MenuItem>
<MenuItem key="clear" onClick={props.resetDataAction}>
<ListItemIcon>
<Clear />
</ListItemIcon>
<Typography variant="inherit">Clear data</Typography>
</MenuItem>
<MoveUp chart={props.chart} close={props.close} />
</Menu>
@@ -94,4 +122,4 @@ function ChartSettings(props: {
)
}
export default ChartSettings
export default memo(ChartSettings)
@@ -1,6 +1,6 @@
import * as q from '../../../../backend/src/Model'
import React from 'react'
import Chart from './Chart'
import TopicChart from './TopicChart'
import { ChartParameters } from '../../reducers/Charts'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
@@ -16,5 +16,5 @@ export function ChartWithTreeNode(props: Props) {
}
const treeNode = usePollingToFetchTreeNode(tree, parameters.topic)
return <Chart treeNode={treeNode} parameters={parameters} />
return <TopicChart treeNode={treeNode} parameters={parameters} />
}
@@ -1,15 +1,31 @@
import * as q from '../../../../backend/src/Model'
import React, { useState } from 'react'
import ChartTitle from './ChartTitle'
import React, { useState, useCallback, memo, useRef } from 'react'
import TopicPlot from '../TopicPlot'
import { bindActionCreators } from 'redux'
import { ChartActions } from './ChartActions'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { Paper } from '@material-ui/core'
import ChartTitle from './ChartTitle'
import { ChartActions } from './ChartActions'
const throttle = require('lodash.throttle')
class ClearableMessageBuffer extends q.RingBuffer<q.Message> {
public clear() {
this.items = []
this.start = 0
this.end = 0
}
public static fromMessageBuffer(buffer: q.RingBuffer<q.Message>): ClearableMessageBuffer {
return new ClearableMessageBuffer(buffer.capacity, buffer.maxItems, buffer.compactionFactor, buffer)
}
public clone(): ClearableMessageBuffer {
return ClearableMessageBuffer.fromMessageBuffer(this)
}
}
interface Props {
parameters: ChartParameters
treeNode?: q.TreeNode<any>
@@ -19,18 +35,18 @@ interface Props {
}
/**
* Subscribes to onMessages keeping track of additional data points
* Subscribes to onMessages, stores more data points then the default
*/
function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
const [lastUpdated, setLastUpdate] = useState(0)
const [messageHistory, setMessageHistory] = useState<q.MessageHistory | undefined>()
const [messageHistory, setMessageHistory] = useState<ClearableMessageBuffer | undefined>()
let amendMessageCallback: any
function subscribeToMessageUpdates() {
const throttledUpdate = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
if (treeNode) {
const newMessageHistory = treeNode.messageHistory.clone()
const newMessageHistory = ClearableMessageBuffer.fromMessageBuffer(treeNode.messageHistory)
newMessageHistory.setCapacity(500, 2 * 500 * 10000)
amendMessageCallback = (message: q.Message) => {
@@ -43,6 +59,7 @@ function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
return function cleanup() {
treeNode && treeNode.onMessage.unsubscribe(amendMessageCallback)
setMessageHistory(undefined)
}
}
React.useEffect(subscribeToMessageUpdates, [treeNode])
@@ -50,22 +67,34 @@ function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
return messageHistory
}
function Chart(props: Props) {
function useResetDataCallback(messageHistory: ClearableMessageBuffer | undefined) {
const [lastUpdated, setLastUpdate] = useState(0)
return React.useCallback(() => {
messageHistory && messageHistory.clear()
setLastUpdate(Date.now())
}, [messageHistory])
}
function TopicChart(props: Props) {
const { parameters, treeNode } = props
const [frozenHistory, setFrozenHistory] = React.useState<q.MessageHistory | undefined>()
const [frozenHistory, setFrozenHistory] = useState<q.MessageHistory | undefined>()
const messageHistory = useMessageSubscriptionToUpdate(treeNode)
const togglePause = React.useCallback(() => {
if (!props.treeNode) {
const togglePause = useCallback(() => {
if (!treeNode) {
return
}
setFrozenHistory(frozenHistory ? undefined : messageHistory && messageHistory.clone())
}, [props.treeNode, frozenHistory])
}, [props.treeNode, frozenHistory, messageHistory])
const onRemove = React.useCallback(() => {
props.actions.chart.removeChart(props.parameters)
}, [props.parameters])
const resetData = useResetDataCallback(messageHistory)
return (
<Paper
style={{ padding: '8px' }}
@@ -76,6 +105,7 @@ function Chart(props: Props) {
<div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
<ChartTitle parameters={parameters} />
<ChartActions
resetDataAction={resetData}
parameters={parameters}
onRemove={onRemove}
paused={Boolean(frozenHistory)}
@@ -83,18 +113,14 @@ function Chart(props: Props) {
/>
</div>
</div>
{messageHistory ? (
<TopicPlot
color={props.parameters.color}
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
history={frozenHistory || messageHistory}
dotPath={parameters.dotPath}
/>
) : (
<span>No data</span>
)}
<TopicPlot
color={props.parameters.color}
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
history={frozenHistory || messageHistory || new ClearableMessageBuffer(1)}
dotPath={parameters.dotPath}
/>
</Paper>
)
}
@@ -107,7 +133,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(Chart)
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
+1 -4
View File
@@ -126,7 +126,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ChartPanel))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel))
+61
View File
@@ -0,0 +1,61 @@
import React, { useRef, useCallback, memo } from 'react'
import { ConfirmationRequest } from '../reducers/Global'
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core'
import { KeyCodes } from '../utils/KeyCodes'
function ConfirmationDialog(props: { confirmationRequests: Array<ConfirmationRequest> }) {
const request = props.confirmationRequests[0]
const yesRef = useRef<HTMLButtonElement>()
const noRef = useRef<HTMLButtonElement>()
const arrowKeyHandler = useCallback((event: React.KeyboardEvent) => {
const isArrowKey = event.keyCode === KeyCodes.arrow_left || event.keyCode === KeyCodes.arrow_right
if (!isArrowKey) {
return
}
event.stopPropagation()
if (document.activeElement === noRef.current) {
yesRef.current && yesRef.current.focus()
} else {
noRef.current && noRef.current.focus()
}
}, [])
const confirm = React.useCallback(() => {
request && request.callback(true)
}, [request])
const reject = React.useCallback(() => {
request && request.callback(false)
}, [request])
if (!request) {
return null
}
return (
<Dialog
open={true}
onClose={reject}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
onKeyDown={arrowKeyHandler}
>
<DialogTitle id="alert-dialog-title">{request.title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description" style={{ whiteSpace: 'pre-wrap' }}>
{request.inquiry}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button ref={yesRef as any} variant="contained" onClick={confirm} color="primary" autoFocus>
Yes
</Button>
<Button ref={noRef as any} variant="contained" onClick={reject} color="secondary" style={{ marginLeft: '8px' }}>
No
</Button>
</DialogActions>
</Dialog>
)
}
export default memo(ConfirmationDialog)
@@ -1,7 +1,6 @@
import * as React from 'react'
import { useState, useCallback, memo } from 'react'
import Add from '@material-ui/icons/Add'
import ClearAdornment from '../helper/ClearAdornment'
import Delete from '@material-ui/icons/Delete'
import Lock from '@material-ui/icons/Lock'
import Undo from '@material-ui/icons/Undo'
import { bindActionCreators } from 'redux'
@@ -9,18 +8,10 @@ import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
Grid,
IconButton,
TextField,
List,
ListItem,
ListItemText,
Tooltip,
Typography,
} from '@material-ui/core'
import { Button, Grid, TextField, Tooltip } from '@material-ui/core'
import { QosSelect } from '../QosSelect'
import { QoS } from '../../../../backend/src/DataSource/MqttSource'
import Subscriptions from './Subscriptions'
interface Props {
connection: ConnectionOptions
@@ -28,116 +19,93 @@ interface Props {
managerActions: typeof connectionManagerActions
}
interface State {
subscription: string
}
const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
const [qos, setQos] = useState<QoS>(0)
const [topic, setTopic] = useState('')
const { classes } = props
class ConnectionSettings extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { subscription: '' }
}
private handleChange = (name: string) => (event: any) => {
this.props.managerActions.updateConnection(this.props.connection.id, {
[name]: event.target.value,
})
}
private renderSubscriptions() {
const connection = this.props.connection
return connection.subscriptions.map(subscription => (
<Subscription
deleteAction={() => this.props.managerActions.deleteSubscription(subscription, connection.id)}
subscription={subscription}
key={subscription}
/>
))
}
public render() {
const { classes } = this.props
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={10} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="Subscription"
margin="normal"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ subscription: event.target.value })
}
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
className={classes.button}
color="secondary"
onClick={() =>
this.props.managerActions.addSubscription(this.state.subscription, this.props.connection.id)
}
variant="contained"
>
<Add /> Add
</Button>
</Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}>
<List className={`${classes.topicList} advanced-connection-settings-topic-list`} component="nav">
<div className={classes.list}>{this.renderSubscriptions()}</div>
</List>
</Grid>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="MQTT Client ID"
margin="normal"
value={this.props.connection.clientId}
onChange={this.handleChange('clientId')}
/>
</Grid>
<Grid item={true} xs={3} className={classes.gridPadding}>
<div>
<Tooltip title="Manage tls connection certificates" placement="top">
<Button
variant="contained"
className={classes.button}
onClick={() => this.props.managerActions.toggleCertificateSettings()}
>
<Lock /> Certificates
</Button>
</Tooltip>
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
onClick={this.props.managerActions.toggleAdvancedSettings}
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
)
}
}
const Subscription = (props: { subscription: string; deleteAction: any }) => {
return (
<ListItem style={{ padding: '0 0 0 8px' }}>
<ListItemText>
<IconButton onClick={props.deleteAction} style={{ padding: '6px' }}>
<Delete />
</IconButton>
{props.subscription}
</ListItemText>
</ListItem>
const updateSubscription = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => setTopic(event.target.value),
[]
)
}
const handleChange = useCallback(
(name: string) => (event: any) => {
props.managerActions.updateConnection(props.connection.id, {
[name]: event.target.value,
})
},
[]
)
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={8} className={classes.gridPadding}>
<TextField
className={`${classes.fullWidth} advanced-connection-settings-topic-input`}
label="Topic"
placeholder="example/topic"
margin="normal"
value={topic}
onChange={updateSubscription}
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<div className={classes.qos}>
<QosSelect label="QoS" selected={qos} onChange={setQos} />
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
className={classes.button}
color="secondary"
onClick={() => props.managerActions.addSubscription({ topic, qos }, props.connection.id)}
variant="contained"
>
<Add /> Add
</Button>
</Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}>
<Subscriptions connection={props.connection} />
</Grid>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="MQTT Client ID"
margin="normal"
value={props.connection.clientId}
onChange={handleChange('clientId')}
/>
</Grid>
<Grid item={true} xs={3} className={classes.gridPadding}>
<div>
<Tooltip title="Manage tls connection certificates" placement="top">
<Button
variant="contained"
className={classes.button}
onClick={() => props.managerActions.toggleCertificateSettings()}
>
<Lock /> Certificates
</Button>
</Tooltip>
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
onClick={props.managerActions.toggleAdvancedSettings}
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
)
})
const mapDispatchToProps = (dispatch: any) => {
return {
@@ -152,19 +120,13 @@ const styles = (theme: Theme) => ({
gridPadding: {
padding: '0 12px !important',
},
topicList: {
height: '180px',
overflowY: 'scroll' as 'scroll',
margin: '8px 16px',
backgroundColor: theme.palette.background.default,
},
button: {
marginTop: theme.spacing(3),
float: 'right' as 'right',
},
qos: {
marginTop: theme.spacing(1),
},
})
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(ConnectionSettings))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
@@ -81,7 +81,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(CertificateFileSelection))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection))
@@ -18,7 +18,7 @@ interface State {
subscription: string
}
class Certificates extends React.Component<Props, State> {
class Certificates extends React.PureComponent<Props, State> {
constructor(props: any) {
super(props)
this.state = { subscription: '' }
@@ -104,7 +104,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(Certificates))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Certificates))
@@ -1,7 +1,7 @@
import ConnectButton from './ConnectButton'
import Delete from '@material-ui/icons/Delete'
import React, { useCallback, useState } from 'react'
import Save from '@material-ui/icons/Save'
import Delete from '@material-ui/icons/Delete'
import Settings from '@material-ui/icons/Settings'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
@@ -285,7 +285,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSettings))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
@@ -20,7 +20,7 @@ interface Props {
showCertificateSettings: boolean
}
class ConnectionSetup extends React.Component<Props, {}> {
class ConnectionSetup extends React.PureComponent<Props, {}> {
constructor(props: Props) {
super(props)
}
@@ -134,7 +134,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSetup))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
@@ -51,7 +51,4 @@ export const connectionItemStyle = (theme: Theme) => ({
},
})
export default connect(
null,
mapDispatchToProps
)(withStyles(connectionItemStyle)(ConnectionItem))
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
@@ -77,7 +77,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ProfileList))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
@@ -0,0 +1,90 @@
import React, { useCallback, useState } from 'react'
import Delete from '@material-ui/icons/Delete'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import {
IconButton,
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Theme,
} from '@material-ui/core'
import { bindActionCreators } from 'redux'
import { withStyles } from '@material-ui/styles'
import { connect } from 'react-redux'
function Subscriptions(props: {
classes: any
connection: ConnectionOptions
managerActions: typeof connectionManagerActions
}) {
const { classes, connection, managerActions } = props
return (
<TableContainer component={Paper} className={`${classes.topicList} advanced-connection-settings-topic-list`}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="left" padding="checkbox" className={classes.tableTitleCell}></TableCell>
<TableCell className={classes.tableTitleCell}>Topic</TableCell>
<TableCell align="right" className={classes.tableTitleCell}>
QoS
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{connection.subscriptions.map(subscription => (
<TableRow key={subscription.topic + '_qos_' + subscription.qos}>
<TableCell align="right" className={classes.tableCell}>
<IconButton
onClick={() => managerActions.deleteSubscription(subscription, connection.id)}
style={{ padding: '6px' }}
>
<Delete />
</IconButton>
</TableCell>
<TableCell component="th" scope="row" className={classes.tableCell}>
{subscription.topic}
</TableCell>
<TableCell align="right" className={classes.tableCell}>
{subscription.qos}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
tableCell: {
paddingTop: 0,
paddingBottom: 0,
wordBbreak: 'break-word',
},
tableTitleCell: {
paddingTop: `${theme.spacing(0.5)}px`,
paddingBottom: `${theme.spacing(0.5)}px`,
},
topicList: {
height: '196px',
overflowY: 'scroll' as 'scroll',
margin: `${theme.spacing(1)}px ${theme.spacing(1)}px 0 ${theme.spacing(1)}px`,
backgroundColor: theme.palette.background.default,
width: 'auto',
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions))
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
const cursor = require('./cursor.png')
import cursor from './cursor.png'
interface State {
enabled: boolean
+1 -1
View File
@@ -14,7 +14,7 @@ interface Props {
classes: any
}
class ErrorBoundary extends React.Component<Props, State> {
class ErrorBoundary extends React.PureComponent<Props, State> {
public static getDerivedStateFromError(error: Error) {
return { error }
}
+1 -1
View File
@@ -11,7 +11,7 @@ interface Props {
classes: any
}
class Notification extends React.Component<Props, {}> {
class Notification extends React.PureComponent<Props, {}> {
constructor(props: Props) {
super(props)
}
+2 -5
View File
@@ -28,7 +28,7 @@ interface Props {
tree?: q.Tree<any>
}
class PauseButton extends React.Component<Props, { changes: number }> {
class PauseButton extends React.PureComponent<Props, { changes: number }> {
private timer?: any
constructor(props: Props) {
super(props)
@@ -102,7 +102,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(PauseButton))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
+3 -4
View File
@@ -35,6 +35,7 @@ function SearchBar(props: {
useGlobalKeyEventHandler(undefined, event => {
const isCharacter = event.key.length === 1
const isModifierKey = event.metaKey || event.ctrlKey
const isAllowedControlCharacter = event.keyCode === KeyCodes.backspace || event.keyCode === KeyCodes.delete
const tagNameBlacklist = ['INPUT', 'TEXTAREA', 'RADIO', 'CHECKBOX', 'OPTION', 'FORM']
@@ -43,6 +44,7 @@ function SearchBar(props: {
if (
(isCharacter || isAllowedControlCharacter) &&
!isModifierKey &&
!event.defaultPrevented &&
!hasFocus &&
tagElementIsNotBlacklisted &&
@@ -140,7 +142,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(SearchBar))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SearchBar))
+9 -6
View File
@@ -32,6 +32,8 @@ const styles = (theme: Theme) => ({
},
disconnect: {
margin: 'auto 8px auto auto',
},
disconnectLabel: {
color: theme.palette.primary.contrastText,
},
})
@@ -46,7 +48,7 @@ interface Props {
topicFilter?: string
}
class TitleBar extends React.Component<Props, {}> {
class TitleBar extends React.PureComponent<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
@@ -71,7 +73,11 @@ class TitleBar extends React.Component<Props, {}> {
</Typography>
<SearchBar />
<PauseButton />
<Button className={classes.disconnect} onClick={actions.connection.disconnect}>
<Button
className={classes.disconnect}
classes={{ label: classes.disconnectLabel }}
onClick={actions.connection.disconnect}
>
Disconnect <CloudOff className={classes.disconnectIcon} />
</Button>
<ConnectionHealthIndicator withBackground={true} />
@@ -97,7 +103,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(TitleBar))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
@@ -1,18 +1,8 @@
import * as React from 'react'
import { TextField, MenuItem, Tooltip } from '@material-ui/core'
import { connect } from 'react-redux'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { publishActions } from '../../../actions'
import { QoS } from '../../../backend/src/DataSource/MqttSource'
interface Props {
qos: 0 | 1 | 2
actions: {
publish: typeof publishActions
}
}
function QosSelect(props: Props) {
export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void; label?: string }) {
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
const itemStyle = { padding: '0' }
@@ -22,16 +12,16 @@ function QosSelect(props: Props) {
if (value !== 0 && value !== 1 && value !== 2) {
return
}
props.actions.publish.setQoS(value)
props.onChange(value)
},
[props.actions.publish]
[props.onChange]
)
return (
<TextField
select={true}
value={props.qos}
label={props.label}
value={props.selected}
margin="normal"
style={{ margin: '8px 0 8px 8px' }}
onChange={onChangeQos}
@@ -55,21 +45,4 @@ function QosSelect(props: Props) {
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
publish: bindActionCreators(publishActions, dispatch),
},
}
}
const mapStateToProps = (state: AppState) => {
return {
qos: state.publish.qos,
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(QosSelect)
export default React.memo(QosSelect)
@@ -7,6 +7,7 @@ import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
import { useUpdateComponentWhenNodeUpdates } from '../helper/useUpdateComponentWhenNodeUpdates'
const abbreviate = require('number-abbreviate')
interface Stats {
@@ -32,6 +33,7 @@ interface Props {
function BrokerStatistics(props: Props) {
const { tree, classes } = props
const sysTopic = usePollingToFetchTreeNode(props.tree, '$SYS')
useUpdateComponentWhenNodeUpdates(sysTopic)
return useMemo(() => {
if (!Boolean(sysTopic)) {
@@ -90,7 +92,7 @@ function BrokerStatistics(props: Props) {
{renderPair(tree, stats.heap, stats.heapMax)}
</div>
)
}, [sysTopic && sysTopic.lastUpdate])
}, [sysTopic && sysTopic.lastUpdate, props.classes])
}
const mapStateToProps = (state: AppState) => {
@@ -121,8 +123,8 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
return null
}
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
let value = node.message && node.message.value ? parseFloat(str) : NaN
const str = node.message.payload ? Base64Message.toUnicodeString(node.message.payload) : ''
let value = node.message && node.message.payload ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str
return (
@@ -90,7 +90,7 @@ interface Props {
theme: 'light' | 'dark'
}
class Settings extends React.Component<Props, {}> {
class Settings extends React.PureComponent<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
@@ -255,9 +255,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Settings)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Settings))
@@ -82,9 +82,4 @@ const styles = (theme: Theme) => ({
},
})
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(TimeLocaleSettings)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TimeLocaleSettings))
@@ -85,7 +85,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(ChartPreview)
export default connect(undefined, mapDispatchToProps)(ChartPreview)
@@ -8,7 +8,7 @@ import { isPlottable, lineChangeStyle, trimNewlineRight } from './util'
import { JsonPropertyLocation, literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA'
import { style } from './style'
import { withStyles } from '@material-ui/core'
import { withStyles, Typography } from '@material-ui/core'
import 'prismjs/components/prism-json'
interface Props {
@@ -17,12 +17,13 @@ interface Props {
current: string
nameOfCompareMessage: string
language?: 'json'
title?: string
classes: any
}
interface State {}
class CodeDiff extends React.Component<Props, State> {
class CodeDiff extends React.PureComponent<Props, State> {
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre ~ pre' })
constructor(props: Props) {
@@ -88,7 +89,8 @@ class CodeDiff extends React.Component<Props, State> {
const code = this.renderStyledCodeLines(changes)
return (
<div>
<div style={{ marginTop: '8px' }}>
{this.props.title ? <Typography className={this.props.classes.title}>{this.props.title}</Typography> : null}
<div tabIndex={0} onKeyDown={this.handleCtrlA} className={this.props.classes.codeWrapper}>
<Gutters
className={this.props.classes.gutters}
@@ -11,6 +11,11 @@ export const style = (theme: Theme) => {
}
return {
title: {
font: codeBaseStyle.font,
paddingLeft: '4px',
backgroundColor: codeBlockColors.gutters,
},
line: {
lineHeight: 'normal' as 'normal',
paddingLeft: '4px',
@@ -22,7 +27,6 @@ export const style = (theme: Theme) => {
maxHeight: '15em',
overflow: 'auto',
backgroundColor: `${codeBlockColors.background}`,
margin: '8px 0 0 0',
},
gutters: {
...codeBaseStyle,
+51 -46
View File
@@ -1,7 +1,7 @@
import * as React from 'react'
import React, { useCallback, useState, useEffect, memo } from 'react'
import { Badge, Typography } from '@material-ui/core'
import { selectTextWithCtrlA } from '../../utils/handleTextSelectWithCtrlA'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme, withStyles, emphasize } from '@material-ui/core/styles'
interface HistoryItem {
key: string
@@ -15,51 +15,49 @@ interface Props {
onClick?: (index: number, element: EventTarget) => void
classes: any
contentTypeIndicator?: JSX.Element
autoOpen?: boolean
theme: Theme
children?: any
}
interface State {
collapsed: boolean
}
function HistoryDrawer(props: Props) {
const handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' })
const [expanded, setExpanded] = useState<boolean | undefined>(undefined)
class HistoryDrawer extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = {
collapsed: true,
const toggle = useCallback(() => {
setExpanded(!expanded)
}, [expanded])
useEffect(() => {
if (props.autoOpen && expanded === undefined && props.items.length > 0) {
setExpanded(true)
}
}
}, [props.autoOpen, expanded, props.items])
private toggle = () => {
this.setState({ collapsed: !this.state.collapsed })
}
private createSelectionHandler = (index: number) => (event: React.MouseEvent) => {
this.props.onClick && this.props.onClick(index, event.target)
const createSelectionHandler = (index: number) => (event: React.MouseEvent) => {
props.onClick && props.onClick(index, event.target)
event.preventDefault()
event.stopPropagation()
}
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' })
public renderHistory() {
function renderHistory() {
const style = (element: HistoryItem) => ({
backgroundColor: element.selected
? this.props.theme.palette.action.selected
: this.props.theme.palette.action.hover,
? emphasize(props.theme.palette.background.default, 0.2)
: emphasize(props.theme.palette.background.default, 0.07),
margin: '8px',
padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit',
cursor: props.onClick ? 'pointer' : 'inherit',
})
const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }
const elements = this.props.items.map((element, index) => (
const elements = props.items.map((element, index) => (
<div
key={element.key}
style={style(element)}
onClick={this.createSelectionHandler(index)}
onClick={createSelectionHandler(index)}
tabIndex={0}
onKeyDown={this.handleCtrlA}
onKeyDown={handleCtrlA}
>
<div>
<i>{element.title}</i>
@@ -72,40 +70,47 @@ class HistoryDrawer extends React.Component<Props, State> {
</div>
))
const visible = this.props.items.length > 0 && this.state.collapsed
const visible = props.items.length > 0 && !expanded
return (
<div className={this.props.classes.main}>
<Typography component={'span'} onClick={this.toggle} style={{ cursor: 'pointer' }}>
<Badge
classes={{ badge: this.props.classes.badge }}
invisible={!visible}
badgeContent={this.props.items.length}
color="primary"
>
{this.state.collapsed ? '▶ History' : '▼ History'}
</Badge>
<div style={{ float: 'right' }}>{this.state.collapsed ? this.props.contentTypeIndicator : null}</div>
</Typography>
<div className={props.classes.main}>
<div
style={{
backgroundColor: emphasize(props.theme.palette.background.default, 0.07),
borderBottom: expanded ? `1px solid ${emphasize(props.theme.palette.background.default, 0.2)}` : undefined,
}}
>
<Typography component={'span'} onClick={toggle} style={{ cursor: 'pointer', display: 'flex' }}>
<span style={{ flexGrow: 1 }}>
<Badge
classes={{ badge: props.classes.badge }}
invisible={!visible}
badgeContent={props.items.length}
color="primary"
>
{expanded ? '▼ History' : '▶ History'}
</Badge>
</span>
<span>{props.contentTypeIndicator}</span>
</Typography>
</div>
<div style={{ maxHeight: '230px', overflowY: 'scroll' }}>
{this.state.collapsed ? null : this.props.children}
{this.state.collapsed ? null : elements}
{expanded ? props.children : null}
{expanded ? elements : null}
</div>
</div>
)
}
public render() {
return <div style={{ display: 'block', width: '100%' }}>{this.renderHistory()}</div>
}
return <div style={{ display: 'block', width: '100%' }}>{renderHistory()}</div>
}
const styles = (theme: Theme) => ({
main: {
backgroundColor: 'rgba(170, 170, 170, 0.2)',
backgroundColor: theme.palette.background.default,
marginTop: '16px',
},
badge: { right: '-25px' },
})
export default withStyles(styles, { withTheme: true })(HistoryDrawer)
export default withStyles(styles, { withTheme: true })(memo(HistoryDrawer))
+20
View File
@@ -0,0 +1,20 @@
import React, { memo } from 'react'
import { Message } from '../../../../backend/src/Model'
import { Tooltip } from '@material-ui/core'
export const MessageId = memo(function MessageId(props: { message: Message; addComma?: boolean }) {
const { message, addComma } = props
if (!message.messageId) {
return null
}
return (
<Tooltip title="MessageIds are used to signal a successful transmission of a message.">
<span>
#msg: {message.messageId}
{addComma ? ', ' : ''}
</span>
</Tooltip>
)
})
-119
View File
@@ -1,119 +0,0 @@
import * as React from 'react'
import DateFormatter from '../helper/DateFormatter'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { PlotCurveTypes } from '../../reducers/Charts'
import { Theme, withTheme } from '@material-ui/core'
import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, Hint, XAxis, YAxis, HorizontalGridLines } = require('react-vis')
const abbreviate = require('number-abbreviate')
interface Props {
data: Array<{ x: number; y: number }>
theme: Theme
interpolation?: PlotCurveTypes
range?: [number?, number?]
timeRangeStart?: number
color?: string
}
function mapCurveType(type: PlotCurveTypes | undefined) {
switch (type) {
case 'curve':
return 'curveMonotoneX'
case 'linear':
return 'curveLinear'
case 'cubic_basis_spline':
return 'curveBasis'
case 'step_after':
return 'curveStepAfter'
case 'step_before':
return 'curveStepBefore'
default:
return 'curveMonotoneX'
}
}
export default withTheme((props: Props) => {
const [width, setWidth] = React.useState(300)
const [tooltip, setTooltip] = React.useState({ value: undefined })
const detectResize = React.useCallback(newWidth => setWidth(newWidth), [])
const hintFormatter = React.useCallback((point: any) => {
return [
{ title: <b>Time</b>, value: <DateFormatter date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: point.y },
]
}, [])
const hideTooltip = React.useCallback(() => {
setTooltip({ value: undefined })
}, [])
const showTooltip = React.useCallback((value: any) => {
setTooltip({ value })
}, [])
return React.useMemo(() => {
const data = props.data
const calculatedDomain = domainForData(data)
const yDomain: [number, number] = props.range
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
: calculatedDomain
const xDomain = props.timeRangeStart ? [Date.now() - props.timeRangeStart, Date.now()] : undefined
let color: string =
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
if (props.color) {
color = props.color
}
return (
<div style={{ height: '150px', overflow: 'hidden' }}>
<XYPlot width={width} height={180} yDomain={yDomain} xDomain={xDomain}>
<HorizontalGridLines />
<XAxis />
<YAxis width={45} tickFormat={(num: number) => abbreviate(num)} />
<LineMarkSeries
color={color}
onValueMouseOver={showTooltip}
onValueMouseOut={hideTooltip}
size={3}
data={data}
curve={mapCurveType(props.interpolation)}
/>
{tooltip.value ? <Hint format={hintFormatter} value={tooltip.value} /> : null}
</XYPlot>
<ReactResizeDetector handleWidth={true} onResize={detectResize} />
</div>
)
}, [width, props.data, tooltip, props.interpolation, props.range, props.color, props.theme])
})
function domainForData(data: Array<{ x: number; y: number }>): [number, number] {
if (!data[0]) {
const defaultDomain: [number, number] = [-1, 1]
return defaultDomain
}
let max = data[0].y
let min = data[0].y
data.forEach(d => {
if (max < d.y) {
max = d.y
}
if (min > d.y) {
min = d.y
}
})
if ((max === 1 || max === 0) && (min === 1 || min === 0)) {
return [0, 1]
}
if (min === max) {
return [min - 0.5 * min, min + 0.5 * min]
}
return [min, max]
}
+14 -5
View File
@@ -1,11 +1,14 @@
import * as React from 'react'
import { default as AceEditor } from 'react-ace'
import { Theme, withTheme } from '@material-ui/core'
import 'brace/mode/json'
import 'brace/theme/dawn'
import 'brace/theme/monokai'
import 'brace/mode/xml'
import 'brace/mode/text'
import 'ace-builds'
import 'ace-builds/webpack-resolver'
import 'ace-builds/src-noconflict/mode-json'
import 'ace-builds/src-noconflict/mode-xml'
import 'ace-builds/src-noconflict/snippets/json'
import 'ace-builds/src-noconflict/snippets/xml'
import 'ace-builds/src-noconflict/mode-text'
import 'ace-builds/src-noconflict/theme-monokai'
import 'react-ace'
function Editor(props: {
@@ -13,6 +16,7 @@ function Editor(props: {
theme: Theme
value: string | undefined
onChange: (value: string) => void
editorRef: React.Ref<AceEditor>
}) {
const editorOptions = {
showLineNumbers: false,
@@ -21,12 +25,17 @@ function Editor(props: {
return (
<AceEditor
ref={props.editorRef}
style={{}}
mode={props.editorMode}
theme={props.theme.palette.type === 'dark' ? 'monokai' : 'dawn'}
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
enableSnippets={true}
enableBasicAutocompletion={true}
enableLiveAutocompletion={true}
showPrintMargin={false}
showGutter={true}
value={props.value}
onChange={props.onChange}
@@ -4,6 +4,7 @@ import { FormControlLabel, Radio, RadioGroup } from '@material-ui/core'
interface Props {
value: string
onChange: (event: React.ChangeEvent<{}>, value: string) => void
focusEditor: () => void
}
export function EditorModeSelect(props: Props) {
const labelStyle = { margin: '0 8px 0 8px' }
@@ -11,6 +12,7 @@ export function EditorModeSelect(props: Props) {
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={props.value}
onFocus={props.focusEditor}
onChange={props.onChange}
row={true}
>
+109 -75
View File
@@ -3,7 +3,8 @@ import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import PublishHistory from './PublishHistory'
import React, { useCallback, useState, useMemo } from 'react'
import React, { useCallback, useMemo, useState, useRef, memo } from 'react'
import RetainSwitch from './RetainSwitch'
import TopicInput from './TopicInput'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
@@ -12,7 +13,7 @@ import { connect } from 'react-redux'
import { EditorModeSelect } from './EditorModeSelect'
import { globalActions, publishActions } from '../../../actions'
import { KeyCodes } from '../../../utils/KeyCodes'
import RetainSwitch from './RetainSwitch'
import { default as AceEditor } from 'react-ace'
interface Props {
connectionId?: string
@@ -41,13 +42,12 @@ function useHistory(): [Array<Message>, (topic: string, payload?: string) => voi
}
function Publish(props: Props) {
console.log(props.connectionId)
const updatePayload = props.actions.setPayload
const editorRef = useRef<AceEditor>()
const [history, amendToHistory] = useHistory()
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
props.actions.setEditorMode(value)
}, [])
const focusEditor = useCallback(() => {
editorRef.current?.editor.focus()
}, [editorRef])
const publish = useCallback(() => {
if (!props.connectionId) {
@@ -63,65 +63,6 @@ function Publish(props: Props) {
}
}, [props, props.connectionId, props.topic, props.payload, amendToHistory])
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
publish()
},
[publish]
)
const PublishButton = () => {
return (
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
}
const formatJson = () => {
if (props.payload) {
try {
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str)
} catch (error) {
props.globalActions.showError(`Format error: ${error.message}`)
}
}
}
const renderFormatJson = () => {
if (props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
onClick={formatJson}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
}
function EditorMode() {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
{renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>
<PublishButton />
</div>
</div>
</div>
)
}
const handleSubmit = useCallback(
(e: React.KeyboardEvent) => {
if (e.keyCode === KeyCodes.enter && (e.metaKey || e.ctrlKey)) {
@@ -135,20 +76,116 @@ function Publish(props: Props) {
return useMemo(
() => (
<div style={{ flexGrow: 1, marginLeft: '8px' }} onKeyDown={handleSubmit}>
<div style={{ flexGrow: 1, width: '100%' }} onKeyDown={handleSubmit}>
<TopicInput />
<div style={{ width: '100%', display: 'block' }}>
<EditorMode />
<Editor value={props.payload} editorMode={props.editorMode} onChange={updatePayload} />
<EditorMode
focusEditor={focusEditor}
actions={props.actions}
globalActions={props.globalActions}
payload={props.payload}
editorMode={props.editorMode}
publish={publish}
/>
<Editor
value={props.payload}
editorMode={props.editorMode}
onChange={props.actions.setPayload}
editorRef={editorRef as any}
/>
<RetainSwitch />
</div>
<PublishHistory history={history} />
</div>
),
[props.payload, props.editorMode, history, handleSubmit, updatePayload]
[props.payload, props.editorMode, history, handleSubmit, publish]
)
}
const EditorMode = memo(function EditorMode(props: {
payload?: string
editorMode: string
focusEditor: () => void
actions: typeof publishActions
globalActions: typeof globalActions
publish: () => void
}) {
const updatePayload = props.actions.setPayload
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
props.actions.setEditorMode(value)
}, [])
const formatJson = useCallback(() => {
if (props.payload) {
try {
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str)
} catch (error) {
props.globalActions.showError(`Format error: ${error.message}`)
}
}
}, [props.payload])
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} focusEditor={props.focusEditor} />
<FormatJsonButton editorMode={props.editorMode} focusEditor={props.focusEditor} formatJson={formatJson} />
<div style={{ float: 'right' }}>
<PublishButton publish={props.publish} focusEditor={props.focusEditor} />
</div>
</div>
</div>
)
})
const FormatJsonButton = React.memo(function FormatJsonButton(props: {
editorMode: string
focusEditor: () => void
formatJson: () => void
}) {
if (props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', margin: '0 8px' }}
onClick={props.formatJson}
onFocus={props.focusEditor}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
})
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
props.publish()
},
[props.publish]
)
return (
<Button
variant="contained"
size="small"
color="primary"
onClick={handleClickPublish}
onFocus={props.focusEditor}
id="publish-button"
>
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
})
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(publishActions, dispatch),
@@ -158,14 +195,11 @@ const mapDispatchToProps = (dispatch: any) => {
const mapStateToProps = (state: AppState) => {
return {
topic: state.publish.topic,
topic: state.publish.manualTopic,
payload: state.publish.payload,
editorMode: state.publish.editorMode,
retain: state.publish.retain,
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTheme(Publish))
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(Publish))
@@ -24,7 +24,7 @@ function PublishHistory(props: { history: Array<Message>; actions: typeof publis
value: message.payload || '',
}))
return <History items={items} onClick={didSelectHistoryEntry} />
return <History autoOpen={true} items={items} onClick={didSelectHistoryEntry} />
}, [props.history])
}
@@ -34,7 +34,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(PublishHistory)
export default connect(undefined, mapDispatchToProps)(PublishHistory)
@@ -0,0 +1,34 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { publishActions } from '../../../actions'
import { QosSelect } from '../../QosSelect'
import { QoS } from '../../../../../backend/src/DataSource/MqttSource'
interface Props {
qos: QoS
actions: {
publish: typeof publishActions
}
}
function QosPublishOption(props: Props) {
return <QosSelect onChange={props.actions.publish.setQoS} selected={props.qos} />
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
publish: bindActionCreators(publishActions, dispatch),
},
}
}
const mapStateToProps = (state: AppState) => {
return {
qos: state.publish.qos,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(QosPublishOption)
@@ -1,4 +1,4 @@
import QosSelect from './QosSelect'
import QosSelect from './QosPublishOption'
import React from 'react'
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
import { publishActions } from '../../../actions'
@@ -41,7 +41,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(RetainSwitch)
export default connect(mapStateToProps, mapDispatchToProps)(RetainSwitch)
@@ -1,19 +1,21 @@
import ClearAdornment from '../../helper/ClearAdornment'
import React, { useCallback } from 'react'
import React, { useCallback, useMemo, useRef } from 'react'
import { FormControl, Input, InputLabel } from '@material-ui/core'
import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
console.log(props.topic)
function TopicInput(props: { actions: typeof publishActions; manualTopic?: string; selectedTopic?: string }) {
const inputElement = useRef<HTMLInputElement>(null)
const updateTopic = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
props.actions.setTopic(e.target.value)
}, [])
const clearTopic = useCallback(() => {
props.actions.setTopic('')
inputElement.current?.focus()
}, [])
const onTopicBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
@@ -22,23 +24,28 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
}
}, [])
const topicStr = props.topic || ''
return (
<div>
<FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
<Input
id="publish-topic"
value={topicStr}
startAdornment={<span />}
endAdornment={<ClearAdornment action={clearTopic} value={topicStr} />}
onBlur={onTopicBlur}
onChange={updateTopic}
multiline={true}
placeholder="example/topic"
/>
</FormControl>
</div>
const topicStr = props.manualTopic || ''
return useMemo(
() => (
<div>
<FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
<Input
inputRef={inputElement}
id="publish-topic"
value={topicStr}
startAdornment={<span />}
endAdornment={<ClearAdornment action={clearTopic} value={topicStr} />}
onBlur={onTopicBlur}
onChange={updateTopic}
multiline={false}
placeholder={props.selectedTopic ?? 'example/topic'}
/>
</FormControl>
</div>
),
[topicStr]
)
}
@@ -50,11 +57,9 @@ const mapDispatchToProps = (dispatch: any) => {
const mapStateToProps = (state: AppState) => {
return {
topic: state.publish.topic,
manualTopic: state.publish.manualTopic,
selectedTopic: state.tree.get('selectedTopic')?.path(),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TopicInput)
export default connect(mapStateToProps, mapDispatchToProps)(TopicInput)
+22 -25
View File
@@ -12,23 +12,23 @@ import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import TopicPanel from './TopicPanel/TopicPanel'
import Panel from './Panel'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
const throttle = require('lodash.throttle')
const Publish = React.lazy(() => import('./Publish/Publish'))
interface Props {
node?: q.TreeNode<TopicViewModel>
nodePath?: string
tree?: q.Tree<TopicViewModel>
actions: typeof sidebarActions
settingsActions: typeof settingsActions
classes: any
connectionId?: string
}
function Sidebar(props: Props) {
const { classes, node } = props
function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
const [lastUpdate, setLastUpdate] = useState(0)
const updateNode = useCallback(
throttle(() => {
setLastUpdate(node ? node.lastUpdate : 0)
@@ -46,21 +46,26 @@ function Sidebar(props: Props) {
node && node.onMessage.unsubscribe(updateCallback)
}
}, [node])
}
function Sidebar(props: Props) {
const { classes, tree, nodePath } = props
const node = usePollingToFetchTreeNode(tree, nodePath || '')
useUpdateNodeWhenNodeReceivesUpdates(node)
// console.log(node && node.path(), tree, nodePath)
return (
<div id="Sidebar" className={props.classes.drawer}>
<div id="Sidebar" className={classes.drawer}>
<div>
<TopicPanel node={node} updateNode={updateNode} />
<TopicPanel node={node} />
<ValuePanel lastUpdate={node ? node.lastUpdate : 0} />
<Panel>
<span>Publish</span>
<React.Suspense fallback={<div>Loading...</div>}>
<Publish connectionId={props.connectionId} />
</React.Suspense>
<Publish connectionId={props.connectionId} />
</Panel>
<Panel detailsHidden={!node}>
<span>Stats</span>
<ExpansionPanelDetails className={props.classes.details}>
<ExpansionPanelDetails className={classes.details}>
<NodeStats node={node} />
</ExpansionPanelDetails>
</Panel>
@@ -70,8 +75,10 @@ function Sidebar(props: Props) {
}
const mapStateToProps = (state: AppState) => {
const node = state.tree.get('selectedTopic')
return {
node: state.tree.get('selectedTopic'),
tree: state.connection.tree,
nodePath: node && node.path(),
}
}
@@ -86,20 +93,10 @@ const styles = (theme: Theme) => ({
drawer: {
display: 'block' as 'block',
},
valuePaper: {
margin: theme.spacing(1),
},
summary: { minHeight: '0' },
details: { padding: '0px 16px 8px 8px', display: 'block' },
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
details: {
padding: '0px 16px 8px 8px',
display: 'block',
},
})
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
@@ -8,30 +8,29 @@ export const RecursiveTopicDeleteButton = (props: {
node?: q.TreeNode<any>
deleteTopicAction: (node: q.TreeNode<any>, a: boolean, limit: number) => void
}) => {
const onClick = useCallback(() => {
if (props.node) {
props.deleteTopicAction(props.node, true, deleteLimit)
}
}, [props.node])
const onClick = useCallback(
(event: React.MouseEvent) => {
if (props.node) {
event.stopPropagation()
event.preventDefault()
props.deleteTopicAction(props.node, true, Infinity)
}
},
[props.node]
)
if (!props.node) {
return null
}
const deleteLimit = 50
const topicCount = props.node ? props.node.childTopicCount() : 0
if (topicCount === 0 || (props.node.message && topicCount === 1)) {
return null
}
return (
<Badge
style={{
top: '3px',
right: '3px',
}}
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
color="secondary"
>
<CustomIconButton onClick={onClick} tooltip={`Deletes up to ${deleteLimit} sub-topics with a single click`}>
<Delete style={{ marginTop: '-3px' }} color="action" />
<Badge badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount}</span>} color="secondary">
<CustomIconButton onClick={onClick} tooltip={`Deletes ${topicCount} sub-topics with a single click`}>
<Delete color="action" />
</CustomIconButton>
</Badge>
)
@@ -13,7 +13,6 @@ interface Props {
node?: q.TreeNode<TopicViewModel>
selected?: q.TreeNode<TopicViewModel>
actions: typeof treeActions
didSelectNode: (node: q.TreeNode<TopicViewModel>) => void
}
const styles = (theme: Theme) => ({
@@ -24,7 +23,7 @@ const styles = (theme: Theme) => ({
},
})
class Topic extends React.Component<Props, {}> {
class Topic extends React.PureComponent<Props, {}> {
public render() {
const { node, theme } = this.props
if (!node) {
@@ -67,7 +66,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
null,
mapDispatchToProps
)(withStyles(styles, { withTheme: true })(Topic))
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic))
@@ -8,12 +8,12 @@ export const TopicDeleteButton = (props: {
deleteTopicAction: (node: q.TreeNode<any>) => void
}) => {
const { node } = props
if (!node || !node.message || !node.message.value) {
if (!node || !node.message || !node.message.payload) {
return null
}
return (
<CustomIconButton onClick={() => props.deleteTopicAction(node)} tooltip="Clear this topic">
<Delete style={{ marginTop: '-3px' }} />
<Delete />
</CustomIconButton>
)
}
@@ -1,7 +1,7 @@
import * as q from '../../../../../backend/src/Model'
import Copy from '../../helper/Copy'
import Panel from '../Panel'
import React, { useMemo } from 'react'
import React, { useMemo, useCallback } from 'react'
import Topic from './Topic'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
@@ -9,17 +9,18 @@ import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
import { sidebarActions } from '../../../actions'
import { TopicDeleteButton } from './TopicDeleteButton'
const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions; updateNode: () => void }) => {
const { node, updateNode } = props
const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions }) => {
const { node } = props
console.log(node && node.path())
const copyTopic = node ? <Copy value={node.path()} /> : null
const deleteTopic = (topic?: q.TreeNode<any>, recursive: boolean = false, maxCount = 50) => {
const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
if (!topic) {
return
}
props.actions.clearTopic(topic, recursive, maxCount)
}
props.actions.clearTopic(topic, recursive)
}, [])
return useMemo(
() => (
@@ -29,7 +30,7 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
</span>
<Topic node={node} didSelectNode={updateNode} />
<Topic node={node} />
</Panel>
),
[node, node && node.childTopicCount()]
@@ -42,7 +43,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(TopicPanel)
export default connect(undefined, mapDispatchToProps)(TopicPanel)
@@ -0,0 +1,76 @@
import React, { useCallback } from 'react'
import Code from '@material-ui/icons/Code'
import Reorder from '@material-ui/icons/Reorder'
import ToggleButton from '@material-ui/lab/ToggleButton'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import { settingsActions } from '../../../actions'
import { Tooltip, withStyles, Theme } from '@material-ui/core'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
function ActionButtons(props: {
actions: { settings: typeof settingsActions }
valueRendererDisplayMode: ValueRendererDisplayMode
classes: any
}) {
const handleValue = useCallback(
(mouseEvent: React.MouseEvent, value: any) => {
if (value === null) {
return
}
props.actions.settings.setValueDisplayMode(value)
},
[props.actions.settings.setValueDisplayMode]
)
return (
<ToggleButtonGroup
id="valueRendererDisplayMode"
value={props.valueRendererDisplayMode}
exclusive={true}
onChange={handleValue}
>
<ToggleButton className={props.classes.toggleButton} value="diff" id="valueRendererDisplayMode-diff">
<Tooltip title="Show difference between the current and the last message">
<span>
<Code className={props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
<ToggleButton className={props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw / formatted JSON">
<span>
<Reorder className={props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
)
}
const styles = (theme: Theme) => ({
toggleButton: {
height: '36px',
},
toggleButtonIcon: {
verticalAlign: 'middle',
},
})
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
settings: bindActionCreators(settingsActions, dispatch),
},
}
}
const mapStateToProps = (state: AppState) => {
return {
valueRendererDisplayMode: state.settings.get('valueRendererDisplayMode'),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons))
@@ -0,0 +1,36 @@
import Clear from '@material-ui/icons/Clear'
import React, { useMemo } from 'react'
import { bindActionCreators } from 'redux'
import { Button, Tooltip } from '@material-ui/core'
import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions'
const DeleteSelectedTopicButton = (props: {
actions: {
sidebar: typeof sidebarActions
}
}) =>
useMemo(
() => (
<Tooltip title="Delete retained topic" placement="top">
<Button
size="small"
color="secondary"
variant="contained"
style={{ marginTop: '11px', padding: '0px 4px', minHeight: '24px' }}
onClick={props.actions.sidebar.clearRetainedTopic}
>
retained <Clear style={{ fontSize: '16px', marginLeft: '2px' }} />
</Button>
</Tooltip>
),
[props.actions.sidebar.clearRetainedTopic]
)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: { sidebar: bindActionCreators(sidebarActions, dispatch) },
}
}
export default connect(undefined, mapDispatchToProps)(DeleteSelectedTopicButton)
@@ -12,6 +12,7 @@ import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { connect } from 'react-redux'
import CustomIconButton from '../../helper/CustomIconButton'
import { MessageId } from '../MessageId'
const throttle = require('lodash.throttle')
@@ -27,16 +28,17 @@ interface Props {
interface State {
displayMessage?: q.Message
anchorEl?: HTMLElement
lastUpdate: number
}
class MessageHistory extends React.Component<Props, State> {
class MessageHistory extends React.PureComponent<Props, State> {
private updateNode = throttle(() => {
this.setState(this.state)
this.setState({ lastUpdate: Date.now() })
}, 300)
constructor(props: any) {
super(props)
this.state = {}
this.state = { lastUpdate: 0 }
}
private addNodeToCharts = (event: React.MouseEvent) => {
@@ -80,19 +82,25 @@ class MessageHistory extends React.Component<Props, State> {
const history = node.messageHistory.toArray()
let previousMessage: q.Message | undefined = node.message
const historyElements = [...history].reverse().map((message, idx) => {
const value = message.value ? Base64Message.toUnicodeString(message.value) : ''
const value = message.payload ? Base64Message.toUnicodeString(message.payload) : ''
const element = {
value,
key: `${message.messageNumber}-${message.received}`,
title: (
<span>
<DateFormatter date={message.received} />
{previousMessage && previousMessage !== message ? (
<i>
(-
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
</i>
) : null}
<div style={{ float: 'left' }}>
<DateFormatter date={message.received} />
{previousMessage && previousMessage !== message ? (
<i>
(-
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
</i>
) : null}
</div>
<span>
&nbsp;
<MessageId message={message} />
</span>
<div style={{ float: 'right' }}>
<Copy value={value} />
</div>
@@ -105,7 +113,7 @@ class MessageHistory extends React.Component<Props, State> {
})
const isMessagePlottable =
node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload))
return (
<div>
<History
@@ -119,9 +127,7 @@ class MessageHistory extends React.Component<Props, State> {
>
<ShowChart style={{ marginTop: '-5px' }} />
</CustomIconButton>
) : (
undefined
)
) : undefined
}
onClick={this.displayMessage}
>
@@ -138,7 +144,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
null,
mapDispatchToProps
)(MessageHistory)
export default connect(null, mapDispatchToProps)(MessageHistory)
@@ -1,196 +1,121 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import Code from '@material-ui/icons/Code'
import ActionButtons from './ActionButtons'
import Copy from '../../helper/Copy'
import DateFormatter from '../../helper/DateFormatter'
import ExpandMore from '@material-ui/icons/ExpandMore'
import MessageHistory from './MessageHistory'
import Reorder from '@material-ui/icons/Reorder'
import ToggleButton from '@material-ui/lab/ToggleButton'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import Panel from '../Panel'
import React, { useCallback } from 'react'
import ValueRenderer from './ValueRenderer'
import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { bindActionCreators } from 'redux'
import { Theme, Typography, withStyles } from '@material-ui/core'
import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../../actions'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import {
Button,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Tooltip,
Typography,
StyleRulesCallback,
withStyles,
Theme,
} from '@material-ui/core'
import { sidebarActions } from '../../../actions'
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
import { MessageId } from '../MessageId'
interface Props {
node?: q.TreeNode<any>
valueRendererDisplayMode: ValueRendererDisplayMode
sidebarActions: typeof sidebarActions
settingsActions: typeof settingsActions
classes: any
lastUpdate: number
compareMessage?: q.Message
}
interface State {}
function RenderedValue(props: { node?: q.TreeNode<any>; compareMessage?: q.Message }) {
const { node, compareMessage } = props
class ValuePanel extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {}
if (!node || !node.message) {
return null
}
private renderValue() {
const node = this.props.node
if (!node || !node.message) {
return <ValueRenderer treeNode={node} message={node.message} compareWith={compareMessage} />
}
function ValuePanel(props: Props) {
const { node, compareMessage } = props
function renderViewOptions() {
if (!props.node || !props.node.message) {
return null
}
return <ValueRenderer treeNode={node} message={node.message} compareWith={this.props.compareMessage} />
}
private renderViewOptions() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
return null
}
const retainedButton = (
<Tooltip title="Delete retained topic" placement="top">
<Button
size="small"
color="secondary"
variant="contained"
style={{ marginTop: '11px', padding: '0px 4px', minHeight: '24px' }}
onClick={this.props.sidebarActions.clearRetainedTopic}
>
retained <Clear style={{ fontSize: '16px', marginLeft: '2px' }} />
</Button>
</Tooltip>
)
return (
<div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}>
<span style={{ marginTop: '2px', flexGrow: 1 }}>{this.renderActionButtons()}</span>
<div style={{ flex: 6, textAlign: 'right' }}>{this.props.node.mqttMessage.retain ? retainedButton : null}</div>
{this.messageMetaInfo()}
<div style={{ width: '100%', display: 'flex', flexWrap: 'wrap' }}>
<span style={{ marginTop: '2px', flexGrow: 1 }}>
<ActionButtons />
</span>
<span style={{ flex: 6, textAlign: 'right' }}>
{props.node.message.retain ? <DeleteSelectedTopicButton /> : null}
</span>
{messageMetaInfo()}
</div>
)
}
private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
function messageMetaInfo() {
if (!props.node || !props.node.message) {
return null
}
return (
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
<Typography style={{ textAlign: 'right' }}>QoS: {this.props.node.mqttMessage.qos}</Typography>
<Typography style={{ textAlign: 'right' }}>
<MessageId message={props.node.message} addComma={true} />
{`QoS: ${props.node.message.qos}`}
</Typography>
<Typography style={{ textAlign: 'right' }}>
<i>
<DateFormatter date={this.props.node.message.received} />
<DateFormatter date={props.node.message.received} />
</i>
</Typography>
</span>
)
}
private renderActionButtons() {
const handleValue = (mouseEvent: React.MouseEvent, value: any) => {
if (value === null) {
return
const handleMessageHistorySelect = useCallback(
(message: q.Message) => {
if (message !== compareMessage) {
props.sidebarActions.setCompareMessage(message)
} else {
props.sidebarActions.setCompareMessage(undefined)
}
this.props.settingsActions.setValueDisplayMode(value)
}
},
[compareMessage]
)
return (
<ToggleButtonGroup
id="valueRendererDisplayMode"
value={this.props.valueRendererDisplayMode}
exclusive={true}
onChange={handleValue}
>
<ToggleButton className={this.props.classes.toggleButton} value="diff" id="valueRendererDisplayMode-diff">
<Tooltip title="Show difference between the current and the last message">
<span>
<Code className={this.props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
<ToggleButton className={this.props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw value">
<span>
<Reorder className={this.props.classes.toggleButtonIcon} />
</span>
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
)
}
const copyValue =
node && node.message && node.message.payload ? (
<Copy value={Base64Message.toUnicodeString(node.message.payload)} />
) : null
private panelStyle() {
return {
detailsStyle: { padding: '0px 16px 8px 8px', display: 'block' },
summaryStyle: { minHeight: '0' },
}
}
private handleMessageHistorySelect = (message: q.Message) => {
if (message !== this.props.compareMessage) {
this.props.sidebarActions.setCompareMessage(message)
} else {
this.props.sidebarActions.setCompareMessage(undefined)
}
}
public render() {
const { node, classes } = this.props
const { detailsStyle, summaryStyle } = this.panelStyle()
const copyValue =
node && node.message && node.message.value ? (
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
) : null
return (
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={detailsStyle}>
{this.renderViewOptions()}
<div style={{ marginBottom: '-8px' }}>
<React.Suspense fallback={<div>Loading...</div>}>{this.renderValue()}</React.Suspense>
</div>
<div>
<MessageHistory
onSelect={this.handleMessageHistorySelect}
selected={this.props.compareMessage}
node={this.props.node}
/>
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
)
}
return (
<Panel>
<span>Value {copyValue}</span>
<span style={{ width: '100%' }}>
{renderViewOptions()}
<div style={{ marginBottom: '-8px', marginTop: '8px' }}>
<React.Suspense fallback={<div>Loading...</div>}>
<RenderedValue node={node} compareMessage={compareMessage} />
</React.Suspense>
</div>
<div>
<MessageHistory onSelect={handleMessageHistorySelect} selected={props.compareMessage} node={props.node} />
</div>
</span>
</Panel>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
sidebarActions: bindActionCreators(sidebarActions, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
valueRendererDisplayMode: state.settings.get('valueRendererDisplayMode'),
node: state.tree.get('selectedTopic'),
compareMessage: state.sidebar.get('compareMessage'),
}
@@ -201,15 +126,6 @@ const styles = (theme: Theme) => ({
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
toggleButton: {
height: '36px',
},
toggleButtonIcon: {
verticalAlign: 'middle',
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ValuePanel))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
@@ -6,6 +6,7 @@ import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import { Typography, Fade, Grow } from '@material-ui/core'
interface Props {
message: q.Message
@@ -24,18 +25,34 @@ class ValueRenderer extends React.Component<Props, State> {
this.state = { width: 0 }
}
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
private renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') {
return (
<CodeDiff
treeNode={this.props.treeNode}
previous={previous}
current={current}
title={title}
language={language}
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
/>
)
}
private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] {
if (!msg) {
return [undefined, undefined]
}
const str = Base64Message.toUnicodeString(msg)
try {
JSON.parse(str)
} catch (error) {
return [str, undefined]
}
return [this.messageToPrettyJson(str), 'json']
}
private messageToPrettyJson(str: string): string | undefined {
try {
const json = JSON.parse(str)
@@ -45,61 +62,50 @@ class ValueRenderer extends React.Component<Props, State> {
}
}
private updateWidth = (width: number) => {
if (width !== this.state.width) {
this.setState({ width })
private renderRawMode(message: q.Message, compare?: q.Message) {
if (!message.payload) {
return
}
}
const [value, valueLanguage] = this.convertMessage(message.payload)
const [compareStr, compareStrLanguage] =
compare && compare.payload ? this.convertMessage(compare.payload) : [undefined, undefined]
private renderRawValue(value: string, compare: string) {
return this.renderDiff(value, compare)
return (
<div>
{this.renderDiff(value, value, undefined, valueLanguage)}
<Fade in={Boolean(compareStr)} timeout={400}>
<div>
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null}
</div>
</Fade>
</div>
)
}
public render() {
return (
<div style={{ padding: '0px 0px 8px 8px' }}>
<ReactResizeDetector handleWidth={true} onResize={this.updateWidth} />
{this.renderValue()}
</div>
)
return <div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>{this.renderValue()}</div>
}
public renderValue() {
const { message, treeNode, compareWith, renderMode } = this.props
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
const compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {
compareMessage = message
return this.renderRawMode(message, compareWith)
}
if (!message.value) {
if (!message.payload) {
return null
}
const compareValue = compareMessage.value || message.value
const str = Base64Message.toUnicodeString(message.value)
const compareStr = Base64Message.toUnicodeString(compareValue)
const compareValue = compareMessage.payload || message.payload
const [current, currentLanguage] = this.convertMessage(message.payload)
const [compare, compareLanguage] = this.convertMessage(compareValue)
let json
try {
json = JSON.parse(str)
} catch (error) {
return this.renderRawValue(str, compareStr)
}
const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined
if (typeof json === 'string') {
return this.renderRawValue(str, compareStr)
} else if (typeof json === 'number') {
return this.renderRawValue(str, compareStr)
} else if (typeof json === 'boolean') {
return this.renderRawValue(str, compareStr)
} else {
const current = this.messageToPrettyJson(str) || str
const compare = this.messageToPrettyJson(compareStr) || compareStr
const language = current && compare ? 'json' : undefined
return this.renderDiff(current, compare, language)
}
return this.renderDiff(current, compare, undefined, language)
}
}
+13 -9
View File
@@ -1,7 +1,7 @@
import * as dotProp from 'dot-prop'
import * as q from '../../../backend/src/Model'
import * as React from 'react'
import PlotHistory from './Sidebar/PlotHistory'
import PlotHistory from './Chart/Chart'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './Sidebar/CodeDiff/util'
import { PlotCurveTypes } from '../reducers/Charts'
@@ -28,7 +28,7 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN
return { x: message.received.getTime(), y: toPlottableValue(value) }
})
.filter(data => !isNaN(data.y as any)) as any
@@ -39,21 +39,25 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
.map((message: q.Message) => {
let json = {}
try {
json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {}
} catch (ignore) {}
let value = dotProp.get(json, dotPath)
const value = dotProp.get(json, dotPath)
return { x: message.received.getTime(), y: toPlottableValue(value) }
})
.filter(data => !isNaN(data.y as any)) as any
}
function render(props: Props) {
function TopicPlot(props: Props) {
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
const data = props.dotPath
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
: nodeToHistory(startOffset, props.history)
const data = React.useMemo(
() =>
props.dotPath
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
: nodeToHistory(startOffset, props.history),
[props.history.last(), startOffset, props.dotPath]
)
return (
<PlotHistory
@@ -66,4 +70,4 @@ function render(props: Props) {
)
}
export default render
export default TopicPlot
@@ -29,7 +29,7 @@ function useStagedRendering(treeNode: q.TreeNode<any>) {
if (alreadyAdded < edges.length) {
renderMoreAnimationFrame = (window as any).requestIdleCallback(
() => {
setAlreadyAdded(alreadyAdded * 1.5)
setAlreadyAdded(Math.max(25, alreadyAdded * 1.5))
},
{ timeout: 500 }
)
@@ -53,7 +53,6 @@ function TreeNodeSubnodes(props: Props) {
<TreeNode
key={`${node.hash()}-${props.filter}`}
treeNode={node}
className={props.classes.listItem}
lastUpdate={node.lastUpdate}
selectTopicAction={props.selectTopicAction}
settings={props.settings}
@@ -63,7 +62,7 @@ function TreeNodeSubnodes(props: Props) {
})
return <span className={props.classes.list}>{listItems}</span>
}, [alreadyAdded, props.treeNode.lastUpdate, props.theme])
}, [alreadyAdded, props.treeNode.lastUpdate, props.theme, props.settings])
}
const styles = (theme: Theme) => ({
@@ -1,11 +1,12 @@
import * as React from 'react'
import * as q from '../../../../../backend/src/Model'
import { withStyles, Theme } from '@material-ui/core'
import { TopicViewModel } from '../../../model/TopicViewModel'
import React, { memo } from 'react'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { Theme, withStyles } from '@material-ui/core'
import { TopicViewModel } from '../../../model/TopicViewModel'
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode<TopicViewModel>
lastUpdate: number
name?: string | undefined
collapsed?: boolean | undefined
didSelectNode: any
@@ -13,7 +14,7 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
classes: any
}
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
private renderSourceEdge() {
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
@@ -24,13 +25,23 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
)
}
private truncatedMessage() {
const limit = 400
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) {
return ''
}
const str = Base64Message.toUnicodeString(this.props.treeNode.message.payload)
return str.length > limit ? `${str.slice(0, limit)}` : str
}
private renderValue() {
return this.props.treeNode.message &&
this.props.treeNode.message.value &&
this.props.treeNode.message.payload &&
this.props.treeNode.message.length > 0 ? (
<span key="value" className={this.props.classes.value}>
{' '}
= {Base64Message.toUnicodeString(this.props.treeNode.message.value).slice(0, 120)}
= {this.truncatedMessage()}
</span>
) : null
}
@@ -53,11 +64,11 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
}
const messages = this.props.treeNode.leafMessageCount()
const topicCount = this.props.treeNode.childTopicCount()
return (
<span
key="metadata"
className={this.props.classes.collapsedSubnodes}
>{`(${this.props.treeNode.childTopicCount()} topics, ${messages} messages)`}</span>
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${
topicCount === 1 ? 'topic' : 'topics'
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span>
)
}
@@ -89,4 +100,4 @@ const styles = (theme: Theme) => ({
},
})
export default withStyles(styles)(TreeNodeTitle)
export default withStyles(styles)(memo(TreeNodeTitle))
@@ -1,14 +1,21 @@
import React, { useEffect } from 'react'
const inViewport = require('in-viewport')
export function useAnimationToIndicateTopicUpdate(
ref: React.MutableRefObject<HTMLDivElement | undefined>,
lastUpdate: number,
className: string,
selected: boolean,
selectionLastUpdate: number,
shouldAnimate: boolean
) {
useEffect(() => {
if (ref.current && shouldAnimate && Date.now() - lastUpdate < 3000 && !selected) {
const selectionDidChange = Date.now() - selectionLastUpdate < 500
if (ref.current && shouldAnimate && Date.now() - lastUpdate < 3000 && !selected && !selectionDidChange) {
if (!inViewport(ref.current)) {
return
}
let timeout: any
let animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.add(className)
@@ -27,5 +34,5 @@ export function useAnimationToIndicateTopicUpdate(
animationFrame && cancelAnimationFrame(animationFrame)
}
}
}, [lastUpdate, selected, shouldAnimate, className])
}, [lastUpdate, selected, selectionLastUpdate, shouldAnimate, className])
}
@@ -6,11 +6,10 @@ import { treeActions } from '../../../../actions'
export function useDeleteKeyCallback(topic: q.TreeNode<any>, actions: typeof treeActions) {
return useCallback(
(event: React.KeyboardEvent) => {
return
if (event.keyCode === KeyCodes.delete || event.keyCode === KeyCodes.backspace) {
event.stopPropagation()
event.preventDefault()
actions.clearTopic(topic, true, 50)
actions.clearTopic(topic, true)
}
},
[topic]
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
export function useIsAllowedToAutoExpandState(props: Props): boolean {
const { settings, treeNode, isRoot } = props
const [isAllowedToAutoExpand, setAllowAutoExpand] = useState(false)
useEffect(() => {
const newIsAllowedToAutoExpand = isRoot || treeNode.edgeCount() <= settings.get('autoExpandLimit')
if (newIsAllowedToAutoExpand !== isAllowedToAutoExpand) {
@@ -0,0 +1,14 @@
import { useCallback, useState } from 'react'
interface SelectedState {
selected: boolean
lastUpdate: number
}
export function useSelectionState(initialSelectionState: boolean): [boolean, number, (selected: boolean) => void] {
const [state, setState] = useState<SelectedState>({ selected: initialSelectionState, lastUpdate: 0 })
const setSelected = useCallback((selected: boolean) => {
setState({ selected, lastUpdate: Date.now() })
}, [])
return [state.selected, state.lastUpdate, setSelected]
}
@@ -8,37 +8,37 @@ export function useViewModelSubscriptions(
setSelected: (value: boolean) => void,
setCollapsedOverride: (value: boolean) => void
) {
const selectionDidChange = () => {
const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
treeNode.viewModel && setSelected(Boolean(selected))
if (selected && nodeRef && nodeRef.current) {
nodeRef.current.focus({ preventScroll: false })
}
}
const expandedDidChange = () => {
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded())
}
useEffect(() => {
const selectionDidChange = () => {
const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
treeNode.viewModel && setSelected(Boolean(selected))
if (selected && nodeRef && nodeRef.current) {
nodeRef.current.focus({ preventScroll: false })
}
}
const expandedDidChange = () => {
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded())
}
function addSubscriber() {
treeNode.viewModel = new TopicViewModel()
treeNode.viewModel.selectionChange.subscribe(selectionDidChange)
treeNode.viewModel.expandedChange.subscribe(expandedDidChange)
}
function removeSubscriber() {
if (treeNode.viewModel) {
treeNode.viewModel.selectionChange.unsubscribe(selectionDidChange)
treeNode.viewModel.expandedChange.unsubscribe(expandedDidChange)
treeNode.viewModel = undefined
}
}
addSubscriber()
return function cleanup() {
removeSubscriber()
}
}, [treeNode])
function addSubscriber() {
treeNode.viewModel = new TopicViewModel()
treeNode.viewModel.selectionChange.subscribe(selectionDidChange)
treeNode.viewModel.expandedChange.subscribe(expandedDidChange)
}
function removeSubscriber() {
if (treeNode.viewModel) {
treeNode.viewModel.selectionChange.unsubscribe(selectionDidChange)
treeNode.viewModel.expandedChange.unsubscribe(expandedDidChange)
treeNode.viewModel = undefined
}
}
}
+18 -20
View File
@@ -11,6 +11,7 @@ import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndic
import { useDeleteKeyCallback } from './effects/useDeleteKeyCallback'
import { useIsAllowedToAutoExpandState } from './effects/useIsAllowedToAutoExpandState'
import { useViewModelSubscriptions } from './effects/useViewModelSubscriptions'
import { useSelectionState } from './effects/useSelectionState'
export interface Props {
isRoot?: boolean
@@ -18,7 +19,6 @@ export interface Props {
name?: string | undefined
collapsed?: boolean | undefined
classes: any
className?: string
lastUpdate: number
actions: typeof treeActions
selectTopicAction: (treeNode: q.TreeNode<any>) => void
@@ -27,9 +27,9 @@ export interface Props {
}
function TreeNodeComponent(props: Props) {
const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
const { actions, classes, settings, theme, treeNode, lastUpdate, name } = props
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
const [selected, setSelected] = useState(false)
const [selected, selectionLastUpdate, setSelected] = useSelectionState(false)
const nodeRef = useRef<HTMLDivElement>()
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
@@ -42,16 +42,13 @@ function TreeNodeComponent(props: Props) {
lastUpdate,
animationClass,
selected,
selectionLastUpdate,
settings.get('highlightTopicUpdates')
)
const isCollapsed =
Boolean(collapsedOverride) === collapsedOverride ? Boolean(collapsedOverride) : !isAllowedToAutoExpand
const toggle = useCallback(() => {
setCollapsedOverride(!isCollapsed)
}, [isCollapsed])
const didSelectTopic = useCallback(
(event?: React.MouseEvent) => {
event && event.stopPropagation()
@@ -64,17 +61,17 @@ function TreeNodeComponent(props: Props) {
(event: React.MouseEvent) => {
event.stopPropagation()
didSelectTopic()
toggle()
setCollapsedOverride(!isCollapsed)
},
[toggle, didSelectTopic]
[isCollapsed, didSelectTopic]
)
const toggleCollapsed = useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
toggle()
setCollapsedOverride(!isCollapsed)
},
[toggle]
[isCollapsed]
)
const didObtainFocus = useCallback(() => {
@@ -84,11 +81,11 @@ function TreeNodeComponent(props: Props) {
const mouseOver = useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.payload) {
didSelectTopic()
}
},
[didSelectTopic]
[didSelectTopic, settings]
)
useEffect(() => {
@@ -114,29 +111,30 @@ function TreeNodeComponent(props: Props) {
const highlightClass = selected ? classes.selected : ''
return (
<div>
<div
<div className={classes.node}>
<span
ref={nodeRef as any}
key={treeNode.hash()}
className={`${classes.node} ${className} ${highlightClass} ${classes.title}`}
className={`${classes.title} ${highlightClass}`}
onMouseEnter={mouseOver}
onFocus={didObtainFocus}
onClick={didClickTitle}
ref={nodeRef as any}
tabIndex={-1}
onKeyDown={deleteTopicCallback}
>
<TreeNodeTitle
lastUpdate={treeNode.lastUpdate}
toggleCollapsed={toggleCollapsed}
didSelectNode={didSelectTopic}
collapsed={isCollapsed}
treeNode={treeNode}
name={name}
/>
</div>
</span>
{renderNodes()}
</div>
)
}, [treeNode.lastUpdate, treeNode, name, isCollapsed, selected, theme])
}, [treeNode.lastUpdate, treeNode, name, isCollapsed, selected, theme, mouseOver, settings])
}
export default withStyles(styles, { withTheme: true })(TreeNodeComponent)
export default withStyles(styles, { withTheme: true })(React.memo(TreeNodeComponent))
+14 -10
View File
@@ -6,25 +6,26 @@ export const styles = (theme: Theme) => {
animationLight: {
willChange: 'auto',
translateZ: 0,
animation: `updateLight 0.5s`,
animation: 'updateLight 0.5s',
},
animationDark: {
willChange: 'auto',
translateZ: 0,
animation: `updateLight 0.5s`,
animation: 'updateDark 0.5s',
},
collapsedSubnodes: {
color: theme.palette.text.secondary,
},
displayBlock: {
display: 'block',
display: 'block' as 'block',
},
node: {
display: 'block',
marginLeft: '10px',
'&:hover': {
backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light,
},
display: 'block' as 'block',
width: '100%',
overflow: 'hidden' as 'hidden',
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
padding: '1px 0px 0px 0px',
},
topicSelect: {
float: 'right' as 'right',
@@ -44,9 +45,12 @@ export const styles = (theme: Theme) => {
lineHeight: '1em',
display: 'inline-block' as 'inline-block',
whiteSpace: 'nowrap' as 'nowrap',
padding: '1px 4px 0px 4px',
height: '14px',
margin: '1px 0px 2px 0px',
padding: '1px 4px 0 4px',
margin: '1px 0px',
'&:hover': {
backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light,
},
},
}
}
+6 -13
View File
@@ -1,14 +1,13 @@
import * as q from '../../../../backend/src/Model'
import React, { useCallback } from 'react'
import React from 'react'
import TreeNode from './TreeNode'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { KeyCodes } from '../../utils/KeyCodes'
import { SettingsState } from '../../reducers/Settings'
import { TopicViewModel } from '../../model/TopicViewModel'
import { treeActions } from '../../actions'
import { KeyCodes } from '../../utils/KeyCodes'
const MovingAverage = require('moving-average')
const averagingTimeInterval = 10 * 1000
@@ -70,17 +69,17 @@ class TreeComponent extends React.PureComponent<Props, State> {
public componentWillReceiveProps(nextProps: Props) {
if (this.props.tree !== nextProps.tree) {
if (this.props.tree) {
this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
this.props.tree.didUpdate.unsubscribe(this.throttledTreeUpdate)
}
if (nextProps.tree) {
nextProps.tree.didReceive.subscribe(this.throttledTreeUpdate)
nextProps.tree.didUpdate.subscribe(this.throttledTreeUpdate)
}
this.setState(this.state)
}
}
public componentWillUnmount() {
this.props.tree && this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
this.props.tree && this.props.tree.didUpdate.unsubscribe(this.throttledTreeUpdate)
}
public throttledTreeUpdate = () => {
@@ -99,9 +98,6 @@ class TreeComponent extends React.PureComponent<Props, State> {
this.updateTimer = undefined
this.renderTime = performance.now()
if (!this.props.paused && this.props.tree) {
this.props.tree.applyUnmergedChanges()
}
window.requestIdleCallback(
() => {
this.setState({ lastUpdate: this.renderTime })
@@ -173,7 +169,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TreeComponent)
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
+2 -7
View File
@@ -45,7 +45,7 @@ interface State {
newerVersions: Array<GithubRelease>
}
class UpdateNotifier extends React.Component<Props, State> {
class UpdateNotifier extends React.PureComponent<Props, State> {
constructor(props: any) {
super(props)
this.state = { newerVersions: [] }
@@ -266,9 +266,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(UpdateNotifier)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
@@ -37,7 +37,7 @@ interface Props {
withBackground?: boolean
}
class ConnectionHealthIndicator extends React.Component<Props, {}> {
class ConnectionHealthIndicator extends React.PureComponent<Props, {}> {
constructor(props: any) {
super(props)
}
+5 -12
View File
@@ -19,7 +19,7 @@ interface State {
didCopy: boolean
}
class Copy extends React.Component<Props, State> {
class Copy extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = { didCopy: false }
@@ -44,13 +44,9 @@ class Copy extends React.Component<Props, State> {
)
return (
<span>
<span style={{ fontSize: '16px' }}>
<CustomIconButton onClick={this.handleClick} tooltip="Copy to clipboard">
{icon}
</CustomIconButton>
</span>
</span>
<CustomIconButton onClick={this.handleClick} tooltip="Copy to clipboard">
<div style={{ marginTop: '2px' }}>{icon}</div>
</CustomIconButton>
)
}
}
@@ -63,7 +59,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(Copy)
export default connect(undefined, mapDispatchToProps)(Copy)
@@ -16,12 +16,15 @@ const styles = (theme: Theme) => ({
width: '32px',
height: '32px',
},
tooltip: {
marginTop: '-16px',
},
label: {
marginTop: '-2px',
},
})
class CustomIconButton extends React.Component<Props, {}> {
class CustomIconButton extends React.PureComponent<Props, {}> {
constructor(props: Props) {
super(props)
}
@@ -34,8 +37,8 @@ class CustomIconButton extends React.Component<Props, {}> {
public render() {
return (
<IconButton className={this.props.classes.button} style={this.props.style} onClick={this.onClick}>
<Tooltip title={this.props.tooltip} className={this.props.classes.label}>
<span>{this.props.children}</span>
<Tooltip title={this.props.tooltip} classes={{ popper: this.props.classes.tooltip }}>
<span className={this.props.classes.label}>{this.props.children}</span>
</Tooltip>
</IconButton>
)
+3 -2
View File
@@ -5,6 +5,7 @@ import { connect } from 'react-redux'
interface Props {
date: Date
timeFirst?: boolean
overrideLocale?: string
locale?: string
intervalSince?: Date
@@ -16,7 +17,7 @@ const unitMapping = {
h: 'hours',
}
class DateFormatter extends React.Component<Props, {}> {
class DateFormatter extends React.PureComponent<Props, {}> {
private intervalSince(intervalSince: Date) {
const interval = intervalSince.getTime() - this.props.date.getTime()
const unit = this.unitForInterval(interval)
@@ -30,7 +31,7 @@ class DateFormatter extends React.Component<Props, {}> {
private localizedDate(locale: string) {
return moment(this.props.date)
.locale(locale)
.format('L LTS')
.format(this.props.timeFirst ? 'LTS L' : 'L LTS')
}
private unitForInterval(milliseconds: number) {
@@ -0,0 +1,39 @@
import React from 'react'
import { AppState } from '../../reducers'
import { connect } from 'react-redux'
import { Tooltip } from '@material-ui/core'
function NumberFormatter(props: { locale: string; value: number; grouping?: boolean }) {
let formatter: Intl.NumberFormat | undefined
const formatterOptions = { useGrouping: Boolean(props.grouping) }
const defaultFormatter = Intl.NumberFormat(undefined, formatterOptions)
try {
formatter = Intl.NumberFormat(props.locale, formatterOptions)
} catch {
// locale unknown
}
try {
const formatted = (formatter || defaultFormatter).format(props.value)
return (
<Tooltip title={props.value}>
<span>{formatted}</span>
</Tooltip>
)
} catch {
// localization fail, use fallback
}
return <span>props.value</span>
}
const mapStateToProps = (state: AppState) => {
return {
locale: state.settings.get('timeLocale'),
}
}
export default connect(mapStateToProps)(NumberFormatter)
@@ -6,6 +6,17 @@ import { useState, useEffect } from 'react'
*/
export function usePollingToFetchTreeNode(tree: q.Tree<any> | undefined, path: string) {
const [treeNode, setTreeNode] = useState<q.TreeNode<any> | undefined>()
useEffect(() => {
// If treeNode has been destroyed set treeNode to undefined
if (treeNode) {
const resetTreeNodeCallback = () => setTreeNode(undefined)
treeNode.onDestroy.subscribe(resetTreeNodeCallback)
return function cleanup() {
treeNode.onDestroy.unsubscribe(resetTreeNodeCallback)
}
}
}, [treeNode])
function pollUntilTreeNodeHasBeenFound() {
if (!tree) {
@@ -13,7 +24,7 @@ export function usePollingToFetchTreeNode(tree: q.Tree<any> | undefined, path: s
}
const initialTreeNode = tree.findNode(path)
if (initialTreeNode) {
if (initialTreeNode && initialTreeNode !== treeNode) {
setTreeNode(initialTreeNode)
return
}
@@ -33,6 +44,6 @@ export function usePollingToFetchTreeNode(tree: q.Tree<any> | undefined, path: s
}
}
useEffect(pollUntilTreeNodeHasBeenFound, [tree])
useEffect(pollUntilTreeNodeHasBeenFound, [tree, treeNode, path])
return treeNode
}
@@ -0,0 +1,17 @@
import * as q from '../../../../backend/src/Model'
import { useEffect, useState } from 'react'
export function useUpdateComponentWhenNodeUpdates(node?: q.TreeNode<any>): number {
const [lastUpdate, setLastUpdate] = useState(0)
useEffect(() => {
if (!node) {
return
}
const updateComponent = () => setLastUpdate(Date.now())
node.onMerge.subscribe(updateComponent)
return function cleanup() {
node.onMerge.unsubscribe(updateComponent)
}
}, [node])
return lastUpdate
}
+9 -2
View File
@@ -1,5 +1,6 @@
import { MqttOptions } from '../../../backend/src/DataSource'
import { v4 } from 'uuid'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
const sha1 = require('sha1')
export interface CertificateParameters {
@@ -7,7 +8,9 @@ export interface CertificateParameters {
/** @property data base64 encoded data */
data: string
}
export interface ConnectionOptions {
configVersion: 1
type: 'mqtt'
id: string
host: string
@@ -23,7 +26,7 @@ export interface ConnectionOptions {
clientCertificate?: CertificateParameters
clientKey?: CertificateParameters
clientId?: string
subscriptions: Array<string>
subscriptions: Array<Subscription>
}
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
@@ -52,6 +55,7 @@ function generateClientId() {
export function createEmptyConnection(): ConnectionOptions {
return {
configVersion: 1,
certValidation: true,
clientId: generateClientId(),
id: v4() as string,
@@ -59,7 +63,10 @@ export function createEmptyConnection(): ConnectionOptions {
encryption: false,
password: undefined,
username: undefined,
subscriptions: ['#', '$SYS/#'],
subscriptions: [
{ topic: '#', qos: 0 },
{ topic: '$SYS/#', qos: 0 },
],
type: 'mqtt',
host: '',
port: 1883,
-1
View File
@@ -1,4 +1,3 @@
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record, List } from 'immutable'
import MoveUp from '../components/ChartPanel/ChartSettings/MoveUp'
-1
View File
@@ -1,5 +1,4 @@
import * as q from '../../../backend/src/Model'
import { Action } from 'redux'
import { createReducer } from './lib'
import { MqttOptions } from '../../../backend/src/DataSource'
import { TopicViewModel } from '../model/TopicViewModel'
+28 -5
View File
@@ -1,6 +1,6 @@
import { Action } from 'redux'
import { ConnectionOptions } from '../model/ConnectionOptions'
import { createReducer } from './lib'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
export interface ConnectionManagerState {
connections: { [s: string]: ConnectionOptions }
@@ -51,13 +51,13 @@ export interface SelectConnection {
export interface AddSubscription {
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION
subscription: string
subscription: Subscription
connectionId: string
}
export interface DeleteSubscription {
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION
subscription: string
subscription: Subscription
connectionId: string
}
@@ -160,8 +160,12 @@ function addSubscription(state: ConnectionManagerState, action: AddSubscription)
}
function deleteSubscription(state: ConnectionManagerState, action: AddSubscription): ConnectionManagerState {
function subscriptionsEqual(v1: Subscription, v2: Subscription): boolean {
return v1.topic == v2.topic && v1.qos == v2.qos
}
const connection = state.connections[action.connectionId]
const newSubscriptions = connection.subscriptions.filter(s => s !== action.subscription)
const newSubscriptions = connection.subscriptions.filter(s => !subscriptionsEqual(s, action.subscription))
return {
...state,
@@ -187,10 +191,29 @@ function deleteConnection(state: ConnectionManagerState, action: DeleteConnectio
function updateConnection(state: ConnectionManagerState, action: UpdateConnection): ConnectionManagerState {
let connection = state.connections[action.connectionId]
let changeSet = action.changeSet
// Reset empty username to undefined
if (changeSet.username !== undefined) {
changeSet = {
changeSet,
username: changeSet.username === '' ? undefined : changeSet.username,
}
}
// Reset empty password to undefined
if (changeSet.password !== undefined) {
changeSet = {
changeSet,
password: changeSet.password === '' ? undefined : changeSet.password,
}
}
connection = {
...connection,
...action.changeSet,
...changeSet,
}
return {
...state,
connections: {

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