mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-11 09:03:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2404a7eb6 | ||
|
|
bb602cd28e | ||
|
|
898fd3896c | ||
|
|
7fb0483889 | ||
|
|
7d77110c1f | ||
|
|
a7ce6b4419 | ||
|
|
204f6dbcde | ||
|
|
7fa997087f | ||
|
|
a96b08eaaa | ||
|
|
c9c997d13a | ||
|
|
6f3e0f62e1 | ||
|
|
824b39637c | ||
|
|
0100b2988a | ||
|
|
0e72329c77 | ||
|
|
9b7c3b8e9c | ||
|
|
b9a5a5f1d8 | ||
|
|
cc9cc411f0 | ||
|
|
5f75079f2f | ||
|
|
a6e16dcd17 | ||
|
|
f1b13a2919 | ||
|
|
737afb3c1b | ||
|
|
e21011fce1 | ||
|
|
613d0d7178 | ||
|
|
bb964aba20 | ||
|
|
6e5e2e0dd7 | ||
|
|
5afccac2ac | ||
|
|
4626df0bf7 | ||
|
|
252780a51a | ||
|
|
245e661159 | ||
|
|
659033e4eb | ||
|
|
82fbf91f8a | ||
|
|
4275060003 | ||
|
|
6fe74d49fd | ||
|
|
736145d9b4 | ||
|
|
d5ad716086 | ||
|
|
c3f1e7c5e8 | ||
|
|
e1b6c709b0 | ||
|
|
42de570a98 | ||
|
|
30469e63e5 | ||
|
|
899db30e3b | ||
|
|
51d0eaafcc | ||
|
|
cfd1333989 | ||
|
|
6c9d98a763 | ||
|
|
040549c40a | ||
|
|
0c6d777999 | ||
|
|
65c53fd670 | ||
|
|
1b7c9c52f6 | ||
|
|
a63b12b266 | ||
|
|
a8ff4adde7 | ||
|
|
3b8418ccfd | ||
|
|
b51b3065b0 | ||
|
|
1f0c6771e5 | ||
|
|
67277b4652 | ||
|
|
b0e30a896f | ||
|
|
93010dc06e | ||
|
|
302b8f3c21 | ||
|
|
62d087bb0e | ||
|
|
f25cab4682 | ||
|
|
d1de0770f2 | ||
|
|
2a3f481a24 | ||
|
|
ed492ccbf4 | ||
|
|
ea7535b250 | ||
|
|
b17b54490e | ||
|
|
72400af679 | ||
|
|
f8f1ddfebb | ||
|
|
6e4d08e4b5 | ||
|
|
5da8fe0f90 | ||
|
|
df9eda4866 | ||
|
|
6b030ab5ee | ||
|
|
e1493db7c8 | ||
|
|
205ea00c41 | ||
|
|
d428428a6e | ||
|
|
24e9c4cd22 | ||
|
|
d253c6c764 | ||
|
|
13b8f8d5da | ||
|
|
e769ddece4 | ||
|
|
64e807beef | ||
|
|
531af31490 | ||
|
|
e708e1d0c7 | ||
|
|
8fcf8b2478 | ||
|
|
4fdd5b2063 | ||
|
|
e4add31793 | ||
|
|
0cab2169c2 | ||
|
|
86f5b94852 | ||
|
|
f60449c253 | ||
|
|
c1c8b9aa61 | ||
|
|
dcf692d193 | ||
|
|
7f757b9f8a | ||
|
|
23b46cd432 | ||
|
|
68ef9ac913 | ||
|
|
3fa47f0318 | ||
|
|
9cdfa2de7b | ||
|
|
355e9177fc | ||
|
|
dbb6ead7ba | ||
|
|
ad888f6f9a | ||
|
|
07458cd712 | ||
|
|
6fc0d3f28d | ||
|
|
7a91e4dee6 | ||
|
|
3c40be97b7 | ||
|
|
ff00f7a99e | ||
|
|
b72fc48bdb | ||
|
|
e87a7115c5 | ||
|
|
14dac22732 | ||
|
|
ccdaaa6ce2 | ||
|
|
487eee93cd | ||
|
|
59737edfb4 | ||
|
|
30af13f793 | ||
|
|
19e8bfdb37 | ||
|
|
a89d7cf62e | ||
|
|
65e9f2e074 | ||
|
|
ff66d10aaa | ||
|
|
5203d40a24 | ||
|
|
237c718a0a | ||
|
|
fbfbe94d19 | ||
|
|
e24e505cc0 | ||
|
|
28743ba646 | ||
|
|
f63e73ee0a |
+16
-7
@@ -1,20 +1,27 @@
|
||||
{
|
||||
"import": [
|
||||
"@cspell/dict-typescript/cspell-ext.json"
|
||||
],
|
||||
"ignoreRegExpList": [
|
||||
"import(?:(?:(?:[ \\n\\t]+([^ *\\n\\t\\{\\},]+)[ \\n\\t]*(?:,|[ \\n\\t]+))?([ \\n\\t]*\\{(?:[ \\n\\t]*[^ \\n\\t\"'\\{\\}]+[ \\n\\t]*,?)+\\})?[ \\n\\t]*)|[ \n\\n\\t]*\\*[ \\n\\t]*as[ \\n\\t]+([^ \\n\\t\\{\\}]+)[ \\n\\t]+)from[ \\n\\t]*(?:['\"])([^'\"\\n]+)(['\"])\n",
|
||||
"^import\\s+(['\"]).*\\1$"
|
||||
],
|
||||
"language": "en",
|
||||
"words": [
|
||||
"Bbreak",
|
||||
"nodered",
|
||||
"goog",
|
||||
"thomasnordquist",
|
||||
"nowrap",
|
||||
"subheader",
|
||||
"basepath",
|
||||
"webdriverio",
|
||||
"repo",
|
||||
"hexagonalize",
|
||||
"pixelize",
|
||||
"Transistions",
|
||||
"squashfs",
|
||||
"squashfs",
|
||||
"provisionprofile",
|
||||
"Nsis",
|
||||
"webdriverio",
|
||||
"Appx",
|
||||
"Hashable",
|
||||
"clickaway",
|
||||
@@ -24,8 +31,6 @@
|
||||
"Monokai",
|
||||
"plottable",
|
||||
"snackbar",
|
||||
"webdriverio",
|
||||
"prismjs",
|
||||
"Nordquist",
|
||||
"debounced",
|
||||
"mosquitto",
|
||||
@@ -44,6 +49,10 @@
|
||||
"DEVTOOLS",
|
||||
"mixins",
|
||||
"Explorerdmg",
|
||||
"heapsnapshot"
|
||||
"heapsnapshot",
|
||||
"noconflict",
|
||||
"sparkplugb",
|
||||
"protojson",
|
||||
"typesafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package.ts @thomasnordquist
|
||||
.github @thomasnordquist
|
||||
scripts @thomasnordquist
|
||||
@@ -0,0 +1,29 @@
|
||||
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
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
- beta
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
platform_matrix:
|
||||
strategy:
|
||||
matrix:
|
||||
build:
|
||||
- os: ubuntu-latest
|
||||
task: linux
|
||||
- os: windows-latest
|
||||
task: win
|
||||
# - os: macos-latest
|
||||
# task: mac
|
||||
runs-on: ${{ matrix.build.os }}
|
||||
steps:
|
||||
- if: matrix.build.os == 'ubuntu-latest'
|
||||
run: sudo snap install snapcraft --classic
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm install -g yarn
|
||||
- run: yarn
|
||||
- run: npx semantic-release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: yarn build
|
||||
- run: yarn prepare-release
|
||||
# - run: yarn package ${{ matrix.build.task }}
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
||||
@@ -0,0 +1,38 @@
|
||||
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 '' >> $GITHUB_STEP_SUMMARY
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"branches": [
|
||||
"release",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"semantic-release-export-data",
|
||||
"@semantic-release/changelog",
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"npmPublish": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"package.json",
|
||||
"yarn.lock"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
language: node_js
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- $HOME/.cache/electron
|
||||
- $HOME/.cache/electron-builder
|
||||
- $HOME/.npm/_prebuilds
|
||||
|
||||
node_js:
|
||||
- "10"
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
osx_image: xcode10.2
|
||||
|
||||
dist: bionic
|
||||
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:11-stretch
|
||||
FROM node:20
|
||||
|
||||
RUN DEBIAN_FRONTEND="noninteractive" apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
|
||||
&& apt-get install -y --no-install-recommends ca-certificates nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
|
||||
RUN apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
|
||||
|
||||
# Generate locales for TMUX
|
||||
@@ -12,6 +12,5 @@ ENV LC_ALL en_US.UTF-8
|
||||
|
||||
CMD /bin/bash
|
||||
|
||||
COPY cloneBuildAndTest.sh ./
|
||||
VOLUME /app
|
||||
EXPOSE 5900
|
||||
@@ -6,11 +6,12 @@
|
||||
[](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
|
||||
[](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)
|
||||
|
||||
| | | |
|
||||
|:---:|:---:|:---:|
|
||||
|[](https://mqtt-explorer.com/img/screen-composite.png)|[](https://mqtt-explorer.com/img/screen2.png)|[](https://mqtt-explorer.com/img/screen3.png)|
|
||||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: |
|
||||
| [](https://mqtt-explorer.com/img/screen-composite.png) | [](https://mqtt-explorer.com/img/screen2.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.
|
||||
|
||||
@@ -29,6 +30,7 @@ yarn start
|
||||
## Develop
|
||||
|
||||
Launch Application
|
||||
|
||||
```bash
|
||||
npm install -g yarn
|
||||
yarn
|
||||
@@ -41,25 +43,60 @@ 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
|
||||
|
||||

|
||||
|
||||
+170
-153
@@ -1,180 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
|
||||
<title>MQTT Explorer</title>
|
||||
<script src="./bugtracking.bundle.js"></script>
|
||||
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
|
||||
<title>MQTT Explorer</title>
|
||||
<script src="./bugtracking.bundle.js"></script>
|
||||
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[tabindex] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@keyframes updateDark {
|
||||
0% {
|
||||
background-color: none;
|
||||
}
|
||||
|
||||
[tabindex] {
|
||||
outline: none;
|
||||
25% {
|
||||
background-color: #595585;
|
||||
}
|
||||
|
||||
@keyframes updateDark {
|
||||
0% {
|
||||
background-color: none;
|
||||
}
|
||||
25% {
|
||||
background-color: #595585;
|
||||
}
|
||||
50% {
|
||||
background-color: #595585;
|
||||
}
|
||||
100% {
|
||||
background-color: none;
|
||||
}
|
||||
50% {
|
||||
background-color: #595585;
|
||||
}
|
||||
|
||||
@keyframes updateLight {
|
||||
0% {
|
||||
background-color: none;
|
||||
color: inherit;
|
||||
}
|
||||
25% {
|
||||
background-color: #c0c8c0;
|
||||
}
|
||||
50% {
|
||||
background-color: #c0c8c0;
|
||||
}
|
||||
100% {
|
||||
background-color: none;
|
||||
color: inherit;
|
||||
}
|
||||
100% {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes updateLight {
|
||||
0% {
|
||||
background-color: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
25% {
|
||||
background-color: #c0c8c0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
50% {
|
||||
background-color: #c0c8c0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
|
||||
background-color: rgba(140, 140, 140, 0.1);
|
||||
100% {
|
||||
background-color: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(140, 140, 140, 0.8);
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.Resizer {
|
||||
background: rgba(200, 200, 200, 0);
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.Resizer:hover {
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.Resizer.horizontal {
|
||||
height: 10px;
|
||||
margin: -10px 0 0 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
|
||||
background-color: rgba(140, 140, 140, 0.1);
|
||||
}
|
||||
|
||||
.Resizer.horizontal::before {
|
||||
content: '•••';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-top: -22px;
|
||||
color: #aaa;
|
||||
opacity: 1;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(140, 140, 140, 0.8);
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.Resizer {
|
||||
background: rgba(200, 200, 200, 0);
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(120, 120, 120, 0.3);
|
||||
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
|
||||
}
|
||||
.Resizer:hover {
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.Resizer.vertical {
|
||||
width: 2px;
|
||||
margin: 0px -8px 0px 0px;
|
||||
border-left: 0px solid rgba(128, 128, 128, 0);
|
||||
border-right: 8px solid rgba(128, 128, 128, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
.Resizer.horizontal {
|
||||
height: 10px;
|
||||
margin: -10px 0 0 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Resizer.vertical::before {
|
||||
content: '•••';
|
||||
margin-left: -11px;
|
||||
height: 3em;
|
||||
margin-top: calc(50vh - 32px);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
opacity: 1;
|
||||
writing-mode: vertical-lr;
|
||||
text-orientation: sideways;
|
||||
}
|
||||
.Resizer.horizontal::before {
|
||||
content: '•••';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-top: -22px;
|
||||
color: #aaa;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 0px solid rgba(130, 130, 130, 0.3);
|
||||
border-right: 8px solid rgba(140, 140, 140, 0.3);
|
||||
}
|
||||
.Resizer.horizontal:hover {
|
||||
border-top: 5px solid rgba(120, 120, 120, 0.3);
|
||||
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
.Resizer.vertical {
|
||||
width: 2px;
|
||||
margin: 0px -8px 0px 0px;
|
||||
border-left: 0px solid rgba(128, 128, 128, 0);
|
||||
border-right: 8px solid rgba(128, 128, 128, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.example-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
.example-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
.example-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
.example-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" style="font:-webkit-control"></div>
|
||||
<script>
|
||||
function loadScript(path) {
|
||||
var script = document.createElement('script')
|
||||
script.src = path
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
.Resizer.vertical::before {
|
||||
content: '•••';
|
||||
margin-left: -11px;
|
||||
height: 3em;
|
||||
margin-top: calc(50vh - 32px);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
opacity: 1;
|
||||
writing-mode: vertical-lr;
|
||||
text-orientation: sideways;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', onLoad(), false)
|
||||
function onLoad() {
|
||||
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
|
||||
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
|
||||
}
|
||||
</script>
|
||||
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script
|
||||
><% }); %>
|
||||
</body>
|
||||
</html>
|
||||
.Resizer.vertical:hover {
|
||||
border-left: 0px solid rgba(130, 130, 130, 0.3);
|
||||
border-right: 8px solid rgba(140, 140, 140, 0.3);
|
||||
}
|
||||
|
||||
.Resizer.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.example-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.example-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
|
||||
.example-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.example-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
global = globalThis //<- this should be enough
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app" style="font: -webkit-control;"></div>
|
||||
<script>
|
||||
function loadScript(path) {
|
||||
var script = document.createElement('script')
|
||||
script.src = path
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', onLoad(), false)
|
||||
function onLoad() {
|
||||
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
|
||||
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
|
||||
}
|
||||
</script>
|
||||
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>
|
||||
<script src="<%- file %>"></script>
|
||||
<% }); %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+29
-23
@@ -4,28 +4,30 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "yarn rebuild && webpack --mode production",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
|
||||
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=7.1.1 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
|
||||
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
|
||||
"mochatest": "mocha --require ts-node/register --require source-map-support/register --recursive src/*/**/*.spec.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"author": "",
|
||||
"license": "CC-BY-ND-4.0",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4",
|
||||
"@material-ui/core": "4.12",
|
||||
"@material-ui/icons": "^4",
|
||||
"@material-ui/lab": "^4.0.0-alpha",
|
||||
"@material-ui/styles": "^4",
|
||||
"@material-ui/styles": "4.11",
|
||||
"@types/react-transition-group": "^4",
|
||||
"axios": "^0.19.0",
|
||||
"brace": "^0.11.1",
|
||||
"ace-builds": "^1.4.11",
|
||||
"axios": "^0.28.0",
|
||||
"compare-versions": "^3.5.0",
|
||||
"copy-text-to-clipboard": "^2.1.0",
|
||||
"d3": "^5.9.7",
|
||||
"d3-shape": "^1.3.5",
|
||||
"diff": "^4.0.1",
|
||||
"dot-prop": "^5.0.0",
|
||||
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
|
||||
"file-loader": "^4.2.0",
|
||||
"get-value": "^3.0.1",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"in-viewport": "^3.6.0",
|
||||
@@ -33,7 +35,6 @@
|
||||
"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",
|
||||
@@ -47,13 +48,14 @@
|
||||
"react-transition-group": "^4",
|
||||
"react-vis": "^1.11.6",
|
||||
"redux": "^4.0.1",
|
||||
"redux-batched-actions": "^0.4.1",
|
||||
"redux-batched-actions": "0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sha1": "^1.1.1",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"uuid": "^3.3.2"
|
||||
"uuid": "7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"@types/d3": "^5.7.2",
|
||||
"@types/diff": "^4.0.1",
|
||||
"@types/get-value": "^3.0.1",
|
||||
@@ -65,23 +67,27 @@
|
||||
"@types/react-resize-detector": "^4.0.1",
|
||||
"@types/sha1": "^1.1.1",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"@types/uuid": "^7.0.2",
|
||||
"@types/vis": "^4.21.9",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"chai": "^4.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^3.0.0",
|
||||
"hard-source-webpack-plugin": "^0.13.1",
|
||||
"heapdump": "^0.3.12",
|
||||
"html-webpack-plugin": "^4.0.0-beta.5",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mocha": "^10.4.0",
|
||||
"moment": "^2.29.1",
|
||||
"node-loader": "^0.6.0",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"style-loader": "^1",
|
||||
"typescript": "^3.6.3",
|
||||
"webpack": "^4.28.2",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-cli": "^3.3.6",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.5.5",
|
||||
"webpack": "^5.69.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"electron": "^5.0.5"
|
||||
"electron": "^29"
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-56
@@ -45,9 +45,7 @@ export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () =>
|
||||
return
|
||||
}
|
||||
|
||||
const charts = getState()
|
||||
.charts.get('charts')
|
||||
.toArray()
|
||||
const charts = getState().charts.get('charts').toArray()
|
||||
|
||||
let viewStates: ConnectionViewStateDictionary | undefined
|
||||
try {
|
||||
@@ -62,63 +60,55 @@ export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () =>
|
||||
}
|
||||
}
|
||||
|
||||
export const addChart = (chartParameters: ChartParameters) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
const chartExists = Boolean(
|
||||
getState()
|
||||
.charts.get('charts')
|
||||
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
|
||||
)
|
||||
if (chartExists) {
|
||||
dispatch(showNotification('Already added'))
|
||||
return
|
||||
export const addChart =
|
||||
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
const chartExists = Boolean(
|
||||
getState()
|
||||
.charts.get('charts')
|
||||
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
|
||||
)
|
||||
if (chartExists) {
|
||||
dispatch(showNotification('Already added'))
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.CHARTS_ADD,
|
||||
chart: chartParameters,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
dispatch(showNotification('Added to chart panel'))
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.CHARTS_ADD,
|
||||
chart: chartParameters,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
dispatch(showNotification('Added to chart panel'))
|
||||
}
|
||||
export const updateChart =
|
||||
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CHARTS_UPDATE,
|
||||
topic: chartParameters.topic,
|
||||
dotPath: chartParameters.dotPath,
|
||||
parameters: chartParameters,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
}
|
||||
|
||||
export const updateChart = (chartParameters: ChartParameters) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CHARTS_UPDATE,
|
||||
topic: chartParameters.topic,
|
||||
dotPath: chartParameters.dotPath,
|
||||
parameters: chartParameters,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
}
|
||||
export const removeChart =
|
||||
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
dispatch({
|
||||
chart: chartParameters,
|
||||
type: ActionTypes.CHARTS_REMOVE,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
}
|
||||
|
||||
export const removeChart = (chartParameters: ChartParameters) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
dispatch({
|
||||
chart: chartParameters,
|
||||
type: ActionTypes.CHARTS_REMOVE,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
}
|
||||
|
||||
export const moveChartUp = (parameters: { topic: string; dotPath?: string }) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
dispatch({
|
||||
topic: parameters.topic,
|
||||
dotPath: parameters.dotPath,
|
||||
type: ActionTypes.CHARTS_MOVE_UP,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
}
|
||||
export const moveChartUp =
|
||||
(parameters: { topic: string; dotPath?: string }) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
dispatch({
|
||||
topic: parameters.topic,
|
||||
dotPath: parameters.dotPath,
|
||||
type: ActionTypes.CHARTS_MOVE_UP,
|
||||
})
|
||||
dispatch(saveCharts())
|
||||
}
|
||||
|
||||
export const setCharts = (charts: Array<ChartParameters>): Action => {
|
||||
return {
|
||||
|
||||
@@ -11,31 +11,29 @@ import { showError } from './Global'
|
||||
import { TopicViewModel } from '../model/TopicViewModel'
|
||||
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../events'
|
||||
|
||||
export const connect = (options: MqttOptions, connectionId: string) => (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
dispatch(connecting(connectionId))
|
||||
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
|
||||
const event = makeConnectionStateEvent(connectionId)
|
||||
const host = url.parse(options.url).hostname
|
||||
export const connect =
|
||||
(options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
dispatch(connecting(connectionId))
|
||||
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
|
||||
const event = makeConnectionStateEvent(connectionId)
|
||||
const host = url.parse(options.url).hostname
|
||||
|
||||
rendererEvents.subscribe(event, dataSourceState => {
|
||||
if (dataSourceState.connected) {
|
||||
const didReconnect = Boolean(getState().connection.tree)
|
||||
if (!didReconnect) {
|
||||
const tree = new q.Tree<TopicViewModel>()
|
||||
tree.updateWithConnection(rendererEvents, connectionId)
|
||||
dispatch(showTree(tree))
|
||||
dispatch(connected(tree, host!))
|
||||
rendererEvents.subscribe(event, dataSourceState => {
|
||||
if (dataSourceState.connected) {
|
||||
const didReconnect = Boolean(getState().connection.tree)
|
||||
if (!didReconnect) {
|
||||
const tree = new q.Tree<TopicViewModel>()
|
||||
tree.updateWithConnection(rendererEvents, connectionId)
|
||||
dispatch(showTree(tree))
|
||||
dispatch(connected(tree, host!))
|
||||
}
|
||||
} else if (dataSourceState.error) {
|
||||
dispatch(showError(dataSourceState.error))
|
||||
dispatch(disconnect())
|
||||
}
|
||||
} else if (dataSourceState.error) {
|
||||
dispatch(showError(dataSourceState.error))
|
||||
dispatch(disconnect())
|
||||
}
|
||||
dispatch(updateHealth(dataSourceState))
|
||||
})
|
||||
}
|
||||
dispatch(updateHealth(dataSourceState))
|
||||
})
|
||||
}
|
||||
|
||||
const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
let state
|
||||
|
||||
@@ -9,13 +9,15 @@ import {
|
||||
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
|
||||
import { Dispatch } from 'redux'
|
||||
import { showError } from './Global'
|
||||
import { remote } from 'electron'
|
||||
import { promises as fsPromise } from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
import { ActionTypes, Action } from '../reducers/ConnectionManager'
|
||||
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
|
||||
import { connectionsMigrator } from './migrations/Connection'
|
||||
import { rendererRpc } from '../../../events'
|
||||
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
|
||||
|
||||
interface ConnectionDictionary {
|
||||
export interface ConnectionDictionary {
|
||||
[s: string]: ConnectionOptions
|
||||
}
|
||||
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
|
||||
@@ -27,6 +29,12 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
|
||||
try {
|
||||
await ensureConnectionsHaveBeenInitialized()
|
||||
connections = await persistentStorage.load(storedConnectionsIdentifier)
|
||||
|
||||
// Apply migrations
|
||||
if (connections && connectionsMigrator.isMigrationNecessary(connections)) {
|
||||
connections = connectionsMigrator.applyMigrations(connections)
|
||||
await persistentStorage.store(storedConnectionsIdentifier, connections)
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(showError(error))
|
||||
}
|
||||
@@ -43,21 +51,19 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
|
||||
}
|
||||
|
||||
export type CertificateTypes = 'selfSignedCertificate' | 'clientCertificate' | 'clientKey'
|
||||
export const selectCertificate = (type: CertificateTypes, connectionId: string) => async (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
) => {
|
||||
try {
|
||||
const certificate = await openCertificate()
|
||||
dispatch(
|
||||
updateConnection(connectionId, {
|
||||
[type]: certificate,
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
dispatch(showError(error))
|
||||
export const selectCertificate =
|
||||
(type: CertificateTypes, connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
try {
|
||||
const certificate = await openCertificate()
|
||||
dispatch(
|
||||
updateConnection(connectionId, {
|
||||
[type]: certificate,
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
dispatch(showError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openCertificate(): Promise<CertificateParameters> {
|
||||
const rejectReasons = {
|
||||
@@ -65,7 +71,7 @@ async function openCertificate(): Promise<CertificateParameters> {
|
||||
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
|
||||
}
|
||||
|
||||
const openDialogReturnValue = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
|
||||
const openDialogReturnValue = await rendererRpc.call(makeOpenDialogRpc(), {
|
||||
properties: ['openFile'],
|
||||
securityScopedBookmarks: true,
|
||||
})
|
||||
@@ -76,7 +82,7 @@ async function openCertificate(): Promise<CertificateParameters> {
|
||||
}
|
||||
|
||||
const data = await fsPromise.readFile(selectedFile)
|
||||
if (data.length > 16_384 || data.length < 128) {
|
||||
if (data.length > 16_384 || data.length < 64) {
|
||||
throw rejectReasons.certificateSizeDoesNotMatch
|
||||
}
|
||||
|
||||
@@ -101,13 +107,13 @@ export const updateConnection = (connectionId: string, changeSet: Partial<Connec
|
||||
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,
|
||||
})
|
||||
|
||||
export const addSubscription = (subscription: string, connectionId: string): Action => ({
|
||||
export const addSubscription = (subscription: Subscription, connectionId: string): Action => ({
|
||||
connectionId,
|
||||
subscription,
|
||||
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION,
|
||||
})
|
||||
|
||||
export const deleteSubscription = (subscription: string, connectionId: string): Action => ({
|
||||
export const deleteSubscription = (subscription: Subscription, connectionId: string): Action => ({
|
||||
connectionId,
|
||||
subscription,
|
||||
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION,
|
||||
@@ -174,31 +180,4 @@ async function ensureConnectionsHaveBeenInitialized() {
|
||||
|
||||
clearLegacyConnectionOptions()
|
||||
}
|
||||
|
||||
// Migrate connections, rewrite dictionary to "keep" it "ordered" (dictionaries do not have a guaranteed order)
|
||||
const mayNeedMigrations = connections && connections['iot.eclipse.org']
|
||||
if (connections && mayNeedMigrations) {
|
||||
const newConnections = {}
|
||||
for (const connection of Object.values(connections)) {
|
||||
addMigratedConnection(newConnections, connection)
|
||||
}
|
||||
|
||||
await persistentStorage.store(storedConnectionsIdentifier, newConnections)
|
||||
}
|
||||
}
|
||||
|
||||
function addMigratedConnection(newConnections: { [key: string]: ConnectionOptions }, connection: ConnectionOptions) {
|
||||
// The host has been renamed, only change the host if it has not been changed
|
||||
// Also check for ssl since SSL is not yet working
|
||||
if (
|
||||
connection.id === 'iot.eclipse.org' &&
|
||||
connection.host === 'iot.eclipse.org' &&
|
||||
connection.port === 1883 &&
|
||||
!connection.encryption
|
||||
) {
|
||||
connection.id = 'mqtt.eclipse.org'
|
||||
connection.host = 'mqtt.eclipse.org'
|
||||
connection.name = 'mqtt.eclipse.org'
|
||||
}
|
||||
newConnections[connection.id] = connection
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionTypes, ConfirmationRequest } from '../reducers/Global'
|
||||
import { Dispatch } from 'redux'
|
||||
|
||||
export const showError = (error?: string) => ({
|
||||
export const showError = (error?: string | unknown) => ({
|
||||
error,
|
||||
type: ActionTypes.showError,
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ export const setEditorMode = (editorMode: string): Action => {
|
||||
|
||||
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
|
||||
const state = getState()
|
||||
const topic = state.publish.topic
|
||||
const topic = state.publish.manualTopic ?? state.tree.get('selectedTopic')?.path()
|
||||
|
||||
if (!topic) {
|
||||
return
|
||||
|
||||
+20
-22
@@ -43,12 +43,14 @@ export const storeSettings = () => async (dispatch: Dispatch<any>, getState: ()
|
||||
}
|
||||
}
|
||||
|
||||
export const setAutoExpandLimit = (autoExpandLimit: number = 0) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
autoExpandLimit,
|
||||
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
|
||||
})
|
||||
}
|
||||
export const setAutoExpandLimit =
|
||||
(autoExpandLimit: number = 0) =>
|
||||
(dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
autoExpandLimit,
|
||||
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
|
||||
})
|
||||
}
|
||||
|
||||
export const setTimeLocale = (timeLocale: string) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
@@ -81,13 +83,15 @@ export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
|
||||
dispatch(storeSettings())
|
||||
}
|
||||
|
||||
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
topicOrder,
|
||||
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
|
||||
})
|
||||
dispatch(storeSettings())
|
||||
}
|
||||
export const setTopicOrder =
|
||||
(topicOrder: TopicOrder = TopicOrder.none) =>
|
||||
(dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
topicOrder,
|
||||
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
|
||||
})
|
||||
dispatch(storeSettings())
|
||||
}
|
||||
|
||||
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
const { tree } = getState().connection
|
||||
@@ -105,21 +109,15 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
|
||||
const topicFilter = filterStr.toLowerCase()
|
||||
|
||||
const nodeFilter = (node: q.TreeNode<TopicViewModel>): boolean => {
|
||||
const topicMatches =
|
||||
node
|
||||
.path()
|
||||
.toLowerCase()
|
||||
.indexOf(topicFilter) !== -1
|
||||
const topicMatches = node.path().toLowerCase().indexOf(topicFilter) !== -1
|
||||
if (topicMatches) {
|
||||
return true
|
||||
}
|
||||
|
||||
const messageMatches =
|
||||
node.message &&
|
||||
node.message.value &&
|
||||
Base64Message.toUnicodeString(node.message.value)
|
||||
.toLowerCase()
|
||||
.indexOf(filterStr) !== -1
|
||||
node.message.payload &&
|
||||
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1
|
||||
|
||||
return Boolean(messageMatches)
|
||||
}
|
||||
|
||||
+23
-24
@@ -12,12 +12,10 @@ 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) => {
|
||||
@@ -29,9 +27,9 @@ const debouncedSelectTopic = debounce(
|
||||
|
||||
// Update publish topic
|
||||
let setTopicDispatch: any | undefined
|
||||
if (!getState().publish.topic) {
|
||||
if (!getState().publish.manualTopic) {
|
||||
setTopicDispatch = setTopic(topic.path())
|
||||
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.topic) {
|
||||
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.manualTopic) {
|
||||
setTopicDispatch = setTopic(topic.path())
|
||||
}
|
||||
|
||||
@@ -74,25 +72,26 @@ 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')
|
||||
|
||||
@@ -5,51 +5,50 @@ 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) {
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(moveSelectionUpOrDownwards('next'))
|
||||
|
||||
const { connectionId } = getState().connection
|
||||
if (!connectionId) {
|
||||
return
|
||||
}
|
||||
const publishEvent = makePublishEvent(connectionId)
|
||||
|
||||
topicsForPurging
|
||||
.filter(t => t.path() !== '' && t.hasMessage())
|
||||
.map(t => t.path())
|
||||
.forEach((path, idx) => {
|
||||
const mqttMessage = {
|
||||
topic: 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ConfigMigrator, Migration } from '../../utils/ConfigMigrator'
|
||||
import { ConnectionDictionary } from '../ConnectionManager'
|
||||
import { ConnectionOptions } from '../../model/ConnectionOptions'
|
||||
|
||||
export interface ConnectionOptionsV0 {
|
||||
type: 'mqtt'
|
||||
id: string
|
||||
host: string
|
||||
protocol: 'mqtt' | 'ws'
|
||||
basePath?: string
|
||||
port: number
|
||||
name: string
|
||||
username?: string
|
||||
password?: string
|
||||
encryption: boolean
|
||||
certValidation: boolean
|
||||
// selfSignedCertificate?: CertificateParameters
|
||||
// clientCertificate?: CertificateParameters
|
||||
// clientKey?: CertificateParameters
|
||||
clientId?: string
|
||||
subscriptions: Array<string>
|
||||
}
|
||||
|
||||
let migrations: Migration[] = [
|
||||
// iot.eclipse.org ha moved to mqtt.eclipse.org
|
||||
{
|
||||
from: undefined,
|
||||
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
|
||||
if (connection.id == 'iot.eclipse.org' && connection.host == 'iot.eclipse.org' && connection.port == 1883) {
|
||||
return {
|
||||
...connection,
|
||||
id: 'mqtt.eclipse.org',
|
||||
host: 'mqtt.eclipse.org',
|
||||
name: 'mqtt.eclipse.org',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...connection,
|
||||
}
|
||||
},
|
||||
},
|
||||
// Remove stored clientId if it is the default generated client id. This allows to connect multiple instances of mqtt explorer to the same broker.
|
||||
// A randomly generated clientId will be used if no clientId is set.
|
||||
{
|
||||
from: undefined,
|
||||
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
|
||||
if (connection.clientId && /mqtt-explorer-[0-9a-f]{8}/.test(connection.clientId)) {
|
||||
return {
|
||||
...connection,
|
||||
clientId: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...connection,
|
||||
}
|
||||
},
|
||||
},
|
||||
// Added QoS level to subscription options
|
||||
{
|
||||
from: undefined,
|
||||
apply: (connection: ConnectionOptionsV0): ConnectionOptions => {
|
||||
return {
|
||||
...connection,
|
||||
configVersion: 1,
|
||||
subscriptions: connection.subscriptions.map(topic => ({ topic, qos: 0 })),
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const connectionMigrator = new ConfigMigrator(migrations)
|
||||
|
||||
function isMigrationNecessary(connections: ConnectionDictionary): boolean {
|
||||
return Object.values(connections)
|
||||
.map(connection => connectionMigrator.isMigrationNecessary(connection))
|
||||
.reduce((a, b) => a || b, false)
|
||||
}
|
||||
|
||||
function applyMigrations(connections: ConnectionDictionary): ConnectionDictionary {
|
||||
let newConnectionDictionary: ConnectionDictionary = {}
|
||||
Object.keys(connections).forEach(key => {
|
||||
let newConnection = connectionMigrator.applyMigrations(connections[key]) as any
|
||||
newConnectionDictionary[newConnection.id] = newConnection
|
||||
})
|
||||
|
||||
return newConnectionDictionary
|
||||
}
|
||||
|
||||
export const connectionsMigrator = {
|
||||
isMigrationNecessary,
|
||||
applyMigrations,
|
||||
}
|
||||
@@ -6,53 +6,56 @@ import { SettingsState } from '../reducers/Settings'
|
||||
import { sortedNodes } from '../sortedNodes'
|
||||
import { TopicViewModel } from '../model/TopicViewModel'
|
||||
|
||||
export const moveSelectionUpOrDownwards = (direction: 'next' | 'previous') => (
|
||||
dispatch: Dispatch<any>,
|
||||
getState: () => AppState
|
||||
): any => {
|
||||
const state = getState()
|
||||
const selected = state.tree.get('selectedTopic')
|
||||
const tree = state.tree.get('tree')
|
||||
export const moveSelectionUpOrDownwards =
|
||||
(direction: 'next' | 'previous') =>
|
||||
(dispatch: Dispatch<any>, getState: () => AppState): any => {
|
||||
const state = getState()
|
||||
const selected = state.tree.get('selectedTopic')
|
||||
const tree = state.tree.get('tree')
|
||||
|
||||
if (!selected || !tree) {
|
||||
if (tree) {
|
||||
dispatch(selectTopic(tree))
|
||||
if (!selected || !tree) {
|
||||
if (tree) {
|
||||
dispatch(selectTopic(tree))
|
||||
}
|
||||
return
|
||||
}
|
||||
const nextTreeNode = nextVisibleElementInTree(state.settings, tree, selected, direction)
|
||||
if (nextTreeNode && nextTreeNode.viewModel) {
|
||||
dispatch(selectTopic(nextTreeNode))
|
||||
}
|
||||
return
|
||||
}
|
||||
const nextTreeNode = nextVisibleElementInTree(state.settings, tree, selected, direction)
|
||||
if (nextTreeNode && nextTreeNode.viewModel) {
|
||||
dispatch(selectTopic(nextTreeNode))
|
||||
}
|
||||
}
|
||||
|
||||
export const moveInward = () => (dispatch: Dispatch<any>, getState: () => AppState): any => {
|
||||
const state = getState()
|
||||
const selected = state.tree.get('selectedTopic')
|
||||
if (!selected || !selected.viewModel) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
|
||||
selected.viewModel.setExpanded(true, true)
|
||||
} else {
|
||||
dispatch(moveSelectionUpOrDownwards('next'))
|
||||
}
|
||||
}
|
||||
export const moveInward =
|
||||
() =>
|
||||
(dispatch: Dispatch<any>, getState: () => AppState): any => {
|
||||
const state = getState()
|
||||
const selected = state.tree.get('selectedTopic')
|
||||
if (!selected || !selected.viewModel) {
|
||||
return
|
||||
}
|
||||
|
||||
export const moveOutward = () => (dispatch: Dispatch<any>, getState: () => AppState): any => {
|
||||
const state = getState()
|
||||
const selected = state.tree.get('selectedTopic')
|
||||
if (!selected || !selected.viewModel) {
|
||||
return
|
||||
if (!selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
|
||||
selected.viewModel.setExpanded(true, true)
|
||||
} else {
|
||||
dispatch(moveSelectionUpOrDownwards('next'))
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
|
||||
selected.viewModel.setExpanded(false, true)
|
||||
} else {
|
||||
dispatch(moveSelectionUpOrDownwards('previous'))
|
||||
export const moveOutward =
|
||||
() =>
|
||||
(dispatch: Dispatch<any>, getState: () => AppState): any => {
|
||||
const state = getState()
|
||||
const selected = state.tree.get('selectedTopic')
|
||||
if (!selected || !selected.viewModel) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
|
||||
selected.viewModel.setExpanded(false, true)
|
||||
} else {
|
||||
dispatch(moveSelectionUpOrDownwards('previous'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTreeNodeVisible(treeNode: q.TreeNode<any>) {
|
||||
return Boolean(treeNode.viewModel)
|
||||
|
||||
@@ -12,6 +12,7 @@ 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'))
|
||||
@@ -66,6 +67,8 @@ class App extends React.PureComponent<Props, {}> {
|
||||
return null
|
||||
}
|
||||
|
||||
const anyProps: any = {}
|
||||
|
||||
return (
|
||||
<div className={centerContent}>
|
||||
<CssBaseline />
|
||||
@@ -73,7 +76,7 @@ class App extends React.PureComponent<Props, {}> {
|
||||
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
|
||||
{this.renderNotification()}
|
||||
<React.Suspense fallback={<div></div>}>
|
||||
<Settings />
|
||||
<Settings {...anyProps} />
|
||||
</React.Suspense>
|
||||
<div className={centerContent}>
|
||||
<div className={`${settingsVisible ? contentShift : content}`}>
|
||||
@@ -157,9 +160,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
|
||||
|
||||
@@ -65,7 +65,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(ColorSettings))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(ColorSettings))
|
||||
|
||||
@@ -60,7 +60,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(InterpolationSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(InterpolationSettings)
|
||||
|
||||
@@ -33,7 +33,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(MoveUp)
|
||||
export default connect(undefined, mapDispatchToProps)(MoveUp)
|
||||
|
||||
@@ -95,10 +95,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(RangeSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(RangeSettings)
|
||||
|
||||
function useRangeStateToFireUpdateAction(
|
||||
rangeFrom: string | number | undefined,
|
||||
|
||||
@@ -47,7 +47,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(Size))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(Size))
|
||||
|
||||
@@ -96,7 +96,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(TimeRangeSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
|
||||
|
||||
@@ -133,7 +133,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(TopicChart))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
|
||||
|
||||
@@ -126,7 +126,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ChartPanel))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import { useState, useCallback, memo } from 'react'
|
||||
import Add from '@material-ui/icons/Add'
|
||||
import ClearAdornment from '../helper/ClearAdornment'
|
||||
import Delete from '@material-ui/icons/Delete'
|
||||
import Lock from '@material-ui/icons/Lock'
|
||||
import Undo from '@material-ui/icons/Undo'
|
||||
import { bindActionCreators } from 'redux'
|
||||
@@ -9,8 +8,10 @@ import { connect } from 'react-redux'
|
||||
import { connectionManagerActions } from '../../actions'
|
||||
import { ConnectionOptions } from '../../model/ConnectionOptions'
|
||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
|
||||
import { Button, Grid, IconButton, TextField, List, ListItem, ListItemText, Tooltip } from '@material-ui/core'
|
||||
import { Button, Grid, TextField, Tooltip } from '@material-ui/core'
|
||||
import { QosSelect } from '../QosSelect'
|
||||
import { QoS } from '../../../../backend/src/DataSource/MqttSource'
|
||||
import Subscriptions from './Subscriptions'
|
||||
|
||||
interface Props {
|
||||
connection: ConnectionOptions
|
||||
@@ -18,116 +19,93 @@ interface Props {
|
||||
managerActions: typeof connectionManagerActions
|
||||
}
|
||||
|
||||
interface State {
|
||||
subscription: string
|
||||
}
|
||||
const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
|
||||
const [qos, setQos] = useState<QoS>(0)
|
||||
const [topic, setTopic] = useState('')
|
||||
const { classes } = props
|
||||
|
||||
class ConnectionSettings extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { subscription: '' }
|
||||
}
|
||||
|
||||
private handleChange = (name: string) => (event: any) => {
|
||||
this.props.managerActions.updateConnection(this.props.connection.id, {
|
||||
[name]: event.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private renderSubscriptions() {
|
||||
const connection = this.props.connection
|
||||
return connection.subscriptions.map(subscription => (
|
||||
<Subscription
|
||||
deleteAction={() => this.props.managerActions.deleteSubscription(subscription, connection.id)}
|
||||
subscription={subscription}
|
||||
key={subscription}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { classes } = this.props
|
||||
return (
|
||||
<div>
|
||||
<form className={classes.container} noValidate={true} autoComplete="off">
|
||||
<Grid container={true} spacing={3}>
|
||||
<Grid item={true} xs={10} className={classes.gridPadding}>
|
||||
<TextField
|
||||
className={classes.fullWidth}
|
||||
label="Subscription"
|
||||
margin="normal"
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ subscription: event.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={2} className={classes.gridPadding}>
|
||||
<Button
|
||||
className={classes.button}
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
this.props.managerActions.addSubscription(this.state.subscription, this.props.connection.id)
|
||||
}
|
||||
variant="contained"
|
||||
>
|
||||
<Add /> Add
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item={true} xs={12} style={{ padding: 0 }}>
|
||||
<List className={`${classes.topicList} advanced-connection-settings-topic-list`} component="nav">
|
||||
<div className={classes.list}>{this.renderSubscriptions()}</div>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item={true} xs={7} className={classes.gridPadding}>
|
||||
<TextField
|
||||
className={classes.fullWidth}
|
||||
label="MQTT Client ID"
|
||||
margin="normal"
|
||||
value={this.props.connection.clientId}
|
||||
onChange={this.handleChange('clientId')}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={3} className={classes.gridPadding}>
|
||||
<div>
|
||||
<Tooltip title="Manage tls connection certificates" placement="top">
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={() => this.props.managerActions.toggleCertificateSettings()}
|
||||
>
|
||||
<Lock /> Certificates
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item={true} xs={2} className={classes.gridPadding}>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={this.props.managerActions.toggleAdvancedSettings}
|
||||
>
|
||||
<Undo /> Back
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Subscription = (props: { subscription: string; deleteAction: any }) => {
|
||||
return (
|
||||
<ListItem style={{ padding: '0 0 0 8px' }}>
|
||||
<ListItemText>
|
||||
<IconButton onClick={props.deleteAction} style={{ padding: '6px' }}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
{props.subscription}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
const updateSubscription = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => setTopic(event.target.value),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
(name: string) => (event: any) => {
|
||||
props.managerActions.updateConnection(props.connection.id, {
|
||||
[name]: event.target.value,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className={classes.container} noValidate={true} autoComplete="off">
|
||||
<Grid container={true} spacing={3}>
|
||||
<Grid item={true} xs={8} className={classes.gridPadding}>
|
||||
<TextField
|
||||
className={`${classes.fullWidth} advanced-connection-settings-topic-input`}
|
||||
label="Topic"
|
||||
placeholder="example/topic"
|
||||
margin="normal"
|
||||
value={topic}
|
||||
onChange={updateSubscription}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={2} className={classes.gridPadding}>
|
||||
<div className={classes.qos}>
|
||||
<QosSelect label="QoS" selected={qos} onChange={setQos} />
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item={true} xs={2} className={classes.gridPadding}>
|
||||
<Button
|
||||
className={classes.button}
|
||||
color="secondary"
|
||||
onClick={() => props.managerActions.addSubscription({ topic, qos }, props.connection.id)}
|
||||
variant="contained"
|
||||
>
|
||||
<Add /> Add
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item={true} xs={12} style={{ padding: 0 }}>
|
||||
<Subscriptions connection={props.connection} />
|
||||
</Grid>
|
||||
<Grid item={true} xs={7} className={classes.gridPadding}>
|
||||
<TextField
|
||||
className={classes.fullWidth}
|
||||
label="MQTT Client ID"
|
||||
margin="normal"
|
||||
value={props.connection.clientId}
|
||||
onChange={handleChange('clientId')}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={3} className={classes.gridPadding}>
|
||||
<div>
|
||||
<Tooltip title="Manage tls connection certificates" placement="top">
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={() => props.managerActions.toggleCertificateSettings()}
|
||||
>
|
||||
<Lock /> Certificates
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item={true} xs={2} className={classes.gridPadding}>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={props.managerActions.toggleAdvancedSettings}
|
||||
>
|
||||
<Undo /> Back
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
@@ -142,19 +120,13 @@ const styles = (theme: Theme) => ({
|
||||
gridPadding: {
|
||||
padding: '0 12px !important',
|
||||
},
|
||||
topicList: {
|
||||
height: '180px',
|
||||
overflowY: 'scroll' as 'scroll',
|
||||
margin: '8px 16px',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing(3),
|
||||
float: 'right' as 'right',
|
||||
},
|
||||
qos: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ConnectionSettings))
|
||||
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as React from 'react'
|
||||
import Add from '@material-ui/icons/Add'
|
||||
import ClearAdornment from '../helper/ClearAdornment'
|
||||
import Delete from '@material-ui/icons/Delete'
|
||||
import Lock from '@material-ui/icons/Lock'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Button, Theme, Tooltip, Typography } from '@material-ui/core'
|
||||
@@ -81,7 +79,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(CertificateFileSelection))
|
||||
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection))
|
||||
|
||||
@@ -104,7 +104,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(Certificates))
|
||||
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Certificates))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ConnectButton from './ConnectButton'
|
||||
import Delete from '@material-ui/icons/Delete'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Save from '@material-ui/icons/Save'
|
||||
import Delete from '@material-ui/icons/Delete'
|
||||
import Settings from '@material-ui/icons/Settings'
|
||||
import Visibility from '@material-ui/icons/Visibility'
|
||||
import VisibilityOff from '@material-ui/icons/VisibilityOff'
|
||||
@@ -285,7 +285,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ConnectionSettings))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
|
||||
|
||||
@@ -134,7 +134,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ConnectionSetup))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
|
||||
|
||||
@@ -51,7 +51,4 @@ export const connectionItemStyle = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
|
||||
@@ -77,7 +77,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ProfileList))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Delete from '@material-ui/icons/Delete'
|
||||
import { connectionManagerActions } from '../../actions'
|
||||
import { ConnectionOptions } from '../../model/ConnectionOptions'
|
||||
import {
|
||||
IconButton,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Theme,
|
||||
} from '@material-ui/core'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { withStyles } from '@material-ui/styles'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
function Subscriptions(props: {
|
||||
classes: any
|
||||
connection: ConnectionOptions
|
||||
managerActions: typeof connectionManagerActions
|
||||
}) {
|
||||
const { classes, connection, managerActions } = props
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper} className={`${classes.topicList} advanced-connection-settings-topic-list`}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="left" padding="checkbox" className={classes.tableTitleCell}></TableCell>
|
||||
<TableCell className={classes.tableTitleCell}>Topic</TableCell>
|
||||
<TableCell align="right" className={classes.tableTitleCell}>
|
||||
QoS
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{connection.subscriptions.map(subscription => (
|
||||
<TableRow key={subscription.topic + '_qos_' + subscription.qos}>
|
||||
<TableCell align="right" className={classes.tableCell}>
|
||||
<IconButton
|
||||
onClick={() => managerActions.deleteSubscription(subscription, connection.id)}
|
||||
style={{ padding: '6px' }}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" scope="row" className={classes.tableCell}>
|
||||
{subscription.topic}
|
||||
</TableCell>
|
||||
<TableCell align="right" className={classes.tableCell}>
|
||||
{subscription.qos}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
managerActions: bindActionCreators(connectionManagerActions, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
tableCell: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
wordBbreak: 'break-word',
|
||||
},
|
||||
tableTitleCell: {
|
||||
paddingTop: `${theme.spacing(0.5)}px`,
|
||||
paddingBottom: `${theme.spacing(0.5)}px`,
|
||||
},
|
||||
topicList: {
|
||||
height: '196px',
|
||||
overflowY: 'scroll' as 'scroll',
|
||||
margin: `${theme.spacing(1)}px ${theme.spacing(1)}px 0 ${theme.spacing(1)}px`,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
width: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions))
|
||||
@@ -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,7 +2,6 @@ import * as React from 'react'
|
||||
import PersistentStorage from '../utils/PersistentStorage'
|
||||
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
|
||||
import Warning from '@material-ui/icons/Warning'
|
||||
import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
import { Button, Modal, Paper, Toolbar, Typography } from '@material-ui/core'
|
||||
|
||||
@@ -33,7 +32,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: any) {
|
||||
electronRendererTelemetry.trackError(error)
|
||||
// electronRendererTelemetry.trackError(error)
|
||||
console.log('did catch', error)
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(PauseButton))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
|
||||
|
||||
@@ -35,6 +35,7 @@ function SearchBar(props: {
|
||||
|
||||
useGlobalKeyEventHandler(undefined, event => {
|
||||
const isCharacter = event.key.length === 1
|
||||
const isModifierKey = event.metaKey || event.ctrlKey
|
||||
const isAllowedControlCharacter = event.keyCode === KeyCodes.backspace || event.keyCode === KeyCodes.delete
|
||||
const tagNameBlacklist = ['INPUT', 'TEXTAREA', 'RADIO', 'CHECKBOX', 'OPTION', 'FORM']
|
||||
|
||||
@@ -43,6 +44,7 @@ function SearchBar(props: {
|
||||
|
||||
if (
|
||||
(isCharacter || isAllowedControlCharacter) &&
|
||||
!isModifierKey &&
|
||||
!event.defaultPrevented &&
|
||||
!hasFocus &&
|
||||
tagElementIsNotBlacklisted &&
|
||||
@@ -140,7 +142,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(SearchBar))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SearchBar))
|
||||
|
||||
@@ -103,7 +103,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(TitleBar))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
|
||||
|
||||
+7
-34
@@ -1,18 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { TextField, MenuItem, Tooltip } from '@material-ui/core'
|
||||
import { connect } from 'react-redux'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { publishActions } from '../../../actions'
|
||||
import { QoS } from '../../../backend/src/DataSource/MqttSource'
|
||||
|
||||
interface Props {
|
||||
qos: 0 | 1 | 2
|
||||
actions: {
|
||||
publish: typeof publishActions
|
||||
}
|
||||
}
|
||||
|
||||
function QosSelect(props: Props) {
|
||||
export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void; label?: string }) {
|
||||
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
|
||||
const itemStyle = { padding: '0' }
|
||||
|
||||
@@ -22,16 +12,16 @@ function QosSelect(props: Props) {
|
||||
if (value !== 0 && value !== 1 && value !== 2) {
|
||||
return
|
||||
}
|
||||
|
||||
props.actions.publish.setQoS(value)
|
||||
props.onChange(value)
|
||||
},
|
||||
[props.actions.publish]
|
||||
[props.onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<TextField
|
||||
select={true}
|
||||
value={props.qos}
|
||||
label={props.label}
|
||||
value={props.selected}
|
||||
margin="normal"
|
||||
style={{ margin: '8px 0 8px 8px' }}
|
||||
onChange={onChangeQos}
|
||||
@@ -55,21 +45,4 @@ function QosSelect(props: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
publish: bindActionCreators(publishActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
qos: state.publish.qos,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(QosSelect)
|
||||
export default React.memo(QosSelect)
|
||||
@@ -123,8 +123,8 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
|
||||
return null
|
||||
}
|
||||
|
||||
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
|
||||
let value = node.message && node.message.value ? parseFloat(str) : NaN
|
||||
const str = node.message.payload ? Base64Message.toUnicodeString(node.message.payload) : ''
|
||||
let value = node.message && node.message.payload ? parseFloat(str) : NaN
|
||||
value = !isNaN(value) ? abbreviate(value) : str
|
||||
|
||||
return (
|
||||
|
||||
@@ -255,9 +255,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Settings)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Settings))
|
||||
|
||||
@@ -3,10 +3,17 @@ import DateFormatter from '../helper/DateFormatter'
|
||||
import { AppState } from '../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { Input, InputLabel, MenuItem, Select, StyleRulesCallback, Theme } from '@material-ui/core'
|
||||
import { Input, InputLabel, MenuItem, Select, Theme } from '@material-ui/core'
|
||||
import { settingsActions } from '../../actions'
|
||||
import { withStyles } from '@material-ui/styles'
|
||||
const moment = require('moment/min/moment-with-locales')
|
||||
|
||||
function importAll(r: any) {
|
||||
r.keys().forEach(r)
|
||||
}
|
||||
// @ts-expect-error -- webpack require
|
||||
importAll(require.context('moment/locale', true, /\.js$/))
|
||||
|
||||
const moment = require('moment')
|
||||
|
||||
interface Props {
|
||||
actions: {
|
||||
@@ -82,9 +89,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TimeLocaleSettings)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TimeLocaleSettings))
|
||||
|
||||
@@ -85,7 +85,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(ChartPreview)
|
||||
export default connect(undefined, mapDispatchToProps)(ChartPreview)
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { memo } from 'react'
|
||||
import { Message } from '../../../../backend/src/Model'
|
||||
import { Tooltip } from '@material-ui/core'
|
||||
|
||||
export const MessageId = memo(function MessageId(props: { message: Message; addComma?: boolean }) {
|
||||
const { message, addComma } = props
|
||||
|
||||
if (!message.messageId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="MessageIds are used to signal a successful transmission of a message.">
|
||||
<span>
|
||||
#msg: {message.messageId}
|
||||
{addComma ? ', ' : ''}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
@@ -30,4 +30,5 @@ const Panel = (props: {
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default withStyles(styles)(Panel)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as React from 'react'
|
||||
import { default as AceEditor } from 'react-ace'
|
||||
import { Theme, withTheme } from '@material-ui/core'
|
||||
import 'brace/mode/json'
|
||||
import 'brace/theme/dawn'
|
||||
import 'brace/theme/monokai'
|
||||
import 'brace/mode/xml'
|
||||
import 'brace/mode/text'
|
||||
import 'ace-builds'
|
||||
import 'ace-builds/webpack-resolver'
|
||||
import 'ace-builds/src-noconflict/mode-json'
|
||||
import 'ace-builds/src-noconflict/mode-xml'
|
||||
import 'ace-builds/src-noconflict/snippets/json'
|
||||
import 'ace-builds/src-noconflict/snippets/xml'
|
||||
import 'ace-builds/src-noconflict/mode-text'
|
||||
import 'ace-builds/src-noconflict/theme-monokai'
|
||||
import 'react-ace'
|
||||
|
||||
function Editor(props: {
|
||||
@@ -13,6 +16,7 @@ function Editor(props: {
|
||||
theme: Theme
|
||||
value: string | undefined
|
||||
onChange: (value: string) => void
|
||||
editorRef: React.Ref<AceEditor>
|
||||
}) {
|
||||
const editorOptions = {
|
||||
showLineNumbers: false,
|
||||
@@ -21,12 +25,17 @@ function Editor(props: {
|
||||
|
||||
return (
|
||||
<AceEditor
|
||||
ref={props.editorRef}
|
||||
style={{}}
|
||||
mode={props.editorMode}
|
||||
theme={props.theme.palette.type === 'dark' ? 'monokai' : 'dawn'}
|
||||
name="UNIQUE_ID_OF_DIV"
|
||||
width="100%"
|
||||
height="200px"
|
||||
enableSnippets={true}
|
||||
enableBasicAutocompletion={true}
|
||||
enableLiveAutocompletion={true}
|
||||
showPrintMargin={false}
|
||||
showGutter={true}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FormControlLabel, Radio, RadioGroup } from '@material-ui/core'
|
||||
interface Props {
|
||||
value: string
|
||||
onChange: (event: React.ChangeEvent<{}>, value: string) => void
|
||||
focusEditor: () => void
|
||||
}
|
||||
export function EditorModeSelect(props: Props) {
|
||||
const labelStyle = { margin: '0 8px 0 8px' }
|
||||
@@ -11,6 +12,7 @@ export function EditorModeSelect(props: Props) {
|
||||
<RadioGroup
|
||||
style={{ display: 'inline-block', float: 'left' }}
|
||||
value={props.value}
|
||||
onFocus={props.focusEditor}
|
||||
onChange={props.onChange}
|
||||
row={true}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
|
||||
import Message from './Model/Message'
|
||||
import Navigation from '@material-ui/icons/Navigation'
|
||||
import PublishHistory from './PublishHistory'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useMemo, useState, useRef, memo } from 'react'
|
||||
import RetainSwitch from './RetainSwitch'
|
||||
import TopicInput from './TopicInput'
|
||||
import { AppState } from '../../../reducers'
|
||||
@@ -13,6 +13,7 @@ import { connect } from 'react-redux'
|
||||
import { EditorModeSelect } from './EditorModeSelect'
|
||||
import { globalActions, publishActions } from '../../../actions'
|
||||
import { KeyCodes } from '../../../utils/KeyCodes'
|
||||
import { default as AceEditor } from 'react-ace'
|
||||
|
||||
interface Props {
|
||||
connectionId?: string
|
||||
@@ -41,8 +42,13 @@ function useHistory(): [Array<Message>, (topic: string, payload?: string) => voi
|
||||
}
|
||||
|
||||
function Publish(props: Props) {
|
||||
const editorRef = useRef<AceEditor>()
|
||||
const [history, amendToHistory] = useHistory()
|
||||
|
||||
const focusEditor = useCallback(() => {
|
||||
editorRef.current?.editor.focus()
|
||||
}, [editorRef])
|
||||
|
||||
const publish = useCallback(() => {
|
||||
if (!props.connectionId) {
|
||||
return
|
||||
@@ -74,13 +80,19 @@ function Publish(props: Props) {
|
||||
<TopicInput />
|
||||
<div style={{ width: '100%', display: 'block' }}>
|
||||
<EditorMode
|
||||
focusEditor={focusEditor}
|
||||
actions={props.actions}
|
||||
globalActions={props.globalActions}
|
||||
payload={props.payload}
|
||||
editorMode={props.editorMode}
|
||||
publish={publish}
|
||||
/>
|
||||
<Editor value={props.payload} editorMode={props.editorMode} onChange={props.actions.setPayload} />
|
||||
<Editor
|
||||
value={props.payload}
|
||||
editorMode={props.editorMode}
|
||||
onChange={props.actions.setPayload}
|
||||
editorRef={editorRef as any}
|
||||
/>
|
||||
<RetainSwitch />
|
||||
</div>
|
||||
<PublishHistory history={history} />
|
||||
@@ -90,9 +102,10 @@ function Publish(props: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
function EditorMode(props: {
|
||||
const EditorMode = memo(function EditorMode(props: {
|
||||
payload?: string
|
||||
editorMode: string
|
||||
focusEditor: () => void
|
||||
actions: typeof publishActions
|
||||
globalActions: typeof globalActions
|
||||
publish: () => void
|
||||
@@ -109,46 +122,48 @@ function EditorMode(props: {
|
||||
const str = JSON.stringify(JSON.parse(props.payload), undefined, ' ')
|
||||
updatePayload(str)
|
||||
} catch (error) {
|
||||
props.globalActions.showError(`Format error: ${error.message}`)
|
||||
props.globalActions.showError(`Format error: ${(error as Error)?.message}`)
|
||||
}
|
||||
}
|
||||
}, [props.payload])
|
||||
|
||||
const renderFormatJson = useCallback(() => {
|
||||
if (props.editorMode !== 'json') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Format JSON">
|
||||
<Fab
|
||||
style={{ width: '36px', height: '36px', margin: '0 8px' }}
|
||||
onClick={formatJson}
|
||||
id="sidebar-publish-format-json"
|
||||
>
|
||||
<FormatAlignLeft style={{ fontSize: '20px' }} />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
)
|
||||
}, [formatJson, props.editorMode, props.publish])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
|
||||
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
|
||||
{renderFormatJson()}
|
||||
<div style={{ float: 'right' }}>
|
||||
<PublishButton publish={props.publish} />
|
||||
</div>
|
||||
return (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
|
||||
<EditorModeSelect value={props.editorMode} onChange={updateMode} focusEditor={props.focusEditor} />
|
||||
<FormatJsonButton editorMode={props.editorMode} focusEditor={props.focusEditor} formatJson={formatJson} />
|
||||
<div style={{ float: 'right' }}>
|
||||
<PublishButton publish={props.publish} focusEditor={props.focusEditor} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[props.editorMode, renderFormatJson]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const PublishButton = (props: { publish: () => void }) => {
|
||||
const FormatJsonButton = React.memo(function FormatJsonButton(props: {
|
||||
editorMode: string
|
||||
focusEditor: () => void
|
||||
formatJson: () => void
|
||||
}) {
|
||||
if (props.editorMode !== 'json') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Format JSON">
|
||||
<Fab
|
||||
style={{ width: '36px', height: '36px', margin: '0 8px' }}
|
||||
onClick={props.formatJson}
|
||||
onFocus={props.focusEditor}
|
||||
id="sidebar-publish-format-json"
|
||||
>
|
||||
<FormatAlignLeft style={{ fontSize: '20px' }} />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
|
||||
const handleClickPublish = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -157,15 +172,19 @@ const PublishButton = (props: { publish: () => void }) => {
|
||||
[props.publish]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
|
||||
<Navigation style={{ marginRight: '8px' }} /> Publish
|
||||
</Button>
|
||||
),
|
||||
[handleClickPublish]
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleClickPublish}
|
||||
onFocus={props.focusEditor}
|
||||
id="publish-button"
|
||||
>
|
||||
<Navigation style={{ marginRight: '8px' }} /> Publish
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
@@ -176,14 +195,11 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topic: state.publish.topic,
|
||||
topic: state.publish.manualTopic,
|
||||
payload: state.publish.payload,
|
||||
editorMode: state.publish.editorMode,
|
||||
retain: state.publish.retain,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withTheme(Publish))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(Publish))
|
||||
|
||||
@@ -34,7 +34,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(PublishHistory)
|
||||
export default connect(undefined, mapDispatchToProps)(PublishHistory)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { publishActions } from '../../../actions'
|
||||
import { QosSelect } from '../../QosSelect'
|
||||
import { QoS } from '../../../../../backend/src/DataSource/MqttSource'
|
||||
|
||||
interface Props {
|
||||
qos: QoS
|
||||
actions: {
|
||||
publish: typeof publishActions
|
||||
}
|
||||
}
|
||||
|
||||
function QosPublishOption(props: Props) {
|
||||
return <QosSelect onChange={props.actions.publish.setQoS} selected={props.qos} />
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
publish: bindActionCreators(publishActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
qos: state.publish.qos,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QosPublishOption)
|
||||
@@ -1,4 +1,4 @@
|
||||
import QosSelect from './QosSelect'
|
||||
import QosSelect from './QosPublishOption'
|
||||
import React from 'react'
|
||||
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
|
||||
import { publishActions } from '../../../actions'
|
||||
@@ -41,7 +41,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(RetainSwitch)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RetainSwitch)
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import ClearAdornment from '../../helper/ClearAdornment'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import { FormControl, Input, InputLabel } from '@material-ui/core'
|
||||
import { publishActions } from '../../../actions'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
function TopicInput(props: { actions: typeof publishActions; manualTopic?: string; selectedTopic?: string }) {
|
||||
const inputElement = useRef<HTMLInputElement>(null)
|
||||
|
||||
const updateTopic = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.actions.setTopic(e.target.value)
|
||||
}, [])
|
||||
|
||||
const clearTopic = useCallback(() => {
|
||||
props.actions.setTopic('')
|
||||
inputElement.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onTopicBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
|
||||
@@ -21,7 +24,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const topicStr = props.topic || ''
|
||||
const topicStr = props.manualTopic || ''
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
@@ -29,6 +32,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
<FormControl style={{ width: '100%' }}>
|
||||
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
|
||||
<Input
|
||||
inputRef={inputElement}
|
||||
id="publish-topic"
|
||||
value={topicStr}
|
||||
startAdornment={<span />}
|
||||
@@ -36,7 +40,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
onBlur={onTopicBlur}
|
||||
onChange={updateTopic}
|
||||
multiline={false}
|
||||
placeholder="example/topic"
|
||||
placeholder={props.selectedTopic ?? 'example/topic'}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -53,11 +57,9 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topic: state.publish.topic,
|
||||
manualTopic: state.publish.manualTopic,
|
||||
selectedTopic: state.tree.get('selectedTopic')?.path(),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TopicInput)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TopicInput)
|
||||
|
||||
@@ -99,9 +99,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Sidebar)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
|
||||
|
||||
@@ -66,7 +66,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles, { withTheme: true })(Topic))
|
||||
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic))
|
||||
|
||||
@@ -8,7 +8,7 @@ export const TopicDeleteButton = (props: {
|
||||
deleteTopicAction: (node: q.TreeNode<any>) => void
|
||||
}) => {
|
||||
const { node } = props
|
||||
if (!node || !node.message || !node.message.value) {
|
||||
if (!node || !node.message || !node.message.payload) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -43,7 +43,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(TopicPanel)
|
||||
export default connect(undefined, mapDispatchToProps)(TopicPanel)
|
||||
|
||||
@@ -40,7 +40,7 @@ function ActionButtons(props: {
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
<ToggleButton className={props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
|
||||
<Tooltip title="Raw / formatted JSON">
|
||||
<Tooltip title="Raw / formatted JSON / formatted sparkplugb protojson">
|
||||
<span>
|
||||
<Reorder className={props.classes.toggleButtonIcon} />
|
||||
</span>
|
||||
@@ -73,7 +73,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ActionButtons))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons))
|
||||
|
||||
@@ -33,7 +33,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(DeleteSelectedTopicButton)
|
||||
export default connect(undefined, mapDispatchToProps)(DeleteSelectedTopicButton)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { bindActionCreators } from 'redux'
|
||||
import { chartActions } from '../../../actions'
|
||||
import { connect } from 'react-redux'
|
||||
import CustomIconButton from '../../helper/CustomIconButton'
|
||||
import { MessageId } from '../MessageId'
|
||||
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
@@ -81,19 +82,25 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
const history = node.messageHistory.toArray()
|
||||
let previousMessage: q.Message | undefined = node.message
|
||||
const historyElements = [...history].reverse().map((message, idx) => {
|
||||
const value = message.value ? Base64Message.toUnicodeString(message.value) : ''
|
||||
const value = message.payload ? Base64Message.toUnicodeString(message.payload) : ''
|
||||
const element = {
|
||||
value,
|
||||
key: `${message.messageNumber}-${message.received}`,
|
||||
title: (
|
||||
<span>
|
||||
<DateFormatter date={message.received} />
|
||||
{previousMessage && previousMessage !== message ? (
|
||||
<i>
|
||||
(-
|
||||
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
|
||||
</i>
|
||||
) : null}
|
||||
<div style={{ float: 'left' }}>
|
||||
<DateFormatter date={message.received} />
|
||||
{previousMessage && previousMessage !== message ? (
|
||||
<i>
|
||||
(-
|
||||
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
|
||||
</i>
|
||||
) : null}
|
||||
</div>
|
||||
<span>
|
||||
|
||||
<MessageId message={message} />
|
||||
</span>
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value} />
|
||||
</div>
|
||||
@@ -106,7 +113,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
})
|
||||
|
||||
const isMessagePlottable =
|
||||
node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
|
||||
node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload))
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
@@ -120,9 +127,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
undefined
|
||||
)
|
||||
) : undefined
|
||||
}
|
||||
onClick={this.displayMessage}
|
||||
>
|
||||
@@ -139,7 +144,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(MessageHistory)
|
||||
export default connect(null, mapDispatchToProps)(MessageHistory)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Theme, Typography, withStyles } from '@material-ui/core'
|
||||
import { connect } from 'react-redux'
|
||||
import { sidebarActions } from '../../../actions'
|
||||
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
|
||||
import { MessageId } from '../MessageId'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode<any>
|
||||
@@ -36,7 +37,7 @@ function ValuePanel(props: Props) {
|
||||
const { node, compareMessage } = props
|
||||
|
||||
function renderViewOptions() {
|
||||
if (!props.node || !props.node.message || !props.node.mqttMessage) {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ function ValuePanel(props: Props) {
|
||||
<ActionButtons />
|
||||
</span>
|
||||
<span style={{ flex: 6, textAlign: 'right' }}>
|
||||
{props.node.mqttMessage.retain ? <DeleteSelectedTopicButton /> : null}
|
||||
{props.node.message.retain ? <DeleteSelectedTopicButton /> : null}
|
||||
</span>
|
||||
{messageMetaInfo()}
|
||||
</div>
|
||||
@@ -54,13 +55,16 @@ function ValuePanel(props: Props) {
|
||||
}
|
||||
|
||||
function messageMetaInfo() {
|
||||
if (!props.node || !props.node.message || !props.node.mqttMessage) {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
|
||||
<Typography style={{ textAlign: 'right' }}>QoS: {props.node.mqttMessage.qos}</Typography>
|
||||
<Typography style={{ textAlign: 'right' }}>
|
||||
<MessageId message={props.node.message} addComma={true} />
|
||||
{`QoS: ${props.node.message.qos}`}
|
||||
</Typography>
|
||||
<Typography style={{ textAlign: 'right' }}>
|
||||
<i>
|
||||
<DateFormatter date={props.node.message.received} />
|
||||
@@ -82,8 +86,8 @@ function ValuePanel(props: Props) {
|
||||
)
|
||||
|
||||
const copyValue =
|
||||
node && node.message && node.message.value ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
|
||||
node && node.message && node.message.payload ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.payload)} />
|
||||
) : null
|
||||
|
||||
return (
|
||||
@@ -124,7 +128,5 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ValuePanel))
|
||||
// @ts-ignore
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
|
||||
|
||||
@@ -4,9 +4,9 @@ import CodeDiff from '../CodeDiff'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { connect } from 'react-redux'
|
||||
import { default as ReactResizeDetector } from 'react-resize-detector'
|
||||
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
|
||||
import { Typography, Fade, Grow } from '@material-ui/core'
|
||||
import { Fade } from '@material-ui/core'
|
||||
import { Decoder } from '../../../../../backend/src/Model/Decoder'
|
||||
|
||||
interface Props {
|
||||
message: q.Message
|
||||
@@ -63,12 +63,12 @@ class ValueRenderer extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
private renderRawMode(message: q.Message, compare?: q.Message) {
|
||||
if (!message.value) {
|
||||
if (!message.payload) {
|
||||
return
|
||||
}
|
||||
const [value, valueLanguage] = this.convertMessage(message.value)
|
||||
const [value, valueLanguage] = this.convertMessage(message.payload)
|
||||
const [compareStr, compareStrLanguage] =
|
||||
compare && compare.value ? this.convertMessage(compare.value) : [undefined, undefined]
|
||||
compare && compare.payload ? this.convertMessage(compare.payload) : [undefined, undefined]
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -83,7 +83,12 @@ class ValueRenderer extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>{this.renderValue()}</div>
|
||||
return (
|
||||
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
|
||||
{this.props.message?.payload?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
|
||||
{this.renderValue()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public renderValue() {
|
||||
@@ -95,12 +100,12 @@ class ValueRenderer extends React.Component<Props, State> {
|
||||
if (renderMode === 'raw') {
|
||||
return this.renderRawMode(message, compareWith)
|
||||
}
|
||||
if (!message.value) {
|
||||
if (!message.payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
const compareValue = compareMessage.value || message.value
|
||||
const [current, currentLanguage] = this.convertMessage(message.value)
|
||||
const compareValue = compareMessage.payload || message.payload
|
||||
const [current, currentLanguage] = this.convertMessage(message.payload)
|
||||
const [compare, compareLanguage] = this.convertMessage(compareValue)
|
||||
|
||||
const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined
|
||||
|
||||
@@ -28,7 +28,7 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
|
||||
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
|
||||
return filterUsingTimeRange(startTime, history.toArray())
|
||||
.map((message: q.Message) => {
|
||||
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
|
||||
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN
|
||||
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
||||
})
|
||||
.filter(data => !isNaN(data.y as any)) as any
|
||||
@@ -37,9 +37,9 @@ function nodeToHistory(startTime: number | undefined, history: q.MessageHistory)
|
||||
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
|
||||
return filterUsingTimeRange(startTime, history.toArray())
|
||||
.map((message: q.Message) => {
|
||||
let json = {}
|
||||
let json: any = {}
|
||||
try {
|
||||
json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
|
||||
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {}
|
||||
} catch (ignore) {}
|
||||
|
||||
const value = dotProp.get(json, dotPath)
|
||||
|
||||
@@ -19,7 +19,7 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
|
||||
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
|
||||
|
||||
return (
|
||||
<span key="edge" className={this.props.classes.sourceEdge}>
|
||||
<span key="edge" className={this.props.classes.sourceEdge} data-test-topic={name}>
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
@@ -27,17 +27,17 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
|
||||
|
||||
private truncatedMessage() {
|
||||
const limit = 400
|
||||
if (!this.props.treeNode.message || !this.props.treeNode.message.value) {
|
||||
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const str = Base64Message.toUnicodeString(this.props.treeNode.message.value)
|
||||
const str = Base64Message.toUnicodeString(this.props.treeNode.message.payload)
|
||||
return str.length > limit ? `${str.slice(0, limit)}…` : str
|
||||
}
|
||||
|
||||
private renderValue() {
|
||||
return this.props.treeNode.message &&
|
||||
this.props.treeNode.message.value &&
|
||||
this.props.treeNode.message.payload &&
|
||||
this.props.treeNode.message.length > 0 ? (
|
||||
<span key="value" className={this.props.classes.value}>
|
||||
{' '}
|
||||
|
||||
@@ -81,7 +81,7 @@ function TreeNodeComponent(props: Props) {
|
||||
const mouseOver = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
|
||||
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.payload) {
|
||||
didSelectTopic()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -91,23 +91,26 @@ class TreeComponent extends React.PureComponent<Props, State> {
|
||||
const updateInterval = Math.max(expectedRenderTime * 7, 300)
|
||||
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
window.requestIdleCallback(
|
||||
() => {
|
||||
this.updateTimer && clearTimeout(this.updateTimer)
|
||||
this.updateTimer = undefined
|
||||
this.renderTime = performance.now()
|
||||
this.updateTimer = setTimeout(
|
||||
() => {
|
||||
window.requestIdleCallback(
|
||||
() => {
|
||||
this.updateTimer && clearTimeout(this.updateTimer)
|
||||
this.updateTimer = undefined
|
||||
this.renderTime = performance.now()
|
||||
|
||||
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() {
|
||||
@@ -169,7 +172,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TreeComponent)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
import { updateNotifierActions } from '../actions'
|
||||
|
||||
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@material-ui/core'
|
||||
import { rendererRpc, getAppVersion } from '../../../events'
|
||||
|
||||
interface Props {
|
||||
showUpdateNotification: boolean
|
||||
@@ -50,18 +51,21 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
|
||||
super(props)
|
||||
this.state = { newerVersions: [] }
|
||||
|
||||
const ownVersion = electron.remote.app.getVersion()
|
||||
this.fetchReleases().then(releases => {
|
||||
const newerVersions = releases
|
||||
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
|
||||
.filter(release => compareVersions(release.tag_name, ownVersion) > 0)
|
||||
.sort((a, b) => compareVersions(b.tag_name, a.tag_name))
|
||||
this.checkForUpdates()
|
||||
}
|
||||
|
||||
if (newerVersions.length > 0) {
|
||||
this.setState({ newerVersions })
|
||||
this.props.actions.showUpdateNotification(true)
|
||||
}
|
||||
})
|
||||
private async checkForUpdates() {
|
||||
const ownVersion = await rendererRpc.call(getAppVersion, undefined, 10000)
|
||||
const releases = await this.fetchReleases()
|
||||
const newerVersions = releases
|
||||
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
|
||||
.filter(release => compareVersions(release.tag_name, ownVersion) > 0)
|
||||
.sort((a, b) => compareVersions(b.tag_name, a.tag_name))
|
||||
|
||||
if (newerVersions.length > 0) {
|
||||
this.setState({ newerVersions })
|
||||
this.props.actions.showUpdateNotification(true)
|
||||
}
|
||||
}
|
||||
|
||||
private allowPrereleaseIfOwnVersionIsBeta(release: GithubRelease, ownVersion: string) {
|
||||
@@ -266,9 +270,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UpdateNotifier)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
|
||||
|
||||
@@ -59,7 +59,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(Copy)
|
||||
export default connect(undefined, mapDispatchToProps)(Copy)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MqttOptions } from '../../../backend/src/DataSource'
|
||||
import { v4 } from 'uuid'
|
||||
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
|
||||
const sha1 = require('sha1')
|
||||
|
||||
export interface CertificateParameters {
|
||||
@@ -7,7 +8,9 @@ export interface CertificateParameters {
|
||||
/** @property data base64 encoded data */
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface ConnectionOptions {
|
||||
configVersion: 1
|
||||
type: 'mqtt'
|
||||
id: string
|
||||
host: string
|
||||
@@ -23,7 +26,7 @@ export interface ConnectionOptions {
|
||||
clientCertificate?: CertificateParameters
|
||||
clientKey?: CertificateParameters
|
||||
clientId?: string
|
||||
subscriptions: Array<string>
|
||||
subscriptions: Array<Subscription>
|
||||
}
|
||||
|
||||
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
|
||||
@@ -52,6 +55,7 @@ function generateClientId() {
|
||||
|
||||
export function createEmptyConnection(): ConnectionOptions {
|
||||
return {
|
||||
configVersion: 1,
|
||||
certValidation: true,
|
||||
clientId: generateClientId(),
|
||||
id: v4() as string,
|
||||
@@ -59,7 +63,10 @@ export function createEmptyConnection(): ConnectionOptions {
|
||||
encryption: false,
|
||||
password: undefined,
|
||||
username: undefined,
|
||||
subscriptions: ['#', '$SYS/#'],
|
||||
subscriptions: [
|
||||
{ topic: '#', qos: 0 },
|
||||
{ topic: '$SYS/#', qos: 0 },
|
||||
],
|
||||
type: 'mqtt',
|
||||
host: '',
|
||||
port: 1883,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ConnectionOptions } from '../model/ConnectionOptions'
|
||||
import { createReducer } from './lib'
|
||||
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
|
||||
|
||||
export interface ConnectionManagerState {
|
||||
connections: { [s: string]: ConnectionOptions }
|
||||
@@ -50,13 +51,13 @@ export interface SelectConnection {
|
||||
|
||||
export interface AddSubscription {
|
||||
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION
|
||||
subscription: string
|
||||
subscription: Subscription
|
||||
connectionId: string
|
||||
}
|
||||
|
||||
export interface DeleteSubscription {
|
||||
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION
|
||||
subscription: string
|
||||
subscription: Subscription
|
||||
connectionId: string
|
||||
}
|
||||
|
||||
@@ -159,8 +160,12 @@ function addSubscription(state: ConnectionManagerState, action: AddSubscription)
|
||||
}
|
||||
|
||||
function deleteSubscription(state: ConnectionManagerState, action: AddSubscription): ConnectionManagerState {
|
||||
function subscriptionsEqual(v1: Subscription, v2: Subscription): boolean {
|
||||
return v1.topic == v2.topic && v1.qos == v2.qos
|
||||
}
|
||||
|
||||
const connection = state.connections[action.connectionId]
|
||||
const newSubscriptions = connection.subscriptions.filter(s => s !== action.subscription)
|
||||
const newSubscriptions = connection.subscriptions.filter(s => !subscriptionsEqual(s, action.subscription))
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -200,7 +205,7 @@ function updateConnection(state: ConnectionManagerState, action: UpdateConnectio
|
||||
if (changeSet.password !== undefined) {
|
||||
changeSet = {
|
||||
changeSet,
|
||||
username: changeSet.password === '' ? undefined : changeSet.password,
|
||||
password: changeSet.password === '' ? undefined : changeSet.password,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createReducer } from './lib'
|
||||
|
||||
export interface PublishState {
|
||||
topic?: string
|
||||
manualTopic?: string
|
||||
payload?: string
|
||||
retain: boolean
|
||||
editorMode: string
|
||||
@@ -59,7 +59,7 @@ export const publishReducer = createReducer(initialState, {
|
||||
function setTopic(state: PublishState, action: SetTopic) {
|
||||
return {
|
||||
...state,
|
||||
topic: action.topic,
|
||||
manualTopic: action.topic,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,9 +45,11 @@ 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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export interface Migration {
|
||||
from: number | undefined
|
||||
apply: (config: any) => any
|
||||
}
|
||||
|
||||
interface MigrationSubject {
|
||||
configVersion: number | undefined
|
||||
}
|
||||
|
||||
export class ConfigMigrator {
|
||||
private migrations: Migration[]
|
||||
|
||||
constructor(migrations: Migration[]) {
|
||||
this.migrations = migrations
|
||||
}
|
||||
|
||||
public applyMigrations(subject: MigrationSubject): Partial<MigrationSubject> {
|
||||
while (this.isMigrationNecessary(subject)) {
|
||||
subject = this.applicableMigrations(subject)
|
||||
.filter(migration => migration.from === subject.configVersion)
|
||||
.reduce((currentSubject, migration) => migration.apply(currentSubject as any), subject)
|
||||
}
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
public isMigrationNecessary(subject: MigrationSubject): boolean {
|
||||
return this.applicableMigrations(subject).length > 0
|
||||
}
|
||||
|
||||
private applicableMigrations(subject: MigrationSubject): Migration[] {
|
||||
return this.migrations.filter(migration => migration.from === subject.configVersion)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,6 @@
|
||||
import { rendererEvents } from '../../../events'
|
||||
import { v4 } from 'uuid'
|
||||
import { rendererRpc } from '../../../events'
|
||||
|
||||
import {
|
||||
storageStoreEvent,
|
||||
makeStorageResponseEvent,
|
||||
storageLoadEvent,
|
||||
storageClearEvent,
|
||||
makeStorageAcknowledgementEvent,
|
||||
} from '../../../events/StorageEvents'
|
||||
import { storageStoreEvent, storageLoadEvent, storageClearEvent } from '../../../events/StorageEvents'
|
||||
|
||||
export interface StorageIdentifier<Model> {
|
||||
id: string
|
||||
@@ -20,71 +13,27 @@ export interface PersistentStorage {
|
||||
}
|
||||
|
||||
class RemoteStorage implements PersistentStorage {
|
||||
private timeoutCallback(event: any, callback: any, reject: any) {
|
||||
setTimeout(() => {
|
||||
reject('remote storage timeout')
|
||||
rendererEvents.unsubscribe(event, callback)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
private expectAck(transactionId: string): Promise<void> {
|
||||
const ack = makeStorageAcknowledgementEvent(transactionId)
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const callback = (msg: any) => {
|
||||
if (msg && msg.error) {
|
||||
reject(msg.error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
rendererEvents.unsubscribe(ack, callback)
|
||||
}
|
||||
rendererEvents.subscribe(ack, callback)
|
||||
this.timeoutCallback(ack, callback, reject)
|
||||
})
|
||||
}
|
||||
|
||||
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
|
||||
const transactionId = v4()
|
||||
const expectation = this.expectAck(transactionId)
|
||||
rendererEvents.emit(storageStoreEvent, {
|
||||
return rendererRpc.call(storageStoreEvent, {
|
||||
data,
|
||||
transactionId,
|
||||
store: identifier.id,
|
||||
})
|
||||
return expectation
|
||||
}
|
||||
|
||||
public load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> {
|
||||
const transactionId = v4()
|
||||
const responseEvent = makeStorageResponseEvent(transactionId)
|
||||
public async load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> {
|
||||
const result = await rendererRpc.call(
|
||||
storageLoadEvent,
|
||||
{
|
||||
store: identifier.id,
|
||||
},
|
||||
10000
|
||||
)
|
||||
|
||||
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
|
||||
return (result as any).data
|
||||
}
|
||||
|
||||
public clear(): Promise<void> {
|
||||
const transactionId = v4()
|
||||
const expectation = this.expectAck(transactionId)
|
||||
|
||||
rendererEvents.emit(storageClearEvent, { transactionId })
|
||||
return expectation
|
||||
return rendererRpc.call(storageClearEvent, undefined, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
// import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
|
||||
const telemetry = electronRendererTelemetry
|
||||
electronRendererTelemetry.registerErrorHandler()
|
||||
// const telemetry = electronRendererTelemetry
|
||||
// electronRendererTelemetry.registerErrorHandler()
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
export const selectTextWithCtrlA = (options?: { targetSelector: string }) => (
|
||||
e: React.KeyboardEvent<HTMLDivElement>
|
||||
) => {
|
||||
const isCtrlA = (e.metaKey || e.ctrlKey) && e.key === 'a'
|
||||
export const selectTextWithCtrlA =
|
||||
(options?: { targetSelector: string }) => (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const isCtrlA = (e.metaKey || e.ctrlKey) && e.key === 'a'
|
||||
|
||||
if (isCtrlA && window.getSelection) {
|
||||
e.persist()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
const eventTarget = e.target as HTMLElement
|
||||
const target: HTMLElement | null = options ? eventTarget.querySelector(options.targetSelector) : eventTarget
|
||||
if (isCtrlA && window.getSelection) {
|
||||
e.persist()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
const eventTarget = e.target as HTMLElement
|
||||
const target: HTMLElement | null = options ? eventTarget.querySelector(options.targetSelector) : eventTarget
|
||||
|
||||
if (!target) {
|
||||
console.error('Could not find matching target for Ctrl+A Event')
|
||||
}
|
||||
if (selection && target) {
|
||||
range.selectNodeContents(target)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
if (!target) {
|
||||
console.error('Could not find matching target for Ctrl+A Event')
|
||||
}
|
||||
if (selection && target) {
|
||||
range.selectNodeContents(target)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { expect } from 'chai'
|
||||
import { ConfigMigrator } from '../ConfigMigrator'
|
||||
import 'mocha'
|
||||
|
||||
describe('ConfigMigrator', () => {
|
||||
it('applies migrations to a subject with undefined configVersion', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 1,
|
||||
drink: 'Beer',
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any) as any
|
||||
|
||||
expect(migratedConfig.drink).to.eq('Beer')
|
||||
expect(migratedConfig.configVersion).to.eq(1)
|
||||
})
|
||||
|
||||
it('applies multiple migrations from the same version', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
drink: 'Beer',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any) as any
|
||||
|
||||
expect(migratedConfig.drink).to.eq('Beer')
|
||||
expect(migratedConfig.configVersion).to.eq(1)
|
||||
})
|
||||
|
||||
it('applies multiple migrations with ascending versions', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: undefined,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
from: 1,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 2,
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any)
|
||||
|
||||
expect(migratedConfig.configVersion).to.eq(2)
|
||||
})
|
||||
|
||||
it('does not apply non-matching migrations', () => {
|
||||
let migrator = new ConfigMigrator([
|
||||
{
|
||||
from: 2,
|
||||
apply: subject => {
|
||||
return {
|
||||
...subject,
|
||||
configVersion: 3,
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
let config = {
|
||||
configVersion: 10,
|
||||
drink: 'water',
|
||||
}
|
||||
|
||||
let migratedConfig = migrator.applyMigrations(config as any)
|
||||
|
||||
expect(migratedConfig.configVersion).to.eq(10)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { connectionsMigrator } from '../../actions/migrations/Connection'
|
||||
|
||||
describe('ConnectionsMigrator', () => {
|
||||
it('applies migrations', () => {
|
||||
let connections: any = {
|
||||
'763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7': {
|
||||
certValidation: true,
|
||||
clientId: 'mqtt-explorer-2783f48c',
|
||||
encryption: false,
|
||||
host: 'nodered',
|
||||
id: '763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7',
|
||||
name: 'nodered',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: ['#', '$SYS/#'],
|
||||
type: 'mqtt',
|
||||
},
|
||||
'iot.eclipse.org': {
|
||||
certValidation: true,
|
||||
clientId: 'mqtt-explorer-d913aad3',
|
||||
encryption: false,
|
||||
host: 'iot.eclipse.org',
|
||||
id: 'iot.eclipse.org',
|
||||
name: 'iot.eclipse.org',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: ['#', '$SYS/#'],
|
||||
type: 'mqtt',
|
||||
},
|
||||
}
|
||||
|
||||
let migratedConnections: any = {
|
||||
'763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7': {
|
||||
configVersion: 1,
|
||||
certValidation: true,
|
||||
clientId: undefined,
|
||||
encryption: false,
|
||||
host: 'nodered',
|
||||
id: '763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7',
|
||||
name: 'nodered',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: [
|
||||
{ topic: '#', qos: 0 },
|
||||
{ topic: '$SYS/#', qos: 0 },
|
||||
],
|
||||
type: 'mqtt',
|
||||
},
|
||||
'mqtt.eclipse.org': {
|
||||
configVersion: 1,
|
||||
certValidation: true,
|
||||
clientId: undefined,
|
||||
encryption: false,
|
||||
host: 'mqtt.eclipse.org',
|
||||
id: 'mqtt.eclipse.org',
|
||||
name: 'mqtt.eclipse.org',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
subscriptions: [
|
||||
{ topic: '#', qos: 0 },
|
||||
{ topic: '$SYS/#', qos: 0 },
|
||||
],
|
||||
type: 'mqtt',
|
||||
},
|
||||
}
|
||||
|
||||
expect(connectionsMigrator.applyMigrations(connections)).to.deep.eq(migratedConnections)
|
||||
})
|
||||
})
|
||||
+23
-23
@@ -1,27 +1,27 @@
|
||||
import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
// import { electronRendererTelemetry } from 'electron-telemetry'
|
||||
|
||||
// Used to determine long-time-stability and memory leaks
|
||||
function trackProcessStatistics() {
|
||||
setInterval(() => {
|
||||
try {
|
||||
electronRendererTelemetry.trackCustomEvent({
|
||||
name: 'heapStatistics',
|
||||
payload: process.getHeapStatistics(),
|
||||
})
|
||||
electronRendererTelemetry.trackCustomEvent({
|
||||
name: 'cpuUsage',
|
||||
payload: process.getCPUUsage(),
|
||||
})
|
||||
electronRendererTelemetry.trackCustomEvent({
|
||||
name: 'runningSince',
|
||||
payload: performance.now(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}, 60 * 1000)
|
||||
}
|
||||
trackProcessStatistics()
|
||||
// function trackProcessStatistics() {
|
||||
// setInterval(() => {
|
||||
// try {
|
||||
// electronRendererTelemetry.trackCustomEvent({
|
||||
// name: 'heapStatistics',
|
||||
// payload: process.getHeapStatistics(),
|
||||
// })
|
||||
// electronRendererTelemetry.trackCustomEvent({
|
||||
// name: 'cpuUsage',
|
||||
// payload: process.getCPUUsage(),
|
||||
// })
|
||||
// electronRendererTelemetry.trackCustomEvent({
|
||||
// name: 'runningSince',
|
||||
// payload: performance.now(),
|
||||
// })
|
||||
// } catch (error) {
|
||||
// console.error(error)
|
||||
// }
|
||||
// }, 60 * 1000)
|
||||
// }
|
||||
// trackProcessStatistics()
|
||||
|
||||
// Log reducer event names to determine what functionality is used and how to reproduce reported errors
|
||||
export function trackEvent(name: string) {
|
||||
@@ -30,6 +30,6 @@ export function trackEvent(name: string) {
|
||||
}
|
||||
const blacklist = ['CONNECTION_SET_HEALTH']
|
||||
if (blacklist.indexOf(name) === -1) {
|
||||
electronRendererTelemetry.trackEvent(name)
|
||||
// electronRendererTelemetry.trackEvent(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
+23
-6
@@ -4,21 +4,38 @@
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"lib": ["es2017", "dom"],
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build/",
|
||||
"sourceMap": true,
|
||||
"module": "esnext",
|
||||
"target": "es2017",
|
||||
"jsx": "react",
|
||||
"types": ["react"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
"paths": {
|
||||
"react": [
|
||||
"./node_modules/@types/react"
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
"react"
|
||||
],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["**/*.d.ts"],
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.d.ts",
|
||||
".src/**/*.png",
|
||||
"./node_modules"
|
||||
],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
"useCache": true,
|
||||
"transpileModule": true,
|
||||
"errorsAsWarnings": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-35
@@ -1,7 +1,6 @@
|
||||
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const webpack = require('webpack')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
@@ -18,12 +17,11 @@ module.exports = {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 30000,
|
||||
maxSize: 0,
|
||||
minChunks: 1,
|
||||
maxAsyncRequests: 5,
|
||||
maxInitialRequests: 3,
|
||||
automaticNameDelimiter: '~',
|
||||
name: true,
|
||||
// name: true,
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
test: /[\\/]node_modules[\\/](react|react-dom|@material-ui|popper\.js|react|react-redux|prop-types|jss|redux|scheduler|react-transition-group)[\\/]/,
|
||||
@@ -32,6 +30,7 @@ module.exports = {
|
||||
priority: -10,
|
||||
},
|
||||
default: {
|
||||
name: 'default',
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
reuseExistingChunk: true,
|
||||
@@ -55,13 +54,7 @@ module.exports = {
|
||||
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'awesome-typescript-loader',
|
||||
options: {
|
||||
reportFiles: [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"../backend/src/**/*.{ts,tsx}"
|
||||
]
|
||||
}
|
||||
loader: 'ts-loader',
|
||||
},
|
||||
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
|
||||
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
|
||||
@@ -70,35 +63,29 @@ module.exports = {
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: {
|
||||
loader: 'node-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
}
|
||||
}
|
||||
test: /\.(png|jpg|gif)$/i,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
// {
|
||||
// test: /\.node$/,
|
||||
// use: {
|
||||
// loader: 'node-loader',
|
||||
// options: {
|
||||
// modules: true,
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
],
|
||||
},
|
||||
|
||||
// node: { global: true },
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }),
|
||||
// new BundleAnalyzerPlugin(),
|
||||
new HardSourceWebpackPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /\.\/build\/Debug\/addon/,
|
||||
contextRegExp: /heapdump$/
|
||||
}),
|
||||
// new webpack.IgnorePlugin({
|
||||
// resourceRegExp: /\.\/build\/Debug\/addon/,
|
||||
// contextRegExp: /heapdump$/
|
||||
// }),
|
||||
],
|
||||
|
||||
// When importing a module whose path matches one of the following, just
|
||||
@@ -109,4 +96,4 @@ module.exports = {
|
||||
// "react": "React",
|
||||
// "react-dom": "ReactDOM"
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+2954
-4499
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -7,10 +7,10 @@ cache:
|
||||
- '%LOCALAPPDATA%\electron-builder\cache'
|
||||
|
||||
install:
|
||||
- ps: Install-Product node 10
|
||||
- ps: Install-Product node 19
|
||||
|
||||
build_script:
|
||||
- yarn
|
||||
- yarn install --frozen-lockfile
|
||||
- yarn build
|
||||
- yarn prepare-release
|
||||
- yarn package appx
|
||||
@@ -21,4 +21,3 @@ test_script:
|
||||
|
||||
artifacts:
|
||||
- path: 'build\clean\build\*.appx'
|
||||
|
||||
|
||||
+10
-5
@@ -4,13 +4,16 @@
|
||||
"description": "",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"test": "mocha --require ts-node/register --require source-map-support/register --recursive ./src/*/**/*.spec.ts",
|
||||
"build": "tsc",
|
||||
"test-inspect": "mocha --inspect-brk",
|
||||
"coverage": "nyc mocha",
|
||||
"test-inspect": "mocha --inspect-brk --require ts-node/register --require source-map-support/register --recursive ./src/*/**/*.spec.ts",
|
||||
"coverage": "nyc mocha --require ts-node/register --require source-map-support/register --recursive ./src/*/**/*.spec.ts",
|
||||
"debug": "ts-node --inspect ./src/index.ts",
|
||||
"postinstall": "yarn build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"author": "",
|
||||
"license": "CC-BY-ND-4.0",
|
||||
"nyc": {
|
||||
@@ -38,7 +41,9 @@
|
||||
"peerDependencies": {
|
||||
"fs-extra": "^8.0.1",
|
||||
"js-base64": "^2.5.1",
|
||||
"long": "^4.0.0",
|
||||
"lowdb": "^1.0.0",
|
||||
"mqtt": "^3.0.0"
|
||||
"mqtt": "^3.0.0",
|
||||
"protobufjs": "^6.11.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,8 @@ import * as FileAsync from 'lowdb/adapters/FileAsync'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as lowdb from 'lowdb'
|
||||
import * as path from 'path'
|
||||
import { backendEvents } from '../../events'
|
||||
import {
|
||||
makeStorageAcknowledgementEvent,
|
||||
makeStorageResponseEvent,
|
||||
storageClearEvent,
|
||||
storageLoadEvent,
|
||||
storageStoreEvent,
|
||||
} from '../../events/StorageEvents'
|
||||
import { backendRpc } from '../../events'
|
||||
import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents'
|
||||
|
||||
export default class ConfigStorage {
|
||||
private file: string
|
||||
@@ -32,56 +26,26 @@ export default class ConfigStorage {
|
||||
}
|
||||
|
||||
public async init() {
|
||||
backendEvents.subscribe(storageStoreEvent, async event => {
|
||||
const ack = makeStorageAcknowledgementEvent(event.transactionId)
|
||||
try {
|
||||
const db = await this.getDb()
|
||||
await db.set(event.store, event.data).write()
|
||||
backendEvents.emit(ack, undefined)
|
||||
} catch (error) {
|
||||
backendEvents.emit(ack, {
|
||||
error,
|
||||
transactionId: event.transactionId,
|
||||
store: event.store,
|
||||
})
|
||||
throw error
|
||||
backendRpc.on(storageStoreEvent, async event => {
|
||||
const db = await this.getDb()
|
||||
await db.set(event.store, event.data).write()
|
||||
return
|
||||
})
|
||||
|
||||
backendRpc.on(storageLoadEvent, async event => {
|
||||
const db = await this.getDb()
|
||||
const data = await db.get(event.store).value()
|
||||
return {
|
||||
data,
|
||||
store: event.store,
|
||||
}
|
||||
})
|
||||
|
||||
backendEvents.subscribe(storageLoadEvent, async event => {
|
||||
const responseEvent = makeStorageResponseEvent(event.transactionId)
|
||||
try {
|
||||
const db = await this.getDb()
|
||||
const data = await db.get(event.store).value()
|
||||
backendEvents.emit(responseEvent, {
|
||||
data,
|
||||
transactionId: event.transactionId,
|
||||
store: event.store,
|
||||
})
|
||||
} catch (error) {
|
||||
backendEvents.emit(responseEvent, {
|
||||
error,
|
||||
transactionId: event.transactionId,
|
||||
store: event.store,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
backendEvents.subscribe(storageClearEvent, async event => {
|
||||
try {
|
||||
const db = await this.getDb()
|
||||
const keys = await db.keys().value()
|
||||
for (const key of keys) {
|
||||
await db.unset(key).write()
|
||||
}
|
||||
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), undefined)
|
||||
} catch (error) {
|
||||
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), {
|
||||
error,
|
||||
transactionId: event.transactionId,
|
||||
})
|
||||
throw error
|
||||
backendRpc.on(storageClearEvent, async event => {
|
||||
const db = await this.getDb()
|
||||
const keys = await db.keys().value()
|
||||
for (const key of keys) {
|
||||
await db.unset(key).write()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataSourceStateMachine } from './'
|
||||
import { MqttMessage } from '../../../events'
|
||||
|
||||
type MessageCallback = (topic: string, payload: Buffer) => void
|
||||
type MessageCallback = (topic: string, payload: Buffer, packet: any) => void
|
||||
|
||||
// A DataSource should automatically reconnect if connection was broken
|
||||
interface DataSource<DataSourceOptions> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Url from 'url'
|
||||
import { URL } from 'url'
|
||||
|
||||
import { Client, connect as mqttConnect } from 'mqtt'
|
||||
import { DataSource, DataSourceStateMachine } from './'
|
||||
@@ -12,12 +12,19 @@ export interface MqttOptions {
|
||||
tls: boolean
|
||||
certValidation: boolean
|
||||
clientId?: string
|
||||
subscriptions: Array<string>
|
||||
subscriptions: Array<Subscription>
|
||||
certificateAuthority?: string
|
||||
clientCertificate?: string
|
||||
clientKey?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
topic: string
|
||||
qos: QoS
|
||||
}
|
||||
|
||||
export type QoS = 0 | 1 | 2
|
||||
|
||||
export class MqttSource implements DataSource<MqttOptions> {
|
||||
public stateMachine: DataSourceStateMachine = new DataSourceStateMachine()
|
||||
private client: Client | undefined
|
||||
@@ -34,13 +41,13 @@ export class MqttSource implements DataSource<MqttOptions> {
|
||||
const urlStr = options.tls ? options.url.replace(/^(mqtt|ws):/, '$1s:') : options.url
|
||||
let url
|
||||
try {
|
||||
url = Url.parse(urlStr)
|
||||
url = new URL(urlStr)
|
||||
} catch (error) {
|
||||
this.stateMachine.setError(error)
|
||||
this.stateMachine.setError(error as Error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const client = mqttConnect(url, {
|
||||
const client = mqttConnect(url.toString(), {
|
||||
resubscribe: false,
|
||||
rejectUnauthorized: options.certValidation,
|
||||
username: options.username,
|
||||
@@ -74,7 +81,7 @@ export class MqttSource implements DataSource<MqttOptions> {
|
||||
client.on('connect', () => {
|
||||
this.stateMachine.setConnected(true)
|
||||
options.subscriptions.forEach(subscription => {
|
||||
client.subscribe(subscription, (err: Error) => {
|
||||
client.subscribe(subscription.topic, { qos: subscription.qos }, (err: Error) => {
|
||||
if (err) {
|
||||
this.stateMachine.setError(err)
|
||||
}
|
||||
|
||||
@@ -70,9 +70,11 @@ function jsonToPropertyPaths(ast: JsonAst, previousPath: Array<string> = []): Ar
|
||||
} else if (ast.type === 'Array') {
|
||||
children = ast.children.map((value, idx) => jsonToPropertyPaths(value, previousPath.slice().concat([String(idx)])))
|
||||
} else if (ast.type === 'Object') {
|
||||
children = ast.children.map(property =>
|
||||
jsonToPropertyPaths(property.value, previousPath.slice().concat([property.key.value]))
|
||||
)
|
||||
children = ast.children.map(property => {
|
||||
const path = property.key.value.replace('.', '\\.')
|
||||
|
||||
return jsonToPropertyPaths(property.value, previousPath.slice().concat([path]))
|
||||
})
|
||||
}
|
||||
|
||||
return children.reduce((a, b) => a.concat(b), [])
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
const { Base64 } = require('js-base64')
|
||||
import { Base64 } from 'js-base64'
|
||||
import { Decoder } from './Decoder'
|
||||
|
||||
export class Base64Message {
|
||||
private base64Message: string
|
||||
private unicodeValue: string
|
||||
|
||||
public decoder: Decoder
|
||||
public length: number
|
||||
|
||||
private constructor(base64Str: string) {
|
||||
this.base64Message = base64Str
|
||||
this.unicodeValue = Base64.decode(base64Str)
|
||||
this.length = base64Str.length
|
||||
this.decoder = Decoder.NONE
|
||||
}
|
||||
|
||||
public static toUnicodeString(message: Base64Message) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum Decoder {
|
||||
NONE,
|
||||
SPARKPLUG,
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import { Base64Message } from './Base64Message'
|
||||
import { QoS } from '../DataSource/MqttSource'
|
||||
|
||||
export interface Message {
|
||||
value?: Base64Message
|
||||
// mqtt based info
|
||||
payload: Base64Message | null
|
||||
messageId?: number
|
||||
retain: boolean
|
||||
qos: QoS
|
||||
|
||||
// meta info
|
||||
length: number
|
||||
received: Date
|
||||
|
||||
// Global message counter, not mqtt related
|
||||
messageNumber: number
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeBuffer } from './ChangeBuffer'
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { EventBusInterface, EventDispatcher, makeConnectionMessageEvent, MqttMessage } from '../../../events'
|
||||
import { EventDispatcher, makeConnectionMessageEvent, MqttMessage, EventBusInterface } from '../../../events'
|
||||
import { TreeNode } from './'
|
||||
import { TreeNodeFactory } from './TreeNodeFactory'
|
||||
|
||||
@@ -75,13 +75,7 @@ export class Tree<ViewModel extends Destroyable> extends TreeNode<ViewModel> {
|
||||
|
||||
public applyUnmergedChanges() {
|
||||
this.unmergedMessages.popAll().forEach(bufferedItem => {
|
||||
const edges = bufferedItem.message.topic.split('/')
|
||||
const node = TreeNodeFactory.fromEdgesAndValue<ViewModel>(
|
||||
edges,
|
||||
bufferedItem.message.payload,
|
||||
bufferedItem.received
|
||||
)
|
||||
node.mqttMessage = bufferedItem.message
|
||||
const node = TreeNodeFactory.fromMessage<ViewModel>(bufferedItem.message, bufferedItem.received)
|
||||
|
||||
if (!this.nodeFilter || this.nodeFilter(node)) {
|
||||
this.updateWithNode(node.firstNode())
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { Edge, Message, RingBuffer, MessageHistory } from './'
|
||||
import { EventDispatcher, MqttMessage } from '../../../events'
|
||||
import { EventDispatcher } from '../../../events'
|
||||
|
||||
export class TreeNode<ViewModel extends Destroyable> {
|
||||
public sourceEdge?: Edge<ViewModel>
|
||||
public message?: Message
|
||||
public mqttMessage?: MqttMessage
|
||||
public messageHistory: MessageHistory = new RingBuffer<Message>(20000, 100)
|
||||
public viewModel?: ViewModel
|
||||
public edges: { [s: string]: Edge<ViewModel> } = {}
|
||||
@@ -105,7 +104,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
}
|
||||
|
||||
public hasMessage() {
|
||||
return this.message && this.message.value && this.message.value.length !== 0
|
||||
return this.message && this.message.payload && this.message.length !== 0
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
@@ -131,7 +130,6 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
public unconnectedClone() {
|
||||
const node = new TreeNode<ViewModel>()
|
||||
node.message = this.message
|
||||
node.mqttMessage = this.mqttMessage
|
||||
node.messageHistory = this.messageHistory.clone()
|
||||
node.messages = this.messages
|
||||
node.lastUpdate = this.lastUpdate
|
||||
@@ -203,7 +201,6 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
if (node.message) {
|
||||
this.setMessage(node.message)
|
||||
this.onMessage.dispatch(node.message)
|
||||
this.mqttMessage = node.mqttMessage
|
||||
}
|
||||
|
||||
this.removeFromTreeIfEmpty()
|
||||
@@ -236,7 +233,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
|
||||
public childTopics(): Array<TreeNode<ViewModel>> {
|
||||
if (this.cachedChildTopics === undefined) {
|
||||
const initialValue = this.message && this.message.value ? [this] : []
|
||||
const initialValue = this.message && this.message.payload ? [this] : []
|
||||
|
||||
this.cachedChildTopics = this.edgeArray
|
||||
.map(e => e.target.childTopics())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Base64Message } from './Base64Message'
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { Edge, Tree, TreeNode } from './'
|
||||
import { MqttMessage } from '../../../events'
|
||||
|
||||
export abstract class TreeNodeFactory {
|
||||
private static messageCounter = 0
|
||||
@@ -20,21 +20,23 @@ export abstract class TreeNodeFactory {
|
||||
node.sourceEdge!.target = node
|
||||
}
|
||||
|
||||
public static fromEdgesAndValue<ViewModel extends Destroyable>(
|
||||
edgeNames: Array<string>,
|
||||
value?: Base64Message | null,
|
||||
public static fromMessage<ViewModel extends Destroyable>(
|
||||
mqttMessage: MqttMessage,
|
||||
receiveDate: Date = new Date()
|
||||
): TreeNode<ViewModel> {
|
||||
const node = new TreeNode<ViewModel>()
|
||||
const edges = mqttMessage.topic.split('/')
|
||||
|
||||
mqttMessage.retain
|
||||
node.setMessage({
|
||||
value: value || undefined,
|
||||
length: value ? value.length : 0,
|
||||
...mqttMessage,
|
||||
length: mqttMessage.payload?.length ?? 0,
|
||||
received: receiveDate,
|
||||
messageNumber: this.messageCounter,
|
||||
})
|
||||
this.messageCounter += 1
|
||||
|
||||
this.insertNodeAtPosition<ViewModel>(edgeNames, node)
|
||||
this.insertNodeAtPosition<ViewModel>(edges, node)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user