Compare commits

..
226 Commits
Author SHA1 Message Date
semantic-release-bot a96b08eaaa chore(release): 0.4.0-beta.2 [skip ci] 2024-05-10 18:12:27 +00:00
Thomas Nordquist c9c997d13a fix: trigger build 2024-05-10 20:04:31 +02:00
semantic-release-bot 6f3e0f62e1 chore(release): 0.4.0-beta.1 [skip ci] 2024-05-10 18:03:44 +00:00
Thomas Nordquist 824b39637c chore: use newer node version 2024-05-10 19:56:01 +02:00
Thomas Nordquist 0100b2988a chore: dry-run 2024-05-10 18:52:25 +02:00
Thomas Nordquist 0e72329c77 fix: update semantic-release 2024-05-10 18:48:38 +02:00
Thomas Nordquist 9b7c3b8e9c feat: use semantic-release 2024-05-10 18:46:06 +02:00
Thomas Nordquist b9a5a5f1d8 add semantic release 2024-05-10 18:05:41 +02:00
Thomas Nordquist cc9cc411f0 skip osx builds for now 2024-05-10 17:50:07 +02:00
Thomas Nordquist 5f75079f2f add workflows 2024-05-10 17:44:23 +02:00
Björn Dalfors a6e16dcd17 Merge pull request #772 from thomasnordquist/chore/fix-spellchecker
upgrade cspell and fix spelling issues
2024-05-02 12:58:52 +02:00
Björn Dalfors f1b13a2919 upgrade cspell and fix spelling issues 2024-04-10 09:14:12 +02:00
Björn Dalfors 737afb3c1b Merge pull request #773 from thomasnordquist/chore/replace-spectron-with-playwright
Chore/replace spectron with playwright
2024-04-10 09:07:32 +02:00
Björn Dalfors e21011fce1 use --frozen-lockfile to ensure dependency integrity across builds 2024-04-08 10:46:22 +02:00
Björn Dalfors 613d0d7178 fix test script and video capture 2024-04-08 10:14:44 +02:00
Björn Dalfors bb964aba20 replace file-loader with assets to get demo mouse working again
fix mouse pointer
2024-04-06 21:19:18 +02:00
Björn Dalfors 6e5e2e0dd7 replace deprecated spectron with playwright 2024-04-05 14:35:18 +02:00
Björn Dalfors 5afccac2ac bump version 2024-04-03 17:32:57 +02:00
Björn Dalfors 4626df0bf7 Merge pull request #771 from thomasnordquist/chore/upgrade-mocha
upgrade mocha, remove deprecated mochaopts
2024-04-03 08:07:52 +01:00
Björn Dalfors 252780a51a upgrade mocha, remove deprecated mochaopts 2024-04-03 00:03:52 +02:00
Björn Dalfors 245e661159 remove accedentally added dependency 2024-04-03 00:02:38 +02:00
Björn Dalfors 659033e4eb Merge pull request #769 from thomasnordquist/dependabot/npm_and_yarn/app/webpack-dev-middleware-5.3.4
Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /app
2024-04-02 21:06:19 +01:00
dependabot[bot] 82fbf91f8a Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /app
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 19:24:03 +00:00
Björn Dalfors 4275060003 upgrade protobufjs due cve 2024-04-02 21:21:46 +02:00
Björn Dalfors 6fe74d49fd Merge pull request #770 from thomasnordquist/dependabot/npm_and_yarn/app/follow-redirects-1.15.6
Bump follow-redirects from 1.15.5 to 1.15.6 in /app
2024-04-02 20:06:39 +01:00
Björn Dalfors 736145d9b4 Merge pull request #421 from jcwillox/patch-1
Simplify config path and increase portability
2024-04-02 19:33:26 +01:00
Björn Dalfors d5ad716086 Merge pull request #767 from thomasnordquist/dependabot/npm_and_yarn/app/express-4.19.2
Bump express from 4.18.3 to 4.19.2 in /app
2024-04-02 19:28:22 +01:00
Björn Dalfors c3f1e7c5e8 Merge pull request #763 from thomasnordquist/dependabot/npm_and_yarn/ua-parser-js-1.0.37
Bump ua-parser-js from 1.0.2 to 1.0.37
2024-04-02 19:27:31 +01:00
Björn Dalfors e1b6c709b0 Merge pull request #708 from thomasnordquist/dependabot/npm_and_yarn/minimist-1.2.8
Bump minimist from 1.2.5 to 1.2.8
2024-04-02 19:26:49 +01:00
dependabot[bot] 42de570a98 Bump follow-redirects from 1.15.5 to 1.15.6 in /app
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 18:24:38 +00:00
Björn Dalfors 30469e63e5 Merge pull request #768 from thomasnordquist/chore/upgrade-dependencies-2024
upgrade dependencies
2024-04-02 19:23:07 +01:00
Björn Dalfors 899db30e3b upgrade axios 2024-04-02 19:35:01 +02:00
Björn Dalfors 51d0eaafcc yarn upgrade 2024-04-02 17:03:29 +02:00
dependabot[bot] cfd1333989 Bump express from 4.18.3 to 4.19.2 in /app
Bumps [express](https://github.com/expressjs/express) from 4.18.3 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.3...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 14:43:11 +00:00
dependabot[bot] 6c9d98a763 Bump ua-parser-js from 1.0.2 to 1.0.37
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.2 to 1.0.37.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/1.0.37/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.2...1.0.37)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 14:39:11 +00:00
dependabot[bot] 040549c40a Bump minimist from 1.2.5 to 1.2.8
Bumps [minimist](https://github.com/minimistjs/minimist) from 1.2.5 to 1.2.8.
- [Release notes](https://github.com/minimistjs/minimist/releases)
- [Changelog](https://github.com/minimistjs/minimist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/minimistjs/minimist/compare/v1.2.5...v1.2.8)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 14:38:09 +00:00
Björn Dalfors 0c6d777999 Merge pull request #757 from thomasnordquist/chore/upgrade-electron
Chore/upgrade electron
2024-04-02 15:34:34 +01:00
Björn Dalfors 65c53fd670 use node 18 to support github electron-builder action 2024-04-02 16:17:40 +02:00
Björn Dalfors 1b7c9c52f6 move sparkplug protocol to js file as file is not included when packaging
maybe there is a better way to resolve this..
2024-04-02 13:16:34 +02:00
Björn Dalfors a63b12b266 upgrade ts-node 2024-03-10 14:10:22 +01:00
Björn Dalfors a8ff4adde7 pin electron version 2024-03-10 13:06:05 +01:00
Björn Dalfors 3b8418ccfd supress type errors for now 2024-03-10 12:57:00 +01:00
Björn Dalfors b51b3065b0 add path to react types 2024-03-10 12:56:35 +01:00
Björn Dalfors 1f0c6771e5 upgrade dependencies to fix hashing algorithm throwing errors 2024-03-10 12:55:07 +01:00
Björn Dalfors 67277b4652 node 19 2024-03-10 12:52:42 +01:00
Björn Dalfors b0e30a896f upgrade electrong dependencies 2024-03-10 12:04:06 +01:00
Björn Dalfors 93010dc06e Merge pull request #756 from thomasnordquist/chore/fix-linting
Chore/fix linting
2024-03-10 11:48:29 +01:00
Björn Dalfors 302b8f3c21 disable spellcheck until later version that supports --no-exit-code is installed 2024-03-08 08:42:45 +01:00
Björn Dalfors 62d087bb0e fix tslint errors 2024-03-08 08:42:45 +01:00
Björn Dalfors f25cab4682 chore: upgrade prettier and fix linting errors 2024-03-08 08:42:41 +01:00
Thomas Nordquist d1de0770f2 Merge pull request #592 from klaernie/patch-1
go for cloning the gh-pages branch directly
2022-02-27 23:03:15 +01:00
Thomas Nordquist 2a3f481a24 Merge pull request #629 from thomasnordquist/dubyte/add_sparkplug
Add sparkplug b support
2022-02-27 23:02:26 +01:00
Thomas Nordquist ed492ccbf4 refactor 2022-02-27 23:01:08 +01:00
Thomas Nordquist ea7535b250 apply suggestions from code review 2022-02-27 22:58:30 +01:00
Thomas Nordquist b17b54490e move sparkplug decoding to backend 2022-02-27 22:49:54 +01:00
Thomas Nordquist 72400af679 Merge branch 'master' into HEAD 2022-02-27 20:47:59 +01:00
Thomas Nordquist f8f1ddfebb fix moment locales bug 2022-02-27 20:22:45 +01:00
Thomas Nordquist 6e4d08e4b5 update mqtt library 2022-02-27 20:06:11 +01:00
Thomas Nordquist 5da8fe0f90 chore: remove after-sign 2022-02-27 19:09:25 +01:00
Thomas Nordquist df9eda4866 use node 16 for appveyor 2022-02-27 18:54:33 +01:00
Thomas Nordquist 6b030ab5ee remove package-lock 2022-02-27 18:50:23 +01:00
Thomas Nordquist e1493db7c8 add rpc system to improve ipc 2022-02-27 18:46:56 +01:00
Thomas Nordquist 205ea00c41 add package lock 2022-02-27 14:22:44 +01:00
Thomas Nordquist d428428a6e enforce npm version 2022-02-27 14:22:22 +01:00
Thomas Nordquist 24e9c4cd22 fix build 2022-02-27 14:15:50 +01:00
Thomas Nordquist d253c6c764 remove tracking 2022-02-27 14:15:01 +01:00
Thomas Nordquist 13b8f8d5da fix certificate selection 2022-02-27 13:33:22 +01:00
Andre Klärner e769ddece4 go for cloning the gh-pages branch directly
This avoids pulling in the entire history of the master branch
2021-10-03 22:36:04 +02:00
Sinuhe Tellez 64e807beef fix linter 2021-08-14 23:26:36 -04:00
Sinuhe Tellez 531af31490 remobe generated ts and load proto file directly 2021-08-14 23:24:17 -04:00
Sinuhe Tellez e708e1d0c7 update dependencies 2021-08-12 02:14:24 -04:00
Sinuhe Tellez 8fcf8b2478 format topicPlot 2021-08-11 19:26:10 -04:00
Sinuhe Tellez RiveraandThomas Nordquist 4fdd5b2063 Update topic plot about sparkplugb render
Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
2021-08-11 10:23:21 -04:00
Pawel Defée e4add31793 Accept smaller key files
I can generate a valid key file for my Mosquitto setup that fails the minimum size check of 128, for example this kind of key:

-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIFXWXY9yVJRKhZRfLz/oaAcjmOzX/2El+QRU+/1Npyfe
-----END PRIVATE KEY-----
2021-08-11 14:10:33 +02:00
Sinuhe Tellez 0cab2169c2 fix yarn build 2021-08-10 23:23:28 -04:00
Sinuhe Tellez 86f5b94852 fix linter issues 2021-08-10 22:59:59 -04:00
Sinuhe Tellez f60449c253 add protobuf dependency 2021-08-10 22:12:21 -04:00
Sinuhe Tellez c1c8b9aa61 update backend package.json and topicPlot 2021-08-10 22:06:07 -04:00
Sinuhe Tellez dcf692d193 remove un used dependencies 2021-08-10 21:51:25 -04:00
Sinuhe Tellez 7f757b9f8a remove unused import 2021-08-10 15:02:55 -04:00
Sinuhe Tellez 23b46cd432 decode diff view for sparkplug 2021-08-08 16:32:22 -04:00
Sinuhe Tellez 68ef9ac913 value render try sparkplug 2021-08-08 15:46:48 -04:00
Josh Willox 3fa47f0318 Simplify config path 2020-08-10 19:35:46 +10: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
Thomas Nordquist 901acf2bed Prepare release 2019-07-08 01:42:29 +02:00
Thomas Nordquist 4ec7c8ca75 Revert "Update material-ui"
This reverts commit 75c3619898.
2019-07-08 01:41:42 +02:00
Thomas Nordquist 75c3619898 Update material-ui 2019-07-08 01:31:27 +02:00
Thomas Nordquist 38f8d2e6ee Improve render performance 2019-07-08 01:22:16 +02:00
Thomas Nordquist e3584add7c Fix broker stats 2019-07-08 00:19:34 +02:00
Thomas Nordquist 77dcbccd5c Add time range support to charts 2019-07-07 23:54:28 +02:00
Thomas Nordquist 0ff6359a41 Allow scrolling in profile list 2019-07-07 23:52:51 +02:00
Thomas Nordquist 66bfcab256 Fix test 2019-07-07 23:11:13 +02:00
Thomas Nordquist 195dcf37d4 Add time range setting for charts 2019-07-07 22:14:49 +02:00
Thomas Nordquist 5830d99d45 Quit ui-tests eary if an error occurs 2019-07-07 16:50:29 +02:00
Thomas Nordquist d50cef7bb3 Repair reducer 2019-07-07 16:24:53 +02:00
Thomas Nordquist b79725bdf0 Refactor sidebar 2019-07-07 16:24:18 +02:00
Thomas Nordquist 6bead5b5a6 Fix animation glitch 2019-07-07 12:59:30 +02:00
Thomas Nordquist 183ea9d8c0 Allow publishing topics with ctrl+return 2019-07-07 12:44:34 +02:00
Thomas Nordquist 282736d2f6 Use default domain if plot data is empty 2019-07-07 12:31:07 +02:00
Thomas Nordquist 45b30e5997 Update dependencies 2019-07-07 12:04:27 +02:00
Thomas Nordquist 3d45e8ce6e Disable code signing for windows 2019-07-07 11:52:36 +02:00
Thomas Nordquist 393cf76839 Update dictionary 2019-07-02 15:34:43 +02:00
202 changed files with 13901 additions and 10972 deletions
+19 -7
View File
@@ -1,17 +1,27 @@
{
"import": [
"@cspell/dict-typescript/cspell-ext.json"
],
"ignoreRegExpList": [
"import(?:(?:(?:[ \\n\\t]+([^ *\\n\\t\\{\\},]+)[ \\n\\t]*(?:,|[ \\n\\t]+))?([ \\n\\t]*\\{(?:[ \\n\\t]*[^ \\n\\t\"'\\{\\}]+[ \\n\\t]*,?)+\\})?[ \\n\\t]*)|[ \n\\n\\t]*\\*[ \\n\\t]*as[ \\n\\t]+([^ \\n\\t\\{\\}]+)[ \\n\\t]+)from[ \\n\\t]*(?:['\"])([^'\"\\n]+)(['\"])\n",
"^import\\s+(['\"]).*\\1$"
],
"language": "en",
"words": [
"Bbreak",
"nodered",
"goog",
"thomasnordquist",
"nowrap",
"subheader",
"basepath",
"webdriverio",
"repo",
"hexagonalize",
"pixelize",
"Transistions",
"squashfs",
"squashfs",
"provisionprofile",
"Nsis",
"webdriverio",
"Appx",
"Hashable",
"clickaway",
@@ -21,8 +31,6 @@
"Monokai",
"plottable",
"snackbar",
"webdriverio",
"prismjs",
"Nordquist",
"debounced",
"mosquitto",
@@ -41,6 +49,10 @@
"DEVTOOLS",
"mixins",
"Explorerdmg",
"heapsnapshot"
"heapsnapshot",
"noconflict",
"sparkplugb",
"protojson",
"typesafe"
]
}
}
+3
View File
@@ -0,0 +1,3 @@
package.ts @thomasnordquist
.github @thomasnordquist
scripts @thomasnordquist
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: thomasnordquist
custom: https://paypal.me/ThomasNordquist
+43
View File
@@ -0,0 +1,43 @@
name: Build
on:
push:
branches:
- master
- alpha
- beta
concurrency:
group: ${{ github.ref }}
cancel-in-progress: false
jobs:
platform_matrix:
strategy:
matrix:
build:
- os: ubuntu-latest
task: linux
- os: windows-latest
task: win
# - os: macos-latest
# task: mac
runs-on: ${{ matrix.build.os }}
steps:
- if: matrix.build.os == 'ubuntu-latest'
run: sudo snap install snapcraft --classic
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g yarn
- run: yarn
- run: npx semantic-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: yarn build
- run: yarn prepare-release
# - run: yarn package ${{ matrix.build.task }}
# env:
# GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
+2
View File
@@ -7,3 +7,5 @@ build
test.dot
test.png
.awcache
.scannerwork
screen*.png
+33
View File
@@ -0,0 +1,33 @@
{
"branches": [
"master",
{
"name": "beta",
"prerelease": true
},
{
"name": "alpha",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"semantic-release-export-data",
"@semantic-release/changelog",
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"yarn.lock"
]
}
]
]
}
-38
View File
@@ -1,38 +0,0 @@
language: node_js
services:
- xvfb
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
node_js:
- "10"
os:
- linux
- osx
dist: xenial
services:
- docker
install:
- yarn install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update && sudo apt-get -y install snap squashfs-tools && sudo snap install snapcraft --classic; fi;
script:
- yarn run build
- yarn lint
- yarn test
- 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" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- win; fi
+31 -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,51 @@ 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 --single-branch -b gh-pages https://github.com/thomasnordquist/MQTT-Explorer.git mqtt-explorer-pages
cd mqtt-explorer-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)
+170 -155
View File
@@ -1,182 +1,197 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
<title>MQTT Explorer</title>
<script src="./bugtracking.bundle.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
<title>MQTT Explorer</title>
<script src="./bugtracking.bundle.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
}
[tabindex] {
outline: none;
}
@keyframes updateDark {
0% {
background-color: none;
}
[tabindex] {
outline: none;
25% {
background-color: #595585;
}
@keyframes updateDark {
0% {
background-color: none;
}
25% {
background-color: #4f5781;
}
50% {
background-color: #4f5781;
}
100% {
background-color: none;
}
50% {
background-color: #595585;
}
@keyframes updateLight {
0% {
background-color: none;
color: inherit;
}
25% {
background-color: #c0c8c0;
color: #000;
}
50% {
background-color: #c0c8c0;
color: #000;
}
100% {
background-color: none;
color: inherit;
}
100% {
background-color: none;
}
}
@keyframes updateLight {
0% {
background-color: none;
color: inherit;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
25% {
background-color: #c0c8c0;
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
50% {
background-color: #c0c8c0;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
background-color: rgba(140, 140, 140, 0.1);
100% {
background-color: none;
color: inherit;
}
}
::-webkit-scrollbar-thumb {
background-color: rgba(140, 140, 140, 0.8);
}
</style>
<style>
.Resizer {
background: rgba(200, 200, 200, 0);
z-index: 10;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
}
.Resizer.horizontal {
height: 10px;
margin: -10px 0 0 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
background-color: rgba(140, 140, 140, 0.1);
}
.Resizer.horizontal::before {
content: '•••';
display: inline-block;
vertical-align: middle;
text-align: center;
width: 100%;
margin-top: -22px;
color: #aaa;
opacity: 1;
}
::-webkit-scrollbar-thumb {
background-color: rgba(140, 140, 140, 0.8);
}
</style>
<style>
.Resizer {
background: rgba(200, 200, 200, 0);
z-index: 10;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.3);
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.Resizer.vertical {
width: 2px;
margin: 0px -8px 0px 0px;
border-left: 0px solid rgba(128, 128, 128, 0);
border-right: 8px solid rgba(128, 128, 128, 0);
cursor: col-resize;
}
.Resizer.horizontal {
height: 10px;
margin: -10px 0 0 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
.Resizer.vertical::before {
content: '•••';
margin-left: -11px;
height: 3em;
margin-top: calc(50vh - 32px);
display: inline-block;
vertical-align: middle;
text-align: center;
color: #aaa;
opacity: 1;
writing-mode: vertical-lr;
text-orientation: sideways;
}
.Resizer.horizontal::before {
content: '•••';
display: inline-block;
vertical-align: middle;
text-align: center;
width: 100%;
margin-top: -22px;
color: #aaa;
opacity: 1;
}
.Resizer.vertical:hover {
border-left: 0px solid rgba(130, 130, 130, 0.3);
border-right: 8px solid rgba(140, 140, 140, 0.3);
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.3);
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.Resizer.vertical {
width: 2px;
margin: 0px -8px 0px 0px;
border-left: 0px solid rgba(128, 128, 128, 0);
border-right: 8px solid rgba(128, 128, 128, 0);
cursor: col-resize;
}
.example-enter {
opacity: 0;
}
.example-enter-active {
opacity: 1;
transition: opacity 300ms ease-in;
}
.example-exit {
opacity: 1;
}
.example-exit-active {
opacity: 0;
transition: opacity 300ms ease-in;
}
</style>
</head>
<body>
<div id="app" style="font:-webkit-control"></div>
<script>
function loadScript(path) {
var script = document.createElement('script')
script.src = path
document.head.appendChild(script)
}
.Resizer.vertical::before {
content: '•••';
margin-left: -11px;
height: 3em;
margin-top: calc(50vh - 32px);
display: inline-block;
vertical-align: middle;
text-align: center;
color: #aaa;
opacity: 1;
writing-mode: vertical-lr;
text-orientation: sideways;
}
document.addEventListener('DOMContentLoaded', onLoad(), false)
function onLoad() {
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script
><% }); %>
</body>
</html>
.Resizer.vertical:hover {
border-left: 0px solid rgba(130, 130, 130, 0.3);
border-right: 8px solid rgba(140, 140, 140, 0.3);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.example-enter {
opacity: 0;
}
.example-enter-active {
opacity: 1;
transition: opacity 300ms ease-in;
}
.example-exit {
opacity: 1;
}
.example-exit-active {
opacity: 0;
transition: opacity 300ms ease-in;
}
</style>
<script>
global = globalThis //<- this should be enough
</script>
</head>
<body>
<div id="app" style="font: -webkit-control;"></div>
<script>
function loadScript(path) {
var script = document.createElement('script')
script.src = path
document.head.appendChild(script)
}
document.addEventListener('DOMContentLoaded', onLoad(), false)
function onLoad() {
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>
<script src="<%- file %>"></script>
<% }); %>
</body>
</html>
+40 -32
View File
@@ -4,82 +4,90 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "yarn rebuild && webpack --mode production",
"build": "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 -"
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
"mochatest": "mocha --require ts-node/register --require source-map-support/register --recursive src/*/**/*.spec.ts"
},
"engines": {
"node": ">=18"
},
"author": "",
"license": "CC-BY-ND-4.0",
"dependencies": {
"@material-ui/core": "^4",
"@material-ui/core": "4.12",
"@material-ui/icons": "^4",
"@material-ui/lab": "^4.0.0-alpha",
"@material-ui/styles": "^4",
"@types/react-transition-group": "^2.9.2",
"axios": "^0.19.0",
"brace": "^0.11.1",
"compare-versions": "^3.4.0",
"@material-ui/styles": "4.11",
"@types/react-transition-group": "^4",
"ace-builds": "^1.4.11",
"axios": "^0.28.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",
"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",
"lodash.throttle": "^4.1.1",
"moment": "^2.24.0",
"moving-average": "^1.0.0",
"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": {
"@babel/runtime": "^7.17.2",
"@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",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"lodash": "^4.17.21",
"mocha": "^10.4.0",
"moment": "^2.29.1",
"node-loader": "^0.6.0",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"typescript": "^3.2.2",
"webpack": "^4.28.2",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
"style-loader": "^1",
"ts-loader": "^9.2.6",
"typescript": "^4.5.5",
"webpack": "^5.69.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
"peerDependencies": {
"electron": "^5.0.5"
"electron": "^29"
}
}
}
+46 -56
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 {
@@ -62,63 +60,55 @@ export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () =>
}
}
export const addChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
const chartExists = Boolean(
getState()
.charts.get('charts')
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
)
if (chartExists) {
dispatch(showNotification('Already added'))
return
export const addChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const chartExists = Boolean(
getState()
.charts.get('charts')
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
)
if (chartExists) {
dispatch(showNotification('Already added'))
return
}
dispatch({
type: ActionTypes.CHARTS_ADD,
chart: chartParameters,
})
dispatch(saveCharts())
dispatch(showNotification('Added to chart panel'))
}
dispatch({
type: ActionTypes.CHARTS_ADD,
chart: chartParameters,
})
dispatch(saveCharts())
dispatch(showNotification('Added to chart panel'))
}
export const updateChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
type: ActionTypes.CHARTS_UPDATE,
topic: chartParameters.topic,
dotPath: chartParameters.dotPath,
parameters: chartParameters,
})
dispatch(saveCharts())
}
export const updateChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
type: ActionTypes.CHARTS_UPDATE,
topic: chartParameters.topic,
dotPath: chartParameters.dotPath,
parameters: chartParameters,
})
dispatch(saveCharts())
}
export const removeChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
chart: chartParameters,
type: ActionTypes.CHARTS_REMOVE,
})
dispatch(saveCharts())
}
export const removeChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
chart: chartParameters,
type: ActionTypes.CHARTS_REMOVE,
})
dispatch(saveCharts())
}
export const moveChartUp = (parameters: { topic: string; dotPath?: string }) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
topic: parameters.topic,
dotPath: parameters.dotPath,
type: ActionTypes.CHARTS_MOVE_UP,
})
dispatch(saveCharts())
}
export const moveChartUp =
(parameters: { topic: string; dotPath?: string }) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
topic: parameters.topic,
dotPath: parameters.dotPath,
type: ActionTypes.CHARTS_MOVE_UP,
})
dispatch(saveCharts())
}
export const setCharts = (charts: Array<ChartParameters>): Action => {
return {
+21 -23
View File
@@ -11,31 +11,29 @@ import { showError } from './Global'
import { TopicViewModel } from '../model/TopicViewModel'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../events'
export const connect = (options: MqttOptions, connectionId: string) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
export const connect =
(options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
rendererEvents.subscribe(event, dataSourceState => {
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
rendererEvents.subscribe(event, dataSourceState => {
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
}
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
dispatch(updateHealth(dataSourceState))
})
}
dispatch(updateHealth(dataSourceState))
})
}
const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<any>, getState: () => AppState) => {
let state
+44 -75
View File
@@ -9,13 +9,15 @@ import {
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
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'
import { rendererRpc } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
interface ConnectionDictionary {
export interface ConnectionDictionary {
[s: string]: ConnectionOptions
}
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
@@ -27,6 +29,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))
}
@@ -43,21 +51,19 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
}
export type CertificateTypes = 'selfSignedCertificate' | 'clientCertificate' | 'clientKey'
export const selectCertificate = (type: CertificateTypes, connectionId: string) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
try {
const certificate = await openCertificate()
dispatch(
updateConnection(connectionId, {
[type]: certificate,
})
)
} catch (error) {
dispatch(showError(error))
export const selectCertificate =
(type: CertificateTypes, connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const certificate = await openCertificate()
dispatch(
updateConnection(connectionId, {
[type]: certificate,
})
)
} catch (error) {
dispatch(showError(error))
}
}
}
async function openCertificate(): Promise<CertificateParameters> {
const rejectReasons = {
@@ -65,35 +71,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 rendererRpc.call(makeOpenDialogRpc(), {
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 < 64) {
throw rejectReasons.certificateSizeDoesNotMatch
}
return {
data: data.toString('base64'),
name: path.basename(selectedFile),
}
}
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
@@ -111,13 +107,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 +180,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
}
+29 -2
View File
@@ -1,7 +1,7 @@
import { ActionTypes } from '../reducers/Global'
import { ActionTypes, ConfirmationRequest } from '../reducers/Global'
import { Dispatch } from 'redux'
export const showError = (error?: string) => ({
export const showError = (error?: string | unknown) => ({
error,
type: ActionTypes.showError,
})
@@ -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
+20 -22
View File
@@ -43,12 +43,14 @@ export const storeSettings = () => async (dispatch: Dispatch<any>, getState: ()
}
}
export const setAutoExpandLimit = (autoExpandLimit: number = 0) => (dispatch: Dispatch<any>) => {
dispatch({
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
})
}
export const setAutoExpandLimit =
(autoExpandLimit: number = 0) =>
(dispatch: Dispatch<any>) => {
dispatch({
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
})
}
export const setTimeLocale = (timeLocale: string) => (dispatch: Dispatch<any>) => {
dispatch({
@@ -81,13 +83,15 @@ export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
dispatch(storeSettings())
}
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none) => (dispatch: Dispatch<any>) => {
dispatch({
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
})
dispatch(storeSettings())
}
export const setTopicOrder =
(topicOrder: TopicOrder = TopicOrder.none) =>
(dispatch: Dispatch<any>) => {
dispatch({
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
})
dispatch(storeSettings())
}
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { tree } = getState().connection
@@ -105,21 +109,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)
}
+27 -26
View File
@@ -11,14 +11,11 @@ const debounce = require('lodash.debounce')
export { clearTopic } from './clearTopic'
export { moveSelectionUpOrDownwards, moveInward, moveOutward } from './visibleTreeTraversal'
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
debouncedSelectTopic(topic, dispatch, getState)
}
export const selectTopic =
(topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
debouncedSelectTopic(topic, dispatch, getState)
}
const debouncedSelectTopic = debounce(
(topic: q.TreeNode<TopicViewModel>, dispatch: Dispatch<any>, getState: () => AppState) => {
@@ -30,9 +27,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())
}
@@ -75,38 +72,42 @@ function destroyUnreferencedTree(state: AppState) {
}
}
export const resetStore = () => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
export const resetStore =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
type: ActionTypes.TREE_RESET_STORE,
})
}
return dispatch({
type: ActionTypes.TREE_RESET_STORE,
})
}
export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (
dispatch: Dispatch<any>,
getState: () => AppState
): AnyAction => {
destroyUnreferencedTree(getState())
export const showTree =
(tree: q.Tree<TopicViewModel> | undefined) =>
(dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
tree,
type: ActionTypes.TREE_SHOW_TREE,
})
}
return dispatch({
tree,
type: ActionTypes.TREE_SHOW_TREE,
})
}
export const togglePause = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const paused = getState().tree.get('paused')
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.`))
}
+38 -25
View File
@@ -3,39 +3,52 @@ 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) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch(moveSelectionUpOrDownwards('next'))
export const clearTopic =
(topic: q.TreeNode<any>, recursive: boolean) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const topicsForPurging = recursive ? [topic, ...topic.childTopics()] : [topic]
const { connectionId } = getState().connection
if (!connectionId) {
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) => {
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
if (!connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
topicsForPurging
.filter(t => t.path() !== '' && t.hasMessage())
.map(t => t.path())
.forEach((path, idx) => {
const mqttMessage = {
topic: topic.path(),
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,
}
+42 -39
View File
@@ -6,53 +6,56 @@ import { SettingsState } from '../reducers/Settings'
import { sortedNodes } from '../sortedNodes'
import { TopicViewModel } from '../model/TopicViewModel'
export const moveSelectionUpOrDownwards = (direction: 'next' | 'previous') => (
dispatch: Dispatch<any>,
getState: () => AppState
): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
const tree = state.tree.get('tree')
export const moveSelectionUpOrDownwards =
(direction: 'next' | 'previous') =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
const tree = state.tree.get('tree')
if (!selected || !tree) {
if (tree) {
dispatch(selectTopic(tree))
if (!selected || !tree) {
if (tree) {
dispatch(selectTopic(tree))
}
return
}
const nextTreeNode = nextVisibleElementInTree(state.settings, tree, selected, direction)
if (nextTreeNode && nextTreeNode.viewModel) {
dispatch(selectTopic(nextTreeNode))
}
return
}
const nextTreeNode = nextVisibleElementInTree(state.settings, tree, selected, direction)
if (nextTreeNode && nextTreeNode.viewModel) {
dispatch(selectTopic(nextTreeNode))
}
}
export const moveInward = () => (dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
if (!selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(true, true)
} else {
dispatch(moveSelectionUpOrDownwards('next'))
}
}
export const moveInward =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
export const moveOutward = () => (dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
if (!selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(true, true)
} else {
dispatch(moveSelectionUpOrDownwards('next'))
}
}
if (selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(false, true)
} else {
dispatch(moveSelectionUpOrDownwards('previous'))
export const moveOutward =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
if (selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(false, true)
} else {
dispatch(moveSelectionUpOrDownwards('previous'))
}
}
}
function isTreeNodeVisible(treeNode: q.TreeNode<any>) {
return Boolean(treeNode.viewModel)
+11 -8
View File
@@ -1,15 +1,18 @@
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'
;(window as any).global = window
const Settings = React.lazy(() => import('./SettingsDrawer/Settings'))
const ContentView = React.lazy(() => import('./Layout/ContentView'))
@@ -23,6 +26,7 @@ interface Props {
actions: typeof globalActions
settingsActions: typeof settingsActions
launching: boolean
confirmationRequests: Array<ConfirmationRequest>
}
class App extends React.PureComponent<Props, {}> {
@@ -63,13 +67,16 @@ class App extends React.PureComponent<Props, {}> {
return null
}
const anyProps: any = {}
return (
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
{this.renderNotification()}
<React.Suspense fallback={<div></div>}>
<Settings />
<Settings {...anyProps} />
</React.Suspense>
<div className={centerContent}>
<div className={`${settingsVisible ? contentShift : content}`}>
@@ -149,12 +156,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'
}
}
-97
View File
@@ -1,97 +0,0 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import TopicPlot from '../TopicPlot'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
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')
interface Props {
parameters: ChartParameters
treeNode?: q.TreeNode<any>
actions: {
chart: typeof chartActions
}
}
/**
* Subscribes to onMessage of treeNode
*/
function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
const [lastUpdated, setLastUpdate] = React.useState(0)
function subscribeToMessageUpdates() {
const onUpdateCallback = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
treeNode && treeNode.onMessage.subscribe(onUpdateCallback)
return function cleanup() {
treeNode && treeNode.onMessage.unsubscribe(onUpdateCallback)
}
}
React.useEffect(subscribeToMessageUpdates, [treeNode])
}
function Chart(props: Props) {
const { parameters, treeNode } = props
const [freezedHistory, setHistory] = React.useState<q.MessageHistory | undefined>()
useMessageSubscriptionToUpdate(treeNode)
const togglePause = React.useCallback(() => {
if (!props.treeNode) {
return
}
setHistory(freezedHistory ? undefined : props.treeNode.messageHistory.clone())
}, [props.treeNode, freezedHistory])
const onRemove = React.useCallback(() => {
props.actions.chart.removeChart(props.parameters)
}, [props.parameters])
return (
<Paper
style={{ padding: '8px' }}
data-test-type="ChartPaper"
data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`}
>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
<ChartTitle parameters={parameters} />
<ChartActions
parameters={parameters}
onRemove={onRemove}
paused={Boolean(freezedHistory)}
togglePause={togglePause}
/>
</div>
</div>
{props.treeNode ? (
<TopicPlot
color={props.parameters.color}
interpolation={props.parameters.interpolation}
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
history={freezedHistory ? freezedHistory : props.treeNode.messageHistory}
dotPath={parameters.dotPath}
/>
) : (
<span>No data</span>
)}
</Paper>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(Chart)
+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))
@@ -0,0 +1,99 @@
import React, { ChangeEvent, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { bindActionCreators } from 'redux'
import { Button, Menu, TextField, Typography } from '@material-ui/core'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
const parseDuration = require('parse-duration')
interface Props {
actions: { chart: typeof chartActions }
chart: ChartParameters
anchorEl?: Element
open: boolean
onClose: () => void
}
function TimeRangeSettings(props: Props) {
const dismissClick = useCallback((e: MouseEvent) => e.stopPropagation(), [])
const [value, setValue] = useState<string | undefined>(
props.chart.timeRange ? props.chart.timeRange.until : undefined
)
const ranges = ['all', '10s', '30s', '1m', '5m', '15m', '1h', '6h', '1d']
const manuallySetIntervalHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}, [])
useEffect(() => {
if (!value) {
props.actions.chart.updateChart({
...props.chart,
timeRange: undefined,
})
return
}
const canBeParsed = Boolean(parseDuration(value))
if (canBeParsed) {
props.actions.chart.updateChart({
...props.chart,
timeRange: {
until: value,
},
})
}
}, [value])
return useMemo(() => {
const createRangeHandler = (range: string) => (e: React.MouseEvent) => setValue(range === 'all' ? undefined : range)
return (
<Menu
style={{ textAlign: 'center' }}
keepMounted={true}
anchorEl={props.anchorEl}
open={props.open}
onClose={props.onClose}
>
<Typography>Chart data within a time interval</Typography>
<div style={{ padding: '0 16px', width: '275px', textAlign: 'center' }}>
{ranges.map(r => {
return (
<Button
style={{ margin: '4px', textTransform: 'none' }}
variant="contained"
key={r}
onClick={createRangeHandler(r)}
>
{r}
</Button>
)
})}
</div>
<Typography style={{ fontSize: '0.75em' }}>
<i>Limited to 500 data points</i>
</Typography>
<br />
<TextField
style={{ marginLeft: '8px', marginTop: '0' }}
onClick={dismissClick}
label="custom interval"
value={value || ''}
onChange={manuallySetIntervalHandler}
margin="normal"
/>
</Menu>
)
}, [value, props.open])
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
@@ -1,22 +1,31 @@
import * as React from 'react'
import InterpolationSettings from './InterpolationSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem } from '@material-ui/core'
import RangeSettings from './RangeSettings'
import Size from './Size'
import MoveUp from './MoveUp'
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, ListItemIcon, Typography } from '@material-ui/core'
function ChartSettings(props: {
open: boolean
close: () => void
resetDataAction: () => void
chart: ChartParameters
anchorEl: React.MutableRefObject<undefined>
}) {
const [rangeVisible, setRangeVisible] = React.useState(false)
const [timeRangeVisible, setTimeRangeVisible] = React.useState(false)
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) {
@@ -25,6 +34,13 @@ function ChartSettings(props: {
setRangeVisible(!rangeVisible)
}, [rangeVisible, open])
const toggleTimeRange = React.useCallback(() => {
if (open) {
props.close()
}
setTimeRangeVisible(!timeRangeVisible)
}, [timeRangeVisible, open])
const toggleInterpolation = React.useCallback(() => {
if (open) {
props.close()
@@ -50,20 +66,50 @@ 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}>
<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>
<RangeSettings chart={props.chart} anchorEl={props.anchorEl.current} open={rangeVisible} onClose={toggleRange} />
<TimeRangeSettings
chart={props.chart}
anchorEl={props.anchorEl.current}
open={timeRangeVisible}
onClose={toggleTimeRange}
/>
<InterpolationSettings
chart={props.chart}
anchorEl={props.anchorEl.current}
@@ -76,4 +122,4 @@ function ChartSettings(props: {
)
}
export default ChartSettings
export default memo(ChartSettings)
@@ -1,7 +1,8 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import Chart from './Chart'
import React from 'react'
import TopicChart from './TopicChart'
import { ChartParameters } from '../../reducers/Charts'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
interface Props {
tree?: q.Tree<any>
@@ -14,37 +15,6 @@ export function ChartWithTreeNode(props: Props) {
return null
}
const initialTreeNode = tree.findNode(parameters.topic)
const [treeNode, setTreeNode] = React.useState<q.TreeNode<any> | undefined>(initialTreeNode)
usePollingToFetchTreeNode(treeNode, tree, parameters.topic, setTreeNode)
return <Chart treeNode={treeNode} parameters={parameters} />
}
/**
* If a node is not available when the plot is shown, keep polling until it has been created
*/
function usePollingToFetchTreeNode(
treeNode: q.TreeNode<any> | undefined,
tree: q.Tree<any>,
path: string,
setTreeNode: React.Dispatch<React.SetStateAction<q.TreeNode<any> | undefined>>
) {
function pollUntilTreeNodeHasBeenFound() {
let intervalTimer: any
if (!treeNode) {
intervalTimer = setInterval(() => {
const node = tree.findNode(path)
if (node) {
setTreeNode(node)
clearInterval(intervalTimer)
}
}, 500)
}
return function cleanup() {
intervalTimer && clearInterval(intervalTimer)
}
}
React.useEffect(pollUntilTreeNodeHasBeenFound, [])
const treeNode = usePollingToFetchTreeNode(tree, parameters.topic)
return <TopicChart treeNode={treeNode} parameters={parameters} />
}
@@ -0,0 +1,136 @@
import * as q from '../../../../backend/src/Model'
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'
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>
actions: {
chart: typeof chartActions
}
}
/**
* 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<ClearableMessageBuffer | undefined>()
let amendMessageCallback: any
function subscribeToMessageUpdates() {
const throttledUpdate = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
if (treeNode) {
const newMessageHistory = ClearableMessageBuffer.fromMessageBuffer(treeNode.messageHistory)
newMessageHistory.setCapacity(500, 2 * 500 * 10000)
amendMessageCallback = (message: q.Message) => {
newMessageHistory.add(message)
throttledUpdate()
}
treeNode.onMessage.subscribe(amendMessageCallback)
setMessageHistory(newMessageHistory)
}
return function cleanup() {
treeNode && treeNode.onMessage.unsubscribe(amendMessageCallback)
setMessageHistory(undefined)
}
}
React.useEffect(subscribeToMessageUpdates, [treeNode])
return messageHistory
}
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] = useState<q.MessageHistory | undefined>()
const messageHistory = useMessageSubscriptionToUpdate(treeNode)
const togglePause = useCallback(() => {
if (!treeNode) {
return
}
setFrozenHistory(frozenHistory ? undefined : messageHistory && messageHistory.clone())
}, [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' }}
data-test-type="ChartPaper"
data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`}
>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
<ChartTitle parameters={parameters} />
<ChartActions
resetDataAction={resetData}
parameters={parameters}
onRemove={onRemove}
paused={Boolean(frozenHistory)}
togglePause={togglePause}
/>
</div>
</div>
<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>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
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))
@@ -1,7 +1,5 @@
import * as React 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 { bindActionCreators } from 'redux'
import { Button, Theme, Tooltip, Typography } from '@material-ui/core'
@@ -81,7 +79,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)
}
@@ -95,9 +95,10 @@ const styles = (theme: Theme) => ({
borderRadius: `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`,
paddingTop: theme.spacing(2),
flex: 3,
overflow: 'hidden',
overflow: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
overflowY: 'auto' as 'auto',
},
right: {
borderRadius: `0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0`,
@@ -133,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))
@@ -6,10 +6,10 @@ import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../../actions'
import { ConnectionOptions } from '../../../model/ConnectionOptions'
import { KeyCodes } from '../../../utils/KeyCodes'
import { List, ListSubheader } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'
import { KeyCodes } from '../../../utils/KeyCodes'
interface Props {
classes: any
@@ -39,10 +39,10 @@ function ProfileList(props: Props) {
useGlobalKeyEventHandler(KeyCodes.arrow_up, selectConnection('previous'))
const createConnectionButton = (
<ListSubheader component="div">
<div style={{ padding: '8px 16px' }}>
<AddButton action={actions.createConnection} />
Connections
</ListSubheader>
</div>
)
return (
@@ -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
@@ -5,7 +5,7 @@ let heapdump: any
function writeHeapdump(path?: string) {
if (!heapdump) {
heapdump = require('heapdump')
//<heapdump = require('heapdump')
}
heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`)
+2 -3
View File
@@ -2,7 +2,6 @@ import * as React from 'react'
import PersistentStorage from '../utils/PersistentStorage'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import Warning from '@material-ui/icons/Warning'
import { electronRendererTelemetry } from 'electron-telemetry'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Button, Modal, Paper, Toolbar, Typography } from '@material-ui/core'
@@ -14,7 +13,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 }
}
@@ -33,7 +32,7 @@ class ErrorBoundary extends React.Component<Props, State> {
}
public componentDidCatch(error: Error, errorInfo: any) {
electronRendererTelemetry.trackError(error)
// electronRendererTelemetry.trackError(error)
console.log('did catch', 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)
@@ -1,13 +1,13 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import React, { useMemo } from 'react'
import { AppState } from '../../reducers'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux'
import { StyleRulesCallback, withStyles, Theme } from '@material-ui/core/styles'
import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import teal from '@material-ui/core/colors/teal'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
import { useUpdateComponentWhenNodeUpdates } from '../helper/useUpdateComponentWhenNodeUpdates'
const abbreviate = require('number-abbreviate')
interface Stats {
@@ -16,10 +16,6 @@ interface Stats {
}
const styles = (theme: Theme) => ({
flex: {
display: 'flex',
width: '100%',
},
container: {
width: '100%',
height: '224px',
@@ -34,24 +30,13 @@ interface Props {
tree?: q.Tree<TopicViewModel>
}
class BrokerStatistics extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
}
function BrokerStatistics(props: Props) {
const { tree, classes } = props
const sysTopic = usePollingToFetchTreeNode(props.tree, '$SYS')
useUpdateComponentWhenNodeUpdates(sysTopic)
private renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div className={this.props.classes.flex}>
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{this.renderStat(tree, b)}</div>
</div>
)
}
public render() {
const { tree, classes } = this.props
if (!tree || !tree.findNode('$SYS/broker/clients/total')) {
return useMemo(() => {
if (!Boolean(sysTopic)) {
return null
}
@@ -94,38 +79,20 @@ class BrokerStatistics extends React.Component<Props, {}> {
},
}
return (
<div className={classes.container}>
{this.renderStat(tree, stats.broker)}
{this.renderPair(tree, stats.sent, stats.received)}
{this.renderPair(tree, stats.clients, stats.subscriptions)}
{this.renderPair(tree, stats.sent5m, stats.received5m)}
{this.renderPair(tree, stats.heap, stats.heapMax)}
</div>
)
}
public renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
const node = tree.findNode(stat.topic)
if (!node || !node.message) {
if (!tree) {
return null
}
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
let value = node.message && node.message.value ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str
return (
<div key={stat.title}>
<Typography>
<b>{stat.title}</b>
</Typography>
<Typography style={{ paddingLeft: '8px' }}>
<i>{value}</i>
</Typography>
<div className={classes.container}>
{renderStat(tree, stats.broker)}
{renderPair(tree, stats.sent, stats.received)}
{renderPair(tree, stats.clients, stats.subscriptions)}
{renderPair(tree, stats.sent5m, stats.received5m)}
{renderPair(tree, stats.heap, stats.heapMax)}
</div>
)
}
}, [sysTopic && sysTopic.lastUpdate, props.classes])
}
const mapStateToProps = (state: AppState) => {
@@ -135,3 +102,39 @@ const mapStateToProps = (state: AppState) => {
}
export default withStyles(styles)(connect(mapStateToProps)(BrokerStatistics))
function renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div
style={{
display: 'flex',
width: '100%',
}}
>
<div style={{ flex: 1 }}>{renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{renderStat(tree, b)}</div>
</div>
)
}
function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
const node = tree.findNode(stat.topic)
if (!node || !node.message) {
return null
}
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 (
<div key={stat.title}>
<Typography>
<b>{stat.title}</b>
</Typography>
<Typography style={{ paddingLeft: '8px' }}>
<i>{value}</i>
</Typography>
</div>
)
}
@@ -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))
@@ -3,10 +3,17 @@ import DateFormatter from '../helper/DateFormatter'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { Input, InputLabel, MenuItem, Select, StyleRulesCallback, Theme } from '@material-ui/core'
import { Input, InputLabel, MenuItem, Select, Theme } from '@material-ui/core'
import { settingsActions } from '../../actions'
import { withStyles } from '@material-ui/styles'
const moment = require('moment/min/moment-with-locales')
function importAll(r: any) {
r.keys().forEach(r)
}
// @ts-expect-error -- webpack require
importAll(require.context('moment/locale', true, /\.js$/))
const moment = require('moment')
interface Props {
actions: {
@@ -82,9 +89,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) {
@@ -42,9 +43,9 @@ class CodeDiff extends React.Component<Props, State> {
private plottableLiteralsIndexedWithLineNumbers() {
const allLiterals = this.isValidJson(this.props.current) ? literalsMappedByLines(this.props.current) || [] : []
return allLiterals.map((l: JsonPropertyLocation) => (isPlottable(l.value) ? l : undefined)) as Array<
JsonPropertyLocation
>
return allLiterals.map((l: JsonPropertyLocation) =>
isPlottable(l.value) ? l : undefined
) as Array<JsonPropertyLocation>
}
private renderStyledCodeLines(changes: Array<Diff.Change>) {
@@ -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,
+54 -43
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,45 +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])
const createSelectionHandler = (index: number) => (event: React.MouseEvent) => {
props.onClick && props.onClick(index, event.target)
event.preventDefault()
event.stopPropagation()
}
private toggle = () => {
this.setState({ collapsed: !this.state.collapsed })
}
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={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
onClick={createSelectionHandler(index)}
tabIndex={0}
onKeyDown={this.handleCtrlA}
onKeyDown={handleCtrlA}
>
<div>
<i>{element.title}</i>
@@ -66,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>
)
})
+4 -1
View File
@@ -4,7 +4,7 @@ import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
interface Props {
node: q.TreeNode<TopicViewModel>
node?: q.TreeNode<TopicViewModel>
}
class NodeStats extends React.Component<Props, {}> {
@@ -14,6 +14,9 @@ class NodeStats extends React.Component<Props, {}> {
public render() {
const { node } = this.props
if (!node) {
return null
}
return (
<div>
+34
View File
@@ -0,0 +1,34 @@
import React from 'react'
import ExpandMore from '@material-ui/icons/ExpandMore'
import { ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography, Theme } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
const styles = (theme: Theme) => ({
summary: { minHeight: '0' },
details: { padding: '0px 16px 8px 8px', display: 'block' },
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
})
const Panel = (props: {
classes: any
children: [React.ReactElement, React.ReactElement]
disabled?: boolean
detailsHidden?: boolean
}) => {
return (
<ExpansionPanel defaultExpanded={true} disabled={props.disabled}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} className={props.classes.summary}>
<Typography className={props.classes.heading}>{props.children[0]}</Typography>
</ExpansionPanelSummary>
{props.detailsHidden ? null : (
<ExpansionPanelDetails className={props.classes.detail}>{props.children[1]}</ExpansionPanelDetails>
)}
</ExpansionPanel>
)
}
// @ts-ignore
export default withStyles(styles)(Panel)
-111
View File
@@ -1,111 +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?]
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)
let yDomain: [number, number] = props.range
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
: calculatedDomain
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}>
<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] {
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}
>
+143 -181
View File
@@ -1,32 +1,19 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ClearAdornment from '../../helper/ClearAdornment'
import Editor from './Editor'
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import History from '../HistoryDrawer'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import PublishHistory from './PublishHistory'
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'
import { Button, Fab, Theme, Tooltip, withTheme } from '@material-ui/core'
import { connect } from 'react-redux'
import { globalActions, publishActions } from '../../../actions'
import {
Button,
FormControlLabel,
FormControl,
InputLabel,
Input,
Checkbox,
Tooltip,
Fab,
Theme,
withTheme,
} from '@material-ui/core'
import { EditorModeSelect } from './EditorModeSelect'
import QosSelect from './QosSelect'
import Editor from './Editor'
const sha1 = require('sha1')
import { globalActions, publishActions } from '../../../actions'
import { KeyCodes } from '../../../utils/KeyCodes'
import { default as AceEditor } from 'react-ace'
interface Props {
connectionId?: string
@@ -39,187 +26,165 @@ interface Props {
theme: Theme
}
interface State {
history: Array<Message>
function useHistory(): [Array<Message>, (topic: string, payload?: string) => void] {
const [history, setHistory] = useState<Array<Message>>([])
const amendToHistory = useCallback(
(topic: string, payload?: string) => {
// Remove duplicates
let filteredHistory = history.filter(e => e.payload !== payload || e.topic !== topic)
filteredHistory = filteredHistory.slice(-7)
setHistory([...filteredHistory, { topic, payload, sent: new Date() }])
},
[history]
)
return [history, amendToHistory]
}
class Publish extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { history: [] }
}
function Publish(props: Props) {
const editorRef = useRef<AceEditor>()
const [history, amendToHistory] = useHistory()
private updatePayload = (payload: string) => {
this.props.actions.setPayload(payload)
}
const focusEditor = useCallback(() => {
editorRef.current?.editor.focus()
}, [editorRef])
private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.actions.setTopic(e.target.value)
}
private updateMode = (e: React.ChangeEvent<{}>, value: string) => {
this.props.actions.setEditorMode(value)
}
private publish = (e: React.MouseEvent) => {
e.stopPropagation()
if (!this.props.connectionId) {
const publish = useCallback(() => {
if (!props.connectionId) {
return
}
this.props.actions.publish(this.props.connectionId)
props.actions.publish(props.connectionId)
const topic = this.props.topic || ''
const payload = this.props.payload
if (this.props.connectionId && topic) {
this.addMessageToHistory(topic, payload)
const topic = props.topic || ''
const payload = props.payload
if (props.connectionId && topic) {
amendToHistory(topic, payload)
}
}
}, [props, props.connectionId, props.topic, props.payload, amendToHistory])
private addMessageToHistory(topic: string, payload?: string) {
// Remove duplicates
let filteredHistory = this.state.history.filter(e => e.payload !== payload || e.topic !== topic)
filteredHistory = filteredHistory.slice(-7)
const history: Array<Message> = [...filteredHistory, { topic, payload, sent: new Date() }]
this.setState({ history })
}
const handleSubmit = useCallback(
(e: React.KeyboardEvent) => {
if (e.keyCode === KeyCodes.enter && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
e.stopPropagation()
publish()
}
},
[publish]
)
private clearTopic = () => {
this.props.actions.setTopic('')
}
private topic() {
const topicStr = this.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={this.clearTopic} value={topicStr} />}
onBlur={this.onTopicBlur}
onChange={this.updateTopic}
multiline={true}
placeholder="example/topic"
return useMemo(
() => (
<div style={{ flexGrow: 1, width: '100%' }} onKeyDown={handleSubmit}>
<TopicInput />
<div style={{ width: '100%', display: 'block' }}>
<EditorMode
focusEditor={focusEditor}
actions={props.actions}
globalActions={props.globalActions}
payload={props.payload}
editorMode={props.editorMode}
publish={publish}
/>
</FormControl>
<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, publish]
)
}
private onTopicBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) {
this.props.actions.setTopic(undefined)
}
}
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
private publishButton() {
return (
<Button variant="contained" size="small" color="primary" onClick={this.publish} id="publish-button">
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
}
const updateMode = useCallback((e: React.ChangeEvent<{}>, value: string) => {
props.actions.setEditorMode(value)
}, [])
private formatJson = () => {
if (this.props.payload) {
const formatJson = useCallback(() => {
if (props.payload) {
try {
const str = JSON.stringify(JSON.parse(this.props.payload), undefined, ' ')
this.updatePayload(str)
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str)
} catch (error) {
this.props.globalActions.showError(`Format error: ${error.message}`)
props.globalActions.showError(`Format error: ${(error as Error)?.message}`)
}
}
}
}, [props.payload])
private renderFormatJson() {
if (this.props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
onClick={this.formatJson}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
}
private editorMode() {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<EditorModeSelect value={this.props.editorMode} onChange={this.updateMode} />
{this.renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>{this.publishButton()}</div>
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
}
private publishMode() {
const labelStyle = { margin: '0 8px 0 8px' }
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>
)
})
return (
<div style={{ marginTop: '8px', clear: 'both' }}>
<div style={{ width: '100%', textAlign: 'right' }}>
<FormControlLabel style={labelStyle} control={<QosSelect />} label="QoS" labelPlacement="start" />
<Tooltip
title="Retained messages only appear to be retained, when client subscribes after the initial publish."
placement="top"
>
<FormControlLabel
value="retain"
style={labelStyle}
control={
<Checkbox color="primary" checked={this.props.retain} onChange={this.props.actions.toggleRetain} />
}
label="retain"
labelPlacement="end"
/>
</Tooltip>
</div>
</div>
)
}
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
props.publish()
},
[props.publish]
)
private history() {
const items = [...this.state.history].reverse().map(message => ({
key: sha1(message.topic + message.payload),
title: message.topic,
value: message.payload || '',
}))
return <History items={items} onClick={this.didSelectHistoryEntry} />
}
private didSelectHistoryEntry = (index: number) => {
const message = this.state.history[index]
this.props.actions.setTopic(message.topic)
this.props.actions.setPayload(message.payload)
}
public render() {
return (
<div style={{ flexGrow: 1, marginLeft: '8px' }}>
{this.topic()}
<div style={{ width: '100%', display: 'block' }}>
{this.editorMode()}
<Editor value={this.props.payload} editorMode={this.props.editorMode} onChange={this.updatePayload} />
{this.publishMode()}
</div>
{this.history()}
</div>
)
}
}
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 {
@@ -230,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))
@@ -0,0 +1,37 @@
import History from '../HistoryDrawer'
import Message from './Model/Message'
import React, { useCallback, useMemo } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { publishActions } from '../../../actions'
const sha1 = require('sha1')
function PublishHistory(props: { history: Array<Message>; actions: typeof publishActions }) {
const didSelectHistoryEntry = useCallback(
(index: number) => {
const items = [...props.history].reverse()
const message = items[index]
props.actions.setTopic(message.topic)
props.actions.setPayload(message.payload)
},
[props.history]
)
return useMemo(() => {
const items = [...props.history].reverse().map(message => ({
key: sha1(message.topic + message.payload),
title: message.topic,
value: message.payload || '',
}))
return <History autoOpen={true} items={items} onClick={didSelectHistoryEntry} />
}, [props.history])
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(publishActions, dispatch),
}
}
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)
@@ -0,0 +1,44 @@
import QosSelect from './QosPublishOption'
import React from 'react'
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
export function RetainSwitch(props: { retain: boolean; actions: typeof publishActions }) {
const labelStyle = { margin: '0 8px 0 8px' }
return (
<div style={{ marginTop: '8px', clear: 'both' }}>
<div style={{ width: '100%', textAlign: 'right' }}>
<FormControlLabel style={labelStyle} control={<QosSelect />} label="QoS" labelPlacement="start" />
<Tooltip
title="Retained messages only appear to be retained, when client subscribes after the initial publish."
placement="top"
>
<FormControlLabel
value="retain"
style={labelStyle}
control={<Checkbox color="primary" checked={props.retain} onChange={props.actions.toggleRetain} />}
label="retain"
labelPlacement="end"
/>
</Tooltip>
</div>
</div>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(publishActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
retain: state.publish.retain,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RetainSwitch)
@@ -0,0 +1,65 @@
import ClearAdornment from '../../helper/ClearAdornment'
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; 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>) => {
if (!e.target.value) {
props.actions.setTopic(undefined)
}
}, [])
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]
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(publishActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
manualTopic: state.publish.manualTopic,
selectedTopic: state.tree.get('selectedTopic')?.path(),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TopicInput)
+52 -159
View File
@@ -1,11 +1,7 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import Copy from '../helper/Copy'
import CustomIconButton from '../helper/CustomIconButton'
import Delete from '@material-ui/icons/Delete'
import React, { useState, useEffect, useCallback } from 'react'
import ExpandMore from '@material-ui/icons/ExpandMore'
import NodeStats from './NodeStats'
import Topic from './Topic'
import ValuePanel from './ValueRenderer/ValuePanel'
import { AppState } from '../../reducers'
import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
@@ -14,166 +10,75 @@ import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../actions'
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
}
interface State {
compareMessage?: q.Message
valueRenderWidth: number
function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
const [lastUpdate, setLastUpdate] = useState(0)
const updateNode = useCallback(
throttle(() => {
setLastUpdate(node ? node.lastUpdate : 0)
}, 300),
[node]
)
useEffect(() => {
const updateCallback = updateNode
node && node.onMerge.subscribe(updateCallback)
node && node.onMessage.subscribe(updateCallback)
return function cleanup() {
node && node.onMerge.unsubscribe(updateCallback)
node && node.onMessage.unsubscribe(updateCallback)
}
}, [node])
}
class Sidebar extends React.Component<Props, State> {
private updateNode = throttle(() => {
this.setState(this.state)
}, 300)
function Sidebar(props: Props) {
const { classes, tree, nodePath } = props
const node = usePollingToFetchTreeNode(tree, nodePath || '')
useUpdateNodeWhenNodeReceivesUpdates(node)
// console.log(node && node.path(), tree, nodePath)
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
constructor(props: any) {
super(props)
this.state = { valueRenderWidth: 300 }
}
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
node.onMerge.subscribe(this.updateNode)
node.onMessage.subscribe(this.updateNode)
}
private removeUpdateListener(node: q.TreeNode<TopicViewModel>) {
node.onMerge.unsubscribe(this.updateNode)
node.onMessage.unsubscribe(this.updateNode)
}
private renderTopicDeleteButton() {
if (!this.props.node || (!this.props.node.message || !this.props.node.message.value)) {
return null
}
return (
<CustomIconButton onClick={() => this.deleteTopic(this.props.node)} tooltip="Clear this topic">
<Delete style={{ marginTop: '-3px' }} />
</CustomIconButton>
)
}
private renderRecursiveTopicDeleteButton() {
const deleteLimit = 50
const topicCount = this.props.node ? this.props.node.childTopicCount() : 0
if (!this.props.node || topicCount === 0 || (this.props.node.message && topicCount === 1)) {
return null
}
return (
<Badge
classes={{ badge: this.props.classes.badge }}
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
color="secondary"
>
<CustomIconButton
onClick={() => this.deleteTopic(this.props.node, true, deleteLimit)}
tooltip={`Deletes up to ${deleteLimit} sub-topics with a single click`}
>
<Delete style={{ marginTop: '-3px' }} color="action" />
</CustomIconButton>
</Badge>
)
}
private deleteTopic = (topic?: q.TreeNode<TopicViewModel>, recursive: boolean = false, maxCount = 50) => {
if (!topic) {
return
}
this.props.actions.clearTopic(topic, recursive, maxCount)
}
private renderNode() {
const { classes, node } = this.props
const copyTopic = node ? <Copy value={node.path()} /> : null
const deleteTopic = this.renderTopicDeleteButton()
const deleteRecursiveTopic = this.renderRecursiveTopicDeleteButton()
const summaryStyle = { minHeight: '0' }
return (
return (
<div id="Sidebar" className={classes.drawer}>
<div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>
Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
<TopicPanel node={node} />
<ValuePanel lastUpdate={node ? node.lastUpdate : 0} />
<Panel>
<span>Publish</span>
<Publish connectionId={props.connectionId} />
</Panel>
<Panel detailsHidden={!node}>
<span>Stats</span>
<ExpansionPanelDetails className={classes.details}>
<NodeStats node={node} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ValuePanel lastUpdate={this.props.node ? this.props.node.lastUpdate : 0} />
<ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Publish</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<React.Suspense fallback={<div>Loading...</div>}>
<Publish connectionId={this.props.connectionId} />
</React.Suspense>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
{this.renderNodeStats()}
</ExpansionPanel>
</Panel>
</div>
)
}
private renderNodeStats() {
if (!this.props.node) {
return null
}
return (
<ExpansionPanelDetails style={this.detailsStyle}>
<NodeStats node={this.props.node} />
</ExpansionPanelDetails>
)
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node)
if (this.props.node !== nextProps.node) {
this.setState({ compareMessage: undefined })
}
}
public componentWillUnmount() {
this.props.node && this.removeUpdateListener(this.props.node)
}
public render() {
return (
<div id="Sidebar" className={this.props.classes.drawer}>
{this.renderNode()}
</div>
)
}
</div>
)
}
const mapStateToProps = (state: AppState) => {
const node = state.tree.get('selectedTopic')
return {
node: state.tree.get('selectedTopic'),
tree: state.connection.tree,
nodePath: node && node.path(),
}
}
@@ -188,22 +93,10 @@ const styles = (theme: Theme) => ({
drawer: {
display: 'block' as 'block',
},
badge: {
top: '3px',
right: '3px',
},
valuePaper: {
margin: theme.spacing(1),
},
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))
@@ -0,0 +1,37 @@
import * as q from '../../../../../backend/src/Model'
import CustomIconButton from '../../helper/CustomIconButton'
import Delete from '@material-ui/icons/Delete'
import React, { useCallback } from 'react'
import { Badge } from '@material-ui/core'
export const RecursiveTopicDeleteButton = (props: {
node?: q.TreeNode<any>
deleteTopicAction: (node: q.TreeNode<any>, a: boolean, limit: number) => void
}) => {
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 topicCount = props.node ? props.node.childTopicCount() : 0
if (topicCount === 0 || (props.node.message && topicCount === 1)) {
return null
}
return (
<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>
)
}
@@ -1,11 +1,11 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import React from 'react'
import * as q from '../../../../../backend/src/Model'
import Button from '@material-ui/core/Button'
import { withStyles, Theme } from '@material-ui/core/styles'
import { treeActions } from '../../actions'
import { treeActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { TopicViewModel } from '../../model/TopicViewModel'
import { TopicViewModel } from '../../../model/TopicViewModel'
interface Props {
classes: any
@@ -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))
@@ -0,0 +1,19 @@
import * as q from '../../../../../backend/src/Model'
import CustomIconButton from '../../helper/CustomIconButton'
import Delete from '@material-ui/icons/Delete'
import React from 'react'
export const TopicDeleteButton = (props: {
node?: q.TreeNode<any>
deleteTopicAction: (node: q.TreeNode<any>) => void
}) => {
const { node } = props
if (!node || !node.message || !node.message.payload) {
return null
}
return (
<CustomIconButton onClick={() => props.deleteTopicAction(node)} tooltip="Clear this topic">
<Delete />
</CustomIconButton>
)
}
@@ -0,0 +1,46 @@
import * as q from '../../../../../backend/src/Model'
import Copy from '../../helper/Copy'
import Panel from '../Panel'
import React, { useMemo, useCallback } from 'react'
import Topic from './Topic'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
import { sidebarActions } from '../../../actions'
import { TopicDeleteButton } from './TopicDeleteButton'
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 = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
if (!topic) {
return
}
props.actions.clearTopic(topic, recursive)
}, [])
return useMemo(
() => (
<Panel disabled={!Boolean(node)}>
<span>
Topic {copyTopic}
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
</span>
<Topic node={node} />
</Panel>
),
[node, node && node.childTopicCount()]
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(sidebarActions, dispatch),
}
}
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 / formatted sparkplugb protojson">
<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,7 @@ 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))
// @ts-ignore
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
@@ -4,8 +4,9 @@ import CodeDiff from '../CodeDiff'
import { AppState } from '../../../reducers'
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 { Fade } from '@material-ui/core'
import { Decoder } from '../../../../../backend/src/Model/Decoder'
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,20 +62,30 @@ 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} />
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
{this.props.message?.payload?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
{this.renderValue()}
</div>
)
@@ -68,38 +95,22 @@ class ValueRenderer extends React.Component<Props, State> {
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)
}
}
+40 -15
View File
@@ -1,48 +1,73 @@
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'
const parseDuration = require('parse-duration')
interface Props {
history: q.MessageHistory
dotPath?: string
timeInterval?: string
interpolation?: PlotCurveTypes
range?: [number?, number?]
color?: string
}
function nodeToHistory(history: q.MessageHistory) {
return history
.toArray()
function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Message>) {
if (startTime) {
const threshold = new Date(Date.now() - startTime)
return data.filter(d => d.received >= threshold)
}
return data
}
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
}
function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
return history
.toArray()
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
let json = {}
let json: any = {}
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) {
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
return <PlotHistory color={props.color} range={props.range} interpolation={props.interpolation} data={data} />
function TopicPlot(props: Props) {
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
const data = React.useMemo(
() =>
props.dotPath
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
: nodeToHistory(startOffset, props.history),
[props.history.last(), startOffset, props.dotPath]
)
return (
<PlotHistory
timeRangeStart={startOffset}
color={props.color}
range={props.range}
interpolation={props.interpolation}
data={data}
/>
)
}
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.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,24 +14,34 @@ 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)
return (
<span key="edge" className={this.props.classes.sourceEdge}>
<span key="edge" className={this.props.classes.sourceEdge} data-test-topic={name}>
{name}
</span>
)
}
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,30 +1,38 @@
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)
timeout = setTimeout(
() =>
(animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.remove(className)
})),
500
)
})
const timeout = setTimeout(
() =>
(animationFrame = requestAnimationFrame(() => {
ref.current && ref.current.classList.remove(className)
})),
500
)
return function cleanup() {
clearTimeout(timeout)
timeout && clearTimeout(timeout)
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
}
}
}
+26 -25
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,12 +27,12 @@ export interface Props {
}
function TreeNodeComponent(props: Props) {
const { actions, classes, className, settings, theme, treeNode, lastUpdate, name } = props
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
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)
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
const animationClass =
props.theme.palette.type === 'light' ? props.classes.animationLight : props.classes.animationDark
@@ -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,29 +61,32 @@ 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(() => {
didSelectTopic()
}, [didSelectTopic])
const mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
didSelectTopic()
}
}
const mouseOver = useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.payload) {
didSelectTopic()
}
},
[didSelectTopic, settings]
)
useEffect(() => {
treeNode.viewModel && treeNode.viewModel.setExpanded(!isCollapsed, false)
@@ -111,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>
)
}, [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,
},
},
}
}
+25 -29
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 = () => {
@@ -92,26 +91,26 @@ class TreeComponent extends React.PureComponent<Props, State> {
const updateInterval = Math.max(expectedRenderTime * 7, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
this.updateTimer = setTimeout(() => {
window.requestIdleCallback(
() => {
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.renderTime = performance.now()
this.updateTimer = setTimeout(
() => {
window.requestIdleCallback(
() => {
this.updateTimer && clearTimeout(this.updateTimer)
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 })
},
{ timeout: 100 }
)
},
{ timeout: 500 }
)
}, Math.max(0, timeUntilNextUpdate))
window.requestIdleCallback(
() => {
this.setState({ lastUpdate: this.renderTime })
},
{ timeout: 100 }
)
},
{ timeout: 500 }
)
},
Math.max(0, timeUntilNextUpdate)
)
}
public componentWillUpdate() {
@@ -173,7 +172,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TreeComponent)
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
+17 -18
View File
@@ -13,6 +13,7 @@ import { Theme, withStyles } from '@material-ui/core/styles'
import { updateNotifierActions } from '../actions'
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@material-ui/core'
import { rendererRpc, getAppVersion } from '../../../events'
interface Props {
showUpdateNotification: boolean
@@ -45,23 +46,26 @@ 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: [] }
const ownVersion = electron.remote.app.getVersion()
this.fetchReleases().then(releases => {
const newerVersions = releases
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
.filter(release => compareVersions(release.tag_name, ownVersion) > 0)
.sort((a, b) => compareVersions(b.tag_name, a.tag_name))
this.checkForUpdates()
}
if (newerVersions.length > 0) {
this.setState({ newerVersions })
this.props.actions.showUpdateNotification(true)
}
})
private async checkForUpdates() {
const ownVersion = await rendererRpc.call(getAppVersion, undefined, 10000)
const releases = await this.fetchReleases()
const newerVersions = releases
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
.filter(release => compareVersions(release.tag_name, ownVersion) > 0)
.sort((a, b) => compareVersions(b.tag_name, a.tag_name))
if (newerVersions.length > 0) {
this.setState({ newerVersions })
this.props.actions.showUpdateNotification(true)
}
}
private allowPrereleaseIfOwnVersionIsBeta(release: GithubRelease, ownVersion: string) {
@@ -266,9 +270,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>
)

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