mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-12 01:23:31 +00:00
Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8f1ddfebb | ||
|
|
6e4d08e4b5 | ||
|
|
5da8fe0f90 | ||
|
|
df9eda4866 | ||
|
|
6b030ab5ee | ||
|
|
e1493db7c8 | ||
|
|
205ea00c41 | ||
|
|
d428428a6e | ||
|
|
24e9c4cd22 | ||
|
|
d253c6c764 | ||
|
|
13b8f8d5da | ||
|
|
e4add31793 | ||
|
|
9cdfa2de7b | ||
|
|
355e9177fc | ||
|
|
dbb6ead7ba | ||
|
|
ad888f6f9a | ||
|
|
07458cd712 | ||
|
|
6fc0d3f28d | ||
|
|
7a91e4dee6 | ||
|
|
3c40be97b7 | ||
|
|
ff00f7a99e | ||
|
|
b72fc48bdb | ||
|
|
e87a7115c5 | ||
|
|
14dac22732 | ||
|
|
ccdaaa6ce2 | ||
|
|
487eee93cd | ||
|
|
59737edfb4 | ||
|
|
30af13f793 | ||
|
|
19e8bfdb37 | ||
|
|
a89d7cf62e | ||
|
|
65e9f2e074 | ||
|
|
ff66d10aaa | ||
|
|
5203d40a24 | ||
|
|
237c718a0a | ||
|
|
fbfbe94d19 | ||
|
|
e24e505cc0 | ||
|
|
28743ba646 | ||
|
|
f63e73ee0a | ||
|
|
fbad7afa79 | ||
|
|
34cc38ab3c | ||
|
|
8645789550 | ||
|
|
4db6e7b1d7 | ||
|
|
1fae61b1fa | ||
|
|
b26eac3edb | ||
|
|
6d1354bf07 | ||
|
|
7148e302f6 | ||
|
|
763a1aea69 | ||
|
|
94a92e9b29 | ||
|
|
922f90c4eb | ||
|
|
ff11973a9a | ||
|
|
29fc2eea4d | ||
|
|
577e1e6b10 | ||
|
|
d113fab279 | ||
|
|
a6c401ab6a | ||
|
|
6cc8c8810b | ||
|
|
040a29da30 | ||
|
|
45f31ad931 | ||
|
|
39ddb984ab | ||
|
|
197788eb79 | ||
|
|
c85bc0323c | ||
|
|
65c2a6eff1 | ||
|
|
5a4f3a0388 | ||
|
|
03f9c6a26e | ||
|
|
ae7ecd4bc9 | ||
|
|
25469936fa | ||
|
|
bc2d066bb9 | ||
|
|
71e376e0e4 | ||
|
|
354b014245 | ||
|
|
d69779c497 | ||
|
|
af1219cda8 | ||
|
|
c7ea6790c8 | ||
|
|
779f1e9769 | ||
|
|
f690b3f5a7 | ||
|
|
39d87d5a8e | ||
|
|
6781170b85 | ||
|
|
7829a27c69 |
+5
-1
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"language": "en",
|
||||
"words": [
|
||||
"Bbreak",
|
||||
"nodered",
|
||||
"goog",
|
||||
"thomasnordquist",
|
||||
"nowrap",
|
||||
"subheader",
|
||||
@@ -43,6 +46,7 @@
|
||||
"DEVTOOLS",
|
||||
"mixins",
|
||||
"Explorerdmg",
|
||||
"heapsnapshot"
|
||||
"heapsnapshot",
|
||||
"noconflict"
|
||||
]
|
||||
}
|
||||
|
||||
+8
-2
@@ -19,7 +19,7 @@ os:
|
||||
|
||||
osx_image: xcode10.2
|
||||
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
|
||||
services:
|
||||
- docker
|
||||
@@ -35,6 +35,12 @@ script:
|
||||
- export TRAVIS_BUILD_NUMBER="" # Override travis build number since it is uses for tagging the binary version https://github.com/electron-userland/electron-builder/issues/3730
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app thomasnordquist/ui-test-recording-env sh -c "cd app && docker/testMounted.sh"; fi
|
||||
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package linux; fi
|
||||
- |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then
|
||||
openssl aes-256-cbc -d -in res/snapstore-credentials.enc -out credentials -k $SNAPSTORE_CREDENTIALS_DECRYPTION_KEY;
|
||||
snapcraft login --with credentials;
|
||||
rm credentials;
|
||||
yarn run package linux;
|
||||
fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then unset CSC_LINK; yarn run package -- win; fi
|
||||
|
||||
@@ -20,6 +20,7 @@ Pull-Requests and error reports are welcome.
|
||||
## Run from sources
|
||||
|
||||
```bash
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn build
|
||||
yarn start
|
||||
@@ -29,6 +30,7 @@ yarn start
|
||||
|
||||
Launch Application
|
||||
```bash
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn dev
|
||||
```
|
||||
@@ -39,25 +41,52 @@ The `app` directory contains all the rendering logic, the `backend` directory cu
|
||||
|
||||
To achieve a reliable product automated tests run regularly on travis.
|
||||
|
||||
- Data model
|
||||
- MQTT integration
|
||||
- UI-Tests (The demo is a recorded ui test)
|
||||
- Data model
|
||||
- MQTT integration
|
||||
- UI-Tests (The demo is a recorded ui test)
|
||||
|
||||
## Run UI-tests
|
||||
|
||||
A [mosquitto](https://mosquitto.org/) MQTT broker is required to run the ui-tests.
|
||||
|
||||
Run tests with
|
||||
|
||||
```bash
|
||||
# Run chromedriver in a separate terminal session
|
||||
./node_modules/.bin/chromedriver --url-base=wd/hub --port=9515 --verbose
|
||||
```
|
||||
|
||||
Compile and execute tests
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
node dist/src/spec/webdriverio.js
|
||||
```
|
||||
|
||||
## Write docs
|
||||
|
||||
```
|
||||
git clone https://github.com/thomasnordquist/MQTT-Explorer.git mqtt-explorer-pages
|
||||
cd mqtt-explorer-pages
|
||||
git checkout gh-pages
|
||||
bundle install
|
||||
bundle exec jekyll serve --incremental
|
||||
```
|
||||
|
||||
Readme file: `Readme.tpl.md`
|
||||
|
||||
Preview is available at
|
||||
http://localhost:4000/Readme.tpl
|
||||
|
||||
## Update docs
|
||||
|
||||
```
|
||||
npm install
|
||||
./updateReadme.ts
|
||||
```
|
||||
|
||||
The readme will be generated from the docs.
|
||||
|
||||
## License
|
||||
|
||||

|
||||
|
||||
+170
-153
@@ -1,180 +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: #595585;
|
||||
}
|
||||
50% {
|
||||
background-color: #595585;
|
||||
}
|
||||
100% {
|
||||
background-color: none;
|
||||
}
|
||||
50% {
|
||||
background-color: #595585;
|
||||
}
|
||||
|
||||
@keyframes updateLight {
|
||||
0% {
|
||||
background-color: none;
|
||||
color: inherit;
|
||||
}
|
||||
25% {
|
||||
background-color: #c0c8c0;
|
||||
}
|
||||
50% {
|
||||
background-color: #c0c8c0;
|
||||
}
|
||||
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>
|
||||
+38
-32
@@ -4,28 +4,31 @@
|
||||
"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.7 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
|
||||
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
|
||||
"mochatest": "mocha --require ts-node/register src/**/*.spec.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
},
|
||||
"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.26.0",
|
||||
"compare-versions": "^3.5.0",
|
||||
"copy-text-to-clipboard": "^2.1.0",
|
||||
"d3": "^5.9.2",
|
||||
"d3": "^5.9.7",
|
||||
"d3-shape": "^1.3.5",
|
||||
"diff": "^4.0.1",
|
||||
"dot-prop": "^5.0.0",
|
||||
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
|
||||
"file-loader": "^4.0.0",
|
||||
"file-loader": "6",
|
||||
"get-value": "^3.0.1",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"in-viewport": "^3.6.0",
|
||||
@@ -33,55 +36,58 @@
|
||||
"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",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mocha": "^9.2.1",
|
||||
"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": "^17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -65,35 +73,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 +109,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 +182,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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -105,21 +105,15 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
|
||||
const topicFilter = filterStr.toLowerCase()
|
||||
|
||||
const nodeFilter = (node: q.TreeNode<TopicViewModel>): boolean => {
|
||||
const topicMatches =
|
||||
node
|
||||
.path()
|
||||
.toLowerCase()
|
||||
.indexOf(topicFilter) !== -1
|
||||
const topicMatches = node.path().toLowerCase().indexOf(topicFilter) !== -1
|
||||
if (topicMatches) {
|
||||
return true
|
||||
}
|
||||
|
||||
const messageMatches =
|
||||
node.message &&
|
||||
node.message.value &&
|
||||
Base64Message.toUnicodeString(node.message.value)
|
||||
.toLowerCase()
|
||||
.indexOf(filterStr) !== -1
|
||||
node.message.payload &&
|
||||
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1
|
||||
|
||||
return Boolean(messageMatches)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ const debouncedSelectTopic = debounce(
|
||||
|
||||
// Update publish topic
|
||||
let setTopicDispatch: any | undefined
|
||||
if (!getState().publish.topic) {
|
||||
if (!getState().publish.manualTopic) {
|
||||
setTopicDispatch = setTopic(topic.path())
|
||||
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.topic) {
|
||||
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.manualTopic) {
|
||||
setTopicDispatch = setTopic(topic.path())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import { makePublishEvent, rendererEvents } from '../../../events'
|
||||
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
|
||||
import { globalActions } from '.'
|
||||
|
||||
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => async (
|
||||
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
const topicsForPurging = recursive ? [topic, ...topic.childTopics()] : [topic]
|
||||
|
||||
if (recursive) {
|
||||
const topicCount = topic.childTopicCount()
|
||||
const deleteLimitMessage =
|
||||
topicCount > subtopicClearLimit ? ` You can only delete ${subtopicClearLimit} child topics at once.` : ''
|
||||
|
||||
const topicDelta = topic.hasMessage() ? -1 : 0
|
||||
const childTopicsMessage =
|
||||
@@ -23,7 +23,7 @@ export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicC
|
||||
const confirmed = await dispatch(
|
||||
globalActions.requestConfirmation(
|
||||
'Confirm delete',
|
||||
`Do you want to delete "${topic.path()}"${childTopicsMessage}?${deleteLimitMessage}`
|
||||
`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) {
|
||||
@@ -38,27 +38,19 @@ export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicC
|
||||
return
|
||||
}
|
||||
const publishEvent = makePublishEvent(connectionId)
|
||||
const mqttMessage = {
|
||||
topic: topic.path(),
|
||||
payload: null,
|
||||
retain: true,
|
||||
qos: 0 as 0,
|
||||
}
|
||||
rendererEvents.emit(publishEvent, mqttMessage)
|
||||
if (recursive) {
|
||||
topic
|
||||
.childTopics()
|
||||
.filter(topic => Boolean(topic.message && topic.message.value))
|
||||
.slice(0, subtopicClearLimit)
|
||||
.forEach((topic, idx) => {
|
||||
const mqttMessage = {
|
||||
topic: topic.path(),
|
||||
payload: null,
|
||||
retain: true,
|
||||
qos: 0 as 0,
|
||||
}
|
||||
// Rate limit deletion
|
||||
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
|
||||
})
|
||||
}
|
||||
|
||||
topicsForPurging
|
||||
.filter(t => t.path() !== '' && t.hasMessage())
|
||||
.map(t => t.path())
|
||||
.forEach((path, idx) => {
|
||||
const mqttMessage = {
|
||||
topic: path,
|
||||
payload: null,
|
||||
retain: true,
|
||||
qos: 0 as 0,
|
||||
messageId: undefined,
|
||||
}
|
||||
// Rate limit deletion
|
||||
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -13,6 +13,8 @@ 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'))
|
||||
|
||||
@@ -66,6 +68,8 @@ class App extends React.PureComponent<Props, {}> {
|
||||
return null
|
||||
}
|
||||
|
||||
const anyProps: any = {};
|
||||
|
||||
return (
|
||||
<div className={centerContent}>
|
||||
<CssBaseline />
|
||||
@@ -73,7 +77,7 @@ class App extends React.PureComponent<Props, {}> {
|
||||
<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}`}>
|
||||
@@ -157,9 +161,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
|
||||
|
||||
@@ -2,16 +2,20 @@ 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
|
||||
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
|
||||
? [defaultFor(props.range[0], calculatedDomain[0]), defaultFor(props.range[1], calculatedDomain[1])]
|
||||
: calculatedDomain
|
||||
|
||||
return yDomain
|
||||
}, [props.data])
|
||||
}, [props.data, props.range])
|
||||
}
|
||||
|
||||
function domainForData(data: Array<Point>): [number, number] {
|
||||
@@ -19,8 +23,10 @@ function domainForData(data: Array<Point>): [number, number] {
|
||||
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
|
||||
|
||||
@@ -65,7 +65,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(ColorSettings))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(ColorSettings))
|
||||
|
||||
@@ -60,7 +60,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(InterpolationSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(InterpolationSettings)
|
||||
|
||||
@@ -33,7 +33,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(MoveUp)
|
||||
export default connect(undefined, mapDispatchToProps)(MoveUp)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -47,7 +47,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(Size))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(Size))
|
||||
|
||||
@@ -96,7 +96,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(TimeRangeSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
|
||||
|
||||
@@ -25,6 +25,7 @@ function ChartSettings(props: {
|
||||
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
|
||||
const [sizeVisible, setSizeVisible] = React.useState(false)
|
||||
const [colorVisible, setColorVisible] = React.useState(false)
|
||||
const open = props.open
|
||||
|
||||
const toggleRange = React.useCallback(() => {
|
||||
if (open) {
|
||||
|
||||
@@ -133,7 +133,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(TopicChart))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
|
||||
|
||||
@@ -126,7 +126,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ChartPanel))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel))
|
||||
|
||||
@@ -42,7 +42,9 @@ function ConfirmationDialog(props: { confirmationRequests: Array<ConfirmationReq
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{request.title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">{request.inquiry}</DialogContentText>
|
||||
<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>
|
||||
|
||||
@@ -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,8 +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 } 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
|
||||
@@ -18,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 {
|
||||
@@ -142,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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -134,7 +134,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ConnectionSetup))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
|
||||
|
||||
@@ -51,7 +51,4 @@ export const connectionItemStyle = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
|
||||
@@ -77,7 +77,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ProfileList))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Delete from '@material-ui/icons/Delete'
|
||||
import { connectionManagerActions } from '../../actions'
|
||||
import { ConnectionOptions } from '../../model/ConnectionOptions'
|
||||
import {
|
||||
IconButton,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Theme,
|
||||
} from '@material-ui/core'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { withStyles } from '@material-ui/styles'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
function Subscriptions(props: {
|
||||
classes: any
|
||||
connection: ConnectionOptions
|
||||
managerActions: typeof connectionManagerActions
|
||||
}) {
|
||||
const { classes, connection, managerActions } = props
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper} className={`${classes.topicList} advanced-connection-settings-topic-list`}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="left" padding="checkbox" className={classes.tableTitleCell}></TableCell>
|
||||
<TableCell className={classes.tableTitleCell}>Topic</TableCell>
|
||||
<TableCell align="right" className={classes.tableTitleCell}>
|
||||
QoS
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{connection.subscriptions.map(subscription => (
|
||||
<TableRow key={subscription.topic + '_qos_' + subscription.qos}>
|
||||
<TableCell align="right" className={classes.tableCell}>
|
||||
<IconButton
|
||||
onClick={() => managerActions.deleteSubscription(subscription, connection.id)}
|
||||
style={{ padding: '6px' }}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" scope="row" className={classes.tableCell}>
|
||||
{subscription.topic}
|
||||
</TableCell>
|
||||
<TableCell align="right" className={classes.tableCell}>
|
||||
{subscription.qos}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
managerActions: bindActionCreators(connectionManagerActions, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
tableCell: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
wordBbreak: 'break-word',
|
||||
},
|
||||
tableTitleCell: {
|
||||
paddingTop: `${theme.spacing(0.5)}px`,
|
||||
paddingBottom: `${theme.spacing(0.5)}px`,
|
||||
},
|
||||
topicList: {
|
||||
height: '196px',
|
||||
overflowY: 'scroll' as 'scroll',
|
||||
margin: `${theme.spacing(1)}px ${theme.spacing(1)}px 0 ${theme.spacing(1)}px`,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
width: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions))
|
||||
@@ -43,15 +43,15 @@ class Demo extends React.Component<{ classes: any }, State> {
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
;(window as any).demo.enableMouse = () => {
|
||||
; (window as any).demo.enableMouse = () => {
|
||||
this.setState({ enabled: true })
|
||||
}
|
||||
;(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
|
||||
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
|
||||
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
|
||||
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
|
||||
this.moveCloser()
|
||||
}
|
||||
; (window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
|
||||
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
|
||||
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
|
||||
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
|
||||
this.moveCloser()
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -5,14 +5,14 @@ let heapdump: any
|
||||
|
||||
function writeHeapdump(path?: string) {
|
||||
if (!heapdump) {
|
||||
heapdump = require('heapdump')
|
||||
//<heapdump = require('heapdump')
|
||||
}
|
||||
|
||||
heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`)
|
||||
return path
|
||||
}
|
||||
|
||||
;(window as any).demo = {
|
||||
; (window as any).demo = {
|
||||
writeHeapdump,
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -33,7 +32,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: any) {
|
||||
electronRendererTelemetry.trackError(error)
|
||||
// electronRendererTelemetry.trackError(error)
|
||||
console.log('did catch', error)
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(PauseButton))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -103,7 +103,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(TitleBar))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
|
||||
|
||||
+7
-34
@@ -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)
|
||||
@@ -123,8 +123,8 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
|
||||
return null
|
||||
}
|
||||
|
||||
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
|
||||
let value = node.message && node.message.value ? parseFloat(str) : NaN
|
||||
const str = node.message.payload ? Base64Message.toUnicodeString(node.message.payload) : ''
|
||||
let value = node.message && node.message.payload ? parseFloat(str) : NaN
|
||||
value = !isNaN(value) ? abbreviate(value) : str
|
||||
|
||||
return (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
|
||||
import Message from './Model/Message'
|
||||
import Navigation from '@material-ui/icons/Navigation'
|
||||
import PublishHistory from './PublishHistory'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useMemo, useState, useRef, memo } from 'react'
|
||||
import RetainSwitch from './RetainSwitch'
|
||||
import TopicInput from './TopicInput'
|
||||
import { AppState } from '../../../reducers'
|
||||
@@ -13,6 +13,7 @@ import { connect } from 'react-redux'
|
||||
import { EditorModeSelect } from './EditorModeSelect'
|
||||
import { globalActions, publishActions } from '../../../actions'
|
||||
import { KeyCodes } from '../../../utils/KeyCodes'
|
||||
import { default as AceEditor } from 'react-ace'
|
||||
|
||||
interface Props {
|
||||
connectionId?: string
|
||||
@@ -41,8 +42,13 @@ function useHistory(): [Array<Message>, (topic: string, payload?: string) => voi
|
||||
}
|
||||
|
||||
function Publish(props: Props) {
|
||||
const editorRef = useRef<AceEditor>()
|
||||
const [history, amendToHistory] = useHistory()
|
||||
|
||||
const focusEditor = useCallback(() => {
|
||||
editorRef.current?.editor.focus()
|
||||
}, [editorRef])
|
||||
|
||||
const publish = useCallback(() => {
|
||||
if (!props.connectionId) {
|
||||
return
|
||||
@@ -70,17 +76,23 @@ function Publish(props: Props) {
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<div style={{ flexGrow: 1 }} onKeyDown={handleSubmit}>
|
||||
<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}
|
||||
/>
|
||||
<Editor value={props.payload} editorMode={props.editorMode} onChange={props.actions.setPayload} />
|
||||
<Editor
|
||||
value={props.payload}
|
||||
editorMode={props.editorMode}
|
||||
onChange={props.actions.setPayload}
|
||||
editorRef={editorRef as any}
|
||||
/>
|
||||
<RetainSwitch />
|
||||
</div>
|
||||
<PublishHistory history={history} />
|
||||
@@ -90,9 +102,10 @@ function Publish(props: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
function EditorMode(props: {
|
||||
const EditorMode = memo(function EditorMode(props: {
|
||||
payload?: string
|
||||
editorMode: string
|
||||
focusEditor: () => void
|
||||
actions: typeof publishActions
|
||||
globalActions: typeof globalActions
|
||||
publish: () => void
|
||||
@@ -109,46 +122,48 @@ function EditorMode(props: {
|
||||
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
|
||||
updatePayload(str)
|
||||
} catch (error) {
|
||||
props.globalActions.showError(`Format error: ${error.message}`)
|
||||
props.globalActions.showError(`Format error: ${(error as Error)?.message}`)
|
||||
}
|
||||
}
|
||||
}, [props.payload])
|
||||
|
||||
const renderFormatJson = useCallback(() => {
|
||||
if (props.editorMode !== 'json') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Format JSON">
|
||||
<Fab
|
||||
style={{ width: '36px', height: '36px', margin: '0 8px' }}
|
||||
onClick={formatJson}
|
||||
id="sidebar-publish-format-json"
|
||||
>
|
||||
<FormatAlignLeft style={{ fontSize: '20px' }} />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
)
|
||||
}, [formatJson, props.editorMode, props.publish])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
|
||||
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
|
||||
{renderFormatJson()}
|
||||
<div style={{ float: 'right' }}>
|
||||
<PublishButton publish={props.publish} />
|
||||
</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>
|
||||
),
|
||||
[props.editorMode, renderFormatJson]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const PublishButton = (props: { publish: () => void }) => {
|
||||
const FormatJsonButton = React.memo(function FormatJsonButton(props: {
|
||||
editorMode: string
|
||||
focusEditor: () => void
|
||||
formatJson: () => void
|
||||
}) {
|
||||
if (props.editorMode !== 'json') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Format JSON">
|
||||
<Fab
|
||||
style={{ width: '36px', height: '36px', margin: '0 8px' }}
|
||||
onClick={props.formatJson}
|
||||
onFocus={props.focusEditor}
|
||||
id="sidebar-publish-format-json"
|
||||
>
|
||||
<FormatAlignLeft style={{ fontSize: '20px' }} />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
|
||||
const handleClickPublish = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -157,15 +172,19 @@ const PublishButton = (props: { publish: () => void }) => {
|
||||
[props.publish]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
|
||||
<Navigation style={{ marginRight: '8px' }} /> Publish
|
||||
</Button>
|
||||
),
|
||||
[handleClickPublish]
|
||||
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 {
|
||||
@@ -176,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))
|
||||
|
||||
@@ -34,7 +34,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(PublishHistory)
|
||||
export default connect(undefined, mapDispatchToProps)(PublishHistory)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { publishActions } from '../../../actions'
|
||||
import { QosSelect } from '../../QosSelect'
|
||||
import { QoS } from '../../../../../backend/src/DataSource/MqttSource'
|
||||
|
||||
interface Props {
|
||||
qos: QoS
|
||||
actions: {
|
||||
publish: typeof publishActions
|
||||
}
|
||||
}
|
||||
|
||||
function QosPublishOption(props: Props) {
|
||||
return <QosSelect onChange={props.actions.publish.setQoS} selected={props.qos} />
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
publish: bindActionCreators(publishActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
qos: state.publish.qos,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QosPublishOption)
|
||||
@@ -1,4 +1,4 @@
|
||||
import QosSelect from './QosSelect'
|
||||
import QosSelect from './QosPublishOption'
|
||||
import React from 'react'
|
||||
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
|
||||
import { publishActions } from '../../../actions'
|
||||
@@ -41,7 +41,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(RetainSwitch)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RetainSwitch)
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import ClearAdornment from '../../helper/ClearAdornment'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import { FormControl, Input, InputLabel } from '@material-ui/core'
|
||||
import { publishActions } from '../../../actions'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
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>) => {
|
||||
@@ -21,7 +24,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const topicStr = props.topic || ''
|
||||
const topicStr = props.manualTopic || ''
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
@@ -29,6 +32,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
<FormControl style={{ width: '100%' }}>
|
||||
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
|
||||
<Input
|
||||
inputRef={inputElement}
|
||||
id="publish-topic"
|
||||
value={topicStr}
|
||||
startAdornment={<span />}
|
||||
@@ -36,7 +40,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
onBlur={onTopicBlur}
|
||||
onChange={updateTopic}
|
||||
multiline={false}
|
||||
placeholder="example/topic"
|
||||
placeholder={props.selectedTopic ?? 'example/topic'}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -53,11 +57,9 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topic: state.publish.topic,
|
||||
manualTopic: state.publish.manualTopic,
|
||||
selectedTopic: state.tree.get('selectedTopic')?.path(),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TopicInput)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TopicInput)
|
||||
|
||||
@@ -99,9 +99,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Sidebar)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
|
||||
|
||||
@@ -13,25 +13,23 @@ export const RecursiveTopicDeleteButton = (props: {
|
||||
if (props.node) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
props.deleteTopicAction(props.node, true, deleteLimit)
|
||||
props.deleteTopicAction(props.node, true, Infinity)
|
||||
}
|
||||
},
|
||||
[props.node]
|
||||
)
|
||||
|
||||
if (!props.node) {
|
||||
return null
|
||||
}
|
||||
const deleteLimit = 50
|
||||
|
||||
const topicCount = props.node ? props.node.childTopicCount() : 0
|
||||
if (topicCount === 0 || (props.node.message && topicCount === 1)) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Badge
|
||||
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
|
||||
color="secondary"
|
||||
>
|
||||
<CustomIconButton onClick={onClick} tooltip={`Deletes up to ${deleteLimit} sub-topics with a single click`}>
|
||||
<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>
|
||||
|
||||
@@ -66,7 +66,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles, { withTheme: true })(Topic))
|
||||
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic))
|
||||
|
||||
@@ -8,7 +8,7 @@ export const TopicDeleteButton = (props: {
|
||||
deleteTopicAction: (node: q.TreeNode<any>) => void
|
||||
}) => {
|
||||
const { node } = props
|
||||
if (!node || !node.message || !node.message.value) {
|
||||
if (!node || !node.message || !node.message.payload) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -14,12 +14,12 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
|
||||
console.log(node && node.path())
|
||||
const copyTopic = node ? <Copy value={node.path()} /> : null
|
||||
|
||||
const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false, maxCount = 50) => {
|
||||
const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
|
||||
if (!topic) {
|
||||
return
|
||||
}
|
||||
|
||||
props.actions.clearTopic(topic, recursive, maxCount)
|
||||
props.actions.clearTopic(topic, recursive)
|
||||
}, [])
|
||||
|
||||
return useMemo(
|
||||
@@ -43,7 +43,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(TopicPanel)
|
||||
export default connect(undefined, mapDispatchToProps)(TopicPanel)
|
||||
|
||||
@@ -73,7 +73,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ActionButtons))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons))
|
||||
|
||||
@@ -33,7 +33,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(DeleteSelectedTopicButton)
|
||||
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')
|
||||
|
||||
@@ -81,19 +82,25 @@ class MessageHistory extends React.PureComponent<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>
|
||||
|
||||
<MessageId message={message} />
|
||||
</span>
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value} />
|
||||
</div>
|
||||
@@ -106,7 +113,7 @@ class MessageHistory extends React.PureComponent<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
|
||||
@@ -120,9 +127,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
undefined
|
||||
)
|
||||
) : undefined
|
||||
}
|
||||
onClick={this.displayMessage}
|
||||
>
|
||||
@@ -139,7 +144,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(MessageHistory)
|
||||
export default connect(null, mapDispatchToProps)(MessageHistory)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Theme, Typography, withStyles } from '@material-ui/core'
|
||||
import { connect } from 'react-redux'
|
||||
import { sidebarActions } from '../../../actions'
|
||||
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
|
||||
import { MessageId } from '../MessageId'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode<any>
|
||||
@@ -36,7 +37,7 @@ function ValuePanel(props: Props) {
|
||||
const { node, compareMessage } = props
|
||||
|
||||
function renderViewOptions() {
|
||||
if (!props.node || !props.node.message || !props.node.mqttMessage) {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ function ValuePanel(props: Props) {
|
||||
<ActionButtons />
|
||||
</span>
|
||||
<span style={{ flex: 6, textAlign: 'right' }}>
|
||||
{props.node.mqttMessage.retain ? <DeleteSelectedTopicButton /> : null}
|
||||
{props.node.message.retain ? <DeleteSelectedTopicButton /> : null}
|
||||
</span>
|
||||
{messageMetaInfo()}
|
||||
</div>
|
||||
@@ -54,13 +55,16 @@ function ValuePanel(props: Props) {
|
||||
}
|
||||
|
||||
function messageMetaInfo() {
|
||||
if (!props.node || !props.node.message || !props.node.mqttMessage) {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
|
||||
<Typography style={{ textAlign: 'right' }}>QoS: {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={props.node.message.received} />
|
||||
@@ -82,8 +86,8 @@ function ValuePanel(props: Props) {
|
||||
)
|
||||
|
||||
const copyValue =
|
||||
node && node.message && node.message.value ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
|
||||
node && node.message && node.message.payload ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.payload)} />
|
||||
) : null
|
||||
|
||||
return (
|
||||
@@ -124,7 +128,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ValuePanel))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
|
||||
|
||||
@@ -63,12 +63,12 @@ class ValueRenderer extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
private renderRawMode(message: q.Message, compare?: q.Message) {
|
||||
if (!message.value) {
|
||||
if (!message.payload) {
|
||||
return
|
||||
}
|
||||
const [value, valueLanguage] = this.convertMessage(message.value)
|
||||
const [value, valueLanguage] = this.convertMessage(message.payload)
|
||||
const [compareStr, compareStrLanguage] =
|
||||
compare && compare.value ? this.convertMessage(compare.value) : [undefined, undefined]
|
||||
compare && compare.payload ? this.convertMessage(compare.payload) : [undefined, undefined]
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -95,12 +95,12 @@ class ValueRenderer extends React.Component<Props, State> {
|
||||
if (renderMode === 'raw') {
|
||||
return this.renderRawMode(message, compareWith)
|
||||
}
|
||||
if (!message.value) {
|
||||
if (!message.payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
const compareValue = compareMessage.value || message.value
|
||||
const [current, currentLanguage] = this.convertMessage(message.value)
|
||||
const compareValue = compareMessage.payload || message.payload
|
||||
const [current, currentLanguage] = this.convertMessage(message.payload)
|
||||
const [compare, compareLanguage] = this.convertMessage(compareValue)
|
||||
|
||||
const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined
|
||||
|
||||
@@ -28,7 +28,7 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
|
||||
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
|
||||
return filterUsingTimeRange(startTime, history.toArray())
|
||||
.map((message: q.Message) => {
|
||||
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
|
||||
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN
|
||||
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
||||
})
|
||||
.filter(data => !isNaN(data.y as any)) as any
|
||||
@@ -39,7 +39,7 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
|
||||
.map((message: q.Message) => {
|
||||
let json = {}
|
||||
try {
|
||||
json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
|
||||
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {}
|
||||
} catch (ignore) {}
|
||||
|
||||
const value = dotProp.get(json, dotPath)
|
||||
|
||||
@@ -27,17 +27,17 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
|
||||
|
||||
private truncatedMessage() {
|
||||
const limit = 400
|
||||
if (!this.props.treeNode.message || !this.props.treeNode.message.value) {
|
||||
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const str = Base64Message.toUnicodeString(this.props.treeNode.message.value)
|
||||
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}>
|
||||
{' '}
|
||||
|
||||
@@ -9,7 +9,7 @@ export function useDeleteKeyCallback(topic: q.TreeNode<any>, actions: typeof tre
|
||||
if (event.keyCode === KeyCodes.delete || event.keyCode === KeyCodes.backspace) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
actions.clearTopic(topic, true, 50)
|
||||
actions.clearTopic(topic, true)
|
||||
}
|
||||
},
|
||||
[topic]
|
||||
|
||||
@@ -81,7 +81,7 @@ function TreeNodeComponent(props: Props) {
|
||||
const mouseOver = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
|
||||
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.payload) {
|
||||
didSelectTopic()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -169,7 +169,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TreeComponent)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
|
||||
|
||||
@@ -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
|
||||
@@ -50,18 +51,21 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
|
||||
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))
|
||||
|
||||
@@ -59,7 +59,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(Copy)
|
||||
export default connect(undefined, mapDispatchToProps)(Copy)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MqttOptions } from '../../../backend/src/DataSource'
|
||||
import { v4 } from 'uuid'
|
||||
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
|
||||
const sha1 = require('sha1')
|
||||
|
||||
export interface CertificateParameters {
|
||||
@@ -7,7 +8,9 @@ export interface CertificateParameters {
|
||||
/** @property data base64 encoded data */
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface ConnectionOptions {
|
||||
configVersion: 1
|
||||
type: 'mqtt'
|
||||
id: string
|
||||
host: string
|
||||
@@ -23,7 +26,7 @@ export interface ConnectionOptions {
|
||||
clientCertificate?: CertificateParameters
|
||||
clientKey?: CertificateParameters
|
||||
clientId?: string
|
||||
subscriptions: Array<string>
|
||||
subscriptions: Array<Subscription>
|
||||
}
|
||||
|
||||
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
|
||||
@@ -52,6 +55,7 @@ function generateClientId() {
|
||||
|
||||
export function createEmptyConnection(): ConnectionOptions {
|
||||
return {
|
||||
configVersion: 1,
|
||||
certValidation: true,
|
||||
clientId: generateClientId(),
|
||||
id: v4() as string,
|
||||
@@ -59,7 +63,10 @@ export function createEmptyConnection(): ConnectionOptions {
|
||||
encryption: false,
|
||||
password: undefined,
|
||||
username: undefined,
|
||||
subscriptions: ['#', '$SYS/#'],
|
||||
subscriptions: [
|
||||
{ topic: '#', qos: 0 },
|
||||
{ topic: '$SYS/#', qos: 0 },
|
||||
],
|
||||
type: 'mqtt',
|
||||
host: '',
|
||||
port: 1883,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Action } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
import { Record, List } from 'immutable'
|
||||
import MoveUp from '../components/ChartPanel/ChartSettings/MoveUp'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { Action } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
import { MqttOptions } from '../../../backend/src/DataSource'
|
||||
import { TopicViewModel } from '../model/TopicViewModel'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Action } from 'redux'
|
||||
import { ConnectionOptions } from '../model/ConnectionOptions'
|
||||
import { createReducer } from './lib'
|
||||
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
|
||||
|
||||
export interface ConnectionManagerState {
|
||||
connections: { [s: string]: ConnectionOptions }
|
||||
@@ -51,13 +51,13 @@ export interface SelectConnection {
|
||||
|
||||
export interface AddSubscription {
|
||||
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION
|
||||
subscription: string
|
||||
subscription: Subscription
|
||||
connectionId: string
|
||||
}
|
||||
|
||||
export interface DeleteSubscription {
|
||||
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION
|
||||
subscription: string
|
||||
subscription: Subscription
|
||||
connectionId: string
|
||||
}
|
||||
|
||||
@@ -160,8 +160,12 @@ function addSubscription(state: ConnectionManagerState, action: AddSubscription)
|
||||
}
|
||||
|
||||
function deleteSubscription(state: ConnectionManagerState, action: AddSubscription): ConnectionManagerState {
|
||||
function subscriptionsEqual(v1: Subscription, v2: Subscription): boolean {
|
||||
return v1.topic == v2.topic && v1.qos == v2.qos
|
||||
}
|
||||
|
||||
const connection = state.connections[action.connectionId]
|
||||
const newSubscriptions = connection.subscriptions.filter(s => s !== action.subscription)
|
||||
const newSubscriptions = connection.subscriptions.filter(s => !subscriptionsEqual(s, action.subscription))
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -187,10 +191,29 @@ function deleteConnection(state: ConnectionManagerState, action: DeleteConnectio
|
||||
|
||||
function updateConnection(state: ConnectionManagerState, action: UpdateConnection): ConnectionManagerState {
|
||||
let connection = state.connections[action.connectionId]
|
||||
let changeSet = action.changeSet
|
||||
|
||||
// Reset empty username to undefined
|
||||
if (changeSet.username !== undefined) {
|
||||
changeSet = {
|
||||
changeSet,
|
||||
username: changeSet.username === '' ? undefined : changeSet.username,
|
||||
}
|
||||
}
|
||||
|
||||
// Reset empty password to undefined
|
||||
if (changeSet.password !== undefined) {
|
||||
changeSet = {
|
||||
changeSet,
|
||||
password: changeSet.password === '' ? undefined : changeSet.password,
|
||||
}
|
||||
}
|
||||
|
||||
connection = {
|
||||
...connection,
|
||||
...action.changeSet,
|
||||
...changeSet,
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
connections: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Action } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
|
||||
export interface PublishState {
|
||||
topic?: string
|
||||
manualTopic?: string
|
||||
payload?: string
|
||||
retain: boolean
|
||||
editorMode: string
|
||||
@@ -60,7 +59,7 @@ export const publishReducer = createReducer(initialState, {
|
||||
function setTopic(state: PublishState, action: SetTopic) {
|
||||
return {
|
||||
...state,
|
||||
topic: action.topic,
|
||||
manualTopic: action.topic,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { Action } from 'redux'
|
||||
import { Action as ReduxAction } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
import { Record } from 'immutable'
|
||||
|
||||
@@ -11,7 +11,8 @@ const initialStateFactory = Record<SidebarModel>({
|
||||
compareMessage: undefined,
|
||||
})
|
||||
|
||||
export type Action = SetCompareMessage
|
||||
export type Action = SetCompareMessage | ResetStore
|
||||
|
||||
export enum ActionTypes {
|
||||
SIDEBAR_SET_COMPARE_MESSAGE = 'SIDEBAR_SET_COMPARE_MESSAGE',
|
||||
SIDEBAR_RESET_STORE = 'SIDEBAR_RESET_STORE',
|
||||
@@ -20,7 +21,7 @@ export enum ActionTypes {
|
||||
export type SidebarState = Record<SidebarModel>
|
||||
|
||||
const actions: {
|
||||
[s: string]: (state: SidebarState, action: Action) => SidebarState
|
||||
[s: string]: (state: SidebarState, action: ReduxAction) => SidebarState
|
||||
} = {
|
||||
SIDEBAR_SET_COMPARE_MESSAGE: setCompareMessage,
|
||||
SIDEBAR_RESET_STORE: resetStore,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { Action } from 'redux'
|
||||
import { Action as ReduxAction } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
import { Record } from 'immutable'
|
||||
import { TopicViewModel } from '../model/TopicViewModel'
|
||||
@@ -50,7 +50,7 @@ const setPaused = (pause: boolean) => (state: TreeState, action: ShowTree): Tree
|
||||
}
|
||||
|
||||
const actions: {
|
||||
[s: string]: (state: TreeState, action: Action) => TreeState
|
||||
[s: string]: (state: TreeState, action: ReduxAction) => TreeState
|
||||
} = {
|
||||
TREE_SHOW_TREE: showTree,
|
||||
TREE_SELECT_TOPIC: selectTopic,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export interface Migration {
|
||||
from: number | undefined
|
||||
apply: (config: any) => any
|
||||
}
|
||||
|
||||
interface MigrationSubject {
|
||||
configVersion: number | undefined
|
||||
}
|
||||
|
||||
export class ConfigMigrator {
|
||||
private migrations: Migration[]
|
||||
|
||||
constructor(migrations: Migration[]) {
|
||||
this.migrations = migrations
|
||||
}
|
||||
|
||||
public applyMigrations(subject: MigrationSubject): Partial<MigrationSubject> {
|
||||
while (this.isMigrationNecessary(subject)) {
|
||||
subject = this.applicableMigrations(subject)
|
||||
.filter(migration => migration.from === subject.configVersion)
|
||||
.reduce((currentSubject, migration) => migration.apply(currentSubject as any), subject)
|
||||
}
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
public isMigrationNecessary(subject: MigrationSubject): boolean {
|
||||
return this.applicableMigrations(subject).length > 0
|
||||
}
|
||||
|
||||
private applicableMigrations(subject: MigrationSubject): Migration[] {
|
||||
return this.migrations.filter(migration => migration.from === subject.configVersion)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import { rendererEvents } from '../../../events'
|
||||
import { v4 } from 'uuid'
|
||||
import { rendererRpc } from '../../../events'
|
||||
|
||||
import {
|
||||
storageStoreEvent,
|
||||
makeStorageResponseEvent,
|
||||
storageLoadEvent,
|
||||
storageClearEvent,
|
||||
makeStorageAcknowledgementEvent,
|
||||
} from '../../../events/StorageEvents'
|
||||
|
||||
export interface StorageIdentifier<Model> {
|
||||
@@ -20,71 +17,23 @@ export interface PersistentStorage {
|
||||
}
|
||||
|
||||
class RemoteStorage implements PersistentStorage {
|
||||
private timeoutCallback(event: any, callback: any, reject: any) {
|
||||
setTimeout(() => {
|
||||
reject('remote storage timeout')
|
||||
rendererEvents.unsubscribe(event, callback)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
private expectAck(transactionId: string): Promise<void> {
|
||||
const ack = makeStorageAcknowledgementEvent(transactionId)
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const callback = (msg: any) => {
|
||||
if (msg && msg.error) {
|
||||
reject(msg.error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
rendererEvents.unsubscribe(ack, callback)
|
||||
}
|
||||
rendererEvents.subscribe(ack, callback)
|
||||
this.timeoutCallback(ack, callback, reject)
|
||||
})
|
||||
}
|
||||
|
||||
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
|
||||
const transactionId = v4()
|
||||
const expectation = this.expectAck(transactionId)
|
||||
rendererEvents.emit(storageStoreEvent, {
|
||||
return rendererRpc.call(storageStoreEvent, {
|
||||
data,
|
||||
transactionId,
|
||||
store: identifier.id,
|
||||
})
|
||||
return expectation
|
||||
}
|
||||
|
||||
public load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> {
|
||||
const transactionId = v4()
|
||||
const responseEvent = makeStorageResponseEvent(transactionId)
|
||||
|
||||
const promise = new Promise<Model>((resolve, reject) => {
|
||||
const callback = (msg: any) => {
|
||||
if (msg.error) {
|
||||
reject(msg.error)
|
||||
} else {
|
||||
resolve(msg.data)
|
||||
}
|
||||
rendererEvents.unsubscribe(responseEvent, callback)
|
||||
}
|
||||
rendererEvents.subscribe(responseEvent, callback)
|
||||
this.timeoutCallback(responseEvent, callback, reject)
|
||||
})
|
||||
|
||||
rendererEvents.emit(storageLoadEvent, {
|
||||
transactionId,
|
||||
public async load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> {
|
||||
const result = await rendererRpc.call(storageLoadEvent, {
|
||||
store: identifier.id,
|
||||
})
|
||||
}, 10000)
|
||||
|
||||
return promise
|
||||
return (result as any).data
|
||||
}
|
||||
|
||||
public clear(): Promise<void> {
|
||||
const transactionId = v4()
|
||||
const expectation = this.expectAck(transactionId)
|
||||
|
||||
rendererEvents.emit(storageClearEvent, { transactionId })
|
||||
return expectation
|
||||
return rendererRpc.call(storageClearEvent, undefined, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
// import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
|
||||
const telemetry = electronRendererTelemetry
|
||||
electronRendererTelemetry.registerErrorHandler()
|
||||
// const telemetry = electronRendererTelemetry
|
||||
// electronRendererTelemetry.registerErrorHandler()
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { expect } from 'chai'
|
||||
import { ConfigMigrator } from '../ConfigMigrator'
|
||||
import 'mocha'
|
||||
|
||||
describe('ConfigMigrator', () => {
|
||||
it('applies migrations to a subject with undefined configVersion', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 1,
|
||||
drink: 'Beer',
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any) as any
|
||||
|
||||
expect(migratedConfig.drink).to.eq('Beer')
|
||||
expect(migratedConfig.configVersion).to.eq(1)
|
||||
})
|
||||
|
||||
it('applies multiple migrations from the same version', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
drink: 'Beer',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any) as any
|
||||
|
||||
expect(migratedConfig.drink).to.eq('Beer')
|
||||
expect(migratedConfig.configVersion).to.eq(1)
|
||||
})
|
||||
|
||||
it('applies multiple migrations with ascending versions', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
from: 1,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any)
|
||||
|
||||
expect(migratedConfig.configVersion).to.eq(2)
|
||||
})
|
||||
|
||||
it('does not apply non-matching migrations', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: 2,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 3,
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
configVersion: 10,
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any)
|
||||
|
||||
expect(migratedConfig.configVersion).to.eq(10)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { connectionsMigrator } from '../../actions/migrations/Connection'
|
||||
|
||||
describe('ConnectionsMigrator', () => {
|
||||
it('applies migrations', () => {
|
||||
let connections: any = {
|
||||
'763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7': {
|
||||
certValidation: true,
|
||||
clientId: 'mqtt-explorer-2783f48c',
|
||||
encryption: false,
|
||||
host: 'nodered',
|
||||
id: '763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7',
|
||||
name: 'nodered',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: ['#', '$SYS/#'],
|
||||
type: 'mqtt',
|
||||
},
|
||||
'iot.eclipse.org': {
|
||||
certValidation: true,
|
||||
clientId: 'mqtt-explorer-d913aad3',
|
||||
encryption: false,
|
||||
host: 'iot.eclipse.org',
|
||||
id: 'iot.eclipse.org',
|
||||
name: 'iot.eclipse.org',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: ['#', '$SYS/#'],
|
||||
type: 'mqtt',
|
||||
},
|
||||
}
|
||||
|
||||
let migratedConnections: any = {
|
||||
'763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7': {
|
||||
configVersion: 1,
|
||||
certValidation: true,
|
||||
clientId: undefined,
|
||||
encryption: false,
|
||||
host: 'nodered',
|
||||
id: '763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7',
|
||||
name: 'nodered',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: [
|
||||
{ topic: '#', qos: 0 },
|
||||
{ topic: '$SYS/#', qos: 0 },
|
||||
],
|
||||
type: 'mqtt',
|
||||
},
|
||||
'mqtt.eclipse.org': {
|
||||
configVersion: 1,
|
||||
certValidation: true,
|
||||
clientId: undefined,
|
||||
encryption: false,
|
||||
host: 'mqtt.eclipse.org',
|
||||
id: 'mqtt.eclipse.org',
|
||||
name: 'mqtt.eclipse.org',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: [
|
||||
{ topic: '#', qos: 0 },
|
||||
{ topic: '$SYS/#', qos: 0 },
|
||||
],
|
||||
type: 'mqtt',
|
||||
},
|
||||
}
|
||||
|
||||
expect(connectionsMigrator.applyMigrations(connections)).to.deep.eq(migratedConnections)
|
||||
})
|
||||
})
|
||||
+23
-23
@@ -1,27 +1,27 @@
|
||||
import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
// import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
|
||||
// Used to determine long-time-stability and memory leaks
|
||||
function trackProcessStatistics() {
|
||||
setInterval(() => {
|
||||
try {
|
||||
electronRendererTelemetry.trackCustomEvent({
|
||||
name: 'heapStatistics',
|
||||
payload: process.getHeapStatistics(),
|
||||
})
|
||||
electronRendererTelemetry.trackCustomEvent({
|
||||
name: 'cpuUsage',
|
||||
payload: process.getCPUUsage(),
|
||||
})
|
||||
electronRendererTelemetry.trackCustomEvent({
|
||||
name: 'runningSince',
|
||||
payload: performance.now(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, 60 * 1000)
|
||||
}
|
||||
trackProcessStatistics()
|
||||
// function trackProcessStatistics() {
|
||||
// setInterval(() => {
|
||||
// try {
|
||||
// electronRendererTelemetry.trackCustomEvent({
|
||||
// name: 'heapStatistics',
|
||||
// payload: process.getHeapStatistics(),
|
||||
// })
|
||||
// electronRendererTelemetry.trackCustomEvent({
|
||||
// name: 'cpuUsage',
|
||||
// payload: process.getCPUUsage(),
|
||||
// })
|
||||
// electronRendererTelemetry.trackCustomEvent({
|
||||
// name: 'runningSince',
|
||||
// payload: performance.now(),
|
||||
// })
|
||||
// } catch (error) {
|
||||
// console.error(error)
|
||||
// }
|
||||
// }, 60 * 1000)
|
||||
// }
|
||||
// trackProcessStatistics()
|
||||
|
||||
// Log reducer event names to determine what functionality is used and how to reproduce reported errors
|
||||
export function trackEvent(name: string) {
|
||||
@@ -30,6 +30,6 @@ export function trackEvent(name: string) {
|
||||
}
|
||||
const blacklist = ['CONNECTION_SET_HEALTH']
|
||||
if (blacklist.indexOf(name) === -1) {
|
||||
electronRendererTelemetry.trackEvent(name)
|
||||
// electronRendererTelemetry.trackEvent(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
--require ts-node/register
|
||||
--require source-map-support/register
|
||||
--recursive ./src/**/*.spec.ts
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
+18
-6
@@ -4,21 +4,33 @@
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"lib": ["es2017", "dom"],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build/",
|
||||
"sourceMap": true,
|
||||
"module": "esnext",
|
||||
"target": "es2017",
|
||||
"jsx": "react",
|
||||
"types": ["react"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
"types": [
|
||||
"react"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["**/*.d.ts"],
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.d.ts",
|
||||
".src/**/*.png",
|
||||
"./node_modules"
|
||||
],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
"useCache": true,
|
||||
"transpileModule": true,
|
||||
"errorsAsWarnings": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-25
@@ -1,7 +1,6 @@
|
||||
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
@@ -18,12 +17,11 @@ module.exports = {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 30000,
|
||||
maxSize: 0,
|
||||
minChunks: 1,
|
||||
maxAsyncRequests: 5,
|
||||
maxInitialRequests: 3,
|
||||
automaticNameDelimiter: '~',
|
||||
name: true,
|
||||
// name: true,
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
test: /[\\/]node_modules[\\/](react|react-dom|@material-ui|popper\.js|react|react-redux|prop-types|jss|redux|scheduler|react-transition-group)[\\/]/,
|
||||
@@ -32,6 +30,7 @@ module.exports = {
|
||||
priority: -10,
|
||||
},
|
||||
default: {
|
||||
name: 'default',
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
reuseExistingChunk: true,
|
||||
@@ -55,13 +54,7 @@ module.exports = {
|
||||
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'awesome-typescript-loader',
|
||||
options: {
|
||||
reportFiles: [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"../backend/src/**/*.{ts,tsx}"
|
||||
]
|
||||
}
|
||||
loader: 'ts-loader',
|
||||
},
|
||||
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
|
||||
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
|
||||
@@ -78,27 +71,26 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: {
|
||||
loader: 'node-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
// {
|
||||
// test: /\.node$/,
|
||||
// use: {
|
||||
// loader: 'node-loader',
|
||||
// options: {
|
||||
// modules: true,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
],
|
||||
},
|
||||
|
||||
// node: { global: true },
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }),
|
||||
// new BundleAnalyzerPlugin(),
|
||||
new HardSourceWebpackPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /\.\/build\/Debug\/addon/,
|
||||
contextRegExp: /heapdump$/
|
||||
}),
|
||||
// new webpack.IgnorePlugin({
|
||||
// resourceRegExp: /\.\/build\/Debug\/addon/,
|
||||
// contextRegExp: /heapdump$/
|
||||
// }),
|
||||
],
|
||||
|
||||
// When importing a module whose path matches one of the following, just
|
||||
|
||||
+2532
-4078
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -7,7 +7,7 @@ cache:
|
||||
- '%LOCALAPPDATA%\electron-builder\cache'
|
||||
|
||||
install:
|
||||
- ps: Install-Product node 10
|
||||
- ps: Install-Product node 16
|
||||
|
||||
build_script:
|
||||
- yarn
|
||||
@@ -21,4 +21,3 @@ test_script:
|
||||
|
||||
artifacts:
|
||||
- path: 'build\clean\build\*.appx'
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"debug": "ts-node --inspect ./src/index.ts",
|
||||
"postinstall": "yarn build"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16"
|
||||
},
|
||||
"author": "",
|
||||
"license": "CC-BY-ND-4.0",
|
||||
"nyc": {
|
||||
@@ -41,4 +44,4 @@
|
||||
"lowdb": "^1.0.0",
|
||||
"mqtt": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@ import * as FileAsync from 'lowdb/adapters/FileAsync'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as lowdb from 'lowdb'
|
||||
import * as path from 'path'
|
||||
import { backendEvents } from '../../events'
|
||||
import { backendRpc } from '../../events'
|
||||
import {
|
||||
makeStorageAcknowledgementEvent,
|
||||
makeStorageResponseEvent,
|
||||
storageClearEvent,
|
||||
storageLoadEvent,
|
||||
storageStoreEvent,
|
||||
@@ -32,56 +30,26 @@ export default class ConfigStorage {
|
||||
}
|
||||
|
||||
public async init() {
|
||||
backendEvents.subscribe(storageStoreEvent, async event => {
|
||||
const ack = makeStorageAcknowledgementEvent(event.transactionId)
|
||||
try {
|
||||
const db = await this.getDb()
|
||||
await db.set(event.store, event.data).write()
|
||||
backendEvents.emit(ack, undefined)
|
||||
} catch (error) {
|
||||
backendEvents.emit(ack, {
|
||||
error,
|
||||
transactionId: event.transactionId,
|
||||
store: event.store,
|
||||
})
|
||||
throw error
|
||||
backendRpc.on(storageStoreEvent, async event => {
|
||||
const db = await this.getDb()
|
||||
await db.set(event.store, event.data).write()
|
||||
return
|
||||
})
|
||||
|
||||
backendRpc.on(storageLoadEvent, async event => {
|
||||
const db = await this.getDb()
|
||||
const data = await db.get(event.store).value()
|
||||
return {
|
||||
data,
|
||||
store: event.store,
|
||||
}
|
||||
})
|
||||
|
||||
backendEvents.subscribe(storageLoadEvent, async event => {
|
||||
const responseEvent = makeStorageResponseEvent(event.transactionId)
|
||||
try {
|
||||
const db = await this.getDb()
|
||||
const data = await db.get(event.store).value()
|
||||
backendEvents.emit(responseEvent, {
|
||||
data,
|
||||
transactionId: event.transactionId,
|
||||
store: event.store,
|
||||
})
|
||||
} catch (error) {
|
||||
backendEvents.emit(responseEvent, {
|
||||
error,
|
||||
transactionId: event.transactionId,
|
||||
store: event.store,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
backendEvents.subscribe(storageClearEvent, async event => {
|
||||
try {
|
||||
const db = await this.getDb()
|
||||
const keys = await db.keys().value()
|
||||
for (const key of keys) {
|
||||
await db.unset(key).write()
|
||||
}
|
||||
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), undefined)
|
||||
} catch (error) {
|
||||
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), {
|
||||
error,
|
||||
transactionId: event.transactionId,
|
||||
})
|
||||
throw error
|
||||
backendRpc.on(storageClearEvent, async event => {
|
||||
const db = await this.getDb()
|
||||
const keys = await db.keys().value()
|
||||
for (const key of keys) {
|
||||
await db.unset(key).write()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataSourceStateMachine } from './'
|
||||
import { MqttMessage } from '../../../events'
|
||||
|
||||
type MessageCallback = (topic: string, payload: Buffer) => void
|
||||
type MessageCallback = (topic: string, payload: Buffer, packet: any) => void
|
||||
|
||||
// A DataSource should automatically reconnect if connection was broken
|
||||
interface DataSource<DataSourceOptions> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Url from 'url'
|
||||
import { URL } from 'url'
|
||||
|
||||
import { Client, connect as mqttConnect } from 'mqtt'
|
||||
import { DataSource, DataSourceStateMachine } from './'
|
||||
@@ -12,12 +12,19 @@ export interface MqttOptions {
|
||||
tls: boolean
|
||||
certValidation: boolean
|
||||
clientId?: string
|
||||
subscriptions: Array<string>
|
||||
subscriptions: Array<Subscription>
|
||||
certificateAuthority?: string
|
||||
clientCertificate?: string
|
||||
clientKey?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
topic: string
|
||||
qos: QoS
|
||||
}
|
||||
|
||||
export type QoS = 0 | 1 | 2
|
||||
|
||||
export class MqttSource implements DataSource<MqttOptions> {
|
||||
public stateMachine: DataSourceStateMachine = new DataSourceStateMachine()
|
||||
private client: Client | undefined
|
||||
@@ -34,13 +41,14 @@ export class MqttSource implements DataSource<MqttOptions> {
|
||||
const urlStr = options.tls ? options.url.replace(/^(mqtt|ws):/, '$1s:') : options.url
|
||||
let url
|
||||
try {
|
||||
url = Url.parse(urlStr)
|
||||
url = new URL(urlStr)
|
||||
} catch (error) {
|
||||
this.stateMachine.setError(error)
|
||||
this.stateMachine.setError(error as Error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const client = mqttConnect(url, {
|
||||
|
||||
const client = mqttConnect(url.toString(), {
|
||||
resubscribe: false,
|
||||
rejectUnauthorized: options.certValidation,
|
||||
username: options.username,
|
||||
@@ -74,7 +82,7 @@ export class MqttSource implements DataSource<MqttOptions> {
|
||||
client.on('connect', () => {
|
||||
this.stateMachine.setConnected(true)
|
||||
options.subscriptions.forEach(subscription => {
|
||||
client.subscribe(subscription, (err: Error) => {
|
||||
client.subscribe(subscription.topic, { qos: subscription.qos }, (err: Error) => {
|
||||
if (err) {
|
||||
this.stateMachine.setError(err)
|
||||
}
|
||||
|
||||
@@ -70,9 +70,11 @@ function jsonToPropertyPaths(ast: JsonAst, previousPath: Array<string> = []): Ar
|
||||
} else if (ast.type === 'Array') {
|
||||
children = ast.children.map((value, idx) => jsonToPropertyPaths(value, previousPath.slice().concat([String(idx)])))
|
||||
} else if (ast.type === 'Object') {
|
||||
children = ast.children.map(property =>
|
||||
jsonToPropertyPaths(property.value, previousPath.slice().concat([property.key.value]))
|
||||
)
|
||||
children = ast.children.map(property => {
|
||||
const path = property.key.value.replace('.', '\\.')
|
||||
|
||||
return jsonToPropertyPaths(property.value, previousPath.slice().concat([path]))
|
||||
})
|
||||
}
|
||||
|
||||
return children.reduce((a, b) => a.concat(b), [])
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { Base64Message } from './Base64Message'
|
||||
import { QoS } from '../DataSource/MqttSource'
|
||||
|
||||
export interface Message {
|
||||
value?: Base64Message
|
||||
// mqtt based info
|
||||
payload: Base64Message | null
|
||||
messageId?: number
|
||||
retain: boolean
|
||||
qos: QoS
|
||||
|
||||
// meta info
|
||||
length: number
|
||||
received: Date
|
||||
|
||||
// Global message counter, not mqtt related
|
||||
messageNumber: number
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeBuffer } from './ChangeBuffer'
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { EventBusInterface, EventDispatcher, makeConnectionMessageEvent, MqttMessage } from '../../../events'
|
||||
import { EventDispatcher, makeConnectionMessageEvent, MqttMessage, EventBusInterface } from '../../../events'
|
||||
import { TreeNode } from './'
|
||||
import { TreeNodeFactory } from './TreeNodeFactory'
|
||||
|
||||
@@ -31,7 +31,7 @@ export class Tree<ViewModel extends Destroyable> extends TreeNode<ViewModel> {
|
||||
if (!this.paused && this.applyChangesHasCompleted) {
|
||||
this.applyChangesHasCompleted = false
|
||||
if ((window as any).requestIdleCallback) {
|
||||
;(window as any).requestIdleCallback(() => this.applyUnmergedChanges(), { timeout: 500 })
|
||||
; (window as any).requestIdleCallback(() => this.applyUnmergedChanges(), { timeout: 500 })
|
||||
} else {
|
||||
this.applyUnmergedChanges()
|
||||
}
|
||||
@@ -75,13 +75,7 @@ export class Tree<ViewModel extends Destroyable> extends TreeNode<ViewModel> {
|
||||
|
||||
public applyUnmergedChanges() {
|
||||
this.unmergedMessages.popAll().forEach(bufferedItem => {
|
||||
const edges = bufferedItem.message.topic.split('/')
|
||||
const node = TreeNodeFactory.fromEdgesAndValue<ViewModel>(
|
||||
edges,
|
||||
bufferedItem.message.payload,
|
||||
bufferedItem.received
|
||||
)
|
||||
node.mqttMessage = bufferedItem.message
|
||||
const node = TreeNodeFactory.fromMessage<ViewModel>(bufferedItem.message, bufferedItem.received)
|
||||
|
||||
if (!this.nodeFilter || this.nodeFilter(node)) {
|
||||
this.updateWithNode(node.firstNode())
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { Edge, Message, RingBuffer, MessageHistory } from './'
|
||||
import { EventDispatcher, MqttMessage } from '../../../events'
|
||||
import { EventDispatcher } from '../../../events'
|
||||
|
||||
export class TreeNode<ViewModel extends Destroyable> {
|
||||
public sourceEdge?: Edge<ViewModel>
|
||||
public message?: Message
|
||||
public mqttMessage?: MqttMessage
|
||||
public messageHistory: MessageHistory = new RingBuffer<Message>(20000, 100)
|
||||
public viewModel?: ViewModel
|
||||
public edges: { [s: string]: Edge<ViewModel> } = {}
|
||||
@@ -105,7 +104,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
}
|
||||
|
||||
public hasMessage() {
|
||||
return this.message && this.message.value && this.message.value.length !== 0
|
||||
return this.message && this.message.payload && this.message.length !== 0
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
@@ -131,7 +130,6 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
public unconnectedClone() {
|
||||
const node = new TreeNode<ViewModel>()
|
||||
node.message = this.message
|
||||
node.mqttMessage = this.mqttMessage
|
||||
node.messageHistory = this.messageHistory.clone()
|
||||
node.messages = this.messages
|
||||
node.lastUpdate = this.lastUpdate
|
||||
@@ -203,7 +201,6 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
if (node.message) {
|
||||
this.setMessage(node.message)
|
||||
this.onMessage.dispatch(node.message)
|
||||
this.mqttMessage = node.mqttMessage
|
||||
}
|
||||
|
||||
this.removeFromTreeIfEmpty()
|
||||
@@ -236,7 +233,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
|
||||
public childTopics(): Array<TreeNode<ViewModel>> {
|
||||
if (this.cachedChildTopics === undefined) {
|
||||
const initialValue = this.message && this.message.value ? [this] : []
|
||||
const initialValue = this.message && this.message.payload ? [this] : []
|
||||
|
||||
this.cachedChildTopics = this.edgeArray
|
||||
.map(e => e.target.childTopics())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Base64Message } from './Base64Message'
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { Edge, Tree, TreeNode } from './'
|
||||
import { MqttMessage } from '../../../events'
|
||||
|
||||
export abstract class TreeNodeFactory {
|
||||
private static messageCounter = 0
|
||||
@@ -20,21 +20,23 @@ export abstract class TreeNodeFactory {
|
||||
node.sourceEdge!.target = node
|
||||
}
|
||||
|
||||
public static fromEdgesAndValue<ViewModel extends Destroyable>(
|
||||
edgeNames: Array<string>,
|
||||
value?: Base64Message | null,
|
||||
public static fromMessage<ViewModel extends Destroyable>(
|
||||
mqttMessage: MqttMessage,
|
||||
receiveDate: Date = new Date()
|
||||
): TreeNode<ViewModel> {
|
||||
const node = new TreeNode<ViewModel>()
|
||||
const edges = mqttMessage.topic.split('/')
|
||||
|
||||
mqttMessage.retain
|
||||
node.setMessage({
|
||||
value: value || undefined,
|
||||
length: value ? value.length : 0,
|
||||
...mqttMessage,
|
||||
length: mqttMessage.payload?.length ?? 0,
|
||||
received: receiveDate,
|
||||
messageNumber: this.messageCounter,
|
||||
})
|
||||
this.messageCounter += 1
|
||||
|
||||
this.insertNodeAtPosition<ViewModel>(edgeNames, node)
|
||||
this.insertNodeAtPosition<ViewModel>(edges, node)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Edge, TreeNodeFactory } from '../'
|
||||
import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { Edge } from '../'
|
||||
import { makeTreeNode } from './makeTreeNode'
|
||||
|
||||
describe('Edge', () => {
|
||||
it('should contain a name', () => {
|
||||
@@ -9,8 +10,7 @@ describe('Edge', () => {
|
||||
})
|
||||
|
||||
it('firstEdge should retrieve the first edge', () => {
|
||||
const topics = 'foo/bar/baz'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
|
||||
const leaf = makeTreeNode('foo/bar/baz')
|
||||
const bazEdge = leaf.sourceEdge
|
||||
|
||||
if (!bazEdge) {
|
||||
@@ -34,11 +34,8 @@ describe('Edge', () => {
|
||||
})
|
||||
|
||||
it('hash should include change if parents are different', () => {
|
||||
const topics1 = 'foo/bar/baz'.split('/')
|
||||
const bazEdge1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined).sourceEdge
|
||||
|
||||
const topics2 = 'foo/foo/baz'.split('/')
|
||||
const bazEdge2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined).sourceEdge
|
||||
const bazEdge1 = makeTreeNode('foo/bar/baz').sourceEdge
|
||||
const bazEdge2 = makeTreeNode('foo/foo/baz').sourceEdge
|
||||
|
||||
if (!bazEdge1 || !bazEdge2) {
|
||||
throw Error('should not happen')
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
|
||||
describe('EventDispatcher', async () => {
|
||||
it('should dispatch', async function() {
|
||||
it('should dispatch', async function () {
|
||||
const dispatcher = new EventDispatcher<string>()
|
||||
this.timeout(300)
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('EventDispatcher', async () => {
|
||||
expect(response).to.eq('hello')
|
||||
})
|
||||
|
||||
it('should unsubscribe', async function() {
|
||||
it('should unsubscribe', async function () {
|
||||
const dispatcher = new EventDispatcher<string>()
|
||||
this.timeout(300)
|
||||
let callbackCounter = 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user