Compare commits

..
Author SHA1 Message Date
Thomas Nordquist f8a2af8554 Hint which messages are compared in diff 2019-03-04 18:23:53 +01:00
Thomas Nordquist d1fb0026b6 Reorder imports 2019-03-04 18:01:20 +01:00
Thomas Nordquist 7e9932de0a Show error when json formatting fails 2019-03-04 17:57:12 +01:00
Thomas Nordquist 0dea1f9d19 Clean up connection on disconnect 2019-03-04 17:56:52 +01:00
Thomas Nordquist d7be46c78e Prevent tree-regeneration on reconnect 2019-03-04 17:56:26 +01:00
Thomas Nordquist e95fd5b37d Codestyle 2019-03-04 17:56:00 +01:00
Thomas Nordquist dee1abb1da Show notification when client got disconnected 2019-03-04 17:55:47 +01:00
Thomas Nordquist 4c84a9b5f6 Fix demo 2019-03-04 17:21:11 +01:00
Thomas Nordquist 016bf5dfcd Rework demo video 2019-03-04 16:13:27 +01:00
Thomas Nordquist 9c15e392d1 Add diff line change indicator 2019-03-03 19:04:15 +01:00
Thomas Nordquist 613c6d3aa7 Fix fomat json button 2019-03-03 16:37:23 +01:00
Thomas Nordquist f10cd89647 Demonstrate json formatting 2019-03-03 16:24:55 +01:00
Thomas Nordquist 6000541c32 Visualize diff function in video 2019-03-03 15:52:34 +01:00
Thomas Nordquist 87a86b8c0c Add json formatting 2019-03-03 06:34:06 +01:00
Thomas Nordquist f81d64cc2f Fix diff 2019-03-03 06:33:40 +01:00
Thomas Nordquist 9b31aeccd8 Add +/- signs to code diff 2019-03-03 03:50:02 +01:00
Thomas Nordquist 692054c540 Add quick preview switch 2019-03-03 02:21:05 +01:00
Thomas Nordquist 91486718d8 Fix electron downgrade 2019-03-03 00:55:15 +01:00
Thomas Nordquist 55dd79aebd Change recording resolution 2019-03-03 00:13:56 +01:00
Thomas Nordquist 7141dfa9fc Downgrade electron 2019-03-02 23:59:52 +01:00
Thomas Nordquist f16d0cd271 Clean dependencies 2019-03-02 23:37:04 +01:00
Thomas Nordquist 838e2fa287 Update electron 2019-03-02 23:21:07 +01:00
Thomas Nordquist b9439c3244 Add switch for diffs 2019-03-02 23:20:02 +01:00
Thomas Nordquist 410855e8a0 Fix app build 2019-03-02 21:23:49 +01:00
Thomas Nordquist 71d606d7cd Fix compile errors 2019-03-02 19:57:29 +01:00
Thomas Nordquist a32810417d Fix store logos 2019-03-02 16:08:40 +01:00
Thomas Nordquist 4fcf767079 Change windows store color theme 2019-02-28 15:50:14 +01:00
Thomas Nordquist 1e6fa11f0f Remove Appveyor deploy rule 2019-02-27 17:23:27 +01:00
Thomas Nordquist 9644952d81 Always build appx 2019-02-27 16:15:44 +01:00
Thomas Nordquist d1cd299132 Add appx store logos 2019-02-27 16:14:32 +01:00
Thomas Nordquist e3539015f7 Add appveyor badge 2019-02-26 23:44:52 +01:00
Thomas Nordquist 874cedb422 Add appveyor support (#75)
* Add appveyor build badge
2019-02-26 23:43:53 +01:00
Thomas Nordquist 63530a41ac Fix topic selector xpath 2019-02-25 17:58:24 +01:00
Thomas Nordquist d3bf5e87e1 Clean up dependencies 2019-02-25 17:30:41 +01:00
Thomas Nordquist 10c6c3cecd Add changelog 2019-02-25 17:08:41 +01:00
Thomas Nordquist 4d8fa72491 Refactor 2019-02-25 17:07:49 +01:00
Thomas Nordquist b9c0a66948 Refactor value rendering 2019-02-25 17:07:29 +01:00
Thomas Nordquist 6b859d31e8 Fix loading client id conenction setting
fixes #71
2019-02-25 16:40:43 +01:00
Thomas Nordquist 4fd328e716 Add zoom menu and shortcuts 2019-02-25 13:52:07 +01:00
Thomas Nordquist b606f14836 Clean imports & modules 2019-02-25 12:04:41 +01:00
Thomas Nordquist ad49c5c819 Update diff view 2019-02-25 11:57:44 +01:00
Thomas Nordquist 749a591465 Fix message diffing 2019-02-24 11:46:35 +01:00
Thomas Nordquist a1b1c92648 Remove mouse-over to select topics 2019-02-24 11:38:00 +01:00
Thomas Nordquist 85c6c4ebbc Fix message history comparison selection 2019-02-23 23:26:52 +01:00
Thomas Nordquist de7c9479c9 Highlight selected history entry 2019-02-23 23:16:47 +01:00
Thomas Nordquist 138f51eb39 Add diff view for received messages 2019-02-23 22:49:42 +01:00
Thomas Nordquist 107ca83882 Remove obsolete import 2019-02-23 22:35:20 +01:00
Thomas Nordquist 839ad531ba Fix RingBuffer array type 2019-02-23 22:35:03 +01:00
Thomas Nordquist 174eb0c767 Fix leaking connections 2019-02-23 22:34:32 +01:00
Thomas Nordquist 953422dcd4 Show travis-ci master build status 2019-02-22 12:08:36 +01:00
Thomas Nordquist a4015141c3 Fix zoomFactor 2019-02-21 10:42:51 +01:00
Thomas Nordquist ca3bd0b22b Fix connection window location 2019-02-20 17:09:47 +01:00
Thomas Nordquist 797d11d58d Update video captions 2019-02-20 15:57:32 +01:00
Thomas Nordquist 8281a852ab Change ui test resolution 2019-02-20 15:54:38 +01:00
Thomas Nordquist e5c3f207d9 Update gif image scale 2019-02-20 15:49:13 +01:00
Thomas Nordquist 2efc8f4dda Record videos in 720p 2019-02-20 15:40:35 +01:00
Thomas Nordquist 60a06d8c82 Update Readme & Add snap store link 2019-02-20 15:35:51 +01:00
Thomas Nordquist 857500f9c9 Update electron builder 2019-02-20 11:27:07 +01:00
Thomas Nordquist 396e6985ce Update readme 2019-02-19 00:33:21 +01:00
Thomas Nordquist 6d75f29f03 Update readme 2019-02-19 00:09:25 +01:00
Thomas Nordquist f8cdf0837e Prepare alpha release 2019-02-18 23:48:58 +01:00
Thomas Nordquist 5bb96233d1 Fix asset upload 2019-02-18 23:48:58 +01:00
Thomas Nordquist f3ff6cde7b Fix about window icon 2019-02-18 23:48:58 +01:00
Thomas Nordquist ec88f9822d Fix tests 2019-02-18 23:48:58 +01:00
Thomas Nordquist 235e81826b Add about window 2019-02-18 23:48:58 +01:00
Thomas Nordquist 615ec17b96 Remove empty topics from tree 2019-02-18 23:48:58 +01:00
Thomas Nordquist ddc801fe93 WiP 2019-02-18 23:48:58 +01:00
Thomas Nordquist 24c6b7e7b3 Clean up 2019-02-18 23:48:58 +01:00
Thomas Nordquist 55ea381b3b WiP 2019-02-18 23:48:58 +01:00
70 changed files with 1832 additions and 1125 deletions
+6
View File
@@ -0,0 +1,6 @@
# next release
- Magnify app content with "Ctrl +", "Ctrl -", "Ctrl 0"
- Add message diff view
- Make user interactions more predictable
# 0.2.0
+24 -19
View File
@@ -1,42 +1,47 @@
# [MQTT Explorer](https://thomasnordquist.github.io/MQTT-Explorer/) # [MQTT Explorer](https://thomasnordquist.github.io/MQTT-Explorer/)
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases) [![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases) [![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer) [![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
[![Build status](https://ci.appveyor.com/api/projects/status/c35tkm29rm4m5364/branch/master?svg=true)](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
### Version 0.1.3 ### Version
See the whole picture of your broker. A MQTT Tool to get a quick overview of your MQTT topics and integrate new devices/services more efficiently.
- What topics are active?
- Which retained topics are abandoned? - Visualize topics and topic activity
- Where is this new device / service I connected to the broker? - Delete retained topics
- Search/filter topics
- Delete topic recursively
- Publish topics
- Plot numeric topics
- Keep a history of each topic
MQTT-Explorer strives to be THE swiss-army-knife tool. MQTT-Explorer strives to be THE swiss-army-knife tool.
The perfect tool to integrate new services, IoT devices in your network. The perfect tool to integrate new services, IoT devices in your network.
The hierarchical view allows for a quick understanding what is going on on your broker. The hierarchical view allows for a quick understanding what is going on on your broker.
The app is available for Windows ([portable](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.1.3/MQTT-Explorer-0.1.3.exe), [installer](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.1.3/MQTT-Explorer-Setup-0.1.3.exe)), Linux ([AppImage](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.1.3/MQTT-Explorer-0.1.3-x86_64.AppImage)) and Mac ([dmg](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.1.3/MQTT-Explorer-0.1.3.dmg)). ## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | [portable](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.2.0/MQTT-Explorer-0.2.0.exe), [installer](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.2.0/MQTT-Explorer-Setup-0.2.0.exe) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | [AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.0&#x2F;MQTT-Explorer-0.2.0-x86_64.AppImage)<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | Ubuntu | [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/mqtt-explorer)<br>[Ubuntu Store](snap://mqtt-explorer) |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | [dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.0&#x2F;MQTT-Explorer-0.2.0.dmg) |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Screenshots ## Screenshots
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png) ![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | [portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-Setup-0.1.3.exe) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | [AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3-x86_64.AppImage)<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | [dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.dmg) |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Demo ## Demo
![screencast](https://user-images.githubusercontent.com/7721625/52979302-3f073b80-33d5-11e9-9953-b70ebb349439.gif) ![screencast](https://user-images.githubusercontent.com/7721625/52979302-3f073b80-33d5-11e9-9953-b70ebb349439.gif)
## Performance ## Performance
This App is optimized to handle thousands of topics and at least hundreds of thousands messages per minute. This App is optimized to handle thousands of topics and at least hundreds of thousands messages per minute.
So don't worry if you got >=10k topics and >=100k messages per minute. So don't worry if you got >=30k topics and >=100.000 messages per minute.
Future releases will allow for a more granular selection of subscribed topics to handle One can also subscribe to custom topics insted of the "root wildcard #", therefore limiting the amount of messages.
## Develop ## Develop
PRs and issues are welcome PRs and issues are welcome
+23 -18
View File
@@ -1,42 +1,47 @@
# [MQTT Explorer](https://thomasnordquist.github.io/MQTT-Explorer/) # [MQTT Explorer](https://thomasnordquist.github.io/MQTT-Explorer/)
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases) [![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases) [![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer) [![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
[![Build status](https://ci.appveyor.com/api/projects/status/c35tkm29rm4m5364/branch/master?svg=true)](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
### Version {{ version }} ### Version {{ version }}
See the whole picture of your broker. A MQTT Tool to get a quick overview of your MQTT topics and integrate new devices/services more efficiently.
- What topics are active?
- Which retained topics are abandoned? - Visualize topics and topic activity
- Where is this new device / service I connected to the broker? - Delete retained topics
- Search/filter topics
- Delete topic recursively
- Publish topics
- Plot numeric topics
- Keep a history of each topic
MQTT-Explorer strives to be THE swiss-army-knife tool. MQTT-Explorer strives to be THE swiss-army-knife tool.
The perfect tool to integrate new services, IoT devices in your network. The perfect tool to integrate new services, IoT devices in your network.
The hierarchical view allows for a quick understanding what is going on on your broker. The hierarchical view allows for a quick understanding what is going on on your broker.
The app is available for Windows ({{windowsTargets}}), Linux ({{linuxTargets}}) and Mac ({{macTargets}}). ## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | <a href="https://www.microsoft.com/store/apps/9PP8SFM082WD?ocid=badge"><img src="https://assets.windowsphone.com/85864462-9c82-451e-9355-a3d5f874397a/English_get-it-from-MS_InvariantCulture_Default.png" width="150" /></a><br/>{{windowsTargets}} |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | {{linuxTargets}}<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | Ubuntu | <a href="https://snapcraft.io/mqtt-explorer" title="Get it from the Snap Store"><img src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg" width="150" /><br/>[Ubuntu Store](snap://mqtt-explorer) |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | {{macTargets}} |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Screenshots ## Screenshots
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png) ![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | {{windowsTargets}} |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | {{linuxTargets}}<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | {{macTargets}} |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Demo ## Demo
![screencast](https://user-images.githubusercontent.com/7721625/52979302-3f073b80-33d5-11e9-9953-b70ebb349439.gif) ![screencast](https://user-images.githubusercontent.com/7721625/52979302-3f073b80-33d5-11e9-9953-b70ebb349439.gif)
## Performance ## Performance
This App is optimized to handle thousands of topics and at least hundreds of thousands messages per minute. This App is optimized to handle thousands of topics and at least hundreds of thousands messages per minute.
So don't worry if you got >=10k topics and >=100k messages per minute. So don't worry if you got >=30k topics and >=100.000 messages per minute.
Future releases will allow for a more granular selection of subscribed topics to handle One can also subscribe to custom topics insted of the "root wildcard #", therefore limiting the amount of messages.
## Develop ## Develop
PRs and issues are welcome PRs and issues are welcome
+9 -4
View File
@@ -9,10 +9,13 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@material-ui/core": "^4.0.0-alpha.0", "@material-ui/core": "^4.0.0-alpha.1",
"@material-ui/icons": "^3.0.1", "@material-ui/icons": "^3.0.2",
"@material-ui/styles": "^3.0.0-alpha.8", "@material-ui/lab": "^3.0.0-alpha.30",
"@material-ui/styles": "^3.0.0-alpha.10",
"@types/diff": "^4.0.1",
"@types/node": "^10.12.18", "@types/node": "^10.12.18",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.7.18", "@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11", "@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.12", "@types/react-redux": "^6.0.12",
@@ -26,6 +29,7 @@
"compare-versions": "^3.4.0", "compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^1.0.4", "copy-text-to-clipboard": "^1.0.4",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"diff": "^4.0.1",
"electron-nucleus": "^1.11.0", "electron-nucleus": "^1.11.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist", "electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"html-webpack-plugin": "^4.0.0-beta.5", "html-webpack-plugin": "^4.0.0-beta.5",
@@ -34,16 +38,17 @@
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"moving-average": "^1.0.0", "moving-average": "^1.0.0",
"number-abbreviate": "^2.0.0", "number-abbreviate": "^2.0.0",
"prismjs": "^1.15.0",
"react": "16.8", "react": "16.8",
"react-ace": "^6.3.2", "react-ace": "^6.3.2",
"react-dom": "^16.7.0", "react-dom": "^16.7.0",
"react-json-view": "^1.19.1",
"react-redux": "^6.0.0", "react-redux": "^6.0.0",
"react-resize-detector": "^3.4.0", "react-resize-detector": "^3.4.0",
"react-split-pane": "^0.1.85", "react-split-pane": "^0.1.85",
"react-vis": "^1.11.6", "react-vis": "^1.11.6",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-batched-actions": "^0.4.1", "redux-batched-actions": "^0.4.1",
"redux-thunk": "^2.3.0",
"sha1": "^1.1.1", "sha1": "^1.1.1",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
+1 -2
View File
@@ -17,7 +17,6 @@ import { Theme, withStyles } from '@material-ui/core/styles'
const Settings = React.lazy(() => import('./components/Settings')) const Settings = React.lazy(() => import('./components/Settings'))
interface Props { interface Props {
name: string
connectionId: string connectionId: string
classes: any classes: any
settingsVisible: boolean settingsVisible: boolean
@@ -89,7 +88,7 @@ class App extends React.PureComponent<Props, {}> {
<UpdateNotifier /> <UpdateNotifier />
<ConnectionSetup /> <ConnectionSetup />
</ErrorBoundary> </ErrorBoundary>
</div > </div>
) )
} }
} }
+20 -11
View File
@@ -1,18 +1,19 @@
import * as q from '../../../backend/src/Model' import * as q from '../../../backend/src/Model'
import * as url from 'url' import * as url from 'url'
import { Action, ActionTypes } from '../reducers/Connection' import { Action, ActionTypes } from '../reducers/Connection'
import { AppState } from '../reducers'
import { DataSourceState, MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { globalActions } from '.'
import { showError } from './Global'
import { showTree } from './Tree'
import { TopicViewModel } from '../TopicViewModel'
import { import {
addMqttConnectionEvent, addMqttConnectionEvent,
makeConnectionStateEvent, makeConnectionStateEvent,
removeConnection, removeConnection,
rendererEvents, rendererEvents,
} from '../../../events' } from '../../../events'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { MqttOptions, DataSourceState } from '../../../backend/src/DataSource'
import { showTree } from './Tree'
import { TopicViewModel } from '../TopicViewModel'
import { showError } from './Global'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => { export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId)) dispatch(connecting(connectionId))
@@ -23,10 +24,13 @@ export const connect = (options: MqttOptions, connectionId: string) => (dispatch
rendererEvents.subscribe(event, (dataSourceState) => { rendererEvents.subscribe(event, (dataSourceState) => {
console.log(dataSourceState) console.log(dataSourceState)
if (dataSourceState.connected) { if (dataSourceState.connected) {
const tree = new q.Tree<TopicViewModel>() const didReconnect = Boolean(getState().connection.tree)
tree.updateWithConnection(rendererEvents, connectionId) if (!didReconnect) {
dispatch(connected(tree, host!)) const tree = new q.Tree<TopicViewModel>()
dispatch(showTree(tree)) tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
}
} else if (dataSourceState.error) { } else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error)) dispatch(showError(dataSourceState.error))
dispatch(disconnect()) dispatch(disconnect())
@@ -41,6 +45,7 @@ const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<a
state = 'connecting' state = 'connecting'
} else if (!dataSourceState.connected) { } else if (!dataSourceState.connected) {
state = 'offline' state = 'offline'
dispatch(globalActions.showError('Disconnected from server'))
} else if (dataSourceState.connected) { } else if (dataSourceState.connected) {
state = 'online' state = 'online'
} else { } else {
@@ -68,7 +73,11 @@ export const connecting: (connectionId: string) => Action = (connectionId: strin
export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => { export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId, tree } = getState().connection const { connectionId, tree } = getState().connection
rendererEvents.emit(removeConnection, connectionId) if (connectionId) {
rendererEvents.emit(removeConnection, connectionId)
rendererEvents.unsubscribeAll(makeConnectionStateEvent(connectionId))
}
tree && tree.stopUpdating() tree && tree.stopUpdating()
dispatch(showTree(undefined)) dispatch(showTree(undefined))
+2 -2
View File
@@ -1,7 +1,7 @@
import { ActionTypes, Action } from '../reducers/Publish' import { Action, ActionTypes } from '../reducers/Publish'
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { rendererEvents, makePublishEvent } from '../../../events' import { makePublishEvent, rendererEvents } from '../../../events'
export const setTopic = (topic?: string): Action => { export const setTopic = (topic?: string): Action => {
return { return {
+16
View File
@@ -51,6 +51,22 @@ export const setAutoExpandLimit = (autoExpandLimit: number = 0) => (dispatch: Di
dispatch(storeSettings()) dispatch(storeSettings())
} }
export const selectTopicWithMouseOver = (selectTopicWithMouseOver: boolean) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
selectTopicWithMouseOver,
type: ActionTypes.SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER,
})
dispatch(storeSettings())
}
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
})
dispatch(storeSettings())
}
export const toggleSettingsVisibility = () => (dispatch: Dispatch<any>, _getState: () => AppState) => { export const toggleSettingsVisibility = () => (dispatch: Dispatch<any>, _getState: () => AppState) => {
dispatch({ dispatch({
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY, type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY,
+2 -2
View File
@@ -1,7 +1,7 @@
import { Dispatch, Action } from 'redux' import * as q from '../../../backend/src/Model'
import { Action, Dispatch } from 'redux'
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events' import { makePublishEvent, rendererEvents } from '../../../events'
import * as q from '../../../backend/src/Model'
export const clearRetainedTopic = () => (dispatch: Dispatch<any>, getState: () => AppState) => { export const clearRetainedTopic = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { selectedTopic } = getState().tree const { selectedTopic } = getState().tree
+4 -4
View File
@@ -1,10 +1,10 @@
import { AppState } from '../reducers'
import { ActionTypes } from '../reducers/Tree'
import * as q from '../../../backend/src/Model' import * as q from '../../../backend/src/Model'
import { Dispatch, AnyAction } from 'redux' import { ActionTypes } from '../reducers/Tree'
import { AnyAction, Dispatch } from 'redux'
import { AppState } from '../reducers'
import { batchActions } from 'redux-batched-actions'
import { setTopic } from './Publish' import { setTopic } from './Publish'
import { TopicViewModel } from '../TopicViewModel' import { TopicViewModel } from '../TopicViewModel'
import { batchActions } from 'redux-batched-actions'
const debounce = require('lodash.debounce') const debounce = require('lodash.debounce')
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => { export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
+5 -5
View File
@@ -1,10 +1,10 @@
import * as settingsActions from './Settings'
import * as publishActions from './Publish'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection' import * as connectionActions from './Connection'
import * as sidebarActons from './Sidebar'
import * as connectionManagerActions from './ConnectionManager' import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global' import * as globalActions from './Global'
import * as publishActions from './Publish'
import * as settingsActions from './Settings'
import * as sidebarActons from './Sidebar'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons, connectionManagerActions, globalActions } export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons, connectionManagerActions, globalActions }
+4 -5
View File
@@ -1,12 +1,11 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model' import * as q from '../../../backend/src/Model'
import * as React from 'react'
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../TopicViewModel' import { TopicViewModel } from '../TopicViewModel'
import { Typography } from '@material-ui/core'
const abbreviate = require('number-abbreviate') const abbreviate = require('number-abbreviate')
interface Stats { interface Stats {
+149
View File
@@ -0,0 +1,149 @@
import * as diff from 'diff'
import * as Prism from 'prismjs'
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json'
import 'prismjs/themes/prism-tomorrow.css'
interface Props {
previous: string
current: string
nameOfCompareMessage: string
language?: 'json'
classes: any
}
class CodeDiff extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
public render() {
const changes = diff.diffLines(this.props.previous, this.props.current)
const styledLines = Prism.highlight(this.props.current, Prism.languages.json).split('\n')
let lineNumber = 0
const code = changes.map((change, key) => {
const hasStyledCode = change.removed !== true
const changedLines = change.count || 0
if (hasStyledCode && this.props.language === 'json') {
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
const lines = currentLines.map((l, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)} dangerouslySetInnerHTML={{ __html: l }} /></div>
})
lineNumber += changedLines
return <div key={key}>{lines}</div>
}
return this.trimNewlineRight(change.value)
.split('\n')
.map((line, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)}>{line}</span></div>
})
})
return (
<div>
<pre className={`language-json ${this.props.classes.codeBlock}`}>
{code}
</pre>
{this.renderChangeAmount(changes)}
</div>
)
}
private renderChangeAmount(changes: Diff.Change[]) {
const additions = changes.map(change => (change.added === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
const deletions = changes.map(change => (change.removed === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
if (additions === 0 && deletions === 0) {
return null
}
return (
<span style={{ display: 'block', marginBottom: '8px', float: 'right' }}>
<span>
Comparing with <b>{this.props.nameOfCompareMessage}</b> message:&nbsp;
<span className={this.props.classes.additions}>+ {additions} line{additions === 1 ? '' : 's'}</span>
, <span className={this.props.classes.deletions}>- {deletions} line{deletions === 1 ? '' : 's'}</span>
</span>
</span>
)
}
private trimNewlineRight(str: string) {
if (str.slice(-1) === '\n') {
return str.slice(0, -1)
}
return str
}
private cssClassForChange(change: diff.Change) {
if (change.added === true) {
return this.props.classes.addition
}
if (change.removed === true) {
return this.props.classes.deletion
}
return this.props.classes.noChange
}
}
const style = (theme: Theme) => {
const baseStyle = {
width: '100%',
}
const before = {
margin: '0 2px 0 -9px',
}
return {
additions: {
color: 'rgb(10, 255, 10)',
},
deletions: {
color: 'rgb(255, 10, 10)',
},
line: {
lineHeight: 'normal',
},
codeBlock: {
fontSize: '12px',
maxHeight: '200px',
marginLeft: '-16px',
},
noChange: {
...baseStyle,
'&:before': {
...before,
},
},
deletion: {
...baseStyle,
backgroundColor: 'rgba(255, 10, 10, 0.3)',
'&:before': {
...before,
content: "'-'",
},
'&:hover': {
backgroundColor: 'rgba(255, 10, 10, 0.6)',
},
},
addition: {
...baseStyle,
backgroundColor: 'rgba(10, 255, 10, 0.3)',
'&:hover': {
backgroundColor: 'rgba(10, 255, 10, 0.5)',
},
'&:before': {
...before,
content: "'+'",
},
},
}
}
export default withStyles(style)(CodeDiff)
@@ -66,10 +66,10 @@ class ConnectionSettings extends React.Component<Props, State> {
</Grid> </Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}> <Grid item={true} xs={12} style={{ padding: 0 }}>
<List <List
className={classes.topicList} className={`${classes.topicList} advanced-connection-settings-topic-list`}
component="nav" component="nav"
> >
<div className={this.props.classes.list}> <div className={classes.list}>
{this.renderSubscriptions()} {this.renderSubscriptions()}
</div> </div>
</List> </List>
@@ -79,6 +79,7 @@ class ConnectionSettings extends React.Component<Props, State> {
className={classes.fullWidth} className={classes.fullWidth}
label="MQTT Client ID" label="MQTT Client ID"
margin="normal" margin="normal"
value={this.props.connection.clientId}
onChange={this.handleChange('clientId')} onChange={this.handleChange('clientId')}
/> />
</Grid> </Grid>
@@ -71,16 +71,17 @@ class ConnectionSetup extends React.Component<Props, {}> {
} }
} }
const connectionHeight = '440px'
const styles = (theme: Theme) => ({ const styles = (theme: Theme) => ({
title: { title: {
color: theme.palette.text.primary, color: theme.palette.text.primary,
whiteSpace: 'nowrap' as 'nowrap', whiteSpace: 'nowrap' as 'nowrap',
}, },
root: { root: {
margin: '13vw auto 0 auto', margin: `calc((100vh - ${connectionHeight}) / 2) auto 0 auto`,
minWidth: '800px', minWidth: '800px',
maxWidth: '850px', maxWidth: '850px',
height: '440px', height: connectionHeight,
outline: 'none' as 'none', outline: 'none' as 'none',
display: 'flex' as 'flex', display: 'flex' as 'flex',
}, },
+4 -4
View File
@@ -1,10 +1,10 @@
import * as React from 'react' import * as React from 'react'
import { Snackbar, SnackbarContent, Tooltip } from '@material-ui/core'
import FileCopy from '@material-ui/icons/FileCopy'
import Check from '@material-ui/icons/Check' import Check from '@material-ui/icons/Check'
import CustomIconButton from './CustomIconButton'
import FileCopy from '@material-ui/icons/FileCopy'
import green from '@material-ui/core/colors/green' import green from '@material-ui/core/colors/green'
import { withStyles, Theme } from '@material-ui/core/styles' import { Snackbar, SnackbarContent, Tooltip } from '@material-ui/core'
import CustomIconButton from './CustomIconButton'; import { Theme, withStyles } from '@material-ui/core/styles'
const copy = require('copy-text-to-clipboard') const copy = require('copy-text-to-clipboard')
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { IconButton } from '@material-ui/core' import { IconButton } from '@material-ui/core'
import { withStyles, Theme } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
interface Props { interface Props {
onClick: any, onClick: any,
+59 -22
View File
@@ -1,6 +1,14 @@
import * as React from 'react' import * as React from 'react'
import BrokerStatistics from './BrokerStatistics'
import ChevronRight from '@material-ui/icons/ChevronRight'
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions } from '../actions'
import { shell } from 'electron'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { TopicOrder } from '../reducers/Settings'
import { import {
Divider, Divider,
Drawer, Drawer,
@@ -13,15 +21,7 @@ import {
Switch, Switch,
Tooltip, Tooltip,
} from '@material-ui/core' } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles' const sha1 = require('sha1')
import ChevronRight from '@material-ui/icons/ChevronRight'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions } from '../actions'
import { TopicOrder } from '../reducers/Settings'
import BrokerStatistics from './BrokerStatistics'
import { shell } from 'electron';
export const autoExpandLimitSet = [{ export const autoExpandLimitSet = [{
limit: 0, limit: 0,
@@ -44,6 +44,7 @@ const styles: StyleRulesCallback = theme => ({
drawer: { drawer: {
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
flexShrink: 0, flexShrink: 0,
userSelect: 'none' as 'none',
}, },
paper: { paper: {
width: '300px', width: '300px',
@@ -62,16 +63,20 @@ const styles: StyleRulesCallback = theme => ({
color: theme.palette.text.hint, color: theme.palette.text.hint,
cursor: 'pointer' as 'pointer', cursor: 'pointer' as 'pointer',
}, },
switchBase: {
height: theme.spacing(4),
},
}) })
interface Props { interface Props {
actions: typeof settingsActions
autoExpandLimit: number autoExpandLimit: number
classes: any
highlightTopicUpdates: boolean highlightTopicUpdates: boolean
visible: boolean selectTopicWithMouseOver: boolean
store?: any store?: any
topicOrder: TopicOrder topicOrder: TopicOrder
classes: any visible: boolean
actions: typeof settingsActions
} }
class Settings extends React.Component<Props, {}> { class Settings extends React.Component<Props, {}> {
@@ -105,7 +110,8 @@ class Settings extends React.Component<Props, {}> {
{this.renderAutoExpand()} {this.renderAutoExpand()}
{this.renderNodeOrder()} {this.renderNodeOrder()}
{this.renderhighlightTopicUpdates()} {this.renderHighlightTopicUpdates()}
{this.selectTopicsOnMouseOver()}
</div> </div>
<Tooltip placement="top" title="App Author"> <Tooltip placement="top" title="App Author">
<Typography className={classes.author} onClick={this.openGithubPage}> <Typography className={classes.author} onClick={this.openGithubPage}>
@@ -121,22 +127,52 @@ class Settings extends React.Component<Props, {}> {
shell.openExternal('https://github.com/thomasnordquist') shell.openExternal('https://github.com/thomasnordquist')
} }
private renderhighlightTopicUpdates() { private renderHighlightTopicUpdates() {
const { highlightTopicUpdates, actions } = this.props const { highlightTopicUpdates, actions } = this.props
return this.renderSwitch('Show Activity', highlightTopicUpdates, actions.togglehighlightTopicUpdates, 'Topics blink when a new message arrives')
}
private renderSwitch(title: string, checked: boolean, action: any, tooltip: string) {
const { classes } = this.props
const clickHandler = (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
action()
}
return ( return (
<div style={{ padding: '8px', display: 'flex' }}> <div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="toggle-highlight-activity" style={{ flex: '1', marginTop: '8px' }}>Show Activity</InputLabel> <Tooltip title={tooltip}>
<Switch <InputLabel
name="toggle-highlight-activity" htmlFor={`toggle-${sha1(title)}`}
checked={highlightTopicUpdates} onClick={clickHandler}
onChange={actions.togglehighlightTopicUpdates} style={{ flex: '1', paddingTop: '8px' }}
color="primary" >
/> {title}
</InputLabel>
</Tooltip>
<Tooltip title={tooltip}>
<Switch
name={`toggle-${sha1(title)}`}
checked={checked}
onChange={action}
color="primary"
classes={{ switchBase: classes.switchBase }}
/>
</Tooltip>
</div> </div>
) )
} }
private selectTopicsOnMouseOver() {
const { actions, selectTopicWithMouseOver } = this.props
const toggle = () => actions.selectTopicWithMouseOver(!selectTopicWithMouseOver)
return this.renderSwitch('Quick Preview', selectTopicWithMouseOver, toggle, 'Select topics on mouse over')
}
private renderAutoExpand() { private renderAutoExpand() {
const { classes, autoExpandLimit } = this.props const { classes, autoExpandLimit } = this.props
@@ -196,6 +232,7 @@ const mapStateToProps = (state: AppState) => {
topicOrder: state.settings.topicOrder, topicOrder: state.settings.topicOrder,
visible: state.settings.visible, visible: state.settings.visible,
highlightTopicUpdates: state.settings.highlightTopicUpdates, highlightTopicUpdates: state.settings.highlightTopicUpdates,
selectTopicWithMouseOver: state.settings.selectTopicWithMouseOver,
} }
} }
+5 -4
View File
@@ -6,6 +6,7 @@ import { Theme, withStyles } from '@material-ui/core/styles'
interface HistoryItem { interface HistoryItem {
title: JSX.Element | string title: JSX.Element | string
value: string | any value: string | any
selected?: boolean
} }
interface Props { interface Props {
@@ -28,18 +29,18 @@ class MessageHistory extends React.Component<Props, State> {
} }
public renderHistory() { public renderHistory() {
const style = { const style = (element: HistoryItem) => ({
backgroundColor: 'rgba(80, 80, 80, 0.6)', backgroundColor: element.selected ? 'rgba(120, 120, 120, 0.6)' : 'rgba(80, 80, 80, 0.6)',
margin: '8px', margin: '8px',
padding: '8px 8px 0 8px', padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit', cursor: this.props.onClick ? 'pointer' : 'inherit',
} })
const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' } const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }
const elements = this.props.items.map((element, index) => ( const elements = this.props.items.map((element, index) => (
<div <div
key={index} key={index}
style={style} style={style(element)}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)} onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
> >
<div><i>{element.title}</i></div> <div><i>{element.title}</i></div>
@@ -1,16 +1,17 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import BarChart from '@material-ui/icons/BarChart' import BarChart from '@material-ui/icons/BarChart'
import DateFormatter from '../helper/DateFormatter' import DateFormatter from '../helper/DateFormatter'
import History from './History' import History from './History'
import { TopicViewModel } from '../../TopicViewModel' import { TopicViewModel } from '../../TopicViewModel'
const PlotHistory = React.lazy(() => import('./PlotHistory')) const PlotHistory = React.lazy(() => import('./PlotHistory'))
const throttle = require('lodash.throttle') const throttle = require('lodash.throttle')
interface Props { interface Props {
node?: q.TreeNode<TopicViewModel> node?: q.TreeNode<TopicViewModel>
selected?: q.Message
onSelect: (message: q.Message) => void onSelect: (message: q.Message) => void
} }
@@ -52,6 +53,7 @@ class MessageHistory extends React.Component<Props, State> {
const historyElements = history.reverse().map(message => ({ const historyElements = history.reverse().map(message => ({
title: <DateFormatter date={message.received} />, title: <DateFormatter date={message.received} />,
value: message.value, value: message.value,
selected: message && message === this.props.selected,
})) }))
const numericMessages = history.filter(message => !isNaN(parseFloat(message.value))) const numericMessages = history.filter(message => !isNaN(parseFloat(message.value)))
@@ -79,8 +81,10 @@ class MessageHistory extends React.Component<Props, State> {
} }
private displayMessage = (index: number, eventTarget: EventTarget) => { private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray()[index] const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
this.props.onSelect(message) if (message) {
this.props.onSelect(message)
}
} }
} }
+2 -3
View File
@@ -1,8 +1,7 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { Typography } from '@material-ui/core'
import { TopicViewModel } from '../../TopicViewModel' import { TopicViewModel } from '../../TopicViewModel'
import { Typography } from '@material-ui/core'
interface Props { interface Props {
node: q.TreeNode<TopicViewModel> node: q.TreeNode<TopicViewModel>
+4 -5
View File
@@ -1,10 +1,9 @@
const { XYPlot, XAxis, LineMarkSeries, Hint, YAxis, HorizontalGridLines, LineSeries } = require('react-vis')
import { default as ReactResizeDetector } from 'react-resize-detector'
import DateFormatter from '../helper/DateFormatter'
import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import DateFormatter from '../helper/DateFormatter'
import { default as ReactResizeDetector } from 'react-resize-detector'
import 'react-vis/dist/style.css' import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = require('react-vis')
interface Props { interface Props {
messages: q.Message[] messages: q.Message[]
+31 -2
View File
@@ -19,6 +19,7 @@ import {
Checkbox, Checkbox,
MenuItem, MenuItem,
Tooltip, Tooltip,
Fab,
} from '@material-ui/core' } from '@material-ui/core'
// tslint:disable-next-line // tslint:disable-next-line
@@ -27,10 +28,10 @@ import { AppState } from '../../../reducers'
import History from '../History' import History from '../History'
import Message from './Model/Message' import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation' import Navigation from '@material-ui/icons/Navigation'
import Clear from '@material-ui/icons/Clear' import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { publishActions } from '../../../actions' import { publishActions, globalActions } from '../../../actions'
import ClearAdornment from '../../helper/ClearAdornment' import ClearAdornment from '../../helper/ClearAdornment'
import { TopicViewModel } from '../../../TopicViewModel' import { TopicViewModel } from '../../../TopicViewModel'
@@ -40,6 +41,7 @@ interface Props {
topic?: string topic?: string
payload?: string payload?: string
actions: typeof publishActions actions: typeof publishActions
globalActions: typeof globalActions
retain: boolean retain: boolean
editorMode: string editorMode: string
qos: 0 | 1 | 2 qos: 0 | 1 | 2
@@ -151,11 +153,37 @@ class Publish extends React.Component<Props, State> {
) )
} }
private formatJson = () => {
if (this.props.payload) {
try {
const str = JSON.stringify(JSON.parse(this.props.payload), undefined, ' ')
this.updatePayload(str)
} catch (error) {
this.props.globalActions.showError(`Format error: ${error.message}`)
}
}
}
private renderFormatJson() {
if (this.props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab style={{ width: '36px', height: '36px', marginLeft: '8px' }} onClick={this.formatJson} id="sidebar-publish-format-json">
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
}
private editorMode() { private editorMode() {
return ( return (
<div style={{ marginTop: '16px' }}> <div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}> <div style={{ width: '100%', lineHeight: '64px' }}>
{this.renderEditorModeSelection()} {this.renderEditorModeSelection()}
{this.renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}> <div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()} {this.publishButton()}
</div> </div>
@@ -299,6 +327,7 @@ class Publish extends React.Component<Props, State> {
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
return { return {
actions: bindActionCreators(publishActions, dispatch), actions: bindActionCreators(publishActions, dispatch),
globalActions: bindActionCreators(globalActions, dispatch),
} }
} }
+62 -38
View File
@@ -1,31 +1,31 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import * as React from 'react' import * as React from 'react'
import Clear from '@material-ui/icons/Clear' import Clear from '@material-ui/icons/Clear'
import Code from '@material-ui/icons/Code'
import Copy from '../Copy' import Copy from '../Copy'
import CustomIconButton from '../CustomIconButton' import CustomIconButton from '../CustomIconButton'
import DateFormatter from '../helper/DateFormatter' import DateFormatter from '../helper/DateFormatter'
import Delete from '@material-ui/icons/Delete' import Delete from '@material-ui/icons/Delete'
import DeleteForever from '@material-ui/icons/DeleteForever'
import ExpandMore from '@material-ui/icons/ExpandMore' import ExpandMore from '@material-ui/icons/ExpandMore'
import MessageHistory from './MessageHistory' import MessageHistory from './MessageHistory'
import NodeStats from './NodeStats' import NodeStats from './NodeStats'
import Reorder from '@material-ui/icons/Reorder'
import ToggleButton from '@material-ui/lab/ToggleButton'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import Topic from './Topic' import Topic from './Topic'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { default as ReactResizeDetector } from 'react-resize-detector' import { settingsActions, sidebarActons } from '../../actions'
import { sidebarActons } from '../../actions'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles' import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../TopicViewModel' import { TopicViewModel } from '../../TopicViewModel'
import { ValueRendererDisplayMode } from '../../reducers/Settings'
import { import {
Button, Button,
ExpansionPanel, ExpansionPanel,
ExpansionPanelDetails, ExpansionPanelDetails,
ExpansionPanelSummary, ExpansionPanelSummary,
Fade,
Paper,
Popper,
Typography, Typography,
Tooltip, Tooltip,
Badge, Badge,
@@ -37,10 +37,12 @@ const Publish = React.lazy(() => import('./Publish/Publish'))
const ValueRenderer = React.lazy(() => import('./ValueRenderer')) const ValueRenderer = React.lazy(() => import('./ValueRenderer'))
interface Props { interface Props {
node?: q.TreeNode<TopicViewModel>, node?: q.TreeNode<TopicViewModel>
actions: typeof sidebarActons, actions: typeof sidebarActons
classes: any, settingsActions: typeof settingsActions
connectionId?: string, valueRendererDisplayMode: ValueRendererDisplayMode
classes: any
connectionId?: string
} }
interface State { interface State {
@@ -87,7 +89,7 @@ class Sidebar extends React.Component<Props, State> {
public render() { public render() {
return ( return (
<div className={this.props.classes.drawer}> <div id="Sidebar" className={this.props.classes.drawer}>
{this.renderNode()} {this.renderNode()}
</div> </div>
) )
@@ -96,7 +98,7 @@ class Sidebar extends React.Component<Props, State> {
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' } private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
private renderTopicDeleteButton() { private renderTopicDeleteButton() {
if (!this.props.node) { if (!this.props.node || (!this.props.node.message || !this.props.node.message.value)) {
return null return null
} }
@@ -112,7 +114,7 @@ class Sidebar extends React.Component<Props, State> {
private renderRecursiveTopicDeleteButton() { private renderRecursiveTopicDeleteButton() {
const deleteLimit = 50 const deleteLimit = 50
const topicCount = this.props.node ? this.props.node.childTopicCount() : 0 const topicCount = this.props.node ? this.props.node.childTopicCount() : 0
if (!this.props.node || topicCount === 0) { if ((!this.props.node || topicCount === 0) || (this.props.node.message && topicCount === 1)) {
return null return null
} }
@@ -124,7 +126,7 @@ class Sidebar extends React.Component<Props, State> {
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>} badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
color="secondary" color="secondary"
> >
<DeleteForever color="action" /> <Delete color="action" />
</Badge> </Badge>
</Tooltip> </Tooltip>
</CustomIconButton> </CustomIconButton>
@@ -139,6 +141,27 @@ class Sidebar extends React.Component<Props, State> {
this.props.actions.clearTopic(topic, recursive, maxCount) this.props.actions.clearTopic(topic, recursive, maxCount)
} }
private renderActionButtons() {
const handleValue = (_e: React.MouseEvent, value: any) => {
this.props.settingsActions.setValueDisplayMode(value)
}
return (
<ToggleButtonGroup id="valueRendererDisplayMode" value={this.props.valueRendererDisplayMode} exclusive={true} onChange={handleValue}>
<ToggleButton value="diff" id="valueRendererDisplayMode-diff">
<Tooltip title="Show difference between the current and the last message">
<Code />
</Tooltip>
</ToggleButton>
<ToggleButton value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw value">
<Reorder />
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
)
}
private renderNode() { private renderNode() {
const { classes, node } = this.props const { classes, node } = this.props
@@ -146,11 +169,11 @@ class Sidebar extends React.Component<Props, State> {
const deleteTopic = this.renderTopicDeleteButton() const deleteTopic = this.renderTopicDeleteButton()
const deleteRecursiveTopic = this.renderRecursiveTopicDeleteButton() const deleteRecursiveTopic = this.renderRecursiveTopicDeleteButton()
const copyValue = node && node.message ? <Copy value={node.message.value} /> : null const copyValue = node && node.message ? <Copy value={node.message.value} /> : null
const summeryStyle = { minHeight: '0' } const summaryStyle = { minHeight: '0' }
return ( return (
<div> <div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}> <ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}</Typography> <Typography className={classes.heading}>Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}> <ExpansionPanelDetails style={this.detailsStyle}>
@@ -158,25 +181,21 @@ class Sidebar extends React.Component<Props, State> {
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}> <ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography> <Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}> <ExpansionPanelDetails style={this.detailsStyle}>
{this.messageMetaInfo()} {this.messageMetaInfo()}
<div ref={this.valueRef}> <div ref={this.valueRef}>
<React.Suspense fallback={<div>Loading...</div>}> <React.Suspense fallback={<div>Loading...</div>}>
<ReactResizeDetector handleWidth={true} onResize={this.valueRenderWidthChange} /> {this.renderValue()}
<ValueRenderer message={this.props.node && this.props.node.message} /> </React.Suspense>
</React.Suspense>
</div> </div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} node={this.props.node} /></div> <div><MessageHistory onSelect={this.handleMessageHistorySelect} selected={this.state.compareMessage} node={this.props.node} /></div>
<Popper open={Boolean(this.state.compareMessage)} anchorEl={this.valueRef.current} placement="left" transition={true}>
{this.showValueComparison}
</Popper>
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel defaultExpanded={true}> <ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Publish</Typography> <Typography className={classes.heading}>Publish</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}> <ExpansionPanelDetails style={this.detailsStyle}>
@@ -186,7 +205,7 @@ class Sidebar extends React.Component<Props, State> {
</ExpansionPanelDetails> </ExpansionPanelDetails>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}> <ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}> <ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Stats</Typography> <Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
{this.renderNodeStats()} {this.renderNodeStats()}
@@ -195,17 +214,19 @@ class Sidebar extends React.Component<Props, State> {
) )
} }
private valueRenderWidthChange = (width: number) => { private renderValue() {
this.setState({ valueRenderWidth: width }) const node = this.props.node
} if (!node || !node.message) {
return null
}
private showValueComparison = (a: any) => ( return (
<Fade {...a.TransitionProps} timeout={350}> <ValueRenderer
<Paper style={{ maxWidth: this.state.valueRenderWidth }}> message={node.message}
<ValueRenderer message={this.state.compareMessage} /> messageHistory={node.messageHistory}
</Paper> compareWith={this.state.compareMessage} />
</Fade> )
) }
private messageMetaInfo() { private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) { if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
@@ -227,8 +248,9 @@ class Sidebar extends React.Component<Props, State> {
) )
return ( return (
<div style={{ width: '100%', display: 'flex' }}> <div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}>
<div style={{ flex: 6 }}><Typography>QoS: {this.props.node.mqttMessage.qos}</Typography></div> <div style={{ flex: 6 }}><Typography>QoS: {this.props.node.mqttMessage.qos}</Typography></div>
<span style={{ marginTop: '-8px' }}>{this.renderActionButtons()}</span>
<div style={{ flex: 8, textAlign: 'center' }}> <div style={{ flex: 8, textAlign: 'center' }}>
{this.props.node.mqttMessage.retain ? retainedButton : null} {this.props.node.mqttMessage.retain ? retainedButton : null}
</div> </div>
@@ -261,12 +283,14 @@ class Sidebar extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
node: state.tree.selectedTopic, node: state.tree.selectedTopic,
valueRendererDisplayMode: state.settings.valueRendererDisplayMode,
} }
} }
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
return { return {
actions: bindActionCreators(sidebarActons, dispatch), actions: bindActionCreators(sidebarActons, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
} }
} }
+79 -37
View File
@@ -1,66 +1,108 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { Theme, withTheme } from '@material-ui/core/styles' import CodeDiff from '../CodeDiff'
import { AppState } from '../../reducers'
import { default as ReactJson } from 'react-json-view' import { connect } from 'react-redux'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { ValueRendererDisplayMode } from '../../reducers/Settings'
interface Props { interface Props {
message?: q.Message message: q.Message
theme: Theme messageHistory: q.RingBuffer<q.Message>
compareWith?: q.Message
renderMode: ValueRendererDisplayMode
} }
class ValueRenderer extends React.Component<Props, {}> { interface State {
width: number
}
class ValueRenderer extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { width: 0 }
}
public render() { public render() {
return <div style={{ padding: '8px 0px 8px 8px' }}>{this.renderValue()}</div> return (
<div style={{ padding: '8px 0px 8px 8px' }}>
<ReactResizeDetector handleWidth={true} onResize={this.updateWidth} />
{this.renderValue()}
</div>
)
} }
public renderValue() { public renderValue() {
const { message } = this.props const { message, messageHistory, compareWith, renderMode } = this.props
if (!message) {
return null const previousMessages = messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {
compareMessage = message
} }
let json let json
try { try {
json = JSON.parse(message.value) json = JSON.parse(message.value)
} catch (error) { } catch (error) {
return this.renderRawValue(message.value) return this.renderRawValue(message.value, compareMessage.value)
} }
if (typeof json === 'string') { if (typeof json === 'string') {
return this.renderRawValue(message.value) return this.renderRawValue(message.value, compareMessage.value)
} else if (typeof json === 'number') { } else if (typeof json === 'number') {
return this.renderRawValue(message.value) return this.renderRawValue(message.value, compareMessage.value)
} else if (typeof json === 'boolean') { } else if (typeof json === 'boolean') {
return this.renderRawValue(message.value) return this.renderRawValue(message.value, compareMessage.value)
} else { } else {
const theme = (this.props.theme.palette.type === 'dark') ? 'monokai' : 'bright:inverted' const current = this.messageToPrettyJson(message) || message.value
return ( const compare = this.messageToPrettyJson(compareMessage) || compareMessage.value
<ReactJson const language = current && compare ? 'json' : undefined
style={{ width: '100%', overflowY: 'scroll', wordBreak: 'break-all' }}
src={json} return this.renderDiff(current, compare, language)
name={null}
theme={theme}
/>
)
} }
} }
private renderRawValue(value: string) { private renderDiff(current: string = '', previous: string = '', language?: 'json') {
const style: React.CSSProperties = { return (
padding: '8px 12px 8px 12px', <CodeDiff
backgroundColor: 'rgba(100, 100, 100, 0.55)', previous={previous}
wordBreak: 'break-all', current={current}
width: '100%', language={language}
overflow: 'auto hidden', nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
display: 'block', />
lineHeight: '1.2em', )
color: this.props.theme.palette.text.primary, }
private messageToPrettyJson(message?: q.Message): string | undefined {
if (!message || !message.value) {
return undefined
} }
return <pre style={style}><code>{value}</code></pre> try {
const json = JSON.parse(message.value)
return JSON.stringify(json, undefined, ' ')
} catch {
return undefined
}
}
private updateWidth = (width: number) => {
if (width !== this.state.width) {
this.setState({ width })
}
}
private renderRawValue(value: string, compare: string) {
return this.renderDiff(value, compare)
} }
} }
export default withTheme()(ValueRenderer) const mapStateToProps = (state: AppState) => {
return {
renderMode: state.settings.valueRendererDisplayMode,
}
}
export default connect(mapStateToProps)(ValueRenderer)
+7 -7
View File
@@ -1,8 +1,15 @@
import * as React from 'react' import * as React from 'react'
import ClearAdornment from './helper/ClearAdornment' import ClearAdornment from './helper/ClearAdornment'
import CloudOff from '@material-ui/icons/CloudOff' import CloudOff from '@material-ui/icons/CloudOff'
import ConnectionHealthIndicator from './ConnectionHealthIndicator'
import Menu from '@material-ui/icons/Menu' import Menu from '@material-ui/icons/Menu'
import Search from '@material-ui/icons/Search' import Search from '@material-ui/icons/Search'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, settingsActions } from '../actions'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { import {
AppBar, AppBar,
Button, Button,
@@ -11,13 +18,6 @@ import {
Toolbar, Toolbar,
Typography, Typography,
} from '@material-ui/core' } from '@material-ui/core'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, settingsActions } from '../actions'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import ConnectionHealthIndicator from './ConnectionHealthIndicator';
const styles: StyleRulesCallback = theme => ({ const styles: StyleRulesCallback = theme => ({
title: { title: {
+6 -4
View File
@@ -1,13 +1,12 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { AppState } from '../../reducers'
import TreeNode from './TreeNode' import TreeNode from './TreeNode'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { TopicOrder } from '../../reducers/Settings' import { TopicOrder } from '../../reducers/Settings'
import { TopicViewModel } from '../../TopicViewModel' import { TopicViewModel } from '../../TopicViewModel'
import { treeActions } from '../../actions' import { treeActions } from '../../actions'
import { bindActionCreators } from 'redux'
const MovingAverage = require('moving-average') const MovingAverage = require('moving-average')
@@ -25,6 +24,7 @@ interface Props {
topicOrder: TopicOrder topicOrder: TopicOrder
autoExpandLimit: number autoExpandLimit: number
highlightTopicUpdates: boolean highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
} }
interface State { interface State {
@@ -112,6 +112,7 @@ class Tree extends React.PureComponent<Props, State> {
lastUpdate={tree.lastUpdate} lastUpdate={tree.lastUpdate}
didSelectTopic={this.props.actions.selectTopic} didSelectTopic={this.props.actions.selectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates} highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/> />
</div> </div>
) )
@@ -130,6 +131,7 @@ const mapStateToProps = (state: AppState) => {
autoExpandLimit: state.settings.autoExpandLimit, autoExpandLimit: state.settings.autoExpandLimit,
topicOrder: state.settings.topicOrder, topicOrder: state.settings.topicOrder,
highlightTopicUpdates: state.settings.highlightTopicUpdates, highlightTopicUpdates: state.settings.highlightTopicUpdates,
selectTopicWithMouseOver: state.settings.selectTopicWithMouseOver,
} }
} }
+40 -23
View File
@@ -1,12 +1,11 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core/styles'
import TreeNodeSubnodes from './TreeNodeSubnodes' import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle' import TreeNodeTitle from './TreeNodeTitle'
import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicOrder } from '../../reducers/Settings' import { TopicOrder } from '../../reducers/Settings'
import { TopicViewModel } from '../../TopicViewModel' import { TopicViewModel } from '../../TopicViewModel'
const debounce = require('lodash.debounce') const debounce = require('lodash.debounce')
declare var performance: any declare var performance: any
@@ -29,6 +28,9 @@ const styles = (theme: Theme) => {
cursor: 'pointer', cursor: 'pointer',
marginTop: '-1px', marginTop: '-1px',
}, },
subnodes: {
marginLeft: theme.spacing(1.5),
},
selected: { selected: {
backgroundColor: 'rgba(170, 170, 170, 0.55)', backgroundColor: 'rgba(170, 170, 170, 0.55)',
}, },
@@ -52,6 +54,7 @@ interface Props {
lastUpdate: number lastUpdate: number
didSelectTopic: any didSelectTopic: any
highlightTopicUpdates: boolean highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
} }
interface State { interface State {
@@ -187,23 +190,28 @@ class TreeNode extends React.Component<Props, State> {
const highlightClass = this.state.selected ? this.props.classes.selected : (this.state.mouseOver ? this.props.classes.hover : '') const highlightClass = this.state.selected ? this.props.classes.selected : (this.state.mouseOver ? this.props.classes.hover : '')
return ( return (
<div <div>
key={this.props.treeNode.hash()} <div
className={`${classes.node} ${this.props.className}`} key={this.props.treeNode.hash()}
onClick={this.didClickNode} className={`${classes.node} ${this.props.className}`}
onMouseOver={this.mouseOver} onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut} onMouseOut={this.mouseOut}
ref={this.nodeRef} onClick={this.didClickTitle}
> ref={this.nodeRef}
<TreeNodeTitle >
style={animation} <TreeNodeTitle
collapsed={this.collapsed()} style={animation}
treeNode={this.props.treeNode} toggleCollapsed={this.toggleCollapsed}
name={this.props.name} didSelectNode={this.didSelectTopic}
didSelectNode={this.didSelectTopic} collapsed={this.collapsed()}
className={highlightClass} treeNode={this.props.treeNode}
/> name={this.props.name}
{this.renderNodes()} className={highlightClass}
/>
</div>
<div className={classes.subnodes}>
{this.renderNodes()}
</div>
</div> </div>
) )
} }
@@ -212,9 +220,18 @@ class TreeNode extends React.Component<Props, State> {
this.props.didSelectTopic(this.props.treeNode) this.props.didSelectTopic(this.props.treeNode)
} }
private didClickTitle = (event: React.MouseEvent) => {
event.preventDefault()
this.props.didSelectTopic(this.props.treeNode)
this.toggle()
}
private mouseOver = (event: React.MouseEvent) => { private mouseOver = (event: React.MouseEvent) => {
event.stopPropagation() event.stopPropagation()
this.setHover(true) this.setHover(true)
if (this.props.selectTopicWithMouseOver && this.props.treeNode && this.props.treeNode.message && this.props.treeNode.message.value) {
this.props.didSelectTopic(this.props.treeNode)
}
} }
private mouseOut = (event: React.MouseEvent) => { private mouseOut = (event: React.MouseEvent) => {
@@ -226,10 +243,9 @@ class TreeNode extends React.Component<Props, State> {
this.setState({ mouseOver: hover }) this.setState({ mouseOver: hover })
}, 45) }, 45)
private didClickNode = (event: React.MouseEvent) => { private toggleCollapsed = (event: React.MouseEvent) => {
event.stopPropagation() event.stopPropagation()
this.toggle() this.toggle()
this.didSelectTopic()
} }
private renderNodes() { private renderNodes() {
@@ -247,6 +263,7 @@ class TreeNode extends React.Component<Props, State> {
lastUpdate={this.props.treeNode.lastUpdate} lastUpdate={this.props.treeNode.lastUpdate}
didSelectTopic={this.props.didSelectTopic} didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates} highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/> />
) )
} }
+4 -2
View File
@@ -18,6 +18,7 @@ export interface Props {
autoExpandLimit: number autoExpandLimit: number
didSelectTopic: any didSelectTopic: any
highlightTopicUpdates: boolean highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
} }
interface State { interface State {
@@ -51,13 +52,13 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
} }
private renderMore() { private renderMore() {
this.renderMoreAnimationFrame = window.requestIdleCallback(() => { this.renderMoreAnimationFrame = (window as any).requestIdleCallback(() => {
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 }) this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
}, { timeout: 500 }) }, { timeout: 500 })
} }
public componentWillUnmount() { public componentWillUnmount() {
window.cancelIdleCallback(this.renderMoreAnimationFrame) (window as any).cancelIdleCallback(this.renderMoreAnimationFrame)
} }
public render() { public render() {
@@ -83,6 +84,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
lastUpdate={node.lastUpdate} lastUpdate={node.lastUpdate}
didSelectTopic={this.props.didSelectTopic} didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates} highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/> />
) )
}) })
+6 -14
View File
@@ -11,29 +11,19 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
collapsed?: boolean | undefined collapsed?: boolean | undefined
classes: any classes: any
didSelectNode: any didSelectNode: any
toggleCollapsed: any
} }
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> { class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
private mouseOver = (event: React.MouseEvent) => {
event.preventDefault()
this.selectTopic()
}
private selectTopic = debounce(() => {
if (this.props.treeNode.message) {
this.props.didSelectNode(this.props.treeNode)
}
}, 5)
public render() { public render() {
const { classes, treeNode, style, className } = this.props const { classes, treeNode, style, className } = this.props
return ( return (
<span <span
className={`${classes.title} ${className}`} className={`${classes.title} ${className}`}
onMouseOver={treeNode.message ? this.mouseOver : undefined}
style={style} style={style}
> >
<span className={classes.expander}>{this.renderExpander()}</span> {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()} <span className={classes.expander} onClick={this.props.toggleCollapsed}>{this.renderExpander()}</span>
{this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span> </span>
) )
} }
@@ -81,6 +71,8 @@ const styles = (theme: Theme) => ({
}, },
expander: { expander: {
color: theme.palette.type === 'light' ? '#222' : '#eee', color: theme.palette.type === 'light' ? '#222' : '#eee',
cursor: 'pointer' as 'pointer',
paddingRight: theme.spacing(0.25),
}, },
title: { title: {
borderRadius: '4px', borderRadius: '4px',
@@ -88,7 +80,7 @@ const styles = (theme: Theme) => ({
display: 'inline-block' as 'inline-block', display: 'inline-block' as 'inline-block',
whiteSpace: 'nowrap' as 'nowrap', whiteSpace: 'nowrap' as 'nowrap',
padding: '1px 4px 0px 4px', padding: '1px 4px 0px 4px',
height: '16px', height: '14px',
margin: '1px 0px 2px 0px', margin: '1px 0px 2px 0px',
}, },
collapsedSubnodes: { collapsedSubnodes: {
+44
View File
@@ -0,0 +1,44 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
interface Props {
keyboardKey: string
classes: any
}
class Key extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = { location: 'bottom' }
}
public render() {
return (
<div className={this.props.classes.keyStyle}>
<div className={this.props.classes.keyTextStyle}>{this.props.keyboardKey}</div>
</div>
)
}
}
const style = (theme: Theme) => ({
keyStyle: {
display: 'inline-block' as 'inline-block',
width: '1em',
height: '1em',
backgroundColor: '#bbb',
borderRadius: '10%',
verticalAlign: 'middle' as 'middle',
textAlign: 'center' as 'center',
textShadow: '1px 1px rgba(255,255,255,0.45)',
boxShadow: '0.08em 0.15em 0.01em 0px rgba(100,100,100,0.75)',
},
keyTextStyle: {
marginTop: '0.65em',
fontSize: '0.4em',
fontWeight: 'bold' as 'bold',
},
})
export default withStyles(style)(Key)
+78
View File
@@ -0,0 +1,78 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
interface State {
enabled: boolean
target: {x: number, y: number}
position: {x: number, y: number}
stepSizeX: number,
stepSizeY: number,
}
class Demo extends React.Component<{}, State> {
private timer: any
private frameInterval = 20
constructor(props: any) {
super(props)
this.state = { enabled: false, target: { x: 0, y: 0 }, position: { x: 0, y: 0 }, stepSizeX: 1, stepSizeY: 1 }
}
public componentDidMount() {
(window as any).demo.enableMouse = () => {
this.setState({ enabled: true })
}
(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
this.moveCloser()
}
}
private moveCloser(steps: number = 0) {
const steSizeX = Math.min(this.state.stepSizeX, Math.abs(this.state.position.x - this.state.target.x))
const steSizeY = Math.min(this.state.stepSizeY, Math.abs(this.state.position.y - this.state.target.y))
const dirX = this.state.position.x > this.state.target.x ? -1 : 1
const dirY = this.state.position.y > this.state.target.y ? -1 : 1
if (steSizeX <= 0.1 && steSizeY <= 0.1) {
this.timer && clearTimeout(this.timer)
return
}
this.setState({
position: {
x: this.state.position.x + (dirX * steSizeX),
y: this.state.position.y + (dirY * steSizeY),
},
})
this.timer = setTimeout(() => {
this.moveCloser(steps + 1)
}, this.frameInterval)
}
public render() {
if (!this.state.enabled) {
return null
}
const style = {
width: '32px',
height: '32px',
position: 'fixed' as 'fixed',
zIndex: 1000000,
filter: 'invert(100%)',
left: this.state.position.x + 2,
top: this.state.position.y + 2,
}
return (
<img src="../cursor.png" style={style} />
)
}
}
const style = (theme: Theme) => {}
export default withStyles(style)(Demo)
+91
View File
@@ -0,0 +1,91 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import Key from './Key';
interface State {
message?: string
keys: string[]
location: string
}
class Demo extends React.Component<{classes: any}, State> {
private timer: any
constructor(props: any) {
super(props)
this.state = { location: 'bottom', keys: [] }
}
private clearTimer() {
this.timer && clearTimeout(this.timer)
}
public componentDidMount() {
(window as any).demo.showMessage = (message: string, location: string, duration: number, keys: string[] = []) => {
this.clearTimer()
this.setState({ message, location, keys })
this.timer = setTimeout(() => this.setState({ message: undefined }), duration)
}
(window as any).demo.hideMessage = () => {
this.clearTimer()
this.setState({ message: undefined })
}
}
public render() {
const positions: {[s: string]: number} = {
top: 0,
bottom: -65,
middle: -32,
}
const style = {
position: 'fixed' as 'fixed',
left: '5vw',
zIndex: 1000000,
margin: '30vw auto 50vw',
right: '5vw',
bottom: `${positions[this.state.location]}vh`,
}
const style2 = {
textAlign: 'center' as 'center',
fontSize: '4em',
color: 'white',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: '16px',
}
if (!this.state.message) {
return null
}
let keys: any[] = []
if (this.state.keys.length > 0) {
keys = this.state.keys.map(key => [<Key key={key} keyboardKey={key} />])
.reduce((prev, current) => {
return [prev, '+' as any, current]
})
}
return (
<div style={style}>
<div style={style2}>
<span>{this.state.message}</span>
{keys.length > 0 ? <div className={this.props.classes.keysStyle}>{keys}</div> : null}
</div>
</div>
)
}
}
const style = (theme: Theme) => ({
keysStyle: {
fontSize: '1em',
display: 'inline-block' as 'inline-block',
transform: 'translateY(0.3em) translateX(0.8em)',
},
})
export default withStyles(style)(Demo)
+14
View File
@@ -0,0 +1,14 @@
import * as React from 'react'
import ShowText from './ShowText'
import Mouse from './Mouse';
(window as any).demo = {}
export default function render(props: any) {
return (
<span>
<ShowText />
<Mouse />
</span>
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react' import * as React from 'react'
import { IconButton } from '@material-ui/core'
import Clear from '@material-ui/icons/Clear' import Clear from '@material-ui/icons/Clear'
import { IconButton } from '@material-ui/core'
interface Props { interface Props {
value?: string value?: string
+12 -11
View File
@@ -1,16 +1,17 @@
import './tracking'
import * as React from 'react' import * as React from 'react'
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom'
import reduxThunk from 'redux-thunk'
import { batchDispatchMiddleware } from 'redux-batched-actions'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers from './reducers'
import App from './App' import App from './App'
import Demo from './components/demo'
import reducers from './reducers'
import reduxThunk from 'redux-thunk'
import { applyMiddleware, compose, createStore } from 'redux'
import { batchDispatchMiddleware } from 'redux-batched-actions'
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux' import './tracking'
const composeEnhancers = /*(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || */ compose const composeEnhancers = /*(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || */ compose
const store = createStore( const store = createStore(
@@ -27,7 +28,6 @@ const theme = createMuiTheme({
palette: { palette: {
type: 'dark', type: 'dark',
}, },
typography: { useNextVariants: true },
}) })
setTimeout(() => { setTimeout(() => {
@@ -41,7 +41,8 @@ setTimeout(() => {
ReactDOM.render( ReactDOM.render(
<MuiThemeProvider theme={theme}> <MuiThemeProvider theme={theme}>
<Provider store={store}> <Provider store={store}>
<App name="" /> <App />
<Demo />
</Provider> </Provider>
</MuiThemeProvider>, </MuiThemeProvider>,
document.getElementById('app'), document.getElementById('app'),
+1 -1
View File
@@ -1,6 +1,6 @@
import * as q from '../../../backend/src/Model'
import { Action } from 'redux' import { Action } from 'redux'
import { createReducer } from './lib' import { createReducer } from './lib'
import * as q from '../../../backend/src/Model'
import { MqttOptions } from '../../../backend/src/DataSource' import { MqttOptions } from '../../../backend/src/DataSource'
import { TopicViewModel } from '../TopicViewModel' import { TopicViewModel } from '../TopicViewModel'
+1 -1
View File
@@ -44,7 +44,7 @@ export interface ToggleRetain {
} }
const initialState: PublishState = { const initialState: PublishState = {
editorMode: 'text', editorMode: 'json',
retain: false, retain: false,
qos: 0, qos: 0,
} }
+37 -1
View File
@@ -8,14 +8,19 @@ export enum TopicOrder {
topics = '#topics', topics = '#topics',
} }
export type ValueRendererDisplayMode = 'diff' | 'raw'
export interface SettingsState { export interface SettingsState {
autoExpandLimit: number autoExpandLimit: number
visible: boolean visible: boolean
topicOrder: TopicOrder topicOrder: TopicOrder
topicFilter?: string topicFilter?: string
highlightTopicUpdates: boolean highlightTopicUpdates: boolean
valueRendererDisplayMode: ValueRendererDisplayMode
selectTopicWithMouseOver: boolean
} }
export type Action = SetAutoExpandLimit | ToggleVisibility | SetTopicOrder | FilterTopics | TogglehighlightTopicUpdates
export type Action = SetAutoExpandLimit | ToggleVisibility | SetTopicOrder | FilterTopics | TogglehighlightTopicUpdates | SetValueRendererDisplayMode
export enum ActionTypes { export enum ActionTypes {
SETTINGS_SET_AUTO_EXPAND_LIMIT = 'SETTINGS_SET_AUTO_EXPAND_LIMIT', SETTINGS_SET_AUTO_EXPAND_LIMIT = 'SETTINGS_SET_AUTO_EXPAND_LIMIT',
@@ -24,6 +29,9 @@ export enum ActionTypes {
SETTINGS_FILTER_TOPICS = 'SETTINGS_FILTER_TOPICS', SETTINGS_FILTER_TOPICS = 'SETTINGS_FILTER_TOPICS',
SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY = 'SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY', SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY = 'SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY',
SETTINGS_DID_LOAD_SETTINGS = 'SETTINGS_DID_LOAD_SETTINGS', SETTINGS_DID_LOAD_SETTINGS = 'SETTINGS_DID_LOAD_SETTINGS',
SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE = 'SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE',
SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER = 'SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER',
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@@ -31,6 +39,8 @@ const initialState: SettingsState = {
topicOrder: TopicOrder.none, topicOrder: TopicOrder.none,
visible: false, visible: false,
highlightTopicUpdates: true, highlightTopicUpdates: true,
valueRendererDisplayMode: 'diff',
selectTopicWithMouseOver: false,
} }
export const settingsReducer = createReducer(initialState, { export const settingsReducer = createReducer(initialState, {
@@ -40,6 +50,8 @@ export const settingsReducer = createReducer(initialState, {
SETTINGS_FILTER_TOPICS: filterTopics, SETTINGS_FILTER_TOPICS: filterTopics,
SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY: togglehighlightTopicUpdates, SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY: togglehighlightTopicUpdates,
SETTINGS_DID_LOAD_SETTINGS: didLoadSettings, SETTINGS_DID_LOAD_SETTINGS: didLoadSettings,
SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE: setValueRendererDisplayMode,
SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER: setSelectTopicWithMouseOver,
}) })
export interface DidLoadSettings { export interface DidLoadSettings {
@@ -54,6 +66,30 @@ function didLoadSettings(state: SettingsState, action: DidLoadSettings) {
} }
} }
export interface SetSelectTopicWithMouseOver {
type: ActionTypes.SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER
selectTopicWithMouseOver: boolean
}
export function setSelectTopicWithMouseOver(state: SettingsState, action: SetSelectTopicWithMouseOver) {
return {
...state,
selectTopicWithMouseOver: action.selectTopicWithMouseOver,
}
}
export interface SetValueRendererDisplayMode {
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE
valueRendererDisplayMode: ValueRendererDisplayMode
}
export function setValueRendererDisplayMode(state: SettingsState, action: SetValueRendererDisplayMode) {
return {
...state,
valueRendererDisplayMode: action.valueRendererDisplayMode,
}
}
export interface SetAutoExpandLimit { export interface SetAutoExpandLimit {
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT
autoExpandLimit: number autoExpandLimit: number
+7 -7
View File
@@ -1,11 +1,11 @@
import { Action, Reducer, combineReducers } from 'redux' import { Action, combineReducers, Reducer } from 'redux'
import { connectionManagerReducer, ConnectionManagerState } from './ConnectionManager'
import { connectionReducer, ConnectionState } from './Connection'
import { publishReducer, PublishState } from './Publish'
import { settingsReducer, SettingsState } from './Settings'
import { trackEvent } from '../tracking' import { trackEvent } from '../tracking'
import { PublishState, publishReducer } from './Publish' import { treeReducer, TreeState } from './Tree'
import { ConnectionState, connectionReducer } from './Connection'
import { SettingsState, settingsReducer } from './Settings'
import { TreeState, treeReducer } from './Tree'
import { ConnectionManagerState, connectionManagerReducer } from './ConnectionManager'
export enum ActionTypes { export enum ActionTypes {
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION', showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
+556 -610
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
image: Visual Studio 2017
platform: x64
cache:
- node_modules
- '%LOCALAPPDATA%\electron\Cache'
- '%LOCALAPPDATA%\electron-builder\cache'
install:
- ps: Install-Product node 10
build_script:
- yarn
- yarn build
- yarn prepare-release
- yarn package appx
test_script:
- yarn test
artifacts:
- path: 'build\clean\build\MQTT-Explorer*.appx'
+1 -1
View File
@@ -6,7 +6,7 @@ export class RingBuffer<T extends Lengthwise> {
private capacity: number private capacity: number
private maxItems: number private maxItems: number
private usage: number = 0 private usage: number = 0
private items: (any)[] = [] private items: T[] = []
private start: number = 0 private start: number = 0
private end: number = 0 private end: number = 0
+12 -6
View File
@@ -11,8 +11,8 @@ import {
removeConnection, removeConnection,
updateAvailable, updateAvailable,
} from '../../events' } from '../../events'
import { DataSource, MqttSource } from './DataSource' import { DataSource, MqttSource } from './DataSource'
import ConfigStorage from './ConfigStorage'
import { UpdateInfo } from 'builder-util-runtime' import { UpdateInfo } from 'builder-util-runtime'
export class ConnectionManager { export class ConnectionManager {
@@ -21,13 +21,18 @@ export class ConnectionManager {
public manageConnections() { public manageConnections() {
backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest) backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest)
backendEvents.subscribe(removeConnection, (connectionId: string) => { backendEvents.subscribe(removeConnection, (connectionId: string) => {
backendEvents.unsubscribeAll(makePublishEvent(connectionId))
this.removeConnection(connectionId) this.removeConnection(connectionId)
}) })
} }
private handleConnectionRequest = (event: AddMqttConnection) => { private handleConnectionRequest = (event: AddMqttConnection) => {
const connectionId = event.id const connectionId = event.id
// Prevent double connections when reloading
if (this.connections[connectionId]) {
this.removeConnection(connectionId)
}
const options = event.options const options = event.options
const connection = new MqttSource() const connection = new MqttSource()
this.connections[connectionId] = connection this.connections[connectionId] = connection
@@ -56,18 +61,19 @@ export class ConnectionManager {
}) })
} }
public removeConnection(hash: string) { public removeConnection(conenctionId: string) {
const connection = this.connections[hash] const connection = this.connections[conenctionId]
if (connection) { if (connection) {
backendEvents.unsubscribeAll(makePublishEvent(conenctionId))
connection.disconnect() connection.disconnect()
delete this.connections[hash] delete this.connections[conenctionId]
connection.stateMachine.onUpdate.removeAllListeners() connection.stateMachine.onUpdate.removeAllListeners()
} }
} }
public closeAllConnections() { public closeAllConnections() {
Object.keys(this.connections) Object.keys(this.connections)
.forEach(hash => this.removeConnection(hash)) .forEach(conenctionId => this.removeConnection(conenctionId))
} }
} }
+14 -6
View File
@@ -1,15 +1,15 @@
{ {
"name": "MQTT-Explorer", "name": "MQTT-Explorer",
"version": "0.2.0-alpha", "version": "0.2.1-alpha4",
"description": "Explore your message queues", "description": "Explore your message queues",
"main": "dist/src/electron.js", "main": "dist/src/electron.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"test": "yarn run test-backend", "test": "yarn run test-backend",
"install": "cd app; yarn; cd ..", "install": "cd app && yarn && cd ..",
"build": "tsc && cd app && yarn run build && cd ..", "build": "tsc && cd app && yarn run build && cd ..",
"test-backend": "cd backend && yarn run test && cd ..", "test-backend": "cd backend && yarn run test && cd ..",
"prepare-release": "./scripts/prepare-release.sh", "prepare-release": "ts-node scripts/prepare-release.ts",
"package": "ts-node package.ts", "package": "ts-node package.ts",
"ui-test": "./scripts/uiTests.sh", "ui-test": "./scripts/uiTests.sh",
"upload-video-artifacts": "./scripts/uploadVideoAsset.ts ui-test.mp4 ui-test.gif", "upload-video-artifacts": "./scripts/uploadVideoAsset.ts ui-test.mp4 ui-test.gif",
@@ -32,6 +32,13 @@
"category": "Development", "category": "Development",
"maintainer": "Thomas Nordquist" "maintainer": "Thomas Nordquist"
}, },
"appx": {
"applicationId": "mqttexplorer",
"identityName": "51031thomas.nordquist.MQTT-Explorer",
"publisherDisplayName": "Thomas Nordquist",
"publisher": "CN=0A6DE643-4AA2-4FF2-9729-6935C9ED8C13",
"backgroundColor": "#FFFFFF"
},
"directories": { "directories": {
"app": "./", "app": "./",
"buildResources": "res", "buildResources": "res",
@@ -44,6 +51,7 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/fs-extra": "^5.0.5",
"@types/lowdb": "^1.0.6", "@types/lowdb": "^1.0.6",
"@types/mime": "^2.0.0", "@types/mime": "^2.0.0",
"@types/mocha": "^5.2.5", "@types/mocha": "^5.2.5",
@@ -53,8 +61,9 @@
"@types/sha1": "^1.1.1", "@types/sha1": "^1.1.1",
"axios": "^0.18.0", "axios": "^0.18.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"electron": "^4.0.2", "electron": "^4.0.6",
"electron-builder": "^20.38.4", "electron-builder": "^20.38.5",
"fs-extra": "^7.0.1",
"mime": "^2.4.0", "mime": "^2.4.0",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mustache": "^3.0.1", "mustache": "^3.0.1",
@@ -72,7 +81,6 @@
}, },
"dependencies": { "dependencies": {
"about-window": "^1.12.1", "about-window": "^1.12.1",
"electron-debug": "^2.0.0",
"electron-is-dev": "^1.0.1", "electron-is-dev": "^1.0.1",
"electron-log": "^2.2.17", "electron-log": "^2.2.17",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist", "electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
+13 -1
View File
@@ -30,6 +30,16 @@ const win: builder.CliOptions = {
publish: 'always', publish: 'always',
} }
const winAppx: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
win: ['appx'],
projectDir: './build/clean',
publish: 'never',
}
const mac: builder.CliOptions = { const mac: builder.CliOptions = {
x64: true, x64: true,
ia32: true, ia32: true,
@@ -41,11 +51,13 @@ const mac: builder.CliOptions = {
} }
async function executeBuild() { async function executeBuild() {
console.log(process.argv[2])
switch (process.argv[2]) { switch (process.argv[2]) {
case 'win': case 'win':
await builder.build(win) await builder.build(win)
break break
case 'appx':
await builder.build(winAppx)
break
case 'linux': case 'linux':
await builder.build(linux) await builder.build(linux)
break break
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

-26
View File
@@ -1,26 +0,0 @@
#!/bin/bash
set -e
ORIGINAL_DIR=`pwd`
DIR=build/clean
rm -rf "$DIR" || echo "Directory did not exist"
mkdir -p "$DIR"
git clone .git "$DIR"
cd $DIR
# App
cd app
yarn
cd ..
# Build
yarn
yarn build
rm -rf node_modules
yarn install --production
rm -rf app/node_modules
cd "$ORIGINAL_DIR"
+66
View File
@@ -0,0 +1,66 @@
import * as fs from 'fs-extra'
import * as path from 'path'
import { spawn, ChildProcess } from 'child_process'
import { chdir } from 'process'
async function exec(cmd: string, args: string[] = []) {
const child = spawn(cmd, args, { shell: true })
redirectOutputFor(child)
await waitFor(child)
}
function redirectOutputFor(child: ChildProcess) {
const printStdout = (data: Buffer) => {
process.stdout.write(data.toString())
}
const printStderr = (data: Buffer) => {
process.stderr.write(data.toString())
}
child.stdout.on('data', printStdout)
child.stderr.on('data', printStderr)
child.once('close', () => {
child.stdout.off('data', printStdout)
child.stderr.off('data', printStderr)
})
}
async function waitFor(child: ChildProcess) {
return new Promise((resolve) => {
child.once('close', () => resolve())
})
}
const targetDir = path.join('build', 'clean')
async function prepareRelease() {
const originalDir = __dirname
await fs.remove(targetDir)
await fs.mkdirp(targetDir)
// Create fresh clone of the local git repo
await exec('git', ['clone', '.git', targetDir])
// Enter git repo
chdir(targetDir)
// Install app dependencies
chdir('app')
await exec('yarn')
chdir('..')
// Install electron dependencies
await exec('yarn')
// Build App and Electron backend
await exec('yarn', ['build'])
// Clean up
await fs.remove('node_modules')
await exec('yarn', ['install', '--production'])
await fs.remove(path.join('app', 'node_modules'))
chdir(originalDir)
}
prepareRelease()
+8 -5
View File
@@ -1,5 +1,8 @@
#!/bin/bash #!/bin/bash
ffmpeg -s:v 1024x700 -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv app2.mp4 DIMENSIONS="1024x720"
GIF_SCALE="1024"
ffmpeg -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv app2.mp4
# The video starts with a few blank frames, we want to know when the stop # The video starts with a few blank frames, we want to know when the stop
ffprobe -f lavfi -i "movie=app2.mp4,blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet > ffmpeg_info ffprobe -f lavfi -i "movie=app2.mp4,blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet > ffmpeg_info
@@ -9,16 +12,16 @@ END_OF_BLACK=`cat ffmpeg_info | grep end | head -n1 | cut -d'=' -f2`
END_OF_BLACK=`awk "BEGIN {print $END_OF_BLACK+0.8; exit}"` END_OF_BLACK=`awk "BEGIN {print $END_OF_BLACK+0.8; exit}"`
# Trim black frames at start # Trim black frames at start
ffmpeg -s:v 1024x700 -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -ss $END_OF_BLACK app.mp4 ffmpeg -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -ss $END_OF_BLACK app.mp4
# Generate gif palette # Generate gif palette
ffmpeg -y -s:v 1024x700 -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -vf fps=10,scale=1024:-1:flags=lanczos,palettegen palette1024.png ffmpeg -y -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -vf "fps=10,scale=$GIF_SCALE:-1:flags=lanczos,palettegen" palette1024.png
# Create gif # Create gif
ffmpeg -s:v 1024x700 -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -i palette1024.png -ss $END_OF_BLACK -filter_complex "fps=10,scale=1024:-1:flags=lanczos[x];[x][1:v]paletteuse" app1024.gif ffmpeg -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -i palette1024.png -ss $END_OF_BLACK -filter_complex "fps=10,scale=$GIF_SCALE:-1:flags=lanczos[x];[x][1:v]paletteuse" app720.gif
# Clean up # Clean up
rm ffmpeg_info palette*.png qrawvideorgb24.yuv rm ffmpeg_info palette*.png qrawvideorgb24.yuv
mv app.mp4 ui-test.mp4 mv app.mp4 ui-test.mp4
mv app1024.gif ui-test.gif mv app720.gif ui-test.gif
+1 -1
View File
@@ -9,7 +9,7 @@ function finish {
trap finish EXIT trap finish EXIT
DIMENSIONS="1024x700" DIMENSIONS="1024x720"
SCR=99 SCR=99
# Start new window manager # Start new window manager
Xvfb :$SCR -screen 0 "$DIMENSIONS"x24 -ac & Xvfb :$SCR -screen 0 "$DIMENSIONS"x24 -ac &
+53 -13
View File
@@ -1,14 +1,13 @@
import { Menu, app, BrowserWindow, webContents } from 'electron' import { Menu, app, BrowserWindow, webContents, MenuItemConstructorOptions, MenuItem } from 'electron'
import openAboutWindow from 'about-window' import openAboutWindow from 'about-window'
import * as path from 'path' import * as path from 'path'
const applicationMenu = { const applicationMenu: MenuItemConstructorOptions = {
label: 'Application', label: 'Application',
submenu: [ submenu: [
{ {
label: 'About Application', label: 'About Application',
click: () => { click: () => {
console.log(path.join(__dirname, 'icon.png'))
openAboutWindow({ openAboutWindow({
icon_path: path.join(__dirname, '..', '..', 'icon.png'), icon_path: path.join(__dirname, '..', '..', 'icon.png'),
license: 'AGPL-3.0', license: 'AGPL-3.0',
@@ -24,7 +23,7 @@ const applicationMenu = {
{ {
label: 'Dev Tools', label: 'Dev Tools',
accelerator: 'CmdOrCtrl+Alt+I', accelerator: 'CmdOrCtrl+Alt+I',
role: 'toggleDevTools', role: 'toggledevtools' as 'toggledevtools',
}, },
{ {
label: 'Quit', label: 'Quit',
@@ -36,48 +35,89 @@ const applicationMenu = {
], ],
} }
const editMenu = { const editMenu: MenuItemConstructorOptions = {
label: 'Edit', label: 'Edit',
submenu: [ submenu: [
{ {
label: 'Undo', label: 'Undo',
accelerator: 'CmdOrCtrl+Z', accelerator: 'CmdOrCtrl+Z',
selector: 'undo:', role: 'undo',
}, },
{ {
label: 'Redo', label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z', accelerator: 'Shift+CmdOrCtrl+Z',
selector: 'redo:', role: 'redo',
}, },
{ {
type: 'separator' as 'separator', type: 'separator',
}, },
{ {
label: 'Cut', label: 'Cut',
accelerator: 'CmdOrCtrl+X', accelerator: 'CmdOrCtrl+X',
selector: 'cut:', role: 'cut',
}, },
{ {
label: 'Copy', label: 'Copy',
accelerator: 'CmdOrCtrl+C', accelerator: 'CmdOrCtrl+C',
selector: 'copy:', role: 'copy',
}, },
{ {
label: 'Paste', label: 'Paste',
accelerator: 'CmdOrCtrl+V', accelerator: 'CmdOrCtrl+V',
selector: 'paste:', role: 'paste',
}, },
{ {
label: 'Select All', label: 'Select All',
accelerator: 'CmdOrCtrl+A', accelerator: 'CmdOrCtrl+A',
selector: 'selectAll:', role: 'selectall',
}, },
], ],
} }
const template = [ const viewMenu: MenuItemConstructorOptions = {
label: 'View',
submenu: [
{
label: 'Default size',
accelerator: 'CmdOrCtrl+0',
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.setZoomFactor(1)
}
},
},
{
label: 'Increase size',
accelerator: 'CmdOrCtrl+Plus',
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.getZoomFactor((zoom) => {
window.webContents.setZoomFactor(Math.min(zoom + 0.1, 2.0))
})
}
},
},
{
label: 'Reduce size',
accelerator: 'CmdOrCtrl+-',
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.getZoomFactor((zoom) => {
window.webContents.setZoomFactor(Math.max(zoom - 0.1, 0.5))
})
}
},
},
],
}
const template: any = [
applicationMenu, applicationMenu,
editMenu, editMenu,
viewMenu,
] ]
export const menuTemplate = Menu.buildFromTemplate(template) export const menuTemplate = Menu.buildFromTemplate(template)
+5 -8
View File
@@ -9,15 +9,12 @@ import { menuTemplate } from './MenuTemplate'
import { UpdateInfo } from '../events' import { UpdateInfo } from '../events'
const isDev = require('electron-is-dev') const isDev = require('electron-is-dev')
let electronTelemetry: any
if (!isDev) { if (!isDev) {
electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5') const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5')
} }
const isDebugEnabled = Boolean(process.argv.find(arg => arg === 'debug')) // const isDebugEnabled = Boolean(process.argv.find(arg => arg === 'debug'))
const isFullscreen = Boolean(process.argv.find(arg => arg === '--fullscreen')) const runningUiTestOnCi = Boolean(process.argv.find(arg => arg === '--runningUiTestOnCi'))
require('electron-debug')({ enabled: isDebugEnabled })
autoUpdater.logger = log autoUpdater.logger = log
log.info('App starting...') log.info('App starting...')
@@ -37,7 +34,7 @@ function createWindow() {
// Create the browser window. // Create the browser window.
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1024, width: 1024,
height: 700, height: 720,
show: false, show: false,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
@@ -48,7 +45,7 @@ function createWindow() {
mainWindow.once('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
if (mainWindow) { if (mainWindow) {
isFullscreen && mainWindow.setFullScreen(true) runningUiTestOnCi && mainWindow.setFullScreen(true)
mainWindow.show() mainWindow.show()
} }
}) })
+3
View File
@@ -0,0 +1,3 @@
export const Control = '\uE009'
export const Unknown = '\uE000'
export const Plus = '\uE025'
+12 -10
View File
@@ -87,17 +87,19 @@ function generateData(client: mqtt.MqttClient) {
let state = true let state = true
intervals.push(setInterval(() => { intervals.push(setInterval(() => {
state = !state state = !state
const enitityId = Math.round(Math.random() * 3000) const js = {
tags:{
entityId: 33512,
entityType: 'person',
host: 'd44ad81e10f9',
server: 'http://localhost/dataActuality',
status: state ? 'live' : 'inactive',
},
timestamp: Date.now(),
}
client.publish( client.publish(
'actuality/showcase', `{ 'actuality/showcase',
"tags":{ JSON.stringify(js),
"entityId":${enitityId},
"entityType":"person",
"host":"d44ad81e10f9",
"server":"${state ? 'http://localhost/dataActuality' : 'http://localhost/dataStorage' }",
"status":"${state ? 'live' : 'inactive'}"},
"timestamp":${Date.now()}
}`.replace(/\s/g, ''),
{ retain: true, qos: 0 }, { retain: true, qos: 0 },
) )
}, 2102)) }, 2102))
@@ -1,14 +0,0 @@
import { clickOn, sleep, writeText, expandTopic, hideText } from '../util'
import { Browser } from 'webdriverio'
// Expects a topic with at least two messages to be selected
export async function compareJsonSideBySide(browser: Browser<void>) {
const firstEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[1]/div')
const secondEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[2]/div')
await clickOn(secondEntry, browser)
await sleep(2000)
await clickOn(firstEntry, browser)
await sleep(2000)
await clickOn(firstEntry, browser)
await sleep(1000)
}
+1 -10
View File
@@ -1,4 +1,4 @@
import { clickOn, sleep, writeText } from '../util' import { clickOn, writeTextToInput } from '../util'
import { Browser } from 'webdriverio' import { Browser } from 'webdriverio'
export async function connectTo(host: string, browser: Browser<void>) { export async function connectTo(host: string, browser: Browser<void>) {
@@ -9,12 +9,3 @@ export async function connectTo(host: string, browser: Browser<void>) {
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]') const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
clickOn(connectButton, browser) clickOn(connectButton, browser)
} }
async function writeTextToInput(name: string, text: string, browser: Browser<void>, wait: boolean = true) {
const input = await browser.$(`//label[contains(text(), "${name}")]/..//input`)
await clickOn(input, browser, 1)
wait && await sleep(500)
input.clearValue()
wait && await sleep(300)
await writeText(text, browser)
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { clickOn, sleep, writeText, expandTopic } from '../util' import { clickOn } from '../util'
import { Browser } from 'webdriverio' import { Browser } from 'webdriverio'
export async function copyTopicToClipboard(browser: Browser<void>) { export async function copyTopicToClipboard(browser: Browser<void>) {
@@ -0,0 +1,29 @@
import { clickOn, writeTextToInput, sleep } from '../util'
import { Browser } from 'webdriverio'
export async function showAdvancedConnectionSettings(browser: Browser<void>) {
const advancedSettingsButton = await browser.$('//button/span[contains(text(),"Advanced")]')
const addButton = await browser.$('//button/span[contains(text(),"Add")]')
await clickOn(advancedSettingsButton, browser)
await writeTextToInput('Subscription', 'garden/#', browser, false)
await clickOn(addButton, browser)
await writeTextToInput('Subscription', 'livingroom/#', browser, false)
await clickOn(addButton, browser)
await deleteFirstSubscribedTopic(browser)
await deleteFirstSubscribedTopic(browser)
await sleep(1000)
const backButton = await browser.$('//button/span[contains(text(),"Back")]')
await clickOn(backButton, browser)
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
await clickOn(connectButton, browser)
}
async function deleteFirstSubscribedTopic(browser: Browser<void>) {
const deleteButton = await browser.$('.advanced-connection-settings-topic-list button')
await clickOn(deleteButton, browser)
}
+31
View File
@@ -0,0 +1,31 @@
import { Browser } from 'webdriverio'
import {
clickOn,
sleep,
} from '../util'
export async function showJsonFormatting(browser: Browser<void>) {
const editor = await browser.$('//*[contains(@class, "ace_editor")]')
const formatJsonButton = await browser.$('#sidebar-publish-format-json')
const payloadInput = await browser.$('//*[contains(@class, "ace_text-input")]')
await clickOn(editor, browser)
await browser.keys(['\uE009', 'A']) // Ctrl + A
await browser.keys(['\uE000']) // End keyboard modifier
await browser.keys(['\uE003']) // Backspace
await sleep(300)
await writeTextPayload(payloadInput, '{"action": "setState", "state": "on" }')
await sleep(300)
await clickOn(formatJsonButton, browser)
await sleep(1200)
const sidebarDrawer = await browser.$('#Sidebar')
await sidebarDrawer.scrollIntoView()
}
async function writeTextPayload(payloadInput: any, text: string) {
const chars = text.split('')
for (const char of chars) {
await payloadInput.setValue(char)
await sleep(10)
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { clickOn, sleep, writeText, expandTopic } from '../util'
import { Browser } from 'webdriverio' import { Browser } from 'webdriverio'
import { expandTopic, sleep } from '../util'
export async function showJsonPreview(browser: Browser<void>) { export async function showJsonPreview(browser: Browser<void>) {
await expandTopic('actuality/showcase', browser) await expandTopic('actuality/showcase', browser)
@@ -0,0 +1,21 @@
import { clickOn, sleep, showText } from '../util'
import { Browser } from 'webdriverio'
// Expects a topic with at least two messages to be selected
export async function showOffDiffCapability(browser: Browser<void>) {
await showText('Compare messages', 2000, browser, 'top')
await showText('Show raw message', 2000, browser, 'bottom')
const rawMessage = await browser.$('#valueRendererDisplayMode-raw')
await clickOn(rawMessage, browser)
await sleep(1000)
await showText('Compare with others', 2000, browser, 'bottom')
const diffMessages = await browser.$('#valueRendererDisplayMode-diff')
await clickOn(diffMessages, browser)
// const firstEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[1]/div')
const secondEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[2]/div')
await clickOn(secondEntry, browser)
await sleep(2000)
}
+11
View File
@@ -0,0 +1,11 @@
import { sleep, showKeys, showText } from '../util'
import { Browser } from 'webdriverio'
export async function showZoomLevel(browser: Browser<void>) {
await showKeys('Zoom in', 2000, browser, 'top', ['Ctrl', '+'])
await sleep(2000)
await showKeys('Zoom out', 2000, browser, 'middle', ['Ctrl', '-'])
await sleep(2000)
await showKeys('Reset zoom level', 2000, browser, 'bottom', ['Ctrl', 'Zero'])
await sleep(2000)
}
+1 -1
View File
@@ -24,6 +24,6 @@ async function topicMatches(topics: string[], browser: Browser<void>) {
} }
function topicSelector(topics: string[]) { function topicSelector(topics: string[]) {
const suffix = topics.map(topic => `*[contains(text(), "${topic}")]`).join('/../..//') const suffix = topics.map(topic => `*[contains(text(), "${topic}")]`).join('/../../..//')
return `//${suffix}` return `//${suffix}`
} }
+28 -69
View File
@@ -1,4 +1,4 @@
import { Element, Browser } from 'webdriverio' import { Browser, Element } from 'webdriverio'
export { expandTopic } from './expandTopic' export { expandTopic } from './expandTopic'
export function sleep(ms: number, required = false) { export function sleep(ms: number, required = false) {
@@ -26,47 +26,28 @@ export async function delteTextWithBackspaces(element: Element<void>, browser: B
} }
} }
export async function writeTextToInput(name: string, text: string, browser: Browser<void>, wait: boolean = true) {
const input = await browser.$(`//label[contains(text(), "${name}")]/..//input`)
await clickOn(input, browser, 1)
wait && await sleep(500)
input.clearValue()
wait && await sleep(300)
await writeText(text, browser)
}
export async function moveToCenterOfElement(element: Element<void>, browser: Browser<void>) { export async function moveToCenterOfElement(element: Element<void>, browser: Browser<void>) {
const { x, y } = await element.getLocation() const { x, y } = await element.getLocation()
const { width, height } = await element.getSize() const { width, height } = await element.getSize()
const js = `{ const targetX = x + width / 2
const targetX = ${x + width / 2} const targetY = y + height / 2
const targetY = ${y + height / 2}
const duration = 500
const maxStepSize = 10 const duration = 500
const e = document.getElementById("bier")
const top = parseFloat(e.style.top)
const left = parseFloat(e.style.left)
const deltaY = targetY - top
const deltaX = targetX - left
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) const js = `window.demo.moveMouse(${targetX}, ${targetY}, ${duration});`
const steps = Math.ceil(distance / maxStepSize)
const stepX = deltaX / steps
const stepY = deltaY / steps
let currentStep = 0
function getCloser() {
e.style.left = String(left + (stepX * currentStep)) + 'px'
e.style.top = String(top + (stepY * currentStep)) + 'px'
if (currentStep < steps) {
setTimeout(() => {
currentStep += 1
if (currentStep === steps) {
e.style.left = String(targetX + 5) + 'px'
e.style.top = String(targetY + 1) + 'px'
} else {
getCloser()
}
}, duration/steps)
}
}
getCloser()
}`
await browser.execute(js) await browser.execute(js)
await sleep(550, true) await sleep(duration + 500, true)
await element.moveTo() await element.moveTo()
} }
@@ -84,47 +65,25 @@ export async function clickOn(element: Element<void>, browser: Browser<void>, cl
} }
export async function createFakeMousePointer(browser: Browser<void>) { export async function createFakeMousePointer(browser: Browser<void>) {
const addCursorImage = 'const i=document.createElement("img");' const js = 'window.demo.enableMouse();'
+ 'i.src="../cursor.png";'
+ 'i.width="32";'
+ 'i.height="32";'
+ 'i.id="bier";'
+ 'i.style="position: fixed; z-index:10000000; filter: invert(100%);left: 0px; top: 0px;";'
+ 'document.body.appendChild(i)'
await browser.execute(addCursorImage) await browser.execute(js)
} }
export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom') { export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys = []) {
const positions = { const js = `window.demo.showMessage('${text}', '${location}', ${duration});`
top: 0,
bottom: -65, browser.execute(js)
middle: -32, }
}
const js = ` export async function showKeys(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys: string[] = []) {
const postition = "${positions[location]}vh" const js = `window.demo.showMessage('${text}', '${location}', ${duration}, ${JSON.stringify(keys)});`
let previousDiv = document.getElementById('tests-text-overlay')
previousDiv && previousDiv.remove()
let div = document.createElement('div')
div.id = "tests-text-overlay"
div.style = "background-color: rgba(0, 0, 0, 0.8);position: fixed;left: 5vw;z-index: 1000000;margin: 30vw auto 50vw;border-radius: 16px;right: 5vw;bottom: "+ postition +";"
let div2 = document.createElement('div')
div2.style = "text-align: center;font-size: 4em;color: white;"
div2.innerHTML = \`${text}\`
div.appendChild(div2)
document.body.appendChild(div)
if (${duration} > 0) {
setTimeout(() => div.remove(), ${duration})
}
`
browser.execute(js) browser.execute(js)
} }
export async function hideText(browser: Browser<void>) { export async function hideText(browser: Browser<void>) {
const js = ` const js = 'window.demo.hideMessage();'
let previousDiv = document.getElementById('tests-text-overlay')
previousDiv && previousDiv.remove()
`
browser.execute(js) browser.execute(js)
await sleep(600) await sleep(600)
} }
+53 -30
View File
@@ -1,65 +1,76 @@
import * as os from 'os'
import * as webdriverio from 'webdriverio'
import mockMqtt, { stop } from './mock-mqtt'
import { clearOldTopics } from './scenarios/clearOldTopics'
import { clearSearch, searchTree } from './scenarios/searchTree'
import { connectTo } from './scenarios/connect'
import { copyTopicToClipboard } from './scenarios/copyTopicToClipboard'
import { copyValueToClipboard } from './scenarios/copyValueToClipboard'
import { disconnect } from './scenarios/disconnect'
import { publishTopic } from './scenarios/publishTopic'
import { showJsonFormatting } from './scenarios/showJsonFormatting'
import { showJsonPreview } from './scenarios/showJsonPreview'
import { showMenu } from './scenarios/showMenu'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showOffDiffCapability } from './scenarios/showOffDiffCapability'
import { showZoomLevel } from './scenarios/showZoomLevel'
import { showAdvancedConnectionSettings } from './scenarios/showAdvancedConnectionSettings'
import {
clickOnHistory,
createFakeMousePointer,
hideText,
showText,
sleep,
} from './util'
process.on('unhandledRejection', (error: Error) => { process.on('unhandledRejection', (error: Error) => {
console.error('unhandledRejection', error.message, error.stack) console.error('unhandledRejection', error.message, error.stack)
process.exit(1) process.exit(1)
}) })
import * as webdriverio from 'webdriverio' const runningUiTestOnCi = os.platform() === 'darwin' ? [] : ['--runningUiTestOnCi']
import * as os from 'os'
import mockMqtt, { stop } from './mock-mqtt'
import { connectTo } from './scenarios/connect'
import { disconnect } from './scenarios/disconnect'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showJsonPreview } from './scenarios/showJsonPreview'
import { compareJsonSideBySide } from './scenarios/compareJsonSideBySide'
import { searchTree, clearSearch } from './scenarios/searchTree'
import { copyTopicToClipboard } from './scenarios/copyTopicToClipboard'
import { copyValueToClipboard } from './scenarios/copyValueToClipboard'
import { publishTopic } from './scenarios/publishTopic'
import { clearOldTopics } from './scenarios/clearOldTopics'
import { showMenu } from './scenarios/showMenu'
import { createFakeMousePointer, sleep, showText, hideText, clickOnHistory } from './util'
const binary = os.platform() === 'darwin' ? 'Electron.app/Contents/MacOS/Electron' : 'electron'
const fullscreen = os.platform() === 'darwin' ? [] : ['--fullscreen']
console.log(`${__dirname}/../../../node_modules/.bin/electron`)
const options = { const options = {
host: 'localhost', // Use localhost as chrome driver server host: '127.0.0.1', // Use localhost as chrome driver server
port: 9515, // "9515" is the port opened by chrome driver. port: 9515, // "9515" is the port opened by chrome driver.
capabilities: { capabilities: {
browserName: 'electron', browserName: 'electron',
chromeOptions: { chromeOptions: {
binary: `${__dirname}/../../../node_modules/electron/dist/${binary}`, binary: `${__dirname}/../../../node_modules/.bin/electron`,
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(fullscreen), args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(runningUiTestOnCi),
}, },
windowTypes: ['app', 'webview'], windowTypes: ['app', 'webview'],
}, },
} }
async function doStuff() { async function doStuff() {
console.log('mock mqtt')
await mockMqtt() await mockMqtt()
console.log('start webdriver')
const browser = await webdriverio.remote(options) const browser = await webdriverio.remote(options)
await createFakeMousePointer(browser) await createFakeMousePointer(browser)
await connectTo('127.0.0.1', browser) await connectTo('127.0.0.1', browser)
await sleep(1000) await sleep(1000)
await sleep(1000) await sleep(1000)
await showText('Overview of topics', 2000, browser, 'top') await showText('Topics overview', 2000, browser, 'top')
await sleep(2000) await sleep(2000)
await showText('Indicates which topics change', 2000, browser, 'bottom') await showText('Indicate topic updates', 2000, browser, 'bottom')
await sleep(3000) await sleep(3000)
await showText('Plot topics', 2000, browser) await showText('Plot topic history', 2000, browser)
await showNumericPlot(browser) await showNumericPlot(browser)
await sleep(2000) await sleep(2000)
await showText('Formatted messages', 2000, browser, 'top')
await showJsonPreview(browser) await showJsonPreview(browser)
await sleep(2000) await showText('Formatted messages', 1500, browser, 'top')
await sleep(1500)
await showText('Compare messages', 2000, browser, 'top') await showOffDiffCapability(browser)
await compareJsonSideBySide(browser)
await hideText(browser) await hideText(browser)
await showText('Publish topics', 2000, browser, 'top') await showText('Publish topics', 2000, browser, 'top')
@@ -67,6 +78,10 @@ async function doStuff() {
await publishTopic(browser) await publishTopic(browser)
await sleep(1000) await sleep(1000)
await showText('Write JSON with ease', 2000, browser, 'top')
await showJsonFormatting(browser)
await sleep(1000)
await showText('Copy to Clipboard', 2000, browser) await showText('Copy to Clipboard', 2000, browser)
await copyTopicToClipboard(browser) await copyTopicToClipboard(browser)
await hideText(browser) await hideText(browser)
@@ -91,10 +106,18 @@ async function doStuff() {
await sleep(2000) await sleep(2000)
await disconnect(browser) await disconnect(browser)
await showText('Customize Subscriptions', 3000, browser, 'top')
await showAdvancedConnectionSettings(browser)
await showText('Keyboard shortcuts', 1750, browser, 'middle')
await sleep(1750)
await showZoomLevel(browser)
await showText('The End', 3000, browser, 'middle')
await sleep(3000) await sleep(3000)
browser.closeWindow() browser.closeWindow()
stop() stop()
} }
+13 -44
View File
@@ -119,6 +119,13 @@
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a"
integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==
"@types/fs-extra@^5.0.5":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.5.tgz#080d90a792f3fa2c5559eb44bd8ef840aae9104b"
integrity sha512-w7iqhDH9mN8eLClQOYTkhdYUOSpp25eXxfc6VbFOGtzxW34JcvctH2bKjj4jD4++z4R5iO5D+pg48W2e03I65A==
dependencies:
"@types/node" "*"
"@types/lodash@*": "@types/lodash@*":
version "4.14.121" version "4.14.121"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.121.tgz#9327e20d49b95fc2bf983fc2f045b2c6effc80b9" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.121.tgz#9327e20d49b95fc2bf983fc2f045b2c6effc80b9"
@@ -1061,7 +1068,7 @@ ejs@~2.5.6:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5"
integrity sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ== integrity sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==
electron-builder@^20.38.4: electron-builder@^20.38.5:
version "20.38.5" version "20.38.5"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.38.5.tgz#31b3913a68b4911afd4cfc7bcd2522c5808040cd" resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.38.5.tgz#31b3913a68b4911afd4cfc7bcd2522c5808040cd"
integrity sha512-p88IDHhH2J4hA6KwRBJY+OfVZuFtFIShY3Uh/TwYAfbX0v1RhKZytuGdO8sty2zcWxDYX74xDBv+X9oN6qEIRQ== integrity sha512-p88IDHhH2J4hA6KwRBJY+OfVZuFtFIShY3Uh/TwYAfbX0v1RhKZytuGdO8sty2zcWxDYX74xDBv+X9oN6qEIRQ==
@@ -1088,14 +1095,6 @@ electron-chromedriver@~3.0.0:
electron-download "^4.1.0" electron-download "^4.1.0"
extract-zip "^1.6.5" extract-zip "^1.6.5"
electron-debug@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-2.0.0.tgz#3059a6557acbfb091f138d83875f57bac80cea6d"
integrity sha512-orGlw9uErUztdD7cgdKz78txq3czpOnKG/zvvsINkUsugqL+dn77UFrbwRGVgPwuLJ7Ejbjjk9EcxIcgTivMbA==
dependencies:
electron-is-dev "^0.3.0"
electron-localshortcut "^3.0.0"
electron-download@^4.1.0: electron-download@^4.1.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8" resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
@@ -1111,31 +1110,11 @@ electron-download@^4.1.0:
semver "^5.4.1" semver "^5.4.1"
sumchecker "^2.0.2" sumchecker "^2.0.2"
electron-is-accelerator@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b"
integrity sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=
electron-is-dev@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe"
integrity sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=
electron-is-dev@^1.0.1: electron-is-dev@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.0.1.tgz#6e0a184736fe7aea77d18210b0b0f6a02402c4bc" resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.0.1.tgz#6e0a184736fe7aea77d18210b0b0f6a02402c4bc"
integrity sha512-iwM3EotA9HTXqMGpQRkR/kT8OZqBbdfHTnlwcxsjSLYqY8svvsq0MuujsWCn3/vtgRmDv/PC/gKUUpoZvi5C1w== integrity sha512-iwM3EotA9HTXqMGpQRkR/kT8OZqBbdfHTnlwcxsjSLYqY8svvsq0MuujsWCn3/vtgRmDv/PC/gKUUpoZvi5C1w==
electron-localshortcut@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/electron-localshortcut/-/electron-localshortcut-3.1.0.tgz#10c1ffd537b8d39170aaf6e1551341f7780dd2ce"
integrity sha512-MgL/j5jdjW7iA0R6cI7S045B0GlKXWM1FjjujVPjlrmyXRa6yH0bGSaIAfxXAF9tpJm3pLEiQzerYHkRh9JG/A==
dependencies:
debug "^2.6.8"
electron-is-accelerator "^0.1.0"
keyboardevent-from-electron-accelerator "^1.1.0"
keyboardevents-areequal "^0.2.1"
electron-log@^2.2.17: electron-log@^2.2.17:
version "2.2.17" version "2.2.17"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-2.2.17.tgz#e71e2ebb949fc96ded7cdb99eeee7202e48981d2" resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-2.2.17.tgz#e71e2ebb949fc96ded7cdb99eeee7202e48981d2"
@@ -1189,10 +1168,10 @@ electron-updater@^4.0.6:
semver "^5.6.0" semver "^5.6.0"
source-map-support "^0.5.9" source-map-support "^0.5.9"
electron@^4.0.2: electron@^4.0.6:
version "4.0.2" version "4.0.6"
resolved "https://registry.yarnpkg.com/electron/-/electron-4.0.2.tgz#b5e074c149841490825d0785aa2bc4f5e29bbea4" resolved "https://registry.yarnpkg.com/electron/-/electron-4.0.6.tgz#f936772ddb5eaa026591ea14ef806e1385fbbda9"
integrity sha512-H0pmSvOVuC+Mq/+cYNXbCDBmqq5d1xYUVdBOjqGJuwuwJeP7qDHF35JA3cq+ARzq/CbwlXq98zdW6i6+x3U24g== integrity sha512-r2ow/EmDibjoCNJp35mB1CcPs2xBF9fp2eoWuUOJmpVQyzdba6EnqsSD1BxwXzF9vQ5WkQ2UbfXBIvpomrdioQ==
dependencies: dependencies:
"@types/node" "^10.12.18" "@types/node" "^10.12.18"
electron-download "^4.1.0" electron-download "^4.1.0"
@@ -1460,7 +1439,7 @@ fs-extra@^4.0.1:
jsonfile "^4.0.0" jsonfile "^4.0.0"
universalify "^0.1.0" universalify "^0.1.0"
fs-extra@^7.0.0: fs-extra@^7.0.0, fs-extra@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
@@ -2083,16 +2062,6 @@ jsprim@^1.2.2:
json-schema "0.2.3" json-schema "0.2.3"
verror "1.10.0" verror "1.10.0"
keyboardevent-from-electron-accelerator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.0.tgz#324614f6e33490c37ffc5be5876b3e85fe223c84"
integrity sha512-VDC4vKWGrR3VgIKCE4CsXnvObGgP8C2idnTKEMUkuEuvDGE1GEBX9FtNdJzrD00iQlhI3xFxRaeItsUmlERVng==
keyboardevents-areequal@^0.2.1:
version "0.2.2"
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
latest-version@^3.0.0: latest-version@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"