Compare commits

..
Author SHA1 Message Date
Thomas Nordquist 679e319912 Add bad examples 2019-10-10 10:42:09 +02:00
190 changed files with 10415 additions and 13429 deletions
+7 -17
View File
@@ -1,27 +1,19 @@
{
"import": [
"@cspell/dict-typescript/cspell-ext.json"
],
"ignoreRegExpList": [
"import(?:(?:(?:[ \\n\\t]+([^ *\\n\\t\\{\\},]+)[ \\n\\t]*(?:,|[ \\n\\t]+))?([ \\n\\t]*\\{(?:[ \\n\\t]*[^ \\n\\t\"'\\{\\}]+[ \\n\\t]*,?)+\\})?[ \\n\\t]*)|[ \n\\n\\t]*\\*[ \\n\\t]*as[ \\n\\t]+([^ \\n\\t\\{\\}]+)[ \\n\\t]+)from[ \\n\\t]*(?:['\"])([^'\"\\n]+)(['\"])\n",
"^import\\s+(['\"]).*\\1$"
],
"language": "en",
"words": [
"Bbreak",
"nodered",
"goog",
"thomasnordquist",
"nowrap",
"subheader",
"basepath",
"webdriverio",
"repo",
"hexagonalize",
"pixelize",
"Transistions",
"squashfs",
"squashfs",
"provisionprofile",
"Nsis",
"webdriverio",
"Appx",
"Hashable",
"clickaway",
@@ -31,6 +23,8 @@
"Monokai",
"plottable",
"snackbar",
"webdriverio",
"prismjs",
"Nordquist",
"debounced",
"mosquitto",
@@ -49,10 +43,6 @@
"DEVTOOLS",
"mixins",
"Explorerdmg",
"heapsnapshot",
"noconflict",
"sparkplugb",
"protojson",
"typesafe"
"heapsnapshot"
]
}
}
-3
View File
@@ -1,3 +0,0 @@
package.ts @thomasnordquist
.github @thomasnordquist
scripts @thomasnordquist
-29
View File
@@ -1,29 +0,0 @@
on:
push:
branches:
- master
- release
- beta
paths:
- Dockerfile
- .github
jobs:
create-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
-53
View File
@@ -1,53 +0,0 @@
name: Build
on:
push:
branches:
- release
- beta
concurrency:
group: ${{ github.ref }}
cancel-in-progress: false
jobs:
build:
strategy:
matrix:
build:
- os: ubuntu-latest
task: linux
- os: windows-latest
task: win
- os: macos-latest
task: mac
runs-on: ${{ matrix.build.os }}
steps:
- if: matrix.build.os == 'ubuntu-latest'
run: sudo snap install snapcraft --classic
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g yarn
- run: yarn
- id: create_token # get ReleaseBot access token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.RELEASE_BOT_APP_ID }}
private_key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
id: semantic # Need an `id` for output variables
env:
GITHUB_TOKEN: ${{ steps.create_token.outputs.token }}
- run: yarn build
if: steps.semantic.outputs.new_release_published == 'true'
- run: yarn prepare-release
if: steps.semantic.outputs.new_release_published == 'true'
- run: yarn package ${{ matrix.build.task }}
if: steps.semantic.outputs.new_release_published == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
-38
View File
@@ -1,38 +0,0 @@
on:
pull_request_target: # Use pull_request_target
branches: [master, beta, release]
jobs:
test:
runs-on: ubuntu-latest
container:
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
volumes:
- ./:/app
options: --user root
steps:
- uses: actions/checkout@v4
- name: Install Packages
run: yarn install --frozen-lockfile
- name: Build
run: yarn build
- name: Test
run: yarn test
- name: UI-Test
run: yarn ui-test
- name: Post-processing
run: ./scripts/prepareVideo.sh
- uses: hkusu/s3-upload-action@v2
id: upload # specify some ID for use in subsequent steps
with:
aws-access-key-id: ${{ vars.AWS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'eu-central-1'
aws-bucket: ${{ vars.AWS_BUCKET }}
file-path: './ui-test.gif'
content-type: image/gif
output-file-url: 'true'
- name: Show URL
run: echo '${{ steps.upload.outputs.file-url }}'
id: artifact-upload-step
- run: echo '<picture><img src="${{ steps.upload.outputs.file-url }}"></picture>' >> $GITHUB_STEP_SUMMARY
-17
View File
@@ -1,17 +0,0 @@
name: Update Website
on: [release, workflow_dispatch]
jobs:
update-website:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: gh-pages
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run readme
- uses: stefanzweifel/git-auto-commit-action@v5
-30
View File
@@ -1,30 +0,0 @@
{
"branches": [
"release",
{
"name": "beta",
"prerelease": true
}
],
repositoryUrl: "git@github.com:thomasnordquist/MQTT-Explorer.git",
"plugins": [
"@semantic-release/commit-analyzer",
"semantic-release-export-data",
"@semantic-release/changelog",
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"yarn.lock"
]
}
]
]
}
+40
View File
@@ -0,0 +1,40 @@
language: node_js
services:
- xvfb
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
node_js:
- "10"
os:
- linux
- osx
osx_image: xcode10.2
dist: xenial
services:
- docker
install:
- yarn install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update && sudo apt-get -y install snap squashfs-tools && sudo snap install snapcraft --classic; fi;
script:
- yarn run build
- yarn lint
- yarn test
- export TRAVIS_BUILD_NUMBER="" # Override travis build number since it is uses for tagging the binary version https://github.com/electron-userland/electron-builder/issues/3730
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app thomasnordquist/ui-test-recording-env sh -c "cd app && docker/testMounted.sh"; fi
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then unset CSC_LINK; yarn run package -- win; fi
+6 -45
View File
@@ -6,12 +6,11 @@
[![Build status](https://ci.appveyor.com/api/projects/status/c35tkm29rm4m5364/branch/master?svg=true)](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/47b26e03fce543ceac7914214482334a)](https://app.codacy.com/app/thomasnordquist/MQTT-Explorer?utm_source=github.com&utm_medium=referral&utm_content=thomasnordquist/MQTT-Explorer&utm_campaign=Badge_Grade_Dashboard)
| | | |
| :---------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: |
| [![screen_composite](https://mqtt-explorer.com/img/screen-composite_small.png)](https://mqtt-explorer.com/img/screen-composite.png) | [![screen2_small](https://mqtt-explorer.com/img/screen2_small.png)](https://mqtt-explorer.com/img/screen2.png) | [![screen3_small](https://mqtt-explorer.com/img/screen3_small.png)](https://mqtt-explorer.com/img/screen3.png) |
| | | |
|:---:|:---:|:---:|
|[![screen_composite](https://mqtt-explorer.com/img/screen-composite_small.png)](https://mqtt-explorer.com/img/screen-composite.png)|[![screen2_small](https://mqtt-explorer.com/img/screen2_small.png)](https://mqtt-explorer.com/img/screen2.png)|[![screen3_small](https://mqtt-explorer.com/img/screen3_small.png)](https://mqtt-explorer.com/img/screen3.png)|
# The App has moved to [mqtt-explorer.com](https://mqtt-explorer.com)
MQTT Explorer is a comprehensive and easy-to-use MQTT Client.
Downloads can be found at the link above.
@@ -21,7 +20,6 @@ Pull-Requests and error reports are welcome.
## Run from sources
```bash
npm install -g yarn
yarn
yarn build
yarn start
@@ -30,9 +28,7 @@ yarn start
## Develop
Launch Application
```bash
npm install -g yarn
yarn
yarn dev
```
@@ -43,60 +39,25 @@ 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
```
## Create a release
Create a PR to `release` branch.
There needs to be a "feat: some new feature" or "fix: some bugfix" commit for a new release to be created
## Create a beta release
Create a PR to `beta` branch. A "feat" or "fix" commit is necessary to create a new version.
## Write docs
```
git clone --single-branch -b gh-pages https://github.com/thomasnordquist/MQTT-Explorer.git mqtt-explorer-pages
cd mqtt-explorer-pages
bundle install
bundle exec jekyll serve --incremental
```
Readme file: `Readme.tpl.md`
Preview is available at
http://localhost:4000/Readme.tpl
## Update docs
```
npm install
./updateReadme.ts
```
The readme will be generated from the docs.
## License
![CC-BY-ND 4.0](https://img.shields.io/badge/License-CC%20BY--ND%204.0-blue.svg)
+153 -170
View File
@@ -1,197 +1,180 @@
<!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>
<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;
<style>
body,
html {
margin: 0;
padding: 0;
}
25% {
background-color: #595585;
[tabindex] {
outline: none;
}
50% {
background-color: #595585;
@keyframes updateDark {
0% {
background-color: none;
}
25% {
background-color: #595585;
}
50% {
background-color: #595585;
}
100% {
background-color: none;
}
}
100% {
background-color: none;
}
}
@keyframes updateLight {
0% {
background-color: none;
color: inherit;
@keyframes updateLight {
0% {
background-color: none;
color: inherit;
}
25% {
background-color: #c0c8c0;
}
50% {
background-color: #c0c8c0;
}
100% {
background-color: none;
color: inherit;
}
}
25% {
background-color: #c0c8c0;
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
50% {
background-color: #c0c8c0;
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
}
100% {
background-color: none;
color: inherit;
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
background-color: rgba(140, 140, 140, 0.1);
}
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-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-corner {
background-color: rgba(0, 0, 0, 0);
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
::-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 {
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-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::before {
content: '•••';
display: inline-block;
vertical-align: middle;
text-align: center;
width: 100%;
margin-top: -22px;
color: #aaa;
opacity: 1;
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.3);
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
}
.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 {
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::before {
content: '•••';
display: inline-block;
vertical-align: middle;
text-align: center;
width: 100%;
margin-top: -22px;
color: #aaa;
opacity: 1;
}
.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:hover {
border-top: 5px solid rgba(120, 120, 120, 0.3);
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
}
.Resizer.vertical:hover {
border-left: 0px solid rgba(130, 130, 130, 0.3);
border-right: 8px solid rgba(140, 140, 140, 0.3);
}
.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.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.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;
}
.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: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>
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>
+31 -37
View File
@@ -4,30 +4,28 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --mode production",
"build": "yarn rebuild && webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
"mochatest": "mocha --require ts-node/register --require source-map-support/register --recursive src/*/**/*.spec.ts"
},
"engines": {
"node": ">=18"
"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 -"
},
"author": "",
"license": "CC-BY-ND-4.0",
"dependencies": {
"@material-ui/core": "4.12",
"@material-ui/core": "^4",
"@material-ui/icons": "^4",
"@material-ui/lab": "^4.0.0-alpha",
"@material-ui/styles": "4.11",
"@types/react-transition-group": "^4",
"ace-builds": "^1.4.11",
"axios": "^0.28.0",
"compare-versions": "^3.5.0",
"@material-ui/styles": "^4",
"@types/react-transition-group": "^2.9.2",
"axios": "^0.19.0",
"brace": "^0.11.1",
"compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^2.1.0",
"d3": "^5.9.7",
"d3": "^5.9.2",
"d3-shape": "^1.3.5",
"diff": "^4.0.1",
"dot-prop": "^5.0.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"file-loader": "^4.0.0",
"get-value": "^3.0.1",
"immutable": "^4.0.0-rc.12",
"in-viewport": "^3.6.0",
@@ -35,59 +33,55 @@
"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.11",
"react-ace": "^8",
"react": "16.8",
"react-ace": "^7.0.1",
"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",
"react-transition-group": "^4.1.1",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "0.5",
"redux-batched-actions": "^0.4.1",
"redux-thunk": "^2.3.0",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
"uuid": "7"
"uuid": "^3.3.2"
},
"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.7.8",
"@types/node": "^12.0.4",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.9.4",
"@types/react": "^16.7.18",
"@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": "^7.0.2",
"@types/uuid": "^3.4.4",
"@types/vis": "^4.21.9",
"chai": "^4.2.0",
"cross-env": "^7.0.2",
"awesome-typescript-loader": "^5.2.1",
"css-loader": "^3.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"lodash": "^4.17.21",
"mocha": "^10.4.0",
"moment": "^2.29.1",
"hard-source-webpack-plugin": "^0.13.1",
"heapdump": "^0.3.12",
"html-webpack-plugin": "^4.0.0-beta.5",
"node-loader": "^0.6.0",
"source-map-loader": "^0.2.4",
"style-loader": "^1",
"ts-loader": "^9.5.1",
"typescript": "^4.5.5",
"webpack": "^5.91.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.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"
},
"peerDependencies": {
"electron": "^29"
"electron": "^5.0.5"
}
}
+56 -46
View File
@@ -45,7 +45,9 @@ 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 {
@@ -60,55 +62,63 @@ export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () =>
}
}
export const addChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const chartExists = Boolean(
getState()
.charts.get('charts')
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
)
if (chartExists) {
dispatch(showNotification('Already added'))
return
}
dispatch({
type: ActionTypes.CHARTS_ADD,
chart: chartParameters,
})
dispatch(saveCharts())
dispatch(showNotification('Added to chart panel'))
export const addChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
const chartExists = Boolean(
getState()
.charts.get('charts')
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
)
if (chartExists) {
dispatch(showNotification('Already added'))
return
}
export const updateChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
type: ActionTypes.CHARTS_UPDATE,
topic: chartParameters.topic,
dotPath: chartParameters.dotPath,
parameters: chartParameters,
})
dispatch(saveCharts())
}
dispatch({
type: ActionTypes.CHARTS_ADD,
chart: chartParameters,
})
dispatch(saveCharts())
dispatch(showNotification('Added to chart panel'))
}
export const removeChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
chart: chartParameters,
type: ActionTypes.CHARTS_REMOVE,
})
dispatch(saveCharts())
}
export const updateChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
type: ActionTypes.CHARTS_UPDATE,
topic: chartParameters.topic,
dotPath: chartParameters.dotPath,
parameters: chartParameters,
})
dispatch(saveCharts())
}
export const moveChartUp =
(parameters: { topic: string; dotPath?: string }) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
topic: parameters.topic,
dotPath: parameters.dotPath,
type: ActionTypes.CHARTS_MOVE_UP,
})
dispatch(saveCharts())
}
export const removeChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
chart: chartParameters,
type: ActionTypes.CHARTS_REMOVE,
})
dispatch(saveCharts())
}
export const moveChartUp = (parameters: { topic: string; dotPath?: string }) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
topic: parameters.topic,
dotPath: parameters.dotPath,
type: ActionTypes.CHARTS_MOVE_UP,
})
dispatch(saveCharts())
}
export const setCharts = (charts: Array<ChartParameters>): Action => {
return {
+23 -21
View File
@@ -11,29 +11,31 @@ import { showError } from './Global'
import { TopicViewModel } from '../model/TopicViewModel'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../events'
export const connect =
(options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
export const connect = (options: MqttOptions, connectionId: string) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
rendererEvents.subscribe(event, dataSourceState => {
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
}
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
rendererEvents.subscribe(event, dataSourceState => {
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
}
dispatch(updateHealth(dataSourceState))
})
}
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
dispatch(updateHealth(dataSourceState))
})
}
const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<any>, getState: () => AppState) => {
let state
+76 -44
View File
@@ -9,14 +9,13 @@ 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 * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
import { connectionsMigrator } from './migrations/Connection'
import { rendererRpc, readFromFile } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
export interface ConnectionDictionary {
import { ActionTypes, Action } from '../reducers/ConnectionManager'
interface ConnectionDictionary {
[s: string]: ConnectionOptions
}
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
@@ -28,12 +27,6 @@ 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))
}
@@ -50,19 +43,21 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
}
export type CertificateTypes = 'selfSignedCertificate' | 'clientCertificate' | 'clientKey'
export const selectCertificate =
(type: CertificateTypes, connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const certificate = await openCertificate()
dispatch(
updateConnection(connectionId, {
[type]: certificate,
})
)
} catch (error) {
dispatch(showError(error))
}
export const selectCertificate = (type: CertificateTypes, connectionId: string) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
try {
const certificate = await openCertificate()
dispatch(
updateConnection(connectionId, {
[type]: certificate,
})
)
} catch (error) {
dispatch(showError(error))
}
}
async function openCertificate(): Promise<CertificateParameters> {
const rejectReasons = {
@@ -70,25 +65,35 @@ async function openCertificate(): Promise<CertificateParameters> {
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
}
const openDialogReturnValue = await rendererRpc.call(makeOpenDialogRpc(), {
properties: ['openFile'],
securityScopedBookmarks: true,
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 selectedFile = openDialogReturnValue.filePaths && openDialogReturnValue.filePaths[0]
if (!selectedFile) {
throw rejectReasons.noCertificateSelected
}
const data = await rendererRpc.call(readFromFile, { filePath: 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) => {
@@ -106,13 +111,13 @@ export const updateConnection = (connectionId: string, changeSet: Partial<Connec
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,
})
export const addSubscription = (subscription: Subscription, connectionId: string): Action => ({
export const addSubscription = (subscription: string, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION,
})
export const deleteSubscription = (subscription: Subscription, connectionId: string): Action => ({
export const deleteSubscription = (subscription: string, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION,
@@ -179,4 +184,31 @@ 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 -1
View File
@@ -1,7 +1,7 @@
import { ActionTypes, ConfirmationRequest } from '../reducers/Global'
import { Dispatch } from 'redux'
export const showError = (error?: string | unknown) => ({
export const showError = (error?: string) => ({
error,
type: ActionTypes.showError,
})
+3 -49
View File
@@ -2,10 +2,7 @@ import { Action, ActionTypes } from '../reducers/Publish'
import { AppState } from '../reducers'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Dispatch } from 'redux'
import { MqttMessage, makePublishEvent, rendererEvents, rendererRpc, readFromFile } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
import { showError } from './Global'
import { Base64 } from 'js-base64'
import { makePublishEvent, rendererEvents } from '../../../events'
export const setTopic = (topic?: string): Action => {
return {
@@ -14,49 +11,6 @@ export const setTopic = (topic?: string): Action => {
}
}
export const openFile = (encoding: 'utf8' = 'utf8') => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const file = await getFileContent(encoding)
if (file) {
dispatch(
setPayload(file.data))
}
} catch (error) {
dispatch(showError(error))
}
}
type FileParameters = {
name: string,
data: string
}
async function getFileContent(encoding: string): Promise<FileParameters | undefined> {
const rejectReasons = {
noFileSelected: 'No file selected',
errorReadingFile: 'Error reading file'
}
const { canceled, filePaths } = await rendererRpc.call(makeOpenDialogRpc(), {
properties: ['openFile'],
securityScopedBookmarks: true,
})
if (canceled) {
return
}
const selectedFile = filePaths[0]
if (!selectedFile) {
throw rejectReasons.noFileSelected
}
try {
const data = await rendererRpc.call(readFromFile, { filePath: selectedFile, encoding })
return { name: selectedFile, data: data.toString(encoding) }
} catch (error) {
throw rejectReasons.errorReadingFile
}
}
export const setPayload = (payload?: string): Action => {
return {
payload,
@@ -80,14 +34,14 @@ export const setEditorMode = (editorMode: string): Action => {
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const state = getState()
const topic = state.publish.manualTopic ?? state.tree.get('selectedTopic')?.path()
const topic = state.publish.topic
if (!topic) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage: Partial<MqttMessage> = {
const mqttMessage = {
topic,
payload: state.publish.payload ? Base64Message.fromString(state.publish.payload) : null,
retain: state.publish.retain,
+30 -29
View File
@@ -1,5 +1,5 @@
import * as q from '../../../backend/src/Model'
import { ActionTypes, SettingsStateModel, TopicOrder, ValueRendererDisplayMode } from '../reducers/Settings'
import { ActionTypes, SettingsStateModel, TopicOrder } from '../reducers/Settings'
import { AppState } from '../reducers'
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
@@ -43,14 +43,12 @@ export const storeSettings = () => async (dispatch: Dispatch<any>, getState: ()
}
}
export const setAutoExpandLimit =
(autoExpandLimit: number = 0) =>
(dispatch: Dispatch<any>) => {
dispatch({
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
})
}
export const setAutoExpandLimit = (autoExpandLimit: number = 0) => (dispatch: Dispatch<any>) => {
dispatch({
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
})
}
export const setTimeLocale = (timeLocale: string) => (dispatch: Dispatch<any>) => {
dispatch({
@@ -68,14 +66,13 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat
dispatch(storeSettings())
}
export const setValueDisplayMode =
(valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
})
dispatch(storeSettings())
}
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
})
dispatch(storeSettings())
}
export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
dispatch({
@@ -84,15 +81,13 @@ export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
dispatch(storeSettings())
}
export const setTopicOrder =
(topicOrder: TopicOrder = TopicOrder.none) =>
(dispatch: Dispatch<any>) => {
dispatch({
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
})
dispatch(storeSettings())
}
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none) => (dispatch: Dispatch<any>) => {
dispatch({
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
})
dispatch(storeSettings())
}
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { tree } = getState().connection
@@ -110,15 +105,21 @@ 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.payload &&
node.message.payload.toUnicodeString().toLowerCase().indexOf(filterStr) !== -1
node.message.value &&
Base64Message.toUnicodeString(node.message.value)
.toLowerCase()
.indexOf(filterStr) !== -1
return Boolean(messageMatches)
}
+31 -25
View File
@@ -12,10 +12,12 @@ export { clearTopic } from './clearTopic'
export { moveSelectionUpOrDownwards, moveInward, moveOutward } from './visibleTreeTraversal'
export const selectTopic =
(topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
debouncedSelectTopic(topic, dispatch, getState)
}
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
debouncedSelectTopic(topic, dispatch, getState)
}
const debouncedSelectTopic = debounce(
(topic: q.TreeNode<TopicViewModel>, dispatch: Dispatch<any>, getState: () => AppState) => {
@@ -27,14 +29,19 @@ const debouncedSelectTopic = debounce(
// Update publish topic
let setTopicDispatch: any | undefined
if (!getState().publish.manualTopic) {
if (!getState().publish.topic) {
setTopicDispatch = setTopic(topic.path())
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.manualTopic) {
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.topic) {
setTopicDispatch = setTopic(topic.path())
}
previouslySelectedTopic?.viewModel?.setSelected(false)
topic.viewModel?.setSelected(true)
if (previouslySelectedTopic && previouslySelectedTopic.viewModel) {
previouslySelectedTopic.viewModel.setSelected(false)
}
if (topic.viewModel) {
topic.viewModel.setSelected(true)
}
const selectTreeTopicDispatch = {
selectedTopic: topic,
@@ -67,26 +74,25 @@ function destroyUnreferencedTree(state: AppState) {
}
}
export const resetStore =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
export const resetStore = () => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
type: ActionTypes.TREE_RESET_STORE,
})
}
return dispatch({
type: ActionTypes.TREE_RESET_STORE,
})
}
export const showTree =
(tree: q.Tree<TopicViewModel> | undefined) =>
(dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (
dispatch: Dispatch<any>,
getState: () => AppState
): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
tree,
type: ActionTypes.TREE_SHOW_TREE,
})
}
return dispatch({
tree,
type: ActionTypes.TREE_SHOW_TREE,
})
}
export const togglePause = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const paused = getState().tree.get('paused')
+41 -40
View File
@@ -5,50 +5,51 @@ import { makePublishEvent, rendererEvents } from '../../../events'
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
import { globalActions } from '.'
export const clearTopic =
(topic: q.TreeNode<any>, recursive: boolean) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const topicsForPurging = recursive ? [topic, ...topic.childTopics()] : [topic]
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()
if (recursive) {
const topicCount = topic.childTopicCount()
const topicDelta = topic.hasMessage() ? -1 : 0
const childTopicsMessage =
topicCount + topicDelta > 0
? ` and ${topicCount + topicDelta} child ${topicCount + topicDelta === 1 ? 'topic' : 'topics'}`
: ''
const topicDelta = topic.hasMessage() ? -1 : 0
const childTopicsMessage =
topicCount + topicDelta > 0
? ` and ${topicCount + topicDelta} child ${topicCount + topicDelta === 1 ? 'topic' : 'topics'}`
: ''
const confirmed = await dispatch(
globalActions.requestConfirmation(
'Confirm delete',
`Do you want to clear "${topic.path()}"${childTopicsMessage}?\n\nThis function will send an empty payload (QoS 0, retain) to this and every subtopic, clearing retained topics in the process. Only use this function if you know what you are doing.`
)
const confirmed = await dispatch(
globalActions.requestConfirmation(
'Confirm delete',
`Do you want to clear "${topic.path()}"${childTopicsMessage}?\n\nThis function will send an empty payload (QoS 0, retain) to this and every subtopic, clearing retained topics in the process. Only use this function if you know what you are doing.`
)
if (!confirmed) {
return
}
}
dispatch(moveSelectionUpOrDownwards('next'))
const { connectionId } = getState().connection
if (!connectionId) {
)
if (!confirmed) {
return
}
const publishEvent = makePublishEvent(connectionId)
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)
})
}
dispatch(moveSelectionUpOrDownwards('next'))
const { connectionId } = getState().connection
if (!connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
topicsForPurging
.filter(t => t.path() !== '' && t.hasMessage())
.map(t => t.path())
.forEach((path, idx) => {
const mqttMessage = {
topic: path,
payload: null,
retain: true,
qos: 0 as 0,
}
// Rate limit deletion
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
})
}
-94
View File
@@ -1,94 +0,0 @@
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,
}
+39 -42
View File
@@ -6,56 +6,53 @@ import { SettingsState } from '../reducers/Settings'
import { sortedNodes } from '../sortedNodes'
import { TopicViewModel } from '../model/TopicViewModel'
export const moveSelectionUpOrDownwards =
(direction: 'next' | 'previous') =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
const tree = state.tree.get('tree')
export const moveSelectionUpOrDownwards = (direction: 'next' | 'previous') => (
dispatch: Dispatch<any>,
getState: () => AppState
): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
const tree = state.tree.get('tree')
if (!selected || !tree) {
if (tree) {
dispatch(selectTopic(tree))
}
return
}
const nextTreeNode = nextVisibleElementInTree(state.settings, tree, selected, direction)
if (nextTreeNode && nextTreeNode.viewModel) {
dispatch(selectTopic(nextTreeNode))
if (!selected || !tree) {
if (tree) {
dispatch(selectTopic(tree))
}
return
}
const nextTreeNode = nextVisibleElementInTree(state.settings, tree, selected, direction)
if (nextTreeNode && nextTreeNode.viewModel) {
dispatch(selectTopic(nextTreeNode))
}
}
export const moveInward = () => (dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
export const moveInward =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
if (!selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(true, true)
} else {
dispatch(moveSelectionUpOrDownwards('next'))
}
}
if (!selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(true, true)
} else {
dispatch(moveSelectionUpOrDownwards('next'))
}
export const moveOutward = () => (dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
export const moveOutward =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
if (selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(false, true)
} else {
dispatch(moveSelectionUpOrDownwards('previous'))
}
if (selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(false, true)
} else {
dispatch(moveSelectionUpOrDownwards('previous'))
}
}
function isTreeNodeVisible(treeNode: q.TreeNode<any>) {
return Boolean(treeNode.viewModel)
+7 -5
View File
@@ -12,7 +12,6 @@ import { ConfirmationRequest } from '../reducers/Global'
import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../actions'
import { Theme, withStyles } from '@material-ui/core/styles'
;(window as any).global = window
const Settings = React.lazy(() => import('./SettingsDrawer/Settings'))
const ContentView = React.lazy(() => import('./Layout/ContentView'))
@@ -67,8 +66,6 @@ class App extends React.PureComponent<Props, {}> {
return null
}
const anyProps: any = {}
return (
<div className={centerContent}>
<CssBaseline />
@@ -76,7 +73,7 @@ class App extends React.PureComponent<Props, {}> {
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
{this.renderNotification()}
<React.Suspense fallback={<div></div>}>
<Settings {...anyProps} />
<Settings />
</React.Suspense>
<div className={centerContent}>
<div className={`${settingsVisible ? contentShift : content}`}>
@@ -160,4 +157,9 @@ const mapStateToProps = (state: AppState) => {
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(App)
)
@@ -2,20 +2,16 @@ import { Props } from '../Chart'
import { useMemo } from 'react'
import { Point } from '../Model'
function defaultFor(a: number | undefined, b: number) {
return a === undefined ? b : a
}
export function useCustomYDomain(props: Props) {
return useMemo(() => {
const data = props.data
const calculatedDomain = domainForData(data)
const yDomain: [number, number] = props.range
? [defaultFor(props.range[0], calculatedDomain[0]), defaultFor(props.range[1], calculatedDomain[1])]
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
: calculatedDomain
return yDomain
}, [props.data, props.range])
}, [props.data])
}
function domainForData(data: Array<Point>): [number, number] {
@@ -23,10 +19,8 @@ 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,4 +65,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(memo(ColorSettings))
export default connect(
undefined,
mapDispatchToProps
)(memo(ColorSettings))
@@ -60,4 +60,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(InterpolationSettings)
export default connect(
undefined,
mapDispatchToProps
)(InterpolationSettings)
@@ -33,4 +33,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(MoveUp)
export default connect(
undefined,
mapDispatchToProps
)(MoveUp)
@@ -95,7 +95,10 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(RangeSettings)
export default connect(
undefined,
mapDispatchToProps
)(RangeSettings)
function useRangeStateToFireUpdateAction(
rangeFrom: string | number | undefined,
@@ -47,4 +47,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(memo(Size))
export default connect(
undefined,
mapDispatchToProps
)(memo(Size))
@@ -96,4 +96,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
export default connect(
undefined,
mapDispatchToProps
)(TimeRangeSettings)
@@ -25,7 +25,6 @@ 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) {
+4 -2
View File
@@ -114,7 +114,6 @@ function TopicChart(props: Props) {
</div>
</div>
<TopicPlot
node={props.treeNode ? props.treeNode : undefined}
color={props.parameters.color}
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
@@ -134,4 +133,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
export default connect(
undefined,
mapDispatchToProps
)(memo(TopicChart))
+4 -1
View File
@@ -126,4 +126,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ChartPanel))
@@ -1,6 +1,7 @@
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'
@@ -8,10 +9,8 @@ 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, TextField, Tooltip } from '@material-ui/core'
import { QosSelect } from '../QosSelect'
import { QoS } from '../../../../backend/src/DataSource/MqttSource'
import Subscriptions from './Subscriptions'
import { Button, Grid, IconButton, TextField, List, ListItem, ListItemText, Tooltip } from '@material-ui/core'
interface Props {
connection: ConnectionOptions
@@ -19,93 +18,116 @@ interface Props {
managerActions: typeof connectionManagerActions
}
const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
const [qos, setQos] = useState<QoS>(0)
const [topic, setTopic] = useState('')
const { classes } = props
interface State {
subscription: string
}
const updateSubscription = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => setTopic(event.target.value),
[]
)
class ConnectionSettings extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { subscription: '' }
}
const handleChange = useCallback(
(name: string) => (event: any) => {
props.managerActions.updateConnection(props.connection.id, {
[name]: event.target.value,
})
},
[]
)
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 (
<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>
<ListItem style={{ padding: '0 0 0 8px' }}>
<ListItemText>
<IconButton onClick={props.deleteAction} style={{ padding: '6px' }}>
<Delete />
</IconButton>
{props.subscription}
</ListItemText>
</ListItem>
)
})
}
const mapDispatchToProps = (dispatch: any) => {
return {
@@ -120,13 +142,19 @@ 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,5 +1,7 @@
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'
@@ -79,4 +81,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection))
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(CertificateFileSelection))
@@ -104,4 +104,7 @@ 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,4 +285,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSettings))
@@ -134,4 +134,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSetup))
@@ -1,40 +1,26 @@
import React, { useCallback } from 'react'
import React from 'react'
import { connect } from 'react-redux'
import { ListItem, Typography } from '@material-ui/core'
import { toMqttConnection, ConnectionOptions } from '../../../model/ConnectionOptions'
import { withStyles, Theme } from '@material-ui/core/styles'
import { bindActionCreators } from 'redux'
import { connectionActions, connectionManagerActions } from '../../../actions'
import { connectionManagerActions } from '../../../actions'
export interface Props {
connection: ConnectionOptions
actions: {
connection: any
connectionManager: any
}
actions: any
selected: boolean
classes: any
}
const ConnectionItem = (props: Props) => {
const connect = useCallback(() => {
const mqttOptions = toMqttConnection(props.connection)
if (mqttOptions) {
props.actions.connection.connect(mqttOptions, props.connection.id)
}
}, [props.connection, props])
const connection = props.connection.host && toMqttConnection(props.connection)
return (
<ListItem
button={true}
selected={props.selected}
style={{ display: 'block' }}
onClick={() => props.actions.connectionManager.selectConnection(props.connection.id)}
onDoubleClick={() => {
props.actions.connectionManager.selectConnection(props.connection.id)
connect()
}}
onClick={() => props.actions.selectConnection(props.connection.id)}
>
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
@@ -44,12 +30,10 @@ const ConnectionItem = (props: Props) => {
export const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
connection: bindActionCreators(connectionActions, dispatch),
connectionManager: bindActionCreators(connectionManagerActions, dispatch),
},
actions: bindActionCreators(connectionManagerActions, dispatch),
}
}
export const connectionItemStyle = (theme: Theme) => ({
name: {
width: '100%',
@@ -67,4 +51,7 @@ export const connectionItemStyle = (theme: Theme) => ({
},
})
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
export default connect(
null,
mapDispatchToProps
)(withStyles(connectionItemStyle)(ConnectionItem))
@@ -7,7 +7,7 @@ import { connect } from 'react-redux'
import { connectionManagerActions } from '../../../actions'
import { ConnectionOptions } from '../../../model/ConnectionOptions'
import { KeyCodes } from '../../../utils/KeyCodes'
import { List } from '@material-ui/core'
import { List, ListSubheader } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'
@@ -77,4 +77,7 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ProfileList))
@@ -1,90 +0,0 @@
import React, { useCallback, useState } from 'react'
import Delete from '@material-ui/icons/Delete'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import {
IconButton,
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Theme,
} from '@material-ui/core'
import { bindActionCreators } from 'redux'
import { withStyles } from '@material-ui/styles'
import { connect } from 'react-redux'
function Subscriptions(props: {
classes: any
connection: ConnectionOptions
managerActions: typeof connectionManagerActions
}) {
const { classes, connection, managerActions } = props
return (
<TableContainer component={Paper} className={`${classes.topicList} advanced-connection-settings-topic-list`}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="left" padding="checkbox" className={classes.tableTitleCell}></TableCell>
<TableCell className={classes.tableTitleCell}>Topic</TableCell>
<TableCell align="right" className={classes.tableTitleCell}>
QoS
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{connection.subscriptions.map(subscription => (
<TableRow key={subscription.topic + '_qos_' + subscription.qos}>
<TableCell align="right" className={classes.tableCell}>
<IconButton
onClick={() => managerActions.deleteSubscription(subscription, connection.id)}
style={{ padding: '6px' }}
>
<Delete />
</IconButton>
</TableCell>
<TableCell component="th" scope="row" className={classes.tableCell}>
{subscription.topic}
</TableCell>
<TableCell align="right" className={classes.tableCell}>
{subscription.qos}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
tableCell: {
paddingTop: 0,
paddingBottom: 0,
wordBbreak: 'break-word',
},
tableTitleCell: {
paddingTop: `${theme.spacing(0.5)}px`,
paddingBottom: `${theme.spacing(0.5)}px`,
},
topicList: {
height: '196px',
overflowY: 'scroll' as 'scroll',
margin: `${theme.spacing(1)}px ${theme.spacing(1)}px 0 ${theme.spacing(1)}px`,
backgroundColor: theme.palette.background.default,
width: 'auto',
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions))
+1 -1
View File
@@ -5,7 +5,7 @@ let heapdump: any
function writeHeapdump(path?: string) {
if (!heapdump) {
//<heapdump = require('heapdump')
heapdump = require('heapdump')
}
heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`)
+2 -1
View File
@@ -2,6 +2,7 @@ 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'
@@ -32,7 +33,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)
}
+4 -1
View File
@@ -102,4 +102,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(PauseButton))
+4 -3
View File
@@ -35,7 +35,6 @@ 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']
@@ -44,7 +43,6 @@ function SearchBar(props: {
if (
(isCharacter || isAllowedControlCharacter) &&
!isModifierKey &&
!event.defaultPrevented &&
!hasFocus &&
tagElementIsNotBlacklisted &&
@@ -142,4 +140,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SearchBar))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(SearchBar))
+4 -1
View File
@@ -103,4 +103,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(TitleBar))
@@ -123,8 +123,8 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
return null
}
const str = node.message.payload ? node.message.payload.toUnicodeString() : ''
let value = node.message && node.message.payload ? parseFloat(str) : NaN
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
let value = node.message && node.message.value ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str
return (
@@ -255,4 +255,9 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Settings))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Settings)
)
@@ -3,17 +3,10 @@ import DateFormatter from '../helper/DateFormatter'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { Input, InputLabel, MenuItem, Select, Theme } from '@material-ui/core'
import { Input, InputLabel, MenuItem, Select, StyleRulesCallback, Theme } from '@material-ui/core'
import { settingsActions } from '../../actions'
import { withStyles } from '@material-ui/styles'
function importAll(r: any) {
r.keys().forEach(r)
}
// @ts-expect-error -- webpack require
importAll(require.context('moment/locale', true, /\.js$/))
const moment = require('moment')
const moment = require('moment/min/moment-with-locales')
interface Props {
actions: {
@@ -89,4 +82,9 @@ const styles = (theme: Theme) => ({
},
})
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TimeLocaleSettings))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(TimeLocaleSettings)
)
@@ -52,16 +52,16 @@ function ChartPreview(props: Props) {
/>
</Tooltip>
) : (
<Tooltip title="Add to chart panel, not enough data for preview">
<ShowChart
onClick={onClick}
className={props.classes.icon}
style={{ color: '#aaa' }}
data-test-type="ShowChart"
data-test={props.literal.path}
/>
</Tooltip>
)
<Tooltip title="Add to chart panel, not enough data for preview">
<ShowChart
onClick={onClick}
className={props.classes.icon}
style={{ color: '#aaa' }}
data-test-type="ShowChart"
data-test={props.literal.path}
/>
</Tooltip>
)
return (
<span>
@@ -69,7 +69,7 @@ function ChartPreview(props: Props) {
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
<Fade in={open} timeout={300}>
<Paper style={{ width: '300px' }}>
{open ? <TopicPlot node={props.treeNode} history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
{open ? <TopicPlot history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
</Paper>
</Fade>
</Popper>
@@ -85,4 +85,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(ChartPreview)
export default connect(
undefined,
mapDispatchToProps
)(ChartPreview)
@@ -43,9 +43,9 @@ class CodeDiff extends React.PureComponent<Props, State> {
private plottableLiteralsIndexedWithLineNumbers() {
const allLiterals = this.isValidJson(this.props.current) ? literalsMappedByLines(this.props.current) || [] : []
return allLiterals.map((l: JsonPropertyLocation) =>
isPlottable(l.value) ? l : undefined
) as Array<JsonPropertyLocation>
return allLiterals.map((l: JsonPropertyLocation) => (isPlottable(l.value) ? l : undefined)) as Array<
JsonPropertyLocation
>
}
private renderStyledCodeLines(changes: Array<Diff.Change>) {
-20
View File
@@ -1,20 +0,0 @@
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
View File
@@ -30,5 +30,4 @@ const Panel = (props: {
)
}
// @ts-ignore
export default withStyles(styles)(Panel)
+5 -14
View File
@@ -1,14 +1,11 @@
import * as React from 'react'
import { default as AceEditor } from 'react-ace'
import { Theme, withTheme } from '@material-ui/core'
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 'brace/mode/json'
import 'brace/theme/dawn'
import 'brace/theme/monokai'
import 'brace/mode/xml'
import 'brace/mode/text'
import 'react-ace'
function Editor(props: {
@@ -16,7 +13,6 @@ function Editor(props: {
theme: Theme
value: string | undefined
onChange: (value: string) => void
editorRef: React.Ref<AceEditor>
}) {
const editorOptions = {
showLineNumbers: false,
@@ -25,17 +21,12 @@ 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,7 +4,6 @@ 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' }
@@ -12,7 +11,6 @@ export function EditorModeSelect(props: Props) {
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={props.value}
onFocus={props.focusEditor}
onChange={props.onChange}
row={true}
>
+50 -85
View File
@@ -1,9 +1,9 @@
import Editor from './Editor'
import { AttachFileOutlined, FormatAlignLeft } from '@material-ui/icons'
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, useRef, memo } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import RetainSwitch from './RetainSwitch'
import TopicInput from './TopicInput'
import { AppState } from '../../../reducers'
@@ -13,7 +13,6 @@ 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
@@ -42,13 +41,8 @@ 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
@@ -76,23 +70,17 @@ function Publish(props: Props) {
return useMemo(
() => (
<div style={{ flexGrow: 1, width: '100%' }} onKeyDown={handleSubmit}>
<div style={{ flexGrow: 1 }} 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}
editorRef={editorRef as any}
/>
<Editor value={props.payload} editorMode={props.editorMode} onChange={props.actions.setPayload} />
<RetainSwitch />
</div>
<PublishHistory history={history} />
@@ -102,10 +90,9 @@ function Publish(props: Props) {
)
}
const EditorMode = memo(function EditorMode(props: {
function EditorMode(props: {
payload?: string
editorMode: string
focusEditor: () => void
actions: typeof publishActions
globalActions: typeof globalActions
publish: () => void
@@ -116,73 +103,52 @@ const EditorMode = memo(function EditorMode(props: {
props.actions.setEditorMode(value)
}, [])
const openFile = useCallback(() => {
props.actions.openFile()
}, [])
const formatJson = useCallback(() => {
if (props.payload) {
try {
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
updatePayload(str)
} catch (error) {
props.globalActions.showError(`Format error: ${(error as Error)?.message}`)
props.globalActions.showError(`Format error: ${error.message}`)
}
}
}, [props.payload])
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} focusEditor={props.focusEditor} />
<FormatJsonButton editorMode={props.editorMode} focusEditor={props.focusEditor} formatJson={formatJson} />
<OpenFileButton editorMode={props.editorMode} openFile={openFile} />
<div style={{ float: 'right' }}>
<PublishButton publish={props.publish} focusEditor={props.focusEditor} />
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>
</div>
</div>
</div>
),
[props.editorMode, renderFormatJson]
)
})
}
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 OpenFileButton = React.memo(function OpenFileButton(props: { editorMode: string; openFile: () => void }) {
return (
<Tooltip title="Open file">
<Fab
style={{ width: '36px', height: '36px', margin: '0 8px' }}
onClick={props.openFile}
id="sidebar-publish-open-file"
>
<AttachFileOutlined style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
})
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
const PublishButton = (props: { publish: () => void }) => {
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
@@ -191,19 +157,15 @@ const PublishButton = memo(function PublishButton(props: { publish: () => void;
[props.publish]
)
return (
<Button
variant="contained"
size="small"
color="primary"
onClick={handleClickPublish}
onFocus={props.focusEditor}
id="publish-button"
>
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
return useMemo(
() => (
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
),
[handleClickPublish]
)
})
}
const mapDispatchToProps = (dispatch: any) => {
return {
@@ -214,11 +176,14 @@ const mapDispatchToProps = (dispatch: any) => {
const mapStateToProps = (state: AppState) => {
return {
topic: state.publish.manualTopic,
topic: state.publish.topic,
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,4 +34,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(PublishHistory)
export default connect(
undefined,
mapDispatchToProps
)(PublishHistory)
@@ -1,34 +0,0 @@
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,8 +1,18 @@
import * as React from 'react'
import { TextField, MenuItem, Tooltip } from '@material-ui/core'
import { QoS } from '../../../backend/src/DataSource/MqttSource'
import { connect } from 'react-redux'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { publishActions } from '../../../actions'
export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void; label?: string }) {
interface Props {
qos: 0 | 1 | 2
actions: {
publish: typeof publishActions
}
}
function QosSelect(props: Props) {
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
const itemStyle = { padding: '0' }
@@ -12,16 +22,16 @@ export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void
if (value !== 0 && value !== 1 && value !== 2) {
return
}
props.onChange(value)
props.actions.publish.setQoS(value)
},
[props.onChange]
[props.actions.publish]
)
return (
<TextField
select={true}
label={props.label}
value={props.selected}
value={props.qos}
margin="normal"
style={{ margin: '8px 0 8px 8px' }}
onChange={onChangeQos}
@@ -45,4 +55,21 @@ export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void
)
}
export default React.memo(QosSelect)
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)
@@ -1,4 +1,4 @@
import QosSelect from './QosPublishOption'
import QosSelect from './QosSelect'
import React from 'react'
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
import { publishActions } from '../../../actions'
@@ -41,4 +41,7 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RetainSwitch)
export default connect(
mapStateToProps,
mapDispatchToProps
)(RetainSwitch)
@@ -1,21 +1,18 @@
import ClearAdornment from '../../helper/ClearAdornment'
import React, { useCallback, useMemo, useRef } from 'react'
import React, { useCallback, useMemo } from 'react'
import { FormControl, Input, InputLabel } from '@material-ui/core'
import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
function TopicInput(props: { actions: typeof publishActions; manualTopic?: string; selectedTopic?: string }) {
const inputElement = useRef<HTMLInputElement>(null)
function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
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>) => {
@@ -24,7 +21,7 @@ function TopicInput(props: { actions: typeof publishActions; manualTopic?: strin
}
}, [])
const topicStr = props.manualTopic || ''
const topicStr = props.topic || ''
return useMemo(
() => (
@@ -32,7 +29,6 @@ function TopicInput(props: { actions: typeof publishActions; manualTopic?: strin
<FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
<Input
inputRef={inputElement}
id="publish-topic"
value={topicStr}
startAdornment={<span />}
@@ -40,7 +36,7 @@ function TopicInput(props: { actions: typeof publishActions; manualTopic?: strin
onBlur={onTopicBlur}
onChange={updateTopic}
multiline={false}
placeholder={props.selectedTopic ?? 'example/topic'}
placeholder="example/topic"
/>
</FormControl>
</div>
@@ -57,9 +53,11 @@ const mapDispatchToProps = (dispatch: any) => {
const mapStateToProps = (state: AppState) => {
return {
manualTopic: state.publish.manualTopic,
selectedTopic: state.tree.get('selectedTopic')?.path(),
topic: state.publish.topic,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TopicInput)
export default connect(
mapStateToProps,
mapDispatchToProps
)(TopicInput)
+10 -3
View File
@@ -1,9 +1,10 @@
import * as q from '../../../../backend/src/Model'
import React, { useState, useEffect, useCallback } from 'react'
import ExpandMore from '@material-ui/icons/ExpandMore'
import NodeStats from './NodeStats'
import ValuePanel from './ValueRenderer/ValuePanel'
import { AppState } from '../../reducers'
import { ExpansionPanelDetails } from '@material-ui/core'
import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../actions'
@@ -27,7 +28,7 @@ interface Props {
}
function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
const [, setLastUpdate] = useState(0)
const [lastUpdate, setLastUpdate] = useState(0)
const updateNode = useCallback(
throttle(() => {
setLastUpdate(node ? node.lastUpdate : 0)
@@ -51,6 +52,7 @@ function Sidebar(props: Props) {
const { classes, tree, nodePath } = props
const node = usePollingToFetchTreeNode(tree, nodePath || '')
useUpdateNodeWhenNodeReceivesUpdates(node)
// console.log(node && node.path(), tree, nodePath)
return (
<div id="Sidebar" className={classes.drawer}>
@@ -97,4 +99,9 @@ const styles = (theme: Theme) => ({
},
})
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar)
)
@@ -66,4 +66,7 @@ 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.payload) {
if (!node || !node.message || !node.message.value) {
return null
}
return (
@@ -6,19 +6,19 @@ import Topic from './Topic'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
import { TopicDeleteButton } from './TopicDeleteButton'
import { TopicTypeButton } from './TopicTypeButton'
import { sidebarActions } from '../../../actions'
import { TopicDeleteButton } from './TopicDeleteButton'
const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions }) => {
const { node } = props
console.log(node && node.path())
const copyTopic = node ? <Copy value={node.path()} /> : null
const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
if (!topic) {
return
}
props.actions.clearTopic(topic, recursive)
}, [])
@@ -29,12 +29,11 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
Topic {copyTopic}
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<TopicTypeButton node={node} />
</span>
<Topic node={node} />
</Panel>
),
[node, node?.childTopicCount()]
[node, node && node.childTopicCount()]
)
}
@@ -44,4 +43,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(TopicPanel)
export default connect(
undefined,
mapDispatchToProps
)(TopicPanel)
@@ -1,103 +0,0 @@
import React, { useCallback, useMemo } from 'react'
import * as q from '../../../../../backend/src/Model'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import Grow from '@material-ui/core/Grow'
import Button from '@material-ui/core/Button'
import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper'
import MenuItem from '@material-ui/core/MenuItem'
import MenuList from '@material-ui/core/MenuList'
import WarningRounded from '@material-ui/icons/WarningRounded'
import { MessageDecoder, decoders } from '../../../decoders'
import { Tooltip } from '@material-ui/core'
export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
const { node } = props
if (!node || !node.message || !node.message.payload) {
return null
}
const options = decoders.flatMap(decoder => decoder.formats.map(format => [decoder, format] as const))
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
const [open, setOpen] = React.useState(false)
const selectOption = useCallback(
(decoder: MessageDecoder, format: string) => {
if (!node) {
return
}
node.viewModel.decoder = { decoder, format }
setOpen(false)
},
[node]
)
const handleToggle = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation()
if (open === true) {
return
}
setAnchorEl(event.currentTarget)
setOpen(prevOpen => !prevOpen)
},
[open]
)
const handleClose = useCallback((event: React.MouseEvent<Document, MouseEvent>) => {
if (anchorEl && anchorEl.contains(event.target as HTMLElement)) {
return
}
setOpen(false)
}, [])
return (
<Button onClick={handleToggle}>
{props.node?.viewModel.decoder?.format ?? props.node?.type}
<Popper open={open} anchorEl={anchorEl} role={undefined} transition>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="topicTypeMode">
{options.map(([decoder, format], index) => (
<MenuItem
key={format}
selected={node && format === node.type}
onClick={() => selectOption(decoder, format)}
>
<DecoderStatus decoder={decoder} format={format} node={node} />
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</Button>
)
}
function DecoderStatus({ node, decoder, format }: { node: q.TreeNode<any>; decoder: MessageDecoder; format: string }) {
const decoded = useMemo(() => {
return node.message?.payload && decoder.decode(node.message?.payload, format)
}, [node.message, decoder, format])
return decoded?.error ? (
<Tooltip title={decoded.error}>
<div>
{format} <WarningRounded />
</div>
</Tooltip>
) : (
<>{format}</>
)
}
@@ -40,7 +40,7 @@ function ActionButtons(props: {
</Tooltip>
</ToggleButton>
<ToggleButton className={props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw / formatted JSON / formatted sparkplugb protojson">
<Tooltip title="Raw / formatted JSON">
<span>
<Reorder className={props.classes.toggleButtonIcon} />
</span>
@@ -73,4 +73,7 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ActionButtons))
@@ -33,4 +33,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(DeleteSelectedTopicButton)
export default connect(
undefined,
mapDispatchToProps
)(DeleteSelectedTopicButton)
@@ -5,15 +5,13 @@ import Copy from '../../helper/Copy'
import DateFormatter from '../../helper/DateFormatter'
import History from '../HistoryDrawer'
import TopicPlot from '../../TopicPlot'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { isPlottable } from '../CodeDiff/util'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { connect } from 'react-redux'
import CustomIconButton from '../../helper/CustomIconButton'
import { MessageId } from '../MessageId'
import { useSubscription } from '../../hooks/useSubscription'
import { useDecoder } from '../../hooks/useDecoder'
const throttle = require('lodash.throttle')
@@ -26,53 +24,69 @@ interface Props {
}
}
export const MessageHistory: React.FC<Props> = props => {
const [, setLastUpdate] = React.useState(Date.now())
const updateNodeThrottled = React.useCallback(
throttle(() => {
setLastUpdate
}, 300),
[]
)
interface State {
displayMessage?: q.Message
anchorEl?: HTMLElement
lastUpdate: number
}
useSubscription(props.node?.onMessage, updateNodeThrottled)
const decodeMessage = useDecoder(props.node)
class MessageHistory extends React.PureComponent<Props, State> {
private updateNode = throttle(() => {
this.setState({ lastUpdate: Date.now() })
}, 300)
function addNodeToCharts(event: React.MouseEvent) {
constructor(props: any) {
super(props)
this.state = { lastUpdate: 0 }
}
private addNodeToCharts = (event: React.MouseEvent) => {
event.preventDefault()
event.stopPropagation()
const { node } = props
const { node } = this.props
if (!node) {
return null
}
props.actions.charts.addChart({ topic: node.path() })
this.props.actions.charts.addChart({ topic: node.path() })
}
function displayMessage(index: number, eventTarget: EventTarget) {
const message = props.node && props.node.messageHistory.toArray().reverse()[index]
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
if (message) {
props.onSelect(message)
this.props.onSelect(message)
}
}
const { node } = props
if (!node) {
return null
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode)
}
const history = node.messageHistory.toArray()
let previousMessage: q.Message | undefined = node.message
const historyElements = [...history].reverse().map((message, idx) => {
const value = node.message ? decodeMessage(message)?.message?.format()[0] ?? null : null
public componentDidMount() {
this.props.node && this.props.node.onMessage.subscribe(this.updateNode)
}
const element = {
value: value ?? '',
key: `${message.messageNumber}-${message.received}`,
title: (
<span>
<div style={{ float: 'left' }}>
public componentWillUnMount() {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
}
public render() {
const { node } = this.props
if (!node) {
return null
}
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 element = {
value,
key: `${message.messageNumber}-${message.received}`,
title: (
<span>
<DateFormatter date={message.received} />
{previousMessage && previousMessage !== message ? (
<i>
@@ -80,46 +94,43 @@ export const MessageHistory: React.FC<Props> = props => {
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
</i>
) : null}
</div>
<span>
&nbsp;
<MessageId message={message} />
<div style={{ float: 'right' }}>
<Copy value={value} />
</div>
</span>
<div style={{ float: 'right' }}>
<Copy value={value ?? ''} />
</div>
</span>
),
selected: message && message === props.selected,
}
previousMessage = message
return element
})
),
selected: message && message === this.props.selected,
}
previousMessage = message
return element
})
const value = node.message ? decodeMessage(node.message)?.message?.format()[0] ?? null : null
const isMessagePlottable = isPlottable(value)
return (
<div>
<History
items={historyElements}
contentTypeIndicator={
isMessagePlottable ? (
<CustomIconButton
style={{ height: '22px', width: '22px' }}
onClick={addNodeToCharts}
tooltip="Add to chart panel"
>
<ShowChart style={{ marginTop: '-5px' }} />
</CustomIconButton>
) : undefined
}
onClick={displayMessage}
>
{isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
</History>
</div>
)
const isMessagePlottable =
node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
return (
<div>
<History
items={historyElements}
contentTypeIndicator={
isMessagePlottable ? (
<CustomIconButton
style={{ height: '22px', width: '22px' }}
onClick={this.addNodeToCharts}
tooltip="Add to chart panel"
>
<ShowChart style={{ marginTop: '-5px' }} />
</CustomIconButton>
) : (
undefined
)
}
onClick={this.displayMessage}
>
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null}
</History>
</div>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
@@ -128,4 +139,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(null, mapDispatchToProps)(React.memo(MessageHistory))
export default connect(
null,
mapDispatchToProps
)(MessageHistory)
@@ -1,20 +1,18 @@
import * as q from '../../../../../backend/src/Model'
import ActionButtons from './ActionButtons'
import Copy from '../../helper/Copy'
import Save from '../../helper/Save'
import DateFormatter from '../../helper/DateFormatter'
import MessageHistory from './MessageHistory'
import Panel from '../Panel'
import React, { useCallback } from 'react'
import ValueRenderer from './ValueRenderer'
import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { bindActionCreators } from 'redux'
import { Theme, Typography, withStyles } from '@material-ui/core'
import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions'
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
import { MessageId } from '../MessageId'
import { useDecoder } from '../../hooks/useDecoder'
interface Props {
node?: q.TreeNode<any>
@@ -36,10 +34,9 @@ function RenderedValue(props: { node?: q.TreeNode<any>; compareMessage?: q.Messa
function ValuePanel(props: Props) {
const { node, compareMessage } = props
const decodeMessage = useDecoder(node)
function renderViewOptions() {
if (!props.node || !props.node.message) {
if (!props.node || !props.node.message || !props.node.mqttMessage) {
return null
}
@@ -49,34 +46,21 @@ function ValuePanel(props: Props) {
<ActionButtons />
</span>
<span style={{ flex: 6, textAlign: 'right' }}>
{props.node.message.retain ? <DeleteSelectedTopicButton /> : null}
{props.node.mqttMessage.retain ? <DeleteSelectedTopicButton /> : null}
</span>
{messageMetaInfo()}
</div>
)
}
const getDecodedValue = useCallback(() => {
return node?.message && decodeMessage(node.message)?.message?.toUnicodeString()
}, [node, decodeMessage])
const getData = () => {
if (node?.message && node.message.payload) {
return node.message.payload.base64Message
}
}
function messageMetaInfo() {
if (!props.node || !props.node.message) {
if (!props.node || !props.node.message || !props.node.mqttMessage) {
return null
}
return (
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
<Typography style={{ textAlign: 'right' }}>
<MessageId message={props.node.message} addComma={true} />
{`QoS: ${props.node.message.qos}`}
</Typography>
<Typography style={{ textAlign: 'right' }}>QoS: {props.node.mqttMessage.qos}</Typography>
<Typography style={{ textAlign: 'right' }}>
<i>
<DateFormatter date={props.node.message.received} />
@@ -97,16 +81,14 @@ function ValuePanel(props: Props) {
[compareMessage]
)
const [value] =
node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
const copyValue = value ? <Copy getValue={getDecodedValue} /> : null
const saveValue = value ? <Save getData={getData} /> : null
const copyValue =
node && node.message && node.message.value ? (
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
) : null
return (
<Panel>
<span>
Value {copyValue} {saveValue}
</span>
<span>Value {copyValue}</span>
<span style={{ width: '100%' }}>
{renderViewOptions()}
<div style={{ marginBottom: '-8px', marginTop: '8px' }}>
@@ -142,5 +124,7 @@ const styles = (theme: Theme) => ({
},
})
// @ts-ignore
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ValuePanel))
@@ -1,13 +1,12 @@
import * as q from '../../../../../backend/src/Model'
import React, { useMemo } from 'react'
import * as React from 'react'
import CodeDiff from '../CodeDiff'
import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import { Fade } from '@material-ui/core'
import { Decoder } from '../../../../../backend/src/Model/Decoder'
import { useDecoder } from '../../hooks/useDecoder'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { Typography, Fade, Grow } from '@material-ui/core'
interface Props {
message: q.Message
@@ -16,114 +15,98 @@ interface Props {
renderMode: ValueRendererDisplayMode
}
type Language = 'json'
function renderDiff(
treeNode: q.TreeNode<TopicViewModel>,
compareWithPreviousMessage: boolean,
current: string = '',
previous: string = '',
title?: string,
language?: Language
) {
return (
<CodeDiff
treeNode={treeNode}
previous={previous}
current={current}
title={title}
language={language}
nameOfCompareMessage={compareWithPreviousMessage ? 'selected' : 'previous'}
/>
)
interface State {
width: number
}
function renderDiffMode(
treeNode: q.TreeNode<TopicViewModel>,
currentStr: string | undefined,
compareStr: string | undefined,
currentType: Language | undefined,
compareType: Language | undefined,
compareWithPreviousMessage: boolean
) {
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined
class ValueRenderer extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { width: 0 }
}
return <div>{renderDiff(treeNode, compareWithPreviousMessage, currentStr, compareStr, undefined, language)}</div>
}
private renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') {
return (
<CodeDiff
treeNode={this.props.treeNode}
previous={previous}
current={current}
title={title}
language={language}
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
/>
)
}
function renderRawMode(
treeNode: q.TreeNode<TopicViewModel>,
currentStr: string | undefined,
compareStr: string | undefined,
currentType: Language | undefined,
compareType: Language | undefined,
compareWithPreviousMessage: boolean
) {
return (
<div>
{renderDiff(treeNode, compareWithPreviousMessage, currentStr, currentStr, undefined, currentType)}
<Fade in={Boolean(compareStr)} timeout={400}>
<div>
{Boolean(compareStr)
? renderDiff(treeNode, compareWithPreviousMessage, compareStr, compareStr, 'selected', compareType)
: null}
</div>
</Fade>
</div>
)
}
export const ValueRenderer: React.FC<Props> = ({ treeNode, compareWith: compare, message, renderMode }) => {
const decodeMessage = useDecoder(treeNode)
const decodedMessage = useMemo(() => decodeMessage(message), [decodeMessage, message])
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compare || previousMessage || message
const compareWithPreviousMessage = !!compare
const [currentStr, currentType] = useMemo(
() => decodedMessage?.message?.format(treeNode.type) ?? [],
[decodedMessage, treeNode.type]
)
const [compareStr, compareType] = useMemo(
() => decodeMessage(compareMessage)?.message?.format(treeNode.type) ?? [],
[compareMessage, decodeMessage, treeNode.type]
)
function renderValue(
treeNode: q.TreeNode<TopicViewModel>,
currentStr: string | undefined,
compareStr: string | undefined,
currentType: Language | undefined,
compareType: Language | undefined,
renderMode: string,
compareWithPreviousMessage: boolean
) {
if (!decodedMessage) {
return null
private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] {
if (!msg) {
return [undefined, undefined]
}
switch (renderMode) {
case 'diff':
return renderDiffMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
default:
return renderRawMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
const str = Base64Message.toUnicodeString(msg)
try {
JSON.parse(str)
} catch (error) {
return [str, undefined]
}
return [this.messageToPrettyJson(str), 'json']
}
private messageToPrettyJson(str: string): string | undefined {
try {
const json = JSON.parse(str)
return JSON.stringify(json, undefined, ' ')
} catch {
return undefined
}
}
const renderedValue = useMemo(
() =>
renderValue(treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage),
[treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage]
)
private renderRawMode(message: q.Message, compare?: q.Message) {
if (!message.value) {
return
}
const [value, valueLanguage] = this.convertMessage(message.value)
const [compareStr, compareStrLanguage] =
compare && compare.value ? this.convertMessage(compare.value) : [undefined, undefined]
return (
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
{decodedMessage?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
{renderedValue}
</div>
)
return (
<div>
{this.renderDiff(value, value, undefined, valueLanguage)}
<Fade in={Boolean(compareStr)} timeout={400}>
<div>
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null}
</div>
</Fade>
</div>
)
}
public render() {
return <div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>{this.renderValue()}</div>
}
public renderValue() {
const { message, treeNode, compareWith, renderMode } = this.props
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {
return this.renderRawMode(message, compareWith)
}
if (!message.value) {
return null
}
const compareValue = compareMessage.value || message.value
const [current, currentLanguage] = this.convertMessage(message.value)
const [compare, compareLanguage] = this.convertMessage(compareValue)
const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined
return this.renderDiff(current, compare, undefined, language)
}
}
const mapStateToProps = (state: AppState) => {
+14 -25
View File
@@ -2,14 +2,12 @@ import * as dotProp from 'dot-prop'
import * as q from '../../../backend/src/Model'
import * as React from 'react'
import PlotHistory from './Chart/Chart'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './Sidebar/CodeDiff/util'
import { PlotCurveTypes } from '../reducers/Charts'
import { DecoderFunction, useDecoder } from './hooks/useDecoder'
const parseDuration = require('parse-duration')
interface Props {
node?: q.TreeNode<any>
history: q.MessageHistory
dotPath?: string
timeInterval?: string
@@ -27,27 +25,21 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
return data
}
function nodeToHistory(decodeMessage: DecoderFunction, startTime: number | undefined, history: q.MessageHistory) {
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
const decoded = decodeMessage(message)?.message?.toUnicodeString()
return { x: message.received.getTime(), y: toPlottableValue(decoded) }
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
return { x: message.received.getTime(), y: toPlottableValue(value) }
})
.filter(data => !isNaN(data.y as any)) as any
}
function nodeDotPathToHistory(
decodeMessage: DecoderFunction,
startTime: number | undefined,
history: q.MessageHistory,
dotPath: string
) {
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
let json: any = {}
let json = {}
try {
const decoded = decodeMessage(message)?.message
json = decoded ? JSON.parse(decoded.toUnicodeString()) : {}
json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
} catch (ignore) {}
const value = dotProp.get(json, dotPath)
@@ -58,17 +50,14 @@ function nodeDotPathToHistory(
}
function TopicPlot(props: Props) {
const decodeMessage = useDecoder(props.node)
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
const data = React.useMemo(() => {
if (!props.node) {
return []
}
return props.dotPath
? nodeDotPathToHistory(decodeMessage, startOffset, props.history, props.dotPath)
: nodeToHistory(decodeMessage, startOffset, props.history)
}, [props.history.last(), startOffset, props.dotPath])
const data = React.useMemo(
() =>
props.dotPath
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
: nodeToHistory(startOffset, props.history),
[props.history.last(), startOffset, props.dotPath]
)
return (
<PlotHistory
@@ -1,8 +1,8 @@
import * as q from '../../../../../backend/src/Model'
import React, { memo } from 'react'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { Theme, withStyles } from '@material-ui/core'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { useDecoder } from '../../hooks/useDecoder'
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode<TopicViewModel>
@@ -14,72 +14,67 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
classes: any
}
export const TreeNodeTitle = (props: TreeNodeProps) => {
const decodeMessage = useDecoder(props.treeNode)
function renderSourceEdge() {
const name = props.name || (props.treeNode.sourceEdge && props.treeNode.sourceEdge.name)
class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
private renderSourceEdge() {
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
return (
<span key="edge" className={props.classes.sourceEdge} data-test-topic={name}>
<span key="edge" className={this.props.classes.sourceEdge}>
{name}
</span>
)
}
function truncatedMessage() {
private truncatedMessage() {
const limit = 400
if (!props.treeNode.message || !props.treeNode.message.payload) {
if (!this.props.treeNode.message || !this.props.treeNode.message.value) {
return ''
}
const [value = ''] = decodeMessage(props.treeNode.message)?.message?.format(props.treeNode.type) ?? []
return value.length > limit ? `${value.slice(0, limit)}` : value
const str = Base64Message.toUnicodeString(this.props.treeNode.message.value)
return str.length > limit ? `${str.slice(0, limit)}` : str
}
function renderValue() {
return props.treeNode.message && props.treeNode.message.payload && props.treeNode.message.length > 0 ? (
<span key="value" className={props.classes.value}>
private renderValue() {
return this.props.treeNode.message &&
this.props.treeNode.message.value &&
this.props.treeNode.message.length > 0 ? (
<span key="value" className={this.props.classes.value}>
{' '}
= {truncatedMessage()}
= {this.truncatedMessage()}
</span>
) : null
}
function renderExpander() {
if (props.treeNode.edgeCount() === 0) {
private renderExpander() {
if (this.props.treeNode.edgeCount() === 0) {
return null
}
return (
<span key="expander" className={props.classes.expander} onClick={props.toggleCollapsed}>
{props.collapsed ? '▶' : '▼'}
<span key="expander" className={this.props.classes.expander} onClick={this.props.toggleCollapsed}>
{this.props.collapsed ? '▶' : '▼'}
</span>
)
}
function renderMetadata() {
if (props.treeNode.edgeCount() === 0 || !props.collapsed) {
private renderMetadata() {
if (this.props.treeNode.edgeCount() === 0 || !this.props.collapsed) {
return null
}
const messages = props.treeNode.leafMessageCount()
const topicCount = props.treeNode.childTopicCount()
const messages = this.props.treeNode.leafMessageCount()
const topicCount = this.props.treeNode.childTopicCount()
return (
<span key="metadata" className={props.classes.collapsedSubnodes}>{` (${topicCount} ${
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${
topicCount === 1 ? 'topic' : 'topics'
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span>
)
}
return (
<>
{renderExpander()}
{renderSourceEdge()}
{renderMetadata()}
{renderValue()}
</>
)
public render() {
return [this.renderExpander(), this.renderSourceEdge(), this.renderMetadata(), this.renderValue()]
}
}
const styles = (theme: Theme) => ({
@@ -1,18 +0,0 @@
import * as q from '../../../../../../backend/src/Model'
import { useEffect } from 'react'
import { TopicViewModel } from '../../../../model/TopicViewModel'
export function useViewModel(treeNode: q.TreeNode<TopicViewModel> | undefined) {
useEffect(() => {
if (treeNode && !treeNode?.viewModel) {
treeNode.viewModel = new TopicViewModel(treeNode)
}
treeNode?.viewModel?.retain()
return function cleanup() {
treeNode?.viewModel?.release()
}
}, [treeNode])
return treeNode?.viewModel
}
@@ -1,8 +1,6 @@
import * as q from '../../../../../../backend/src/Model'
import React, { useCallback } from 'react'
import React, { useEffect } from 'react'
import { TopicViewModel } from '../../../../model/TopicViewModel'
import { useSubscription } from '../../../hooks/useSubscription'
import { useViewModel } from './useViewModel'
export function useViewModelSubscriptions(
treeNode: q.TreeNode<TopicViewModel>,
@@ -10,21 +8,37 @@ export function useViewModelSubscriptions(
setSelected: (value: boolean) => void,
setCollapsedOverride: (value: boolean) => void
) {
const viewModel = useViewModel(treeNode)
useEffect(() => {
const selectionDidChange = () => {
const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
treeNode.viewModel && setSelected(Boolean(selected))
const selectionDidChange = useCallback(() => {
const selected = viewModel && viewModel.isSelected()
viewModel && setSelected(Boolean(selected))
if (selected && nodeRef && nodeRef.current) {
nodeRef.current.focus({ preventScroll: false })
if (selected && nodeRef && nodeRef.current) {
nodeRef.current.focus({ preventScroll: false })
}
}
}, [viewModel])
const expandedDidChange = useCallback(() => {
viewModel && setCollapsedOverride(!viewModel.isExpanded())
}, [viewModel])
const expandedDidChange = () => {
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded())
}
useSubscription(viewModel?.selectionChange, selectionDidChange)
useSubscription(viewModel?.expandedChange, expandedDidChange)
function addSubscriber() {
treeNode.viewModel = new TopicViewModel()
treeNode.viewModel.selectionChange.subscribe(selectionDidChange)
treeNode.viewModel.expandedChange.subscribe(expandedDidChange)
}
function removeSubscriber() {
if (treeNode.viewModel) {
treeNode.viewModel.selectionChange.unsubscribe(selectionDidChange)
treeNode.viewModel.expandedChange.unsubscribe(expandedDidChange)
treeNode.viewModel = undefined
}
}
addSubscriber()
return function cleanup() {
removeSubscriber()
}
}, [treeNode])
}
+1 -1
View File
@@ -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.payload) {
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
didSelectTopic()
}
},
+20 -20
View File
@@ -91,26 +91,23 @@ class TreeComponent extends React.PureComponent<Props, State> {
const updateInterval = Math.max(expectedRenderTime * 7, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
this.updateTimer = setTimeout(
() => {
window.requestIdleCallback(
() => {
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.renderTime = performance.now()
this.updateTimer = setTimeout(() => {
window.requestIdleCallback(
() => {
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.renderTime = performance.now()
window.requestIdleCallback(
() => {
this.setState({ lastUpdate: this.renderTime })
},
{ timeout: 100 }
)
},
{ timeout: 500 }
)
},
Math.max(0, timeUntilNextUpdate)
)
window.requestIdleCallback(
() => {
this.setState({ lastUpdate: this.renderTime })
},
{ timeout: 100 }
)
},
{ timeout: 500 }
)
}, Math.max(0, timeUntilNextUpdate))
}
public componentWillUpdate() {
@@ -172,4 +169,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
export default connect(
mapStateToProps,
mapDispatchToProps
)(TreeComponent)
+21 -20
View File
@@ -1,7 +1,7 @@
import compareVersions from 'compare-versions'
import electron from 'electron'
import os from 'os'
import React from 'react'
import * as compareVersions from 'compare-versions'
import * as electron from 'electron'
import * as os from 'os'
import * as React from 'react'
import axios from 'axios'
import Close from '@material-ui/icons/Close'
import CloudDownload from '@material-ui/icons/CloudDownload'
@@ -13,7 +13,6 @@ 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
@@ -51,21 +50,18 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
super(props)
this.state = { newerVersions: [] }
this.checkForUpdates()
}
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))
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)
}
if (newerVersions.length > 0) {
this.setState({ newerVersions })
this.props.actions.showUpdateNotification(true)
}
})
}
private allowPrereleaseIfOwnVersionIsBeta(release: GithubRelease, ownVersion: string) {
@@ -270,4 +266,9 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(UpdateNotifier)
)
+6 -4
View File
@@ -9,8 +9,7 @@ import { globalActions } from '../../actions'
const copy = require('copy-text-to-clipboard')
interface Props {
value?: string
getValue?: () => string | undefined
value: string
actions: {
global: typeof globalActions
}
@@ -29,7 +28,7 @@ class Copy extends React.PureComponent<Props, State> {
private handleClick = (event: React.MouseEvent) => {
event.stopPropagation()
copy(this.props.value ?? this.props.getValue?.())
copy(this.props.value)
this.props.actions.global.showNotification('Copied to clipboard')
this.setState({ didCopy: true })
setTimeout(() => {
@@ -60,4 +59,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(Copy)
export default connect(
undefined,
mapDispatchToProps
)(Copy)
+5 -11
View File
@@ -1,5 +1,5 @@
import moment from 'moment'
import React from 'react'
import * as moment from 'moment'
import * as React from 'react'
import { AppState } from '../../reducers'
import { connect } from 'react-redux'
@@ -12,7 +12,6 @@ interface Props {
}
const unitMapping = {
ms: 'milliseconds',
s: 'seconds',
m: 'minutes',
h: 'hours',
@@ -22,7 +21,7 @@ class DateFormatter extends React.PureComponent<Props, {}> {
private intervalSince(intervalSince: Date) {
const interval = intervalSince.getTime() - this.props.date.getTime()
const unit = this.unitForInterval(interval)
return `${moment.duration(interval).as(unit).toFixed(3)} ${unitMapping[unit]}`
return `${Math.round(moment.duration(interval).as(unit) * 100) / 100} ${unitMapping[unit]}`
}
private legacyDate() {
@@ -32,11 +31,10 @@ class DateFormatter extends React.PureComponent<Props, {}> {
private localizedDate(locale: string) {
return moment(this.props.date)
.locale(locale)
.format(this.props.timeFirst ? 'LTS.SSS L' : 'L LTS.SSS')
.format(this.props.timeFirst ? 'LTS L' : 'L LTS')
}
private unitForInterval(milliseconds: number) {
const oneSecond = 1000 * 1
const oneMinute = 1000 * 60
const oneHour = oneMinute * 60
@@ -48,11 +46,7 @@ class DateFormatter extends React.PureComponent<Props, {}> {
return 'm'
}
if (milliseconds > oneSecond * 0.5) {
return 's'
}
return 'ms'
return 's'
}
public render() {
-85
View File
@@ -1,85 +0,0 @@
import * as React from 'react'
import { connect } from 'react-redux'
import Check from '@material-ui/icons/Check'
import CustomIconButton from './CustomIconButton'
import { SaveAlt } from '@material-ui/icons'
import { bindActionCreators } from 'redux'
import { rendererRpc, writeToFile } from '../../../../events'
import { makeSaveDialogRpc } from '../../../../events/OpenDialogRequest'
import { globalActions } from '../../actions'
export async function saveToFile(data: string): Promise<string | undefined> {
const rejectReasons = {
errorWritingFile: 'Error writing file',
}
const { canceled, filePath } = await rendererRpc.call(makeSaveDialogRpc(), {
securityScopedBookmarks: true,
})
if (!canceled && filePath !== undefined) {
try {
const filename = await rendererRpc.call(writeToFile, { filePath, data })
return filePath
} catch (error) {
throw rejectReasons.errorWritingFile
}
}
}
interface Props {
getData: () => string | undefined
actions: {
global: typeof globalActions
}
}
interface State {
didSave: boolean
}
class Save extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = { didSave: false }
}
private handleClick = async (event: React.MouseEvent) => {
event.stopPropagation()
const data = this.props.getData()
if (data != undefined) {
const filename = await saveToFile(data)
this.props.actions.global.showNotification(`Saved to ${filename}`)
this.setState({ didSave: true })
setTimeout(() => {
this.setState({ didSave: false })
}, 1500)
}
}
public render() {
const icon = !this.state.didSave ? (
<SaveAlt fontSize="inherit" />
) : (
<Check fontSize="inherit" style={{ cursor: 'default' }} />
)
return (
<CustomIconButton onClick={this.handleClick} tooltip="Save to file">
<div style={{ marginTop: '2px' }}>{icon}</div>
</CustomIconButton>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
global: bindActionCreators(globalActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(Save)
-31
View File
@@ -1,31 +0,0 @@
import * as q from '../../../../backend/src/Model'
import { useCallback, useState } from 'react'
import { TopicViewModel } from '../../model/TopicViewModel'
import { useSubscription } from './useSubscription'
import { useViewModel } from '../Tree/TreeNode/effects/useViewModel'
import { DecoderEnvelope } from '../../decoders/DecoderEnvelope'
import { Decoder } from '../../../../backend/src/Model/Decoder'
export type DecoderFunction = (message: q.Message) => DecoderEnvelope | undefined
/**
* Provides the latest decoder for a topic
*
* @param treeNode
* @returns
*/
export function useDecoder(treeNode: q.TreeNode<TopicViewModel> | undefined): DecoderFunction {
const viewModel = useViewModel(treeNode)
const [decoder, setDecoder] = useState(viewModel?.decoder)
useSubscription(viewModel?.onDecoderChange, setDecoder)
return useCallback(
message => {
return decoder && message.payload
? decoder.decoder.decode(message.payload, decoder.format)
: { message: message.payload ?? undefined, decoder: Decoder.NONE }
},
[decoder]
)
}
@@ -1,10 +0,0 @@
import { useEffect } from 'react'
import { EventDispatcher } from '../../../../events'
export function useSubscription<T>(dispatcher: EventDispatcher<T> | undefined, callback: (value: T) => void) {
useEffect(() => {
dispatcher?.subscribe(callback)
return () => dispatcher?.unsubscribe(callback)
}, [dispatcher, callback])
}
-56
View File
@@ -1,56 +0,0 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Decoder } from '../../../backend/src/Model/Decoder'
import { DecoderEnvelope } from './DecoderEnvelope'
import { MessageDecoder } from './MessageDecoder'
type BinaryFormats =
| 'int8'
| 'int16'
| 'int32'
| 'int64'
| 'uint8'
| 'uint16'
| 'uint32'
| 'uint64'
| 'float'
| 'double'
/**
* Binary decode primitive binary data type and arrays of these
*/
export const BinaryDecoder: MessageDecoder<BinaryFormats> = {
formats: ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float', 'double'],
decode(input: Base64Message, format: BinaryFormats): DecoderEnvelope {
const decodingOption = {
int8: [Buffer.prototype.readInt8, 1],
int16: [Buffer.prototype.readInt16LE, 2],
int32: [Buffer.prototype.readInt32LE, 4],
int64: [Buffer.prototype.readBigInt64LE, 8],
uint8: [Buffer.prototype.readUint8, 1],
uint16: [Buffer.prototype.readUint16LE, 2],
uint32: [Buffer.prototype.readUint32LE, 4],
uint64: [Buffer.prototype.readBigUint64LE, 8],
float: [Buffer.prototype.readFloatLE, 4],
double: [Buffer.prototype.readDoubleLE, 8],
} as const
const [readNumber, bytesToRead] = decodingOption[format]
const buf = input.toBuffer()
let str: String[] = []
if (buf.length % bytesToRead !== 0) {
return {
error: 'Data type does not align with message',
decoder: Decoder.NONE,
}
}
for (let index = 0; index < buf.length; index += bytesToRead) {
str.push((readNumber as any).apply(buf, [index]).toString())
}
return {
message: Base64Message.fromString(JSON.stringify(str.length === 1 ? str[0] : str)),
decoder: Decoder.NONE,
}
},
}
-8
View File
@@ -1,8 +0,0 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Decoder } from '../../../backend/src/Model/Decoder'
export interface DecoderEnvelope {
message?: Base64Message
error?: string
decoder: Decoder
}
-13
View File
@@ -1,13 +0,0 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { DecoderEnvelope } from './DecoderEnvelope'
export interface MessageDecoder<T = string> {
/**
* Can be used to
* @param topic
*/
formats: T[]
canDecodeTopic?(topic: string): boolean
canDecodeData?(data: Base64Message): boolean
decode(input: Base64Message, format: T | string | undefined): DecoderEnvelope
}
-28
View File
@@ -1,28 +0,0 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Decoder } from '../../../backend/src/Model/Decoder'
import { get } from 'sparkplug-payload'
import { MessageDecoder } from './MessageDecoder'
var sparkplug = get('spBv1.0')
export const SparkplugDecoder: MessageDecoder = {
formats: ['Sparkplug'],
canDecodeTopic(topic: string) {
return !!topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u)
},
decode(input) {
try {
const message = Base64Message.fromString(
JSON.stringify(
// @ts-ignore
sparkplug.decodePayload(new Uint8Array(input.toBuffer()))
)
)
return { message, decoder: Decoder.SPARKPLUG }
} catch {
return {
error: 'Failed to decode sparkplugb payload',
decoder: Decoder.NONE,
}
}
},
}
-10
View File
@@ -1,10 +0,0 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Decoder } from '../../../backend/src/Model/Decoder'
import { MessageDecoder } from './MessageDecoder'
export const StringDecoder: MessageDecoder = {
formats: ['string'],
decode(input: Base64Message) {
return { message: input, decoder: Decoder.NONE }
},
}
-6
View File
@@ -1,6 +0,0 @@
import { StringDecoder } from './StringDecoder'
import { BinaryDecoder } from './BinaryDecoder'
import { SparkplugDecoder } from './SparkplugBDecoder'
export * from './MessageDecoder'
export const decoders = [SparkplugDecoder, BinaryDecoder, StringDecoder] as const
+6 -13
View File
@@ -1,6 +1,5 @@
import { MqttOptions } from '../../../backend/src/DataSource'
import { v4 } from 'uuid'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
const sha1 = require('sha1')
export interface CertificateParameters {
@@ -8,9 +7,7 @@ export interface CertificateParameters {
/** @property data base64 encoded data */
data: string
}
export interface ConnectionOptions {
configVersion: 1
type: 'mqtt'
id: string
host: string
@@ -26,7 +23,7 @@ export interface ConnectionOptions {
clientCertificate?: CertificateParameters
clientKey?: CertificateParameters
clientId?: string
subscriptions: Array<Subscription>
subscriptions: Array<string>
}
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
@@ -55,7 +52,6 @@ function generateClientId() {
export function createEmptyConnection(): ConnectionOptions {
return {
configVersion: 1,
certValidation: true,
clientId: generateClientId(),
id: v4() as string,
@@ -63,10 +59,7 @@ export function createEmptyConnection(): ConnectionOptions {
encryption: false,
password: undefined,
username: undefined,
subscriptions: [
{ topic: '#', qos: 0 },
{ topic: '$SYS/#', qos: 0 },
],
subscriptions: ['#', '$SYS/#'],
type: 'mqtt',
host: '',
port: 1883,
@@ -77,11 +70,11 @@ export function createEmptyConnection(): ConnectionOptions {
export function makeDefaultConnections() {
return {
// remember: there was also iot.eclipse.org once
'mqtt.eclipseprojects.io': {
'mqtt.eclipse.org': {
...createEmptyConnection(),
id: 'mqtt.eclipseprojects.io',
name: 'mqtt.eclipseprojects.io',
host: 'mqtt.eclipseprojects.io',
id: 'mqtt.eclipse.org',
name: 'mqtt.eclipse.org',
host: 'mqtt.eclipse.org',
},
'test.mosquitto.org': {
...createEmptyConnection(),
@@ -1,4 +1,5 @@
import { ConnectionOptions, createEmptyConnection } from './ConnectionOptions'
import { v4 } from 'uuid'
interface LegacyConnectionSettings {
host: string
+1 -59
View File
@@ -1,77 +1,19 @@
import * as q from '../../../backend/src/Model'
import { Destroyable } from '../../../backend/src/Model/Destroyable'
import { MessageDecoder, decoders } from '../decoders'
import { EventDispatcher } from '../../../events'
function findDecoder<T extends Destroyable>(node: q.TreeNode<T>): TopicDecoder | undefined {
const decoder = decoders.find(
decoder =>
decoder.canDecodeTopic?.(node.path()) || (node.message?.payload && decoder.canDecodeData?.(node.message?.payload))
)
return decoder
? {
decoder,
format: undefined,
}
: undefined
}
type TopicDecoder = { decoder: MessageDecoder; format: string | undefined }
export class TopicViewModel implements Destroyable {
private selected: boolean
private expanded: boolean
private owner: q.TreeNode<TopicViewModel> | undefined
private _decoder?: TopicDecoder
/**
* Reference counter for useViewModel hook
*/
private referenceCounter = 0
public selectionChange = new EventDispatcher<void>()
public expandedChange = new EventDispatcher<void>()
public onDecoderChange = new EventDispatcher<TopicDecoder | undefined>()
get decoder(): TopicDecoder | undefined {
if (!this._decoder) {
this._decoder = this.owner && findDecoder(this.owner)
}
return this._decoder
}
set decoder(override: TopicDecoder | undefined) {
this._decoder = override
this.onDecoderChange.dispatch(override)
}
public constructor(treeNode: q.TreeNode<TopicViewModel>) {
this.owner = treeNode
public constructor() {
this.selected = false
this.expanded = false
}
public retain() {
this.referenceCounter += 1
}
public release() {
this.referenceCounter -= 1
if (this.referenceCounter <= 0) {
this.destroy()
}
}
public destroy() {
console.log('destroy', this.referenceCounter)
if (this.owner) {
this.owner.viewModel = undefined
this.owner = undefined
}
this.selectionChange.removeAllListeners()
this.onDecoderChange.removeAllListeners()
this.expandedChange.removeAllListeners()
}
public isSelected() {
+1
View File
@@ -1,3 +1,4 @@
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record, List } from 'immutable'
import MoveUp from '../components/ChartPanel/ChartSettings/MoveUp'
+1
View File
@@ -1,4 +1,5 @@
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'
+5 -28
View File
@@ -1,6 +1,6 @@
import { Action } from 'redux'
import { ConnectionOptions } from '../model/ConnectionOptions'
import { createReducer } from './lib'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
export interface ConnectionManagerState {
connections: { [s: string]: ConnectionOptions }
@@ -51,13 +51,13 @@ export interface SelectConnection {
export interface AddSubscription {
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION
subscription: Subscription
subscription: string
connectionId: string
}
export interface DeleteSubscription {
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION
subscription: Subscription
subscription: string
connectionId: string
}
@@ -160,12 +160,8 @@ 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 => !subscriptionsEqual(s, action.subscription))
const newSubscriptions = connection.subscriptions.filter(s => s !== action.subscription)
return {
...state,
@@ -191,29 +187,10 @@ 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,
...changeSet,
...action.changeSet,
}
return {
...state,
connections: {
+3 -2
View File
@@ -1,7 +1,8 @@
import { Action } from 'redux'
import { createReducer } from './lib'
export interface PublishState {
manualTopic?: string
topic?: string
payload?: string
retain: boolean
editorMode: string
@@ -59,7 +60,7 @@ export const publishReducer = createReducer(initialState, {
function setTopic(state: PublishState, action: SetTopic) {
return {
...state,
manualTopic: action.topic,
topic: action.topic,
}
}
+3 -4
View File
@@ -1,5 +1,5 @@
import * as q from '../../../backend/src/Model'
import { Action as ReduxAction } from 'redux'
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record } from 'immutable'
@@ -11,8 +11,7 @@ const initialStateFactory = Record<SidebarModel>({
compareMessage: undefined,
})
export type Action = SetCompareMessage | ResetStore
export type Action = SetCompareMessage
export enum ActionTypes {
SIDEBAR_SET_COMPARE_MESSAGE = 'SIDEBAR_SET_COMPARE_MESSAGE',
SIDEBAR_RESET_STORE = 'SIDEBAR_RESET_STORE',
@@ -21,7 +20,7 @@ export enum ActionTypes {
export type SidebarState = Record<SidebarModel>
const actions: {
[s: string]: (state: SidebarState, action: ReduxAction) => SidebarState
[s: string]: (state: SidebarState, action: Action) => SidebarState
} = {
SIDEBAR_SET_COMPARE_MESSAGE: setCompareMessage,
SIDEBAR_RESET_STORE: resetStore,
+5 -7
View File
@@ -1,5 +1,5 @@
import * as q from '../../../backend/src/Model'
import { Action as ReduxAction } from 'redux'
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record } from 'immutable'
import { TopicViewModel } from '../model/TopicViewModel'
@@ -45,14 +45,12 @@ const initialStateFactory = Record<TreeStateModel>({
filter: undefined,
})
const setPaused =
(pause: boolean) =>
(state: TreeState, action: ShowTree): TreeState => {
return state.set('paused', pause)
}
const setPaused = (pause: boolean) => (state: TreeState, action: ShowTree): TreeState => {
return state.set('paused', pause)
}
const actions: {
[s: string]: (state: TreeState, action: ReduxAction) => TreeState
[s: string]: (state: TreeState, action: Action) => TreeState
} = {
TREE_SHOW_TREE: showTree,
TREE_SELECT_TOPIC: selectTopic,
-34
View File
@@ -1,34 +0,0 @@
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)
}
}
+67 -16
View File
@@ -1,6 +1,13 @@
import { rendererRpc } from '../../../events'
import { rendererEvents } from '../../../events'
import { v4 } from 'uuid'
import { storageStoreEvent, storageLoadEvent, storageClearEvent } from '../../../events/StorageEvents'
import {
storageStoreEvent,
makeStorageResponseEvent,
storageLoadEvent,
storageClearEvent,
makeStorageAcknowledgementEvent,
} from '../../../events/StorageEvents'
export interface StorageIdentifier<Model> {
id: string
@@ -13,27 +20,71 @@ export interface PersistentStorage {
}
class RemoteStorage implements PersistentStorage {
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
return rendererRpc.call(storageStoreEvent, {
data,
store: identifier.id,
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 async load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> {
const result = await rendererRpc.call(
storageLoadEvent,
{
store: identifier.id,
},
10000
)
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
const transactionId = v4()
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageStoreEvent, {
data,
transactionId,
store: identifier.id,
})
return expectation
}
return (result as any).data
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,
store: identifier.id,
})
return promise
}
public clear(): Promise<void> {
return rendererRpc.call(storageClearEvent, undefined, 10000)
const transactionId = v4()
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageClearEvent, { transactionId })
return expectation
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
// import { electronRendererTelemetry } from 'electron-telemetry'
import { electronRendererTelemetry } from 'electron-telemetry'
// const telemetry = electronRendererTelemetry
// electronRendererTelemetry.registerErrorHandler()
const telemetry = electronRendererTelemetry
electronRendererTelemetry.registerErrorHandler()

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