mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-13 01:54:38 +00:00
Compare commits
161
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf78a4dea6 | ||
|
|
22762f9d79 | ||
|
|
b21692d5d2 | ||
|
|
0b5b0a6391 | ||
|
|
2e7acb6461 | ||
|
|
e9a56ac48d | ||
|
|
b4bdd01808 | ||
|
|
4406bf5de4 | ||
|
|
ae0ce79e26 | ||
|
|
bbe2ae3f29 | ||
|
|
a2c4388c78 | ||
|
|
c88978f0dd | ||
|
|
b3a37e4794 | ||
|
|
1ecb53b397 | ||
|
|
97fedcba08 | ||
|
|
1f23c65484 | ||
|
|
980072f680 | ||
|
|
c452b9f417 | ||
|
|
b04f5dee16 | ||
|
|
7617430a3f | ||
|
|
10aae59c92 | ||
|
|
f4bda3e242 | ||
|
|
a346c48d3e | ||
|
|
8a2c39ba8e | ||
|
|
65b86ac5f6 | ||
|
|
4ead740982 | ||
|
|
ddaf06b682 | ||
|
|
9e18c4db4e | ||
|
|
ee783f15c0 | ||
|
|
0c31843ed5 | ||
|
|
ffc93d20d8 | ||
|
|
3c62da7e19 | ||
|
|
6846dbbb28 | ||
|
|
270e3e3ead | ||
|
|
0d73b0f519 | ||
|
|
ee83022d19 | ||
|
|
f27467ed97 | ||
|
|
ddbf3d9f1f | ||
|
|
60bbbc54d7 | ||
|
|
538aa7dc29 | ||
|
|
b37af291cc | ||
|
|
333a1bcabe | ||
|
|
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 | ||
|
|
626b9cab7d | ||
|
|
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 | ||
|
|
567f6d2d50 | ||
|
|
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,53 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
- beta
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
build:
|
||||
- os: ubuntu-latest
|
||||
task: linux
|
||||
- os: windows-latest
|
||||
task: win
|
||||
- os: macos-latest
|
||||
task: mac
|
||||
runs-on: ${{ matrix.build.os }}
|
||||
steps:
|
||||
- if: matrix.build.os == 'ubuntu-latest'
|
||||
run: sudo snap install snapcraft --classic
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm install -g yarn
|
||||
- run: yarn
|
||||
- id: create_token # get ReleaseBot access token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.RELEASE_BOT_APP_ID }}
|
||||
private_key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
|
||||
- name: Semantic Release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
id: semantic # Need an `id` for output variables
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.create_token.outputs.token }}
|
||||
- run: yarn build
|
||||
if: steps.semantic.outputs.new_release_published == 'true'
|
||||
- run: yarn prepare-release
|
||||
if: steps.semantic.outputs.new_release_published == 'true'
|
||||
- run: yarn package ${{ matrix.build.task }}
|
||||
if: steps.semantic.outputs.new_release_published == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Update Website
|
||||
|
||||
on: [release, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
update-website:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: gh-pages
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm install
|
||||
- run: npm run readme
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"branches": [
|
||||
"release",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
repositoryUrl: "git@github.com:thomasnordquist/MQTT-Explorer.git",
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"semantic-release-export-data",
|
||||
"@semantic-release/changelog",
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"npmPublish": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"package.json",
|
||||
"yarn.lock"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
-40
@@ -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>
|
||||
+31
-22
@@ -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",
|
||||
@@ -46,14 +47,16 @@
|
||||
"react-split-pane": "^0.1.85",
|
||||
"react-transition-group": "^4",
|
||||
"react-vis": "^1.11.6",
|
||||
"react-window": "^1.8.10",
|
||||
"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",
|
||||
@@ -63,25 +66,31 @@
|
||||
"@types/react-dom": "^16.0.11",
|
||||
"@types/react-redux": "^7.0.9",
|
||||
"@types/react-resize-detector": "^4.0.1",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@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.91.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.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,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Action, ActionTypes } from '../reducers/Publish'
|
||||
import { AppState } from '../reducers'
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Dispatch } from 'redux'
|
||||
import { makePublishEvent, rendererEvents } from '../../../events'
|
||||
import { MqttMessage, makePublishEvent, rendererEvents } from '../../../events'
|
||||
|
||||
export const setTopic = (topic?: string): Action => {
|
||||
return {
|
||||
@@ -34,14 +34,14 @@ export const setEditorMode = (editorMode: string): Action => {
|
||||
|
||||
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
|
||||
const state = getState()
|
||||
const topic = state.publish.topic
|
||||
const topic = state.publish.manualTopic ?? state.tree.get('selectedTopic')?.path()
|
||||
|
||||
if (!topic) {
|
||||
return
|
||||
}
|
||||
|
||||
const publishEvent = makePublishEvent(connectionId)
|
||||
const mqttMessage = {
|
||||
const mqttMessage: Partial<MqttMessage> = {
|
||||
topic,
|
||||
payload: state.publish.payload ? Base64Message.fromString(state.publish.payload) : null,
|
||||
retain: state.publish.retain,
|
||||
|
||||
+29
-30
@@ -1,5 +1,5 @@
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { ActionTypes, SettingsStateModel, TopicOrder } from '../reducers/Settings'
|
||||
import { ActionTypes, SettingsStateModel, TopicOrder, ValueRendererDisplayMode } from '../reducers/Settings'
|
||||
import { AppState } from '../reducers'
|
||||
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
@@ -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({
|
||||
@@ -66,13 +68,14 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat
|
||||
dispatch(storeSettings())
|
||||
}
|
||||
|
||||
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
valueRendererDisplayMode,
|
||||
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
|
||||
})
|
||||
dispatch(storeSettings())
|
||||
}
|
||||
export const setValueDisplayMode =
|
||||
(valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
valueRendererDisplayMode,
|
||||
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
|
||||
})
|
||||
dispatch(storeSettings())
|
||||
}
|
||||
|
||||
export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
@@ -81,13 +84,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 +110,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 &&
|
||||
node.message.payload.toUnicodeString().toLowerCase().indexOf(filterStr) !== -1
|
||||
|
||||
return Boolean(messageMatches)
|
||||
}
|
||||
|
||||
+25
-31
@@ -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,19 +27,14 @@ 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())
|
||||
}
|
||||
|
||||
if (previouslySelectedTopic && previouslySelectedTopic.viewModel) {
|
||||
previouslySelectedTopic.viewModel.setSelected(false)
|
||||
}
|
||||
|
||||
if (topic.viewModel) {
|
||||
topic.viewModel.setSelected(true)
|
||||
}
|
||||
previouslySelectedTopic?.viewModel?.setSelected(false)
|
||||
topic.viewModel?.setSelected(true)
|
||||
|
||||
const selectTreeTopicDispatch = {
|
||||
selectedTopic: topic,
|
||||
@@ -74,25 +67,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)
|
||||
|
||||
@@ -114,6 +114,7 @@ function TopicChart(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<TopicPlot
|
||||
node={props.treeNode ? props.treeNode : undefined}
|
||||
color={props.parameters.color}
|
||||
interpolation={props.parameters.interpolation}
|
||||
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
|
||||
@@ -133,7 +134,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))
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { ListItem, Typography } from '@material-ui/core'
|
||||
import { toMqttConnection, ConnectionOptions } from '../../../model/ConnectionOptions'
|
||||
import { withStyles, Theme } from '@material-ui/core/styles'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connectionManagerActions } from '../../../actions'
|
||||
import { connectionActions, connectionManagerActions } from '../../../actions'
|
||||
|
||||
export interface Props {
|
||||
connection: ConnectionOptions
|
||||
actions: any
|
||||
actions: {
|
||||
connection: any
|
||||
connectionManager: any
|
||||
}
|
||||
selected: boolean
|
||||
classes: any
|
||||
}
|
||||
|
||||
const ConnectionItem = (props: Props) => {
|
||||
const connect = useCallback(() => {
|
||||
const mqttOptions = toMqttConnection(props.connection)
|
||||
if (mqttOptions) {
|
||||
props.actions.connection.connect(mqttOptions, props.connection.id)
|
||||
}
|
||||
}, [props.connection, props])
|
||||
|
||||
const connection = props.connection.host && toMqttConnection(props.connection)
|
||||
return (
|
||||
<ListItem
|
||||
button={true}
|
||||
selected={props.selected}
|
||||
style={{ display: 'block' }}
|
||||
onClick={() => props.actions.selectConnection(props.connection.id)}
|
||||
onClick={() => props.actions.connectionManager.selectConnection(props.connection.id)}
|
||||
onDoubleClick={() => {
|
||||
props.actions.connectionManager.selectConnection(props.connection.id)
|
||||
connect()
|
||||
}}
|
||||
>
|
||||
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
|
||||
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
|
||||
@@ -30,10 +44,12 @@ const ConnectionItem = (props: Props) => {
|
||||
|
||||
export const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: bindActionCreators(connectionManagerActions, dispatch),
|
||||
actions: {
|
||||
connection: bindActionCreators(connectionActions, dispatch),
|
||||
connectionManager: bindActionCreators(connectionManagerActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const connectionItemStyle = (theme: Theme) => ({
|
||||
name: {
|
||||
width: '100%',
|
||||
@@ -51,7 +67,4 @@ export const connectionItemStyle = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
|
||||
@@ -7,7 +7,7 @@ import { connect } from 'react-redux'
|
||||
import { connectionManagerActions } from '../../../actions'
|
||||
import { ConnectionOptions } from '../../../model/ConnectionOptions'
|
||||
import { KeyCodes } from '../../../utils/KeyCodes'
|
||||
import { List, ListSubheader } from '@material-ui/core'
|
||||
import { List } from '@material-ui/core'
|
||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'
|
||||
|
||||
@@ -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 ? node.message.payload.toUnicodeString() : ''
|
||||
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))
|
||||
|
||||
@@ -52,16 +52,16 @@ function ChartPreview(props: Props) {
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Add to chart panel, not enough data for preview">
|
||||
<ShowChart
|
||||
onClick={onClick}
|
||||
className={props.classes.icon}
|
||||
style={{ color: '#aaa' }}
|
||||
data-test-type="ShowChart"
|
||||
data-test={props.literal.path}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
<Tooltip title="Add to chart panel, not enough data for preview">
|
||||
<ShowChart
|
||||
onClick={onClick}
|
||||
className={props.classes.icon}
|
||||
style={{ color: '#aaa' }}
|
||||
data-test-type="ShowChart"
|
||||
data-test={props.literal.path}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<span>
|
||||
@@ -69,7 +69,7 @@ function ChartPreview(props: Props) {
|
||||
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
|
||||
<Fade in={open} timeout={300}>
|
||||
<Paper style={{ width: '300px' }}>
|
||||
{open ? <TopicPlot history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
|
||||
{open ? <TopicPlot node={props.treeNode} history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
|
||||
</Paper>
|
||||
</Fade>
|
||||
</Popper>
|
||||
@@ -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)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||
import NodeStats from './NodeStats'
|
||||
import ValuePanel from './ValueRenderer/ValuePanel'
|
||||
import { AppState } from '../../reducers'
|
||||
import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
|
||||
import { ExpansionPanelDetails } from '@material-ui/core'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { settingsActions, sidebarActions } from '../../actions'
|
||||
@@ -28,7 +27,7 @@ interface Props {
|
||||
}
|
||||
|
||||
function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
|
||||
const [lastUpdate, setLastUpdate] = useState(0)
|
||||
const [, setLastUpdate] = useState(0)
|
||||
const updateNode = useCallback(
|
||||
throttle(() => {
|
||||
setLastUpdate(node ? node.lastUpdate : 0)
|
||||
@@ -52,7 +51,6 @@ function Sidebar(props: Props) {
|
||||
const { classes, tree, nodePath } = props
|
||||
const node = usePollingToFetchTreeNode(tree, nodePath || '')
|
||||
useUpdateNodeWhenNodeReceivesUpdates(node)
|
||||
// console.log(node && node.path(), tree, nodePath)
|
||||
|
||||
return (
|
||||
<div id="Sidebar" className={classes.drawer}>
|
||||
@@ -99,9 +97,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 (
|
||||
|
||||
@@ -6,19 +6,19 @@ import Topic from './Topic'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
|
||||
import { sidebarActions } from '../../../actions'
|
||||
import { TopicDeleteButton } from './TopicDeleteButton'
|
||||
import { TopicTypeButton } from './TopicTypeButton'
|
||||
import { sidebarActions } from '../../../actions'
|
||||
|
||||
const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions }) => {
|
||||
const { node } = props
|
||||
console.log(node && node.path())
|
||||
|
||||
const copyTopic = node ? <Copy value={node.path()} /> : null
|
||||
|
||||
const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
|
||||
if (!topic) {
|
||||
return
|
||||
}
|
||||
|
||||
props.actions.clearTopic(topic, recursive)
|
||||
}, [])
|
||||
|
||||
@@ -29,11 +29,12 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
|
||||
Topic {copyTopic}
|
||||
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
|
||||
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
|
||||
<TopicTypeButton node={node} />
|
||||
</span>
|
||||
<Topic node={node} />
|
||||
</Panel>
|
||||
),
|
||||
[node, node && node.childTopicCount()]
|
||||
[node, node?.childTopicCount()]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,7 +44,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(TopicPanel)
|
||||
export default connect(undefined, mapDispatchToProps)(TopicPanel)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
|
||||
import Grow from '@material-ui/core/Grow'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Popper from '@material-ui/core/Popper'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import MenuList from '@material-ui/core/MenuList'
|
||||
import WarningRounded from '@material-ui/icons/WarningRounded'
|
||||
import { MessageDecoder, decoders } from '../../../decoders'
|
||||
import { Tooltip } from '@material-ui/core'
|
||||
|
||||
export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
|
||||
const { node } = props
|
||||
if (!node || !node.message || !node.message.payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
const options = decoders.flatMap(decoder => decoder.formats.map(format => [decoder, format] as const))
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const selectOption = useCallback(
|
||||
(decoder: MessageDecoder, format: string) => {
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
node.viewModel.decoder = { decoder, format }
|
||||
setOpen(false)
|
||||
},
|
||||
[node]
|
||||
)
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation()
|
||||
if (open === true) {
|
||||
return
|
||||
}
|
||||
setAnchorEl(event.currentTarget)
|
||||
setOpen(prevOpen => !prevOpen)
|
||||
},
|
||||
[open]
|
||||
)
|
||||
|
||||
const handleClose = useCallback((event: React.MouseEvent<Document, MouseEvent>) => {
|
||||
if (anchorEl && anchorEl.contains(event.target as HTMLElement)) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Button onClick={handleToggle}>
|
||||
{props.node?.viewModel.decoder?.format ?? props.node?.type}
|
||||
<Popper open={open} anchorEl={anchorEl} role={undefined} transition>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id="topicTypeMode">
|
||||
{options.map(([decoder, format], index) => (
|
||||
<MenuItem
|
||||
key={format}
|
||||
selected={node && format === node.type}
|
||||
onClick={() => selectOption(decoder, format)}
|
||||
>
|
||||
<DecoderStatus decoder={decoder} format={format} node={node} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function DecoderStatus({ node, decoder, format }: { node: q.TreeNode<any>; decoder: MessageDecoder; format: string }) {
|
||||
const decoded = useMemo(() => {
|
||||
return node.message?.payload && decoder.decode(node.message?.payload, format)
|
||||
}, [node.message, decoder, format])
|
||||
|
||||
return decoded?.error ? (
|
||||
<Tooltip title={decoded.error}>
|
||||
<div>
|
||||
{format} <WarningRounded />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>{format}</>
|
||||
)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ function ActionButtons(props: {
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
<ToggleButton className={props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
|
||||
<Tooltip title="Raw / formatted JSON">
|
||||
<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)
|
||||
|
||||
@@ -5,13 +5,15 @@ import Copy from '../../helper/Copy'
|
||||
import DateFormatter from '../../helper/DateFormatter'
|
||||
import History from '../HistoryDrawer'
|
||||
import TopicPlot from '../../TopicPlot'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { isPlottable } from '../CodeDiff/util'
|
||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { chartActions } from '../../../actions'
|
||||
import { connect } from 'react-redux'
|
||||
import CustomIconButton from '../../helper/CustomIconButton'
|
||||
import { MessageId } from '../MessageId'
|
||||
import { useSubscription } from '../../hooks/useSubscription'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
@@ -24,69 +26,53 @@ interface Props {
|
||||
}
|
||||
}
|
||||
|
||||
interface State {
|
||||
displayMessage?: q.Message
|
||||
anchorEl?: HTMLElement
|
||||
lastUpdate: number
|
||||
}
|
||||
export const MessageHistory: React.FC<Props> = props => {
|
||||
const [, setLastUpdate] = React.useState(Date.now())
|
||||
const updateNodeThrottled = React.useCallback(
|
||||
throttle(() => {
|
||||
setLastUpdate
|
||||
}, 300),
|
||||
[]
|
||||
)
|
||||
|
||||
class MessageHistory extends React.PureComponent<Props, State> {
|
||||
private updateNode = throttle(() => {
|
||||
this.setState({ lastUpdate: Date.now() })
|
||||
}, 300)
|
||||
useSubscription(props.node?.onMessage, updateNodeThrottled)
|
||||
const decodeMessage = useDecoder(props.node)
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { lastUpdate: 0 }
|
||||
}
|
||||
|
||||
private addNodeToCharts = (event: React.MouseEvent) => {
|
||||
function addNodeToCharts(event: React.MouseEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const { node } = this.props
|
||||
const { node } = props
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.props.actions.charts.addChart({ topic: node.path() })
|
||||
props.actions.charts.addChart({ topic: node.path() })
|
||||
}
|
||||
|
||||
private displayMessage = (index: number, eventTarget: EventTarget) => {
|
||||
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
|
||||
function displayMessage(index: number, eventTarget: EventTarget) {
|
||||
const message = props.node && props.node.messageHistory.toArray().reverse()[index]
|
||||
if (message) {
|
||||
this.props.onSelect(message)
|
||||
props.onSelect(message)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
|
||||
nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode)
|
||||
const { node } = props
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.node && this.props.node.onMessage.subscribe(this.updateNode)
|
||||
}
|
||||
const history = node.messageHistory.toArray()
|
||||
let previousMessage: q.Message | undefined = node.message
|
||||
const historyElements = [...history].reverse().map((message, idx) => {
|
||||
const value = node.message ? decodeMessage(message)?.message?.format()[0] ?? null : null
|
||||
|
||||
public componentWillUnMount() {
|
||||
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { node } = this.props
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
const history = node.messageHistory.toArray()
|
||||
let previousMessage: q.Message | undefined = node.message
|
||||
const historyElements = [...history].reverse().map((message, idx) => {
|
||||
const value = message.value ? Base64Message.toUnicodeString(message.value) : ''
|
||||
const element = {
|
||||
value,
|
||||
key: `${message.messageNumber}-${message.received}`,
|
||||
title: (
|
||||
<span>
|
||||
const element = {
|
||||
value: value ?? '',
|
||||
key: `${message.messageNumber}-${message.received}`,
|
||||
title: (
|
||||
<span>
|
||||
<div style={{ float: 'left' }}>
|
||||
<DateFormatter date={message.received} />
|
||||
{previousMessage && previousMessage !== message ? (
|
||||
<i>
|
||||
@@ -94,43 +80,46 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
|
||||
</i>
|
||||
) : null}
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value} />
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
|
||||
<MessageId message={message} />
|
||||
</span>
|
||||
),
|
||||
selected: message && message === this.props.selected,
|
||||
}
|
||||
previousMessage = message
|
||||
return element
|
||||
})
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value ?? ''} />
|
||||
</div>
|
||||
</span>
|
||||
),
|
||||
selected: message && message === props.selected,
|
||||
}
|
||||
previousMessage = message
|
||||
return element
|
||||
})
|
||||
|
||||
const isMessagePlottable =
|
||||
node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
items={historyElements}
|
||||
contentTypeIndicator={
|
||||
isMessagePlottable ? (
|
||||
<CustomIconButton
|
||||
style={{ height: '22px', width: '22px' }}
|
||||
onClick={this.addNodeToCharts}
|
||||
tooltip="Add to chart panel"
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
undefined
|
||||
)
|
||||
}
|
||||
onClick={this.displayMessage}
|
||||
>
|
||||
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null}
|
||||
</History>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const value = node.message ? decodeMessage(node.message)?.message?.format()[0] ?? null : null
|
||||
|
||||
const isMessagePlottable = isPlottable(value)
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
items={historyElements}
|
||||
contentTypeIndicator={
|
||||
isMessagePlottable ? (
|
||||
<CustomIconButton
|
||||
style={{ height: '22px', width: '22px' }}
|
||||
onClick={addNodeToCharts}
|
||||
tooltip="Add to chart panel"
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : undefined
|
||||
}
|
||||
onClick={displayMessage}
|
||||
>
|
||||
{isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
|
||||
</History>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
@@ -139,7 +128,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(MessageHistory)
|
||||
export default connect(null, mapDispatchToProps)(React.memo(MessageHistory))
|
||||
|
||||
@@ -7,12 +7,13 @@ import Panel from '../Panel'
|
||||
import React, { useCallback } from 'react'
|
||||
import ValueRenderer from './ValueRenderer'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Theme, Typography, withStyles } from '@material-ui/core'
|
||||
import { connect } from 'react-redux'
|
||||
import { sidebarActions } from '../../../actions'
|
||||
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
|
||||
import { MessageId } from '../MessageId'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode<any>
|
||||
@@ -34,9 +35,10 @@ function RenderedValue(props: { node?: q.TreeNode<any>; compareMessage?: q.Messa
|
||||
|
||||
function ValuePanel(props: Props) {
|
||||
const { node, compareMessage } = props
|
||||
const decodeMessage = useDecoder(node)
|
||||
|
||||
function renderViewOptions() {
|
||||
if (!props.node || !props.node.message || !props.node.mqttMessage) {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -46,21 +48,28 @@ 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>
|
||||
)
|
||||
}
|
||||
|
||||
const getDecodedValue = useCallback(() => {
|
||||
return node?.message && decodeMessage(node.message)?.message?.toUnicodeString()
|
||||
}, [node, decodeMessage])
|
||||
|
||||
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} />
|
||||
@@ -81,10 +90,9 @@ function ValuePanel(props: Props) {
|
||||
[compareMessage]
|
||||
)
|
||||
|
||||
const copyValue =
|
||||
node && node.message && node.message.value ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
|
||||
) : null
|
||||
const [value] =
|
||||
node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
|
||||
const copyValue = value ? <Copy getValue={getDecodedValue} /> : null
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
@@ -124,7 +132,5 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ValuePanel))
|
||||
// @ts-ignore
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import * as React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import CodeDiff from '../CodeDiff'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { connect } from 'react-redux'
|
||||
import { default as ReactResizeDetector } from 'react-resize-detector'
|
||||
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
|
||||
import { Typography, Fade, Grow } from '@material-ui/core'
|
||||
import { Fade } from '@material-ui/core'
|
||||
import { Decoder } from '../../../../../backend/src/Model/Decoder'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||
|
||||
interface Props {
|
||||
message: q.Message
|
||||
@@ -15,98 +16,114 @@ interface Props {
|
||||
renderMode: ValueRendererDisplayMode
|
||||
}
|
||||
|
||||
interface State {
|
||||
width: number
|
||||
type Language = 'json'
|
||||
|
||||
function renderDiff(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
compareWithPreviousMessage: boolean,
|
||||
current: string = '',
|
||||
previous: string = '',
|
||||
title?: string,
|
||||
language?: Language
|
||||
) {
|
||||
return (
|
||||
<CodeDiff
|
||||
treeNode={treeNode}
|
||||
previous={previous}
|
||||
current={current}
|
||||
title={title}
|
||||
language={language}
|
||||
nameOfCompareMessage={compareWithPreviousMessage ? 'selected' : 'previous'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class ValueRenderer extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { width: 0 }
|
||||
}
|
||||
function renderDiffMode(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
currentStr: string | undefined,
|
||||
compareStr: string | undefined,
|
||||
currentType: Language | undefined,
|
||||
compareType: Language | undefined,
|
||||
compareWithPreviousMessage: boolean
|
||||
) {
|
||||
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined
|
||||
|
||||
private renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') {
|
||||
return (
|
||||
<CodeDiff
|
||||
treeNode={this.props.treeNode}
|
||||
previous={previous}
|
||||
current={current}
|
||||
title={title}
|
||||
language={language}
|
||||
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div>{renderDiff(treeNode, compareWithPreviousMessage, currentStr, compareStr, undefined, language)}</div>
|
||||
}
|
||||
|
||||
private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] {
|
||||
if (!msg) {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
function renderRawMode(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
currentStr: string | undefined,
|
||||
compareStr: string | undefined,
|
||||
currentType: Language | undefined,
|
||||
compareType: Language | undefined,
|
||||
compareWithPreviousMessage: boolean
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
{renderDiff(treeNode, compareWithPreviousMessage, currentStr, currentStr, undefined, currentType)}
|
||||
<Fade in={Boolean(compareStr)} timeout={400}>
|
||||
<div>
|
||||
{Boolean(compareStr)
|
||||
? renderDiff(treeNode, compareWithPreviousMessage, compareStr, compareStr, 'selected', compareType)
|
||||
: null}
|
||||
</div>
|
||||
</Fade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const str = Base64Message.toUnicodeString(msg)
|
||||
try {
|
||||
JSON.parse(str)
|
||||
} catch (error) {
|
||||
return [str, undefined]
|
||||
}
|
||||
export const ValueRenderer: React.FC<Props> = ({ treeNode, compareWith: compare, message, renderMode }) => {
|
||||
const decodeMessage = useDecoder(treeNode)
|
||||
const decodedMessage = useMemo(() => decodeMessage(message), [decodeMessage, message])
|
||||
|
||||
return [this.messageToPrettyJson(str), 'json']
|
||||
}
|
||||
const previousMessages = treeNode.messageHistory.toArray()
|
||||
const previousMessage = previousMessages[previousMessages.length - 2]
|
||||
const compareMessage = compare || previousMessage || message
|
||||
const compareWithPreviousMessage = !!compare
|
||||
|
||||
private messageToPrettyJson(str: string): string | undefined {
|
||||
try {
|
||||
const json = JSON.parse(str)
|
||||
return JSON.stringify(json, undefined, ' ')
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
const [currentStr, currentType] = useMemo(
|
||||
() => decodedMessage?.message?.format(treeNode.type) ?? [],
|
||||
[decodedMessage, treeNode.type]
|
||||
)
|
||||
const [compareStr, compareType] = useMemo(
|
||||
() => decodeMessage(compareMessage)?.message?.format(treeNode.type) ?? [],
|
||||
[compareMessage, decodeMessage, treeNode.type]
|
||||
)
|
||||
|
||||
private renderRawMode(message: q.Message, compare?: q.Message) {
|
||||
if (!message.value) {
|
||||
return
|
||||
}
|
||||
const [value, valueLanguage] = this.convertMessage(message.value)
|
||||
const [compareStr, compareStrLanguage] =
|
||||
compare && compare.value ? this.convertMessage(compare.value) : [undefined, undefined]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderDiff(value, value, undefined, valueLanguage)}
|
||||
<Fade in={Boolean(compareStr)} timeout={400}>
|
||||
<div>
|
||||
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null}
|
||||
</div>
|
||||
</Fade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>{this.renderValue()}</div>
|
||||
}
|
||||
|
||||
public renderValue() {
|
||||
const { message, treeNode, compareWith, renderMode } = this.props
|
||||
const previousMessages = treeNode.messageHistory.toArray()
|
||||
const previousMessage = previousMessages[previousMessages.length - 2]
|
||||
const compareMessage = compareWith || previousMessage || message
|
||||
|
||||
if (renderMode === 'raw') {
|
||||
return this.renderRawMode(message, compareWith)
|
||||
}
|
||||
if (!message.value) {
|
||||
function renderValue(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
currentStr: string | undefined,
|
||||
compareStr: string | undefined,
|
||||
currentType: Language | undefined,
|
||||
compareType: Language | undefined,
|
||||
renderMode: string,
|
||||
compareWithPreviousMessage: boolean
|
||||
) {
|
||||
if (!decodedMessage) {
|
||||
return null
|
||||
}
|
||||
|
||||
const compareValue = compareMessage.value || message.value
|
||||
const [current, currentLanguage] = this.convertMessage(message.value)
|
||||
const [compare, compareLanguage] = this.convertMessage(compareValue)
|
||||
|
||||
const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined
|
||||
|
||||
return this.renderDiff(current, compare, undefined, language)
|
||||
switch (renderMode) {
|
||||
case 'diff':
|
||||
return renderDiffMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
|
||||
default:
|
||||
return renderRawMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const renderedValue = useMemo(
|
||||
() =>
|
||||
renderValue(treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage),
|
||||
[treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
|
||||
{decodedMessage?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
|
||||
{renderedValue}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
|
||||
@@ -2,12 +2,14 @@ import * as dotProp from 'dot-prop'
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import * as React from 'react'
|
||||
import PlotHistory from './Chart/Chart'
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { toPlottableValue } from './Sidebar/CodeDiff/util'
|
||||
import { PlotCurveTypes } from '../reducers/Charts'
|
||||
import { DecoderFunction, useDecoder } from './hooks/useDecoder'
|
||||
|
||||
const parseDuration = require('parse-duration')
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode<any>
|
||||
history: q.MessageHistory
|
||||
dotPath?: string
|
||||
timeInterval?: string
|
||||
@@ -25,21 +27,27 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
|
||||
return data
|
||||
}
|
||||
|
||||
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
|
||||
function nodeToHistory(decodeMessage: DecoderFunction, 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
|
||||
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
||||
const decoded = decodeMessage(message)?.message?.toUnicodeString()
|
||||
return { x: message.received.getTime(), y: toPlottableValue(decoded) }
|
||||
})
|
||||
.filter(data => !isNaN(data.y as any)) as any
|
||||
}
|
||||
|
||||
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
|
||||
function nodeDotPathToHistory(
|
||||
decodeMessage: DecoderFunction,
|
||||
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)) : {}
|
||||
const decoded = decodeMessage(message)?.message
|
||||
json = decoded ? JSON.parse(decoded.toUnicodeString()) : {}
|
||||
} catch (ignore) {}
|
||||
|
||||
const value = dotProp.get(json, dotPath)
|
||||
@@ -50,14 +58,17 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
|
||||
}
|
||||
|
||||
function TopicPlot(props: Props) {
|
||||
const decodeMessage = useDecoder(props.node)
|
||||
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
|
||||
const data = React.useMemo(
|
||||
() =>
|
||||
props.dotPath
|
||||
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
|
||||
: nodeToHistory(startOffset, props.history),
|
||||
[props.history.last(), startOffset, props.dotPath]
|
||||
)
|
||||
const data = React.useMemo(() => {
|
||||
if (!props.node) {
|
||||
return []
|
||||
}
|
||||
|
||||
return props.dotPath
|
||||
? nodeDotPathToHistory(decodeMessage, startOffset, props.history, props.dotPath)
|
||||
: nodeToHistory(decodeMessage, startOffset, props.history)
|
||||
}, [props.history.last(), startOffset, props.dotPath])
|
||||
|
||||
return (
|
||||
<PlotHistory
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import React, { MutableRefObject, RefObject, useCallback, useMemo, useRef } from 'react'
|
||||
import { FixedSizeList as List, ListOnItemsRenderedProps, ListOnScrollProps } from 'react-window'
|
||||
import { AutoSizer } from 'react-virtualized'
|
||||
import TreeNode from './TreeNode'
|
||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||
|
||||
class TreeList {
|
||||
tree: q.TreeNode<TopicViewModel>
|
||||
|
||||
constructor(tree: q.TreeNode<TopicViewModel>) {
|
||||
this.tree = tree
|
||||
}
|
||||
|
||||
getVisibleChildAt(index: number): [q.TreeNode<TopicViewModel>, number] | undefined {
|
||||
return this.tree.viewModel?.visibleChildAt(index, 1)
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.tree.viewModel?.visibleChildren() ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
const InfinitreeComponent: React.FC<{
|
||||
tree: q.TreeNode<TopicViewModel>
|
||||
actions: any
|
||||
selectTopicAction: any
|
||||
settings: any
|
||||
listRef: RefObject<List>
|
||||
lastUpdate: number
|
||||
name: string
|
||||
fixedOnTreeNodeRef: MutableRefObject<q.TreeNode<TopicViewModel> | null>
|
||||
}> = ({ tree, actions, settings, listRef, fixedOnTreeNodeRef, name }) => {
|
||||
const list = useMemo(() => new TreeList(tree), [tree])
|
||||
const lastIndex = useRef<number | undefined>(0)
|
||||
const lastScroll = useRef<number>(Date.now())
|
||||
const getKey = useCallback(
|
||||
(index: number) => {
|
||||
let [treeNode] = list.getVisibleChildAt(index) ?? []
|
||||
|
||||
return treeNode?.hash() ?? index.toString()
|
||||
},
|
||||
[list]
|
||||
)
|
||||
|
||||
const afterRender = useCallback(
|
||||
({ visibleStartIndex }: ListOnItemsRenderedProps) => {
|
||||
if (!visibleStartIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
let [treeNode] = list.getVisibleChildAt(visibleStartIndex) ?? []
|
||||
|
||||
if (treeNode) {
|
||||
fixedOnTreeNodeRef.current = treeNode
|
||||
}
|
||||
},
|
||||
[list]
|
||||
)
|
||||
|
||||
const indexOfItem = fixedOnTreeNodeRef.current?.viewModel?.getIndex()
|
||||
|
||||
if (indexOfItem && lastIndex.current !== indexOfItem && Date.now() - lastScroll.current > 300) {
|
||||
// Kind of dangerous to mutate scroll state directly, useEffect causes glitches
|
||||
indexOfItem && listRef.current?.scrollToItem(indexOfItem, 'start')
|
||||
}
|
||||
lastIndex.current = indexOfItem
|
||||
|
||||
const disableScroll = useCallback((args: ListOnScrollProps) => {
|
||||
if (!args.scrollUpdateWasRequested) {
|
||||
lastScroll.current = Date.now()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<List
|
||||
ref={listRef}
|
||||
width={width}
|
||||
height={height}
|
||||
itemSize={20}
|
||||
itemCount={list.length}
|
||||
itemKey={getKey}
|
||||
onItemsRendered={afterRender}
|
||||
onScroll={disableScroll}
|
||||
overscanCount={3}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
let [treeNode, depth = 0] = list.getVisibleChildAt(index) ?? []
|
||||
if (!treeNode) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ ...style, paddingLeft: 12 * (depth - 1) }}>
|
||||
<TreeNode
|
||||
treeNode={treeNode}
|
||||
isRoot={index === 0}
|
||||
doNotRenderSubnodes={true}
|
||||
name={index === 0 ? name : undefined}
|
||||
collapsed={false}
|
||||
settings={settings}
|
||||
lastUpdate={treeNode.lastUpdate}
|
||||
actions={actions}
|
||||
selectTopicAction={actions.selectTopic}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)
|
||||
}
|
||||
|
||||
export const Infinitree = InfinitreeComponent
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import React, { memo } from 'react'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { Theme, withStyles } from '@material-ui/core'
|
||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
|
||||
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
|
||||
treeNode: q.TreeNode<TopicViewModel>
|
||||
@@ -14,67 +14,72 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
|
||||
classes: any
|
||||
}
|
||||
|
||||
class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
|
||||
private renderSourceEdge() {
|
||||
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
|
||||
export const TreeNodeTitle = (props: TreeNodeProps) => {
|
||||
const decodeMessage = useDecoder(props.treeNode)
|
||||
|
||||
function renderSourceEdge() {
|
||||
const name = props.name || (props.treeNode.sourceEdge && props.treeNode.sourceEdge.name)
|
||||
|
||||
return (
|
||||
<span key="edge" className={this.props.classes.sourceEdge}>
|
||||
<span key="edge" className={props.classes.sourceEdge} data-test-topic={name}>
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private truncatedMessage() {
|
||||
function truncatedMessage() {
|
||||
const limit = 400
|
||||
if (!this.props.treeNode.message || !this.props.treeNode.message.value) {
|
||||
if (!props.treeNode.message || !props.treeNode.message.payload) {
|
||||
return ''
|
||||
}
|
||||
const [value = ''] = decodeMessage(props.treeNode.message)?.message?.format(props.treeNode.type) ?? []
|
||||
|
||||
const str = Base64Message.toUnicodeString(this.props.treeNode.message.value)
|
||||
return str.length > limit ? `${str.slice(0, limit)}…` : str
|
||||
return value.length > limit ? `${value.slice(0, limit)}…` : value
|
||||
}
|
||||
|
||||
private renderValue() {
|
||||
return this.props.treeNode.message &&
|
||||
this.props.treeNode.message.value &&
|
||||
this.props.treeNode.message.length > 0 ? (
|
||||
<span key="value" className={this.props.classes.value}>
|
||||
function renderValue() {
|
||||
return props.treeNode.message && props.treeNode.message.payload && props.treeNode.message.length > 0 ? (
|
||||
<span key="value" className={props.classes.value}>
|
||||
{' '}
|
||||
= {this.truncatedMessage()}
|
||||
= {truncatedMessage()}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
|
||||
private renderExpander() {
|
||||
if (this.props.treeNode.edgeCount() === 0) {
|
||||
function renderExpander() {
|
||||
if (props.treeNode.edgeCount() === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span key="expander" className={this.props.classes.expander} onClick={this.props.toggleCollapsed}>
|
||||
{this.props.collapsed ? '▶' : '▼'}
|
||||
<span key="expander" className={props.classes.expander} onClick={props.toggleCollapsed}>
|
||||
{props.collapsed ? '▶' : '▼'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private renderMetadata() {
|
||||
if (this.props.treeNode.edgeCount() === 0 || !this.props.collapsed) {
|
||||
function renderMetadata() {
|
||||
if (props.treeNode.edgeCount() === 0 || !props.collapsed) {
|
||||
return null
|
||||
}
|
||||
|
||||
const messages = this.props.treeNode.leafMessageCount()
|
||||
const topicCount = this.props.treeNode.childTopicCount()
|
||||
const messages = props.treeNode.leafMessageCount()
|
||||
const topicCount = props.treeNode.childTopicCount()
|
||||
return (
|
||||
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${
|
||||
<span key="metadata" className={props.classes.collapsedSubnodes}>{` (${topicCount} ${
|
||||
topicCount === 1 ? 'topic' : 'topics'
|
||||
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return [this.renderExpander(), this.renderSourceEdge(), this.renderMetadata(), this.renderValue()]
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{renderExpander()}
|
||||
{renderSourceEdge()}
|
||||
{renderMetadata()}
|
||||
{renderValue()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as q from '../../../../../../backend/src/Model'
|
||||
import { useEffect } from 'react'
|
||||
import { TopicViewModel } from '../../../../model/TopicViewModel'
|
||||
|
||||
export function useViewModel(treeNode: q.TreeNode<TopicViewModel> | undefined) {
|
||||
useEffect(() => {
|
||||
// if (treeNode && !treeNode?.viewModel) {
|
||||
// treeNode.viewModel = new TopicViewModel(treeNode)
|
||||
// }
|
||||
treeNode?.viewModel?.retain()
|
||||
|
||||
return function cleanup() {
|
||||
treeNode?.viewModel?.release()
|
||||
}
|
||||
}, [treeNode])
|
||||
|
||||
return treeNode?.viewModel
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as q from '../../../../../../backend/src/Model'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { TopicViewModel } from '../../../../model/TopicViewModel'
|
||||
import { useSubscription } from '../../../hooks/useSubscription'
|
||||
import { useViewModel } from './useViewModel'
|
||||
|
||||
export function useViewModelSubscriptions(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
@@ -8,37 +10,21 @@ export function useViewModelSubscriptions(
|
||||
setSelected: (value: boolean) => void,
|
||||
setCollapsedOverride: (value: boolean) => void
|
||||
) {
|
||||
useEffect(() => {
|
||||
const selectionDidChange = () => {
|
||||
const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
|
||||
treeNode.viewModel && setSelected(Boolean(selected))
|
||||
const viewModel = useViewModel(treeNode)
|
||||
|
||||
if (selected && nodeRef && nodeRef.current) {
|
||||
nodeRef.current.focus({ preventScroll: false })
|
||||
}
|
||||
}
|
||||
const selectionDidChange = useCallback(() => {
|
||||
const selected = viewModel && viewModel.isSelected()
|
||||
viewModel && setSelected(Boolean(selected))
|
||||
|
||||
const expandedDidChange = () => {
|
||||
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded())
|
||||
if (selected && nodeRef && nodeRef.current) {
|
||||
nodeRef.current.focus({ preventScroll: false })
|
||||
}
|
||||
}, [viewModel])
|
||||
|
||||
function addSubscriber() {
|
||||
treeNode.viewModel = new TopicViewModel()
|
||||
treeNode.viewModel.selectionChange.subscribe(selectionDidChange)
|
||||
treeNode.viewModel.expandedChange.subscribe(expandedDidChange)
|
||||
}
|
||||
const expandedDidChange = useCallback(() => {
|
||||
viewModel && setCollapsedOverride(!viewModel.isExpanded())
|
||||
}, [viewModel])
|
||||
|
||||
function removeSubscriber() {
|
||||
if (treeNode.viewModel) {
|
||||
treeNode.viewModel.selectionChange.unsubscribe(selectionDidChange)
|
||||
treeNode.viewModel.expandedChange.unsubscribe(expandedDidChange)
|
||||
treeNode.viewModel = undefined
|
||||
}
|
||||
}
|
||||
|
||||
addSubscriber()
|
||||
return function cleanup() {
|
||||
removeSubscriber()
|
||||
}
|
||||
}, [treeNode])
|
||||
useSubscription(viewModel?.selectionChange, selectionDidChange)
|
||||
useSubscription(viewModel?.expandedChange, expandedDidChange)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface Props {
|
||||
treeNode: q.TreeNode<TopicViewModel>
|
||||
name?: string | undefined
|
||||
collapsed?: boolean | undefined
|
||||
doNotRenderSubnodes?: boolean
|
||||
classes: any
|
||||
lastUpdate: number
|
||||
actions: typeof treeActions
|
||||
@@ -27,8 +28,8 @@ export interface Props {
|
||||
}
|
||||
|
||||
function TreeNodeComponent(props: Props) {
|
||||
const { actions, classes, settings, theme, treeNode, lastUpdate, name } = props
|
||||
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(undefined)
|
||||
const { actions, classes, settings, theme, treeNode, lastUpdate, name, doNotRenderSubnodes } = props
|
||||
const [collapsedOverride, setCollapsedOverride] = useState<boolean | undefined>(!treeNode.viewModel?.isExpanded())
|
||||
const [selected, selectionLastUpdate, setSelected] = useSelectionState(false)
|
||||
const nodeRef = useRef<HTMLDivElement>()
|
||||
const isAllowedToAutoExpand = useIsAllowedToAutoExpandState(props)
|
||||
@@ -81,7 +82,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()
|
||||
}
|
||||
},
|
||||
@@ -94,7 +95,7 @@ function TreeNodeComponent(props: Props) {
|
||||
|
||||
return useMemo(() => {
|
||||
function renderNodes() {
|
||||
if (isCollapsed) {
|
||||
if (isCollapsed || doNotRenderSubnodes) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -134,7 +135,7 @@ function TreeNodeComponent(props: Props) {
|
||||
{renderNodes()}
|
||||
</div>
|
||||
)
|
||||
}, [treeNode.lastUpdate, treeNode, name, isCollapsed, selected, theme, mouseOver, settings])
|
||||
}, [treeNode.lastUpdate, treeNode, name, isCollapsed, selected, theme, mouseOver, settings, doNotRenderSubnodes])
|
||||
}
|
||||
|
||||
export default withStyles(styles, { withTheme: true })(React.memo(TreeNodeComponent))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import React from 'react'
|
||||
import TreeNode from './TreeNode'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import { Infinitree } from './Infinitree'
|
||||
import { AppState } from '../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
@@ -8,6 +8,8 @@ import { KeyCodes } from '../../utils/KeyCodes'
|
||||
import { SettingsState } from '../../reducers/Settings'
|
||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||
import { treeActions } from '../../actions'
|
||||
import { FixedSizeList as List } from 'react-window'
|
||||
import { useSubscription } from '../hooks/useSubscription'
|
||||
const MovingAverage = require('moving-average')
|
||||
|
||||
const averagingTimeInterval = 10 * 1000
|
||||
@@ -24,133 +26,119 @@ interface Props {
|
||||
settings: SettingsState
|
||||
}
|
||||
|
||||
interface State {
|
||||
lastUpdate: number
|
||||
}
|
||||
|
||||
function useArrowKeyEventHandler(actions: typeof treeActions) {
|
||||
return (event: React.KeyboardEvent) => {
|
||||
switch (event.keyCode) {
|
||||
case KeyCodes.arrow_down:
|
||||
actions.moveSelectionUpOrDownwards('next')
|
||||
event.preventDefault()
|
||||
break
|
||||
case KeyCodes.arrow_up:
|
||||
actions.moveSelectionUpOrDownwards('previous')
|
||||
event.preventDefault()
|
||||
break
|
||||
case KeyCodes.arrow_left:
|
||||
actions.moveOutward()
|
||||
event.preventDefault()
|
||||
break
|
||||
case KeyCodes.arrow_right:
|
||||
actions.moveInward()
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
}
|
||||
return useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
switch (event.keyCode) {
|
||||
case KeyCodes.arrow_down:
|
||||
actions.moveSelectionUpOrDownwards('next')
|
||||
event.preventDefault()
|
||||
break
|
||||
case KeyCodes.arrow_up:
|
||||
actions.moveSelectionUpOrDownwards('previous')
|
||||
event.preventDefault()
|
||||
break
|
||||
case KeyCodes.arrow_left:
|
||||
actions.moveOutward()
|
||||
event.preventDefault()
|
||||
break
|
||||
case KeyCodes.arrow_right:
|
||||
actions.moveInward()
|
||||
event.preventDefault()
|
||||
break
|
||||
}
|
||||
},
|
||||
[actions]
|
||||
)
|
||||
}
|
||||
|
||||
class TreeComponent extends React.PureComponent<Props, State> {
|
||||
private updateTimer?: any
|
||||
private perf: number = 0
|
||||
private renderTime = 0
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { lastUpdate: 0 }
|
||||
}
|
||||
|
||||
private keyEventHandler = useArrowKeyEventHandler(this.props.actions)
|
||||
private performanceCallback = (ms: number) => {
|
||||
const TreeComponent: React.FC<Props> = props => {
|
||||
const keyEventHandler = useArrowKeyEventHandler(props.actions)
|
||||
const performanceCallback = useCallback((ms: number) => {
|
||||
average.push(Date.now(), ms)
|
||||
}
|
||||
}, [])
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.tree !== nextProps.tree) {
|
||||
if (this.props.tree) {
|
||||
this.props.tree.didUpdate.unsubscribe(this.throttledTreeUpdate)
|
||||
}
|
||||
if (nextProps.tree) {
|
||||
nextProps.tree.didUpdate.subscribe(this.throttledTreeUpdate)
|
||||
}
|
||||
this.setState(this.state)
|
||||
}
|
||||
}
|
||||
const updateTimer = useRef<NodeJS.Timeout | number>()
|
||||
const perf = useRef<number>(performance.now())
|
||||
const renderTime = useRef<number>(0)
|
||||
const listRef = useRef<List>(null)
|
||||
const [lastUpdate, triggerUpdate] = React.useState(0)
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.tree && this.props.tree.didUpdate.unsubscribe(this.throttledTreeUpdate)
|
||||
}
|
||||
|
||||
public throttledTreeUpdate = () => {
|
||||
if (this.updateTimer) {
|
||||
const throttledTreeUpdate = useCallback(() => {
|
||||
if (updateTimer.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const expectedRenderTime = average.forecast()
|
||||
const updateInterval = Math.max(expectedRenderTime * 7, 300)
|
||||
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
|
||||
const updateInterval = Math.max(expectedRenderTime * 7, 500)
|
||||
const timeUntilNextUpdate = updateInterval - (performance.now() - renderTime.current)
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
window.requestIdleCallback(
|
||||
() => {
|
||||
this.updateTimer && clearTimeout(this.updateTimer)
|
||||
this.updateTimer = undefined
|
||||
this.renderTime = performance.now()
|
||||
updateTimer.current = setTimeout(
|
||||
() => {
|
||||
window.requestIdleCallback(
|
||||
() => {
|
||||
updateTimer.current && clearTimeout(updateTimer.current)
|
||||
updateTimer.current = undefined
|
||||
renderTime.current = performance.now()
|
||||
|
||||
window.requestIdleCallback(
|
||||
() => {
|
||||
this.setState({ lastUpdate: this.renderTime })
|
||||
},
|
||||
{ timeout: 100 }
|
||||
)
|
||||
},
|
||||
{ timeout: 500 }
|
||||
)
|
||||
}, Math.max(0, timeUntilNextUpdate))
|
||||
}
|
||||
window.requestIdleCallback(
|
||||
() => {
|
||||
triggerUpdate(renderTime.current)
|
||||
},
|
||||
{ timeout: 100 }
|
||||
)
|
||||
},
|
||||
{ timeout: 500 }
|
||||
)
|
||||
},
|
||||
Math.max(0, timeUntilNextUpdate)
|
||||
)
|
||||
}, [])
|
||||
|
||||
public componentWillUpdate() {
|
||||
this.perf = performance.now()
|
||||
}
|
||||
perf.current = performance.now()
|
||||
window.requestIdleCallback(() => {
|
||||
performanceCallback(performance.now() - perf.current)
|
||||
})
|
||||
const fixedOnTreeNodeRef = useRef<q.TreeNode<TopicViewModel> | null>(null)
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.performanceCallback(performance.now() - this.perf)
|
||||
}
|
||||
useSubscription(props.tree?.didUpdate, throttledTreeUpdate)
|
||||
|
||||
public render() {
|
||||
const { tree } = this.props
|
||||
if (!tree) {
|
||||
return null
|
||||
}
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
const style: React.CSSProperties = useMemo(
|
||||
() => ({
|
||||
lineHeight: '1.1',
|
||||
cursor: 'default',
|
||||
overflowY: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
// overflowY: 'scroll',
|
||||
// overflowX: 'hidden',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
outline: '24px black !important',
|
||||
paddingBottom: '16px', // avoid conflict with chart panel Resizer
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={style} tabIndex={0} onKeyDown={this.keyEventHandler}>
|
||||
<TreeNode
|
||||
key={tree.hash()}
|
||||
isRoot={true}
|
||||
treeNode={tree}
|
||||
name={this.props.host}
|
||||
collapsed={false}
|
||||
settings={this.props.settings}
|
||||
lastUpdate={tree.lastUpdate}
|
||||
actions={this.props.actions}
|
||||
selectTopicAction={this.props.actions.selectTopic}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
const { tree } = props
|
||||
if (!tree) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rendered = (
|
||||
<div style={style} tabIndex={0} onKeyDown={keyEventHandler}>
|
||||
<Infinitree
|
||||
lastUpdate={lastUpdate}
|
||||
listRef={listRef}
|
||||
key={tree.hash()}
|
||||
fixedOnTreeNodeRef={fixedOnTreeNodeRef}
|
||||
tree={tree}
|
||||
name={props.host ?? ''}
|
||||
actions={props.actions}
|
||||
selectTopicAction={props.actions.selectTopic}
|
||||
settings={props.settings}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return rendered
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
@@ -169,7 +157,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TreeComponent)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as compareVersions from 'compare-versions'
|
||||
import * as electron from 'electron'
|
||||
import * as os from 'os'
|
||||
import * as React from 'react'
|
||||
import compareVersions from 'compare-versions'
|
||||
import electron from 'electron'
|
||||
import os from 'os'
|
||||
import React from 'react'
|
||||
import axios from 'axios'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import CloudDownload from '@material-ui/icons/CloudDownload'
|
||||
@@ -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))
|
||||
|
||||
@@ -9,7 +9,8 @@ import { globalActions } from '../../actions'
|
||||
const copy = require('copy-text-to-clipboard')
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
value?: string
|
||||
getValue?: () => string | undefined
|
||||
actions: {
|
||||
global: typeof globalActions
|
||||
}
|
||||
@@ -28,7 +29,7 @@ class Copy extends React.PureComponent<Props, State> {
|
||||
private handleClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
|
||||
copy(this.props.value)
|
||||
copy(this.props.value ?? this.props.getValue?.())
|
||||
this.props.actions.global.showNotification('Copied to clipboard')
|
||||
this.setState({ didCopy: true })
|
||||
setTimeout(() => {
|
||||
@@ -59,7 +60,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(Copy)
|
||||
export default connect(undefined, mapDispatchToProps)(Copy)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as moment from 'moment'
|
||||
import * as React from 'react'
|
||||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { AppState } from '../../reducers'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
@@ -12,6 +12,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const unitMapping = {
|
||||
ms: 'milliseconds',
|
||||
s: 'seconds',
|
||||
m: 'minutes',
|
||||
h: 'hours',
|
||||
@@ -21,7 +22,7 @@ class DateFormatter extends React.PureComponent<Props, {}> {
|
||||
private intervalSince(intervalSince: Date) {
|
||||
const interval = intervalSince.getTime() - this.props.date.getTime()
|
||||
const unit = this.unitForInterval(interval)
|
||||
return `${Math.round(moment.duration(interval).as(unit) * 100) / 100} ${unitMapping[unit]}`
|
||||
return `${moment.duration(interval).as(unit).toFixed(3)} ${unitMapping[unit]}`
|
||||
}
|
||||
|
||||
private legacyDate() {
|
||||
@@ -31,10 +32,11 @@ class DateFormatter extends React.PureComponent<Props, {}> {
|
||||
private localizedDate(locale: string) {
|
||||
return moment(this.props.date)
|
||||
.locale(locale)
|
||||
.format(this.props.timeFirst ? 'LTS L' : 'L LTS')
|
||||
.format(this.props.timeFirst ? 'LTS.SSS L' : 'L LTS.SSS')
|
||||
}
|
||||
|
||||
private unitForInterval(milliseconds: number) {
|
||||
const oneSecond = 1000 * 1
|
||||
const oneMinute = 1000 * 60
|
||||
const oneHour = oneMinute * 60
|
||||
|
||||
@@ -46,7 +48,11 @@ class DateFormatter extends React.PureComponent<Props, {}> {
|
||||
return 'm'
|
||||
}
|
||||
|
||||
return 's'
|
||||
if (milliseconds > oneSecond * 0.5) {
|
||||
return 's'
|
||||
}
|
||||
|
||||
return 'ms'
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||
import { useSubscription } from './useSubscription'
|
||||
import { useViewModel } from '../Tree/TreeNode/effects/useViewModel'
|
||||
import { DecoderEnvelope } from '../../decoders/DecoderEnvelope'
|
||||
import { Decoder } from '../../../../backend/src/Model/Decoder'
|
||||
|
||||
export type DecoderFunction = (message: q.Message) => DecoderEnvelope | undefined
|
||||
|
||||
/**
|
||||
* Provides the latest decoder for a topic
|
||||
*
|
||||
* @param treeNode
|
||||
* @returns
|
||||
*/
|
||||
export function useDecoder(treeNode: q.TreeNode<TopicViewModel> | undefined): DecoderFunction {
|
||||
const viewModel = useViewModel(treeNode)
|
||||
const [decoder, setDecoder] = useState(viewModel?.decoder)
|
||||
|
||||
useSubscription(viewModel?.onDecoderChange, setDecoder)
|
||||
|
||||
return useCallback(
|
||||
message => {
|
||||
return decoder && message.payload
|
||||
? decoder.decoder.decode(message.payload, decoder.format)
|
||||
: { message: message.payload ?? undefined, decoder: Decoder.NONE }
|
||||
},
|
||||
[decoder]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useEffect } from 'react'
|
||||
import { EventDispatcher } from '../../../../events'
|
||||
|
||||
export function useSubscription<T>(dispatcher: EventDispatcher<T> | undefined, callback: (value: T) => void) {
|
||||
useEffect(() => {
|
||||
dispatcher?.subscribe(callback)
|
||||
|
||||
return () => dispatcher?.unsubscribe(callback)
|
||||
}, [dispatcher, callback])
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
import { DecoderEnvelope } from './DecoderEnvelope'
|
||||
import { MessageDecoder } from './MessageDecoder'
|
||||
|
||||
type BinaryFormats =
|
||||
| 'int8'
|
||||
| 'int16'
|
||||
| 'int32'
|
||||
| 'int64'
|
||||
| 'uint8'
|
||||
| 'uint16'
|
||||
| 'uint32'
|
||||
| 'uint64'
|
||||
| 'float'
|
||||
| 'double'
|
||||
|
||||
/**
|
||||
* Binary decode primitive binary data type and arrays of these
|
||||
*/
|
||||
export const BinaryDecoder: MessageDecoder<BinaryFormats> = {
|
||||
formats: ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float', 'double'],
|
||||
decode(input: Base64Message, format: BinaryFormats): DecoderEnvelope {
|
||||
const decodingOption = {
|
||||
int8: [Buffer.prototype.readInt8, 1],
|
||||
int16: [Buffer.prototype.readInt16LE, 2],
|
||||
int32: [Buffer.prototype.readInt32LE, 4],
|
||||
int64: [Buffer.prototype.readBigInt64LE, 8],
|
||||
uint8: [Buffer.prototype.readUint8, 1],
|
||||
uint16: [Buffer.prototype.readUint16LE, 2],
|
||||
uint32: [Buffer.prototype.readUint32LE, 4],
|
||||
uint64: [Buffer.prototype.readBigUint64LE, 8],
|
||||
float: [Buffer.prototype.readFloatLE, 4],
|
||||
double: [Buffer.prototype.readDoubleLE, 8],
|
||||
} as const
|
||||
|
||||
const [readNumber, bytesToRead] = decodingOption[format]
|
||||
|
||||
const buf = input.toBuffer()
|
||||
let str: String[] = []
|
||||
if (buf.length % bytesToRead !== 0) {
|
||||
return {
|
||||
error: 'Data type does not align with message',
|
||||
decoder: Decoder.NONE,
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < buf.length; index += bytesToRead) {
|
||||
str.push((readNumber as any).apply(buf, [index]).toString())
|
||||
}
|
||||
|
||||
return {
|
||||
message: Base64Message.fromString(JSON.stringify(str.length === 1 ? str[0] : str)),
|
||||
decoder: Decoder.NONE,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
|
||||
export interface DecoderEnvelope {
|
||||
message?: Base64Message
|
||||
error?: string
|
||||
decoder: Decoder
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { DecoderEnvelope } from './DecoderEnvelope'
|
||||
|
||||
export interface MessageDecoder<T = string> {
|
||||
/**
|
||||
* Can be used to
|
||||
* @param topic
|
||||
*/
|
||||
formats: T[]
|
||||
canDecodeTopic?(topic: string): boolean
|
||||
canDecodeData?(data: Base64Message): boolean
|
||||
decode(input: Base64Message, format: T | string | undefined): DecoderEnvelope
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
import { get } from 'sparkplug-payload'
|
||||
import { MessageDecoder } from './MessageDecoder'
|
||||
var sparkplug = get('spBv1.0')
|
||||
|
||||
export const SparkplugDecoder: MessageDecoder = {
|
||||
formats: ['Sparkplug'],
|
||||
canDecodeTopic(topic: string) {
|
||||
return !!topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u)
|
||||
},
|
||||
decode(input) {
|
||||
try {
|
||||
const message = Base64Message.fromString(
|
||||
JSON.stringify(
|
||||
// @ts-ignore
|
||||
sparkplug.decodePayload(new Uint8Array(input.toBuffer()))
|
||||
)
|
||||
)
|
||||
return { message, decoder: Decoder.SPARKPLUG }
|
||||
} catch {
|
||||
return {
|
||||
error: 'Failed to decode sparkplugb payload',
|
||||
decoder: Decoder.NONE,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
import { MessageDecoder } from './MessageDecoder'
|
||||
|
||||
export const StringDecoder: MessageDecoder = {
|
||||
formats: ['string'],
|
||||
decode(input: Base64Message) {
|
||||
return { message: input, decoder: Decoder.NONE }
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { StringDecoder } from './StringDecoder'
|
||||
import { BinaryDecoder } from './BinaryDecoder'
|
||||
import { SparkplugDecoder } from './SparkplugBDecoder'
|
||||
export * from './MessageDecoder'
|
||||
|
||||
export const decoders = [SparkplugDecoder, BinaryDecoder, StringDecoder] as const
|
||||
@@ -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,
|
||||
@@ -70,11 +77,11 @@ export function createEmptyConnection(): ConnectionOptions {
|
||||
export function makeDefaultConnections() {
|
||||
return {
|
||||
// remember: there was also iot.eclipse.org once
|
||||
'mqtt.eclipse.org': {
|
||||
'mqtt.eclipseprojects.io': {
|
||||
...createEmptyConnection(),
|
||||
id: 'mqtt.eclipse.org',
|
||||
name: 'mqtt.eclipse.org',
|
||||
host: 'mqtt.eclipse.org',
|
||||
id: 'mqtt.eclipseprojects.io',
|
||||
name: 'mqtt.eclipseprojects.io',
|
||||
host: 'mqtt.eclipseprojects.io',
|
||||
},
|
||||
'test.mosquitto.org': {
|
||||
...createEmptyConnection(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConnectionOptions, createEmptyConnection } from './ConnectionOptions'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
interface LegacyConnectionSettings {
|
||||
host: string
|
||||
|
||||
@@ -1,19 +1,157 @@
|
||||
import { Destroyable } from '../../../backend/src/Model/Destroyable'
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { Destroyable, MemoryLifecycle } from '../../../backend/src/Model/Destroyable'
|
||||
import { MessageDecoder, decoders } from '../decoders'
|
||||
import { EventDispatcher } from '../../../events'
|
||||
|
||||
export class TopicViewModel implements Destroyable {
|
||||
function findDecoder<T extends Destroyable & MemoryLifecycle>(node: q.TreeNode<T>): TopicDecoder | undefined {
|
||||
const decoder = decoders.find(
|
||||
decoder =>
|
||||
decoder.canDecodeTopic?.(node.path()) || (node.message?.payload && decoder.canDecodeData?.(node.message?.payload))
|
||||
)
|
||||
|
||||
return decoder
|
||||
? {
|
||||
decoder,
|
||||
format: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
type TopicDecoder = { decoder: MessageDecoder; format: string | undefined }
|
||||
|
||||
export class TopicViewModel implements Destroyable, MemoryLifecycle {
|
||||
private selected: boolean
|
||||
private expanded: boolean
|
||||
private owner: q.TreeNode<TopicViewModel> | undefined
|
||||
private _decoder?: TopicDecoder
|
||||
/**
|
||||
* Reference counter for useViewModel hook
|
||||
*/
|
||||
private referenceCounter = 0
|
||||
public selectionChange = new EventDispatcher<void>()
|
||||
public expandedChange = new EventDispatcher<void>()
|
||||
public onDecoderChange = new EventDispatcher<TopicDecoder | undefined>()
|
||||
|
||||
public constructor() {
|
||||
get decoder(): TopicDecoder | undefined {
|
||||
if (!this._decoder) {
|
||||
this._decoder = this.owner && findDecoder(this.owner)
|
||||
}
|
||||
|
||||
return this._decoder
|
||||
}
|
||||
|
||||
set decoder(override: TopicDecoder | undefined) {
|
||||
this._decoder = override
|
||||
|
||||
this.onDecoderChange.dispatch(override)
|
||||
}
|
||||
|
||||
private clearCache = () => {
|
||||
if (this._cachedChildTopicCount) {
|
||||
this._cachedChildTopicCount = undefined
|
||||
// when child changes, parents are affected as well
|
||||
this.owner?.sourceEdge?.source?.viewModel?.clearCache()
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(treeNode: q.TreeNode<TopicViewModel>) {
|
||||
this.owner = treeNode
|
||||
this.selected = false
|
||||
this.expanded = false
|
||||
this.expanded = true
|
||||
treeNode.onMerge.subscribe(this.clearCache)
|
||||
}
|
||||
|
||||
private _cachedChildTopicCount: number | undefined = undefined
|
||||
|
||||
/**
|
||||
* This function only returns valid values if parents are expanded
|
||||
* @returns
|
||||
*/
|
||||
public getIndex(): number {
|
||||
if (!this.owner) {
|
||||
throw new Error('integrity error')
|
||||
}
|
||||
|
||||
const source = this.owner.sourceEdge?.source
|
||||
|
||||
const parentIndex = source?.viewModel?.getIndex()
|
||||
// If we have a parent, we have its index + 1 (at least)
|
||||
const parentIndexWithDepth = parentIndex !== undefined ? parentIndex + 1 : 0
|
||||
let position = 0
|
||||
const edgeToMatch = this.owner.sourceEdge
|
||||
for (const edge of source?.edgeArray ?? []) {
|
||||
if (edge === edgeToMatch) {
|
||||
break
|
||||
}
|
||||
position += edge.target.viewModel?.visibleChildren() ?? 1
|
||||
}
|
||||
|
||||
return parentIndexWithDepth + position
|
||||
}
|
||||
|
||||
public visibleChildAt(
|
||||
index: number,
|
||||
depth: number = 0,
|
||||
parentOffset: number = 0
|
||||
): [q.TreeNode<TopicViewModel>, number] | undefined {
|
||||
if (!this.owner) {
|
||||
throw new Error('integrity error')
|
||||
}
|
||||
const node = this.owner
|
||||
|
||||
if (parentOffset === index) {
|
||||
return [node, depth]
|
||||
}
|
||||
|
||||
let position = parentOffset + 1
|
||||
for (const edge of node.edgeArray) {
|
||||
let viewModel = edge.target.viewModel
|
||||
const nextPosition = position + (viewModel?.visibleChildren() ?? 0)
|
||||
if (nextPosition > index) {
|
||||
return viewModel?.visibleChildAt(index, depth + 1, position)
|
||||
}
|
||||
position = nextPosition
|
||||
}
|
||||
}
|
||||
|
||||
public visibleChildren(): number {
|
||||
if (!this.owner) {
|
||||
throw new Error('integrity error')
|
||||
}
|
||||
|
||||
if (this._cachedChildTopicCount === undefined) {
|
||||
if (!this.expanded) {
|
||||
return 1
|
||||
}
|
||||
|
||||
this._cachedChildTopicCount =
|
||||
1 + this.owner.edgeArray.map(e => e.target.viewModel?.visibleChildren() ?? 1).reduce((a, b) => a + b, 0)
|
||||
}
|
||||
|
||||
return this._cachedChildTopicCount as number
|
||||
}
|
||||
|
||||
public retain() {
|
||||
this.referenceCounter += 1
|
||||
}
|
||||
|
||||
public release() {
|
||||
this.referenceCounter -= 1
|
||||
if (this.referenceCounter <= 0) {
|
||||
this.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
// console.log('destroy', this.owner?.path(), this.referenceCounter)
|
||||
this.owner?.onMerge.unsubscribe(this.clearCache)
|
||||
if (this.owner) {
|
||||
this.owner.viewModel = undefined
|
||||
this.owner = undefined
|
||||
}
|
||||
this.selectionChange.removeAllListeners()
|
||||
this.onDecoderChange.removeAllListeners()
|
||||
this.expandedChange.removeAllListeners()
|
||||
}
|
||||
|
||||
public isSelected() {
|
||||
@@ -32,6 +170,8 @@ export class TopicViewModel implements Destroyable {
|
||||
public setExpanded(expanded: boolean, fireEvent: boolean) {
|
||||
const didChange = this.expanded !== expanded
|
||||
this.expanded = expanded
|
||||
this.clearCache()
|
||||
|
||||
if (didChange && fireEvent) {
|
||||
this.expandedChange.dispatch()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user