Compare commits

..
Author SHA1 Message Date
Thomas Nordquist 26fb5f88a4 Work in progress 2019-02-14 14:14:29 +01:00
141 changed files with 2098 additions and 4493 deletions
+1 -3
View File
@@ -24,15 +24,13 @@ services:
install:
- yarn install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update && sudo apt-get -y install snap squashfs-tools && sudo snap install snapcraft --classic; fi;
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker build docker --tag uitest; fi;
script:
- yarn run build
- yarn test
- export TRAVIS_BUILD_NUMBER="" # Override travis build number since it is uses for tagging the binary version https://github.com/electron-userland/electron-builder/issues/3730
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app uitest sh -c "cd app && docker/testMounted.sh"; fi
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package linux; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- win; fi
-1
View File
@@ -1 +0,0 @@
mqtt-explorer.com
-12
View File
@@ -1,12 +0,0 @@
# 0.2.3
- Highlight differences in the last received message with a "diff" view
- Magnify app content with "Ctrl +", "Ctrl -", "Ctrl 0"
- Make user interactions more predictable by removing "select topic on mouse over"
- Add "Quick Preview" setting, to enable topic selection on mouse over
- Add JSON formatter
- Show error when connection to the mqtt broker is lost
- Fix bug where mqtt explorer resets the tree after a reconnect
- Fix MQTT-Client id
# 0.2.0
+30 -64
View File
@@ -1,98 +1,69 @@
# [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/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![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)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
| | | |
|:---:|:---:|:---:|
| [![screen1_small](https://user-images.githubusercontent.com/7721625/53954800-84b34c00-40d7-11e9-842d-bf655c569600.jpg)](https://user-images.githubusercontent.com/7721625/53954364-52551f00-40d6-11e9-93cf-d5a9601897ea.png) | [![screen2_small](https://user-images.githubusercontent.com/7721625/53954801-84b34c00-40d7-11e9-913a-42572e675620.jpg)](https://user-images.githubusercontent.com/7721625/53954365-52551f00-40d6-11e9-823f-afd66f19ed01.png) | [![screen3_small](https://user-images.githubusercontent.com/7721625/53954802-854be280-40d7-11e9-973c-08f23c8dbf89.jpg)](https://user-images.githubusercontent.com/7721625/53954366-52551f00-40d6-11e9-9738-74db830d03ac.png) |
### Version 0.1.3
## Version 0.2.3
See the whole picture of your broker.
- What topics are active?
- Which retained topics are abandoned?
- Where is this new device / service I connected to the broker?
MQTT Explorer is a comprehensive MQTT Client that provides a structured overview of your MQTT topics and makes working with devices/services on your broker dead-simple.
MQTT-Explorer strives to be THE swiss-army-knife tool.
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.
### Features
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)).
- Visualize topics and topic activity
- Delete retained topics
- Search/filter topics
- Delete topics recursively
- Publish topics
- Plot numeric topics
- Retain a history of each topic
- ...
The hierarchical view makes this tool so easy to use and differentiates the **MQTT Explorer** from other great MQTT clients like [MQTTLens](https://chrome.google.com/webstore/detail/mqttlens/hemojaaeigabkbcookmlgmdigohjobjm), [MQTTBox](http://workswithweb.com/mqttbox.html) and [MQTT.fx](https://mqttfx.jensd.de/).
This MQTT Client strives to be a MQTT swiss-army-knife, the perfect tool to integrate new services and IoT devices on your network.
## Screenshots
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
## Download
Developing this tool takes a lot of effort, sweat and time, please consider rating the App on the Windows or Mac app store <img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" />.
If you feel like a feature is missing or you found a bug, please leave me a [comment / issue](https://github.com/thomasnordquist/MQTT-Explorer/issues) and I'll see what I can do.
| Platform | | Downloads |
| 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="165" /></a><br />**[portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.3&#x2F;MQTT-Explorer-0.2.3.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.3&#x2F;MQTT-Explorer-Setup-0.2.3.exe)** |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | **Mac** | <a href="https://geo.itunes.apple.com/us/app/mqtt-explorer/id1455214828?mt=12&app=apps"><img src="https://linkmaker.itunes.apple.com/en-us/badge-lrg.svg?releaseDate=2019-03-07T00:00:00Z&kind=desktopapp&bubble=macos_apps" width="165" height="40"/></a><br />**[dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.3&#x2F;MQTT-Explorer-0.2.3.dmg)** |
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | **Ubuntu**<br />*also for debian, mint, neon, fedora, etc...* | <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="165" /></a><br />`snap install mqtt-explorer`<br />[Ubuntu Store](snap://mqtt-explorer) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | **Linux**<br />*almost every linux* | **[AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.3&#x2F;MQTT-Explorer-0.2.3-x86_64.AppImage)**<br />*Run AppImage:<br />Make it executable and double-click it.* |
| ![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)
## Video
![screencast](https://user-images.githubusercontent.com/7721625/53954172-b7f4db80-40d5-11e9-852c-f8d85e511a00.gif)
## Demo
![screencast](https://user-images.githubusercontent.com/7721625/51978075-5bc0eb00-2489-11e9-953a-0965b154557b.gif)
## Performance
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.
This MQTT Client is optimized to handle thousands of topics and at hundreds of thousands messages per minute.
Custom subscriptions can limit the amount of messages **MQTT Explorer** needs to process, subscriptions can be managed in the advanced connection settings.
In very large productive environments brokers may handle an extreme load of topics, subscribing with a wildcard topic is in this scenario not advised.
## IoT Applications
List of useful IoT applications using MQTT to integrate devices / services
- [Home Assistant](https://www.home-assistant.io/) - Open source home automation gateway
- [OpenHAB](https://www.openhab.org/) - Smart Home Gateway
- [Node-RED](https://nodered.org/) - Flow-based programming for the Internet of Things
- [zigbee2mqtt](https://github.com/Koenkk/zigbee2mqtt) - A ZigBee to MQTT bridge
- [Tasmota](https://github.com/arendst/Sonoff-Tasmota) - ESP8266 firmware with MQTT support
Future releases will allow for a more granular selection of subscribed topics to handle
## Develop
PRs and issues are welcome
PRs and issues are welcome.
Install with `npm run install`, build with `npm run build`
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management, `src` contains all the electron bindings. [mqttjs](https://github.com/mqttjs/MQTT.js) is used to facilitate communication to MQTT brokers.
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## Automated Tests
To achieve a reliable product automated tests run regularly on travis.
To achieve a reliable product automated tests run regulary on travis.
- Data model
- MQTT integration
- UI-Tests (The demo is a recorded ui test)
A [mosquitto](https://mosquitto.org/) MQTT broker is required to run the ui-tests.
## Telemetry
No personal data is processed, sent or stored.
The app sends telemetry and error reports, this enables me to quickly react on bugs/errors and understand what's going on. Responding quickly to errors is one key element in producing a reliable software product.
The App sends telemetry and error reports, this enables me to quickly react on bugs/errors I produced.
This is a difficutlt task since this App runs on three different operating systems and architectures.
It basically sends: app version, processor architecture, operating system, used memory, user interactions and error stacks.
<details>
<summary>Example telemetry
</summary>
This greatly helps to improve the software quality and reliability.
No data about you or your data is send or stored.
Even thoug the data is purely technical, an option to disable telemetry is planned. [#52](https://github.com/thomasnordquist/MQTT-Explorer/issues/52)
Example telemetry:
```javascript
{ system: { arch: 'x64', platform: 'darwin' },
appVersion: '0.0.7',
@@ -108,10 +79,5 @@ It basically sends: app version, processor architecture, operating system, used
transactionId: '53bf9aac-e695-40cc-9a81-b1cf3398843d' }
```
</details>
Even though the data is purely technical, an option to disable telemetry is planned. [#52](https://github.com/thomasnordquist/MQTT-Explorer/issues/52)
## License
[AGPL 3](./LICENSE)
+30 -64
View File
@@ -1,98 +1,69 @@
# [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/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![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)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
| | | |
|:---:|:---:|:---:|
| [![screen1_small](https://user-images.githubusercontent.com/7721625/53954800-84b34c00-40d7-11e9-842d-bf655c569600.jpg)](https://user-images.githubusercontent.com/7721625/53954364-52551f00-40d6-11e9-93cf-d5a9601897ea.png) | [![screen2_small](https://user-images.githubusercontent.com/7721625/53954801-84b34c00-40d7-11e9-913a-42572e675620.jpg)](https://user-images.githubusercontent.com/7721625/53954365-52551f00-40d6-11e9-823f-afd66f19ed01.png) | [![screen3_small](https://user-images.githubusercontent.com/7721625/53954802-854be280-40d7-11e9-973c-08f23c8dbf89.jpg)](https://user-images.githubusercontent.com/7721625/53954366-52551f00-40d6-11e9-9738-74db830d03ac.png) |
### Version {{ version }}
## Version {{ version }}
See the whole picture of your broker.
- What topics are active?
- Which retained topics are abandoned?
- Where is this new device / service I connected to the broker?
MQTT Explorer is a comprehensive MQTT Client that provides a structured overview of your MQTT topics and makes working with devices/services on your broker dead-simple.
MQTT-Explorer strives to be THE swiss-army-knife tool.
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.
### Features
The app is available for Windows ({{windowsTargets}}), Linux ({{linuxTargets}}) and Mac ({{macTargets}}).
- Visualize topics and topic activity
- Delete retained topics
- Search/filter topics
- Delete topics recursively
- Publish topics
- Plot numeric topics
- Retain a history of each topic
- ...
The hierarchical view makes this tool so easy to use and differentiates the **MQTT Explorer** from other great MQTT clients like [MQTTLens](https://chrome.google.com/webstore/detail/mqttlens/hemojaaeigabkbcookmlgmdigohjobjm), [MQTTBox](http://workswithweb.com/mqttbox.html) and [MQTT.fx](https://mqttfx.jensd.de/).
This MQTT Client strives to be a MQTT swiss-army-knife, the perfect tool to integrate new services and IoT devices on your network.
## Screenshots
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
## Download
Developing this tool takes a lot of effort, sweat and time, please consider rating the App on the Windows or Mac app store <img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" /><img src="./res/star.svg" width="16" />.
If you feel like a feature is missing or you found a bug, please leave me a [comment / issue](https://github.com/thomasnordquist/MQTT-Explorer/issues) and I'll see what I can do.
| Platform | | Downloads |
| 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="165" /></a><br />**{{windowsTargets}}** |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | **Mac** | <a href="https://geo.itunes.apple.com/us/app/mqtt-explorer/id1455214828?mt=12&app=apps"><img src="https://linkmaker.itunes.apple.com/en-us/badge-lrg.svg?releaseDate=2019-03-07T00:00:00Z&kind=desktopapp&bubble=macos_apps" width="165" height="40"/></a><br />**{{macTargets}}** |
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | **Ubuntu**<br />*also for debian, mint, neon, fedora, etc...* | <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="165" /></a><br />`snap install mqtt-explorer`<br />[Ubuntu Store](snap://mqtt-explorer) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | **Linux**<br />*almost every linux* | **{{linuxTargets}}**<br />*Run AppImage:<br />Make it executable and double-click it.* |
| ![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)
## Video
![screencast](https://user-images.githubusercontent.com/7721625/53954172-b7f4db80-40d5-11e9-852c-f8d85e511a00.gif)
## Demo
![screencast](https://user-images.githubusercontent.com/7721625/51978075-5bc0eb00-2489-11e9-953a-0965b154557b.gif)
## Performance
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.
This MQTT Client is optimized to handle thousands of topics and at hundreds of thousands messages per minute.
Custom subscriptions can limit the amount of messages **MQTT Explorer** needs to process, subscriptions can be managed in the advanced connection settings.
In very large productive environments brokers may handle an extreme load of topics, subscribing with a wildcard topic is in this scenario not advised.
## IoT Applications
List of useful IoT applications using MQTT to integrate devices / services
- [Home Assistant](https://www.home-assistant.io/) - Open source home automation gateway
- [OpenHAB](https://www.openhab.org/) - Smart Home Gateway
- [Node-RED](https://nodered.org/) - Flow-based programming for the Internet of Things
- [zigbee2mqtt](https://github.com/Koenkk/zigbee2mqtt) - A ZigBee to MQTT bridge
- [Tasmota](https://github.com/arendst/Sonoff-Tasmota) - ESP8266 firmware with MQTT support
Future releases will allow for a more granular selection of subscribed topics to handle
## Develop
PRs and issues are welcome
PRs and issues are welcome.
Install with `npm run install`, build with `npm run build`
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management, `src` contains all the electron bindings. [mqttjs](https://github.com/mqttjs/MQTT.js) is used to facilitate communication to MQTT brokers.
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## Automated Tests
To achieve a reliable product automated tests run regularly on travis.
To achieve a reliable product automated tests run regulary on travis.
- Data model
- MQTT integration
- UI-Tests (The demo is a recorded ui test)
A [mosquitto](https://mosquitto.org/) MQTT broker is required to run the ui-tests.
## Telemetry
No personal data is processed, sent or stored.
The app sends telemetry and error reports, this enables me to quickly react on bugs/errors and understand what's going on. Responding quickly to errors is one key element in producing a reliable software product.
The App sends telemetry and error reports, this enables me to quickly react on bugs/errors I produced.
This is a difficutlt task since this App runs on three different operating systems and architectures.
It basically sends: app version, processor architecture, operating system, used memory, user interactions and error stacks.
<details>
<summary>Example telemetry
</summary>
This greatly helps to improve the software quality and reliability.
No data about you or your data is send or stored.
Even thoug the data is purely technical, an option to disable telemetry is planned. [#52](https://github.com/thomasnordquist/MQTT-Explorer/issues/52)
Example telemetry:
```javascript
{ system: { arch: 'x64', platform: 'darwin' },
appVersion: '0.0.7',
@@ -108,10 +79,5 @@ It basically sends: app version, processor architecture, operating system, used
transactionId: '53bf9aac-e695-40cc-9a81-b1cf3398843d' }
```
</details>
Even though the data is purely technical, an option to disable telemetry is planned. [#52](https://github.com/thomasnordquist/MQTT-Explorer/issues/52)
## License
[AGPL 3](./LICENSE)
-4
View File
@@ -9,7 +9,3 @@ logo: https://user-images.githubusercontent.com/7721625/52227945-5280b580-28b1-1
plugins:
- jekyll-seo-tag
google_analytics: UA-133696837-1
markdown: kramdown
kramdown:
parse_block_html: true
+6
View File
@@ -0,0 +1,6 @@
<html>
<head>
{% include head.html %}
{% seo %}
</head>
</html>
-86
View File
@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}" media="screen" type="text/css">
<link rel="stylesheet" href="{{ '/assets/css/print.css' | relative_url }}" media="print" type="text/css">
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
<style>
.inner img {
border-radius: 8px;
}
</style>
{% seo %}
</head>
<body>
<header>
<div class="inner">
<a href="{{ '/' | absolute_url }}">
<h1 style="display: inline">
<img src="./icon.png" style="
height: 1em;
display: inline;
vertical-align: top;
margin-top: -0.04em;
"></img>
{{ site.title | default: site.github.repository_name }}
</h1>
</a>
<h2>{{ site.description | default: site.github.project_tagline }}</h2>
{% if site.github.is_project_page %}
<a href="{{ site.github.repository_url }}" class="button"><small>View project on</small> GitHub</a>
{% endif %}
{% if site.github.is_user_page %}
<a href="{{ site.github.owner_url }}" class="button"><small>Follow me on</small> GitHub</a>
{% endif %}
</div>
</header>
<div id="content-wrapper">
<div class="inner clearfix">
<section id="main-content">
{{ content }}
</section>
<aside id="sidebar">
{% if site.show_downloads %}
<a href="{{ site.github.zip_url }}" class="button">
<small>Download</small>
.zip file
</a>
<a href="{{ site.github.tar_url }}" class="button">
<small>Download</small>
.tar.gz file
</a>
{% endif %}
{% if site.github.is_project_page %}
<h2>Author</h2>
<img src="{{ site.author.picture }}" width="200" />
<h3>{{ site.author.name }}</h3>
<p class="repo-owner"><a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
{% endif %}
</aside>
</div>
</div>
{% if site.google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}
</body>
</html>
+4 -4
View File
@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>MQTT Explorer</title>
<title>MQTT-Explorer</title>
<script src="./bugtracking.bundle.js"></script>
<style>
@@ -13,7 +13,7 @@
@keyframes example {
0% {background-color: none;}
25% {background-color: #3f51b5 ;}
25% {background-color: #3f51b5;}
50% {background-color: #3f51b5;}
100% {background-color: none;}
}
@@ -28,11 +28,11 @@
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(30,30,30,0.3);
-webkit-box-shadow: inset 0 0 6px rgba(30,30,30,0.3);
}
::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8);
background-color: rgba(140,140,140,0.8);
}
</style>
<style>
+9 -13
View File
@@ -9,57 +9,53 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@material-ui/core": "^4.0.0-alpha.1",
"@material-ui/icons": "^3.0.2",
"@material-ui/lab": "^3.0.0-alpha.30",
"@material-ui/styles": "^3.0.0-alpha.10",
"@types/diff": "^4.0.1",
"@material-ui/core": "^3.9.0",
"@material-ui/icons": "^3.0.1",
"@material-ui/styles": "^3.0.0-alpha.8",
"@types/node": "^10.12.18",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.12",
"@types/react-resize-detector": "^3.1.0",
"@types/react-split-pane": "^0.1.67",
"@types/sha1": "^1.1.1",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"@types/uuid": "^3.4.4",
"@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1",
"compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^1.0.4",
"css-loader": "^2.1.0",
"diff": "^4.0.1",
"electron-nucleus": "^1.11.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"html-webpack-plugin": "^4.0.0-beta.5",
"jquery": "^3.3.1",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"moment": "^2.24.0",
"moving-average": "^1.0.0",
"number-abbreviate": "^2.0.0",
"prismjs": "^1.15.0",
"react": "16.8",
"react": "^16.8.0-alpha.1",
"react-ace": "^6.3.2",
"react-dom": "^16.7.0",
"react-json-view": "^1.19.1",
"react-redux": "^6.0.0",
"react-resize-detector": "^3.4.0",
"react-split-pane": "^0.1.85",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "^0.4.1",
"redux-thunk": "^2.3.0",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"typescript": "^3.2.2",
"uuid": "^3.3.2",
"webpack": "^4.28.2",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14",
"webpack-livereload-plugin": "^2.2.0"
},
"dependencies": {
"socket.io": "^2.2.0"
}
}
+12 -41
View File
@@ -1,28 +1,25 @@
import * as React from 'react'
import ConnectionSetup from './components/ConnectionSetup/ConnectionSetup'
import * as q from '../../backend/src/Model'
import { Theme, withStyles } from '@material-ui/core/styles'
import { AppState } from './reducers'
import Connection from './components/ConnectionSetup/Connection'
import CssBaseline from '@material-ui/core/CssBaseline'
import ErrorBoundary from './ErrorBoundary'
import Notification from './components/Notification'
const Settings = React.lazy(() => import('./components/Settings'))
import Sidebar from './components/Sidebar/Sidebar'
import TitleBar from './components/TitleBar'
import Tree from './components/Tree/Tree'
import UpdateNotifier from './UpdateNotifier'
import { AppState } from './reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import ErrorBoundary from './ErrorBoundary'
import { default as SplitPane } from 'react-split-pane'
import { globalActions, settingsActions } from './actions'
import { Theme, withStyles } from '@material-ui/core/styles'
const Settings = React.lazy(() => import('./components/Settings'))
interface Props {
name: string
connectionId: string
classes: any
settingsVisible: boolean
error?: string
actions: typeof globalActions
settingsActions: typeof settingsActions
}
class App extends React.PureComponent<Props, {}> {
@@ -31,22 +28,6 @@ class App extends React.PureComponent<Props, {}> {
this.state = { }
}
public componentDidMount() {
this.props.settingsActions.loadSettings()
}
private renderError() {
if (this.props.error) {
const error = typeof this.props.error === 'string' ? this.props.error : JSON.stringify(this.props.error)
return (
<Notification
message={error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
}
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes
@@ -55,7 +36,6 @@ class App extends React.PureComponent<Props, {}> {
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
{this.renderError()}
<React.Suspense fallback={<div>Loading...</div>}>
<Settings />
</React.Suspense>
@@ -86,9 +66,9 @@ class App extends React.PureComponent<Props, {}> {
</div>
</div>
<UpdateNotifier />
<ConnectionSetup />
<Connection />
</ErrorBoundary>
</div>
</div >
)
}
}
@@ -97,8 +77,6 @@ const mapStateToProps = (state: AppState) => {
return {
settingsVisible: state.settings.visible,
connectionId: state.connection.connectionId,
error: state.globalState.error,
highlightTopicUpdates: state.settings.highlightTopicUpdates,
}
}
@@ -144,11 +122,4 @@ const styles = (theme: Theme) => {
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(globalActions, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
export default withStyles(styles)(connect(mapStateToProps)(App))
+8 -7
View File
@@ -1,9 +1,5 @@
import * as React from 'react'
import PersistantStorage from './PersistantStorage'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import Warning from '@material-ui/icons/Warning'
import { electronRendererTelementry } from 'electron-telemetry'
import { Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
Modal,
@@ -12,6 +8,11 @@ import {
Typography,
} from '@material-ui/core'
import Warning from '@material-ui/icons/Warning'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import { Theme, withStyles } from '@material-ui/core/styles'
interface State {
error?: Error
}
@@ -40,7 +41,7 @@ class ErrorBoundary extends React.Component<Props, State> {
}
private clearStorage = () => {
PersistantStorage.clear()
localStorage.clear()
window.location = window.location
}
@@ -88,7 +89,7 @@ const styles = (theme: Theme) => ({
maxWidth: 650,
backgroundColor: theme.palette.background.default,
margin: '10vh auto auto auto',
padding: theme.spacing(2),
padding: `${2 * theme.spacing.unit}px`,
outline: 'none',
},
title: {
@@ -104,7 +105,7 @@ const styles = (theme: Theme) => ({
},
buttonPositioning: {
textAlign: 'center' as 'center',
marginTop: theme.spacing(2),
marginTop: `${theme.spacing.unit * 2}px`,
},
})
-87
View File
@@ -1,87 +0,0 @@
import { rendererEvents } from '../../events'
import { v4 } from 'uuid'
import {
storageStoreEvent,
makeStorageResponseEvent,
storageLoadEvent,
storageClearEvent,
makeStorageAcknoledgementEvent,
} from '../../events/StorageEvents'
export interface StorageIdentifier<Model> {
id: string
}
export interface PersistantStorage {
store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void>
load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined>
clear(): Promise<void>
}
class RemoteStorage implements PersistantStorage {
private timeoutCallback(event: any, callback: any, reject: any) {
setTimeout(() => {
reject('remote storage timeout')
rendererEvents.unsubscribe(event, callback)
}, 10000)
}
private expectAck(transactionId: string): Promise<void> {
const ack = makeStorageAcknoledgementEvent(transactionId)
return new Promise<void>((resolve, reject) => {
const callback = (msg: any) => {
if (msg && msg.error) {
reject(msg.error)
} else {
resolve()
}
rendererEvents.unsubscribe(ack, callback)
}
rendererEvents.subscribe(ack, callback)
this.timeoutCallback(ack, callback, reject)
})
}
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
const transactionId = v4()
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageStoreEvent, { data, transactionId, store: identifier.id })
return expectation
}
public load<Model>(identifier: StorageIdentifier<Model>): Promise<Model | undefined> {
const transactionId = v4()
const responseEvent = makeStorageResponseEvent(transactionId)
const promise = new Promise<Model>((resolve, reject) => {
const callback = (msg: any) => {
if (msg.error) {
reject(msg.error)
} else {
resolve(msg.data)
}
rendererEvents.unsubscribe(responseEvent, callback)
}
rendererEvents.subscribe(responseEvent, callback)
this.timeoutCallback(responseEvent, callback, reject)
})
rendererEvents.emit(storageLoadEvent, {
transactionId,
store: identifier.id,
})
return promise
}
public clear(): Promise<void> {
const transactionId = v4()
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageClearEvent, { transactionId })
return expectation
}
}
export default new RemoteStorage()
+6 -5
View File
@@ -115,6 +115,7 @@ class UpdateNotifier extends React.Component<Props, State> {
vertical: 'top',
horizontal: 'right',
}
console.log(this.state.newerVersions)
return (
<Snackbar
@@ -231,14 +232,14 @@ const styles = (theme: Theme) => ({
color: theme.typography.button.color,
},
close: {
padding: '4px',
padding: theme.spacing.unit / 2,
},
root: {
minWidth: '350px',
maxWidth: '500px',
backgroundColor: theme.palette.background.default,
margin: '20vh auto auto auto',
padding: theme.spacing(2),
padding: `${2 * theme.spacing.unit}px`,
outline: 'none',
},
title: {
@@ -251,7 +252,7 @@ const styles = (theme: Theme) => ({
maxHeight: '28vh',
},
paper: {
padding: theme.spacing(2),
padding: `${theme.spacing.unit * 2}px`,
color: theme.palette.text.secondary,
},
download: {
@@ -265,8 +266,8 @@ const styles = (theme: Theme) => ({
const mapStateToProps = (state: AppState) => {
return {
showUpdateNotification: state.globalState.showUpdateNotification,
showUpdateDetails: state.globalState.showUpdateDetails,
showUpdateNotification: state.tooBigReducer.showUpdateNotification,
showUpdateDetails: state.tooBigReducer.showUpdateDetails,
}
}
+21 -49
View File
@@ -1,62 +1,33 @@
import * as q from '../../../backend/src/Model'
import * as url from 'url'
import { Action, ActionTypes } from '../reducers/Connection'
import { AppState } from '../reducers'
import { DataSourceState, MqttOptions } from '../../../backend/src/DataSource'
import { ActionTypes, Action, ConnectionState } from '../reducers/Connection'
import { MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { globalActions } from '.'
import { showError } from './Global'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events'
import { AppState } from '../reducers'
import * as q from '../../../backend/src/Model'
import { showTree } from './Tree'
import * as url from 'url'
import { TopicViewModel } from '../TopicViewModel'
import {
addMqttConnectionEvent,
makeConnectionStateEvent,
removeConnection,
rendererEvents,
} from '../../../events'
import { getRendererEvents } from '../communication'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
export const connect = (options: MqttOptions, connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
const rendererEvents = await getRendererEvents()
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
rendererEvents.subscribe(event, (dataSourceState) => {
console.log(dataSourceState)
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
}
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(connected(tree, host!))
dispatch(showTree(tree))
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
dispatch(updateHealth(dataSourceState))
})
}
const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<any>, getState: () => AppState) => {
let state
if (dataSourceState.connecting) {
state = 'connecting'
} else if (!dataSourceState.connected) {
state = 'offline'
dispatch(globalActions.showError('Disconnected from server'))
} else if (dataSourceState.connected) {
state = 'online'
} else {
state = undefined
}
console.log(state)
dispatch({
type: ActionTypes.CONNECTION_SET_HEALTH,
health: state,
})
}
@@ -71,13 +42,14 @@ export const connecting: (connectionId: string) => Action = (connectionId: strin
type: ActionTypes.CONNECTION_SET_CONNECTING,
})
export const showError = (error?: string) => ({
error,
type: ActionTypes.CONNECTION_SET_SHOW_ERROR,
})
export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId, tree } = getState().connection
if (connectionId) {
rendererEvents.emit(removeConnection, connectionId)
rendererEvents.unsubscribeAll(makeConnectionStateEvent(connectionId))
}
getRendererEvents().then(rendererEvents => rendererEvents.emit(removeConnection, connectionId))
tree && tree.stopUpdating()
dispatch(showTree(undefined))
-170
View File
@@ -1,170 +0,0 @@
import { AppState } from '../reducers'
import { clearLegacyConnectionOptions, loadLegacyConnectionOptions } from '../model/LegacyConnectionSettings'
import { ConnectionOptions, createEmptyConnection, makeDefaultConnections, CertificateParameters } from '../model/ConnectionOptions'
import { default as persistantStorage, StorageIdentifier } from '../PersistantStorage'
import { Dispatch } from 'redux'
import { showError } from './Global'
import { remote } from 'electron'
import * as fs from 'fs'
import * as path from 'path'
import {
ActionTypes,
Action,
} from '../reducers/ConnectionManager'
const storedConnectionsIdentifier: StorageIdentifier<{[s: string]: ConnectionOptions}> = {
id: 'ConnectionManager_connections',
}
export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
let connections
try {
await ensureConnectionsHaveBeenInitialized()
connections = await persistantStorage.load(storedConnectionsIdentifier)
} catch (error) {
dispatch(showError(error))
}
if (!connections) {
return
}
dispatch(setConnections(connections))
const firstKey = Object.keys(connections)[0]
if (firstKey) {
dispatch(selectConnection(firstKey))
}
}
export const selectCertificate = (connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const certificate = await openCertificate()
console.log(certificate)
dispatch(updateConnection(connectionId, {
selfSignedCertificate: certificate,
}))
} catch (error) {
console.log(error)
dispatch(showError(error))
}
}
async function openCertificate(): Promise<CertificateParameters> {
const rejectReasons = {
noCertificateSelected: 'No certificate selected',
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
}
return new Promise((resolve, reject) => {
remote.dialog.showOpenDialog({ properties: ['openFile'], securityScopedBookmarks: true }, (filePaths?: string[]) => {
const selectedFile = filePaths && filePaths[0]
if (!selectedFile) {
reject(rejectReasons.noCertificateSelected)
return
}
fs.readFile(selectedFile, (error, data) => {
if (error) {
reject(error)
return
}
if (data.length > 16_384 || data.length < 128) {
reject(rejectReasons.certificateSizeDoesNotMatch)
return
}
resolve({
data: data.toString('base64'),
name: path.basename(selectedFile),
})
})
})
})
}
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
console.log('store settings')
await persistantStorage.store(storedConnectionsIdentifier, getState().connectionManager.connections)
} catch (error) {
dispatch(showError(error))
}
}
export const updateConnection = (connectionId: string, changeSet: Partial<ConnectionOptions>): Action => ({
connectionId,
changeSet,
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,
})
export const addSubscription = (subscription: string, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION,
})
export const deleteSubscription = (subscription: string, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION,
})
export const createConnection = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const newConnection = createEmptyConnection()
dispatch(addConnection(newConnection))
dispatch(selectConnection(newConnection.id))
}
export const setConnections = (connections: {[s: string]: ConnectionOptions}): Action => ({
connections,
type: ActionTypes.CONNECTION_MANAGER_SET_CONNECTIONS,
})
export const selectConnection = (connectionId: string): Action => ({
selected: connectionId,
type: ActionTypes.CONNECTION_MANAGER_SELECT_CONNECTION,
})
export const addConnection = (connection: ConnectionOptions): Action => ({
connection,
type: ActionTypes.CONNECTION_MANAGER_ADD_CONNECTION,
})
export const toggleAdvancedSettings = (): Action => ({
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS,
})
export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionIds = Object.keys(getState().connectionManager.connections)
const connectionIdLocation = connectionIds.indexOf(connectionId)
const remainingIds = connectionIds.filter(id => id !== connectionId)
const nextSelectedConnectionIndex = Math.min(remainingIds.length - 1, connectionIdLocation)
const nextSelectedConnection = remainingIds[nextSelectedConnectionIndex]
dispatch({
connectionId,
type: ActionTypes.CONNECTION_MANAGER_DELETE_CONNECTION,
})
if (nextSelectedConnection) {
dispatch(selectConnection(nextSelectedConnection))
}
}
async function ensureConnectionsHaveBeenInitialized() {
const connections = await persistantStorage.load(storedConnectionsIdentifier)
const requiresInitialization = !connections
if (requiresInitialization) {
const migratedConnection = loadLegacyConnectionOptions()
const defaultConnections = makeDefaultConnections()
persistantStorage.store(storedConnectionsIdentifier, {
...migratedConnection,
...defaultConnections,
})
clearLegacyConnectionOptions()
}
}
-6
View File
@@ -1,6 +0,0 @@
import { ActionTypes } from '../reducers'
export const showError = (error?: string) => ({
error,
type: ActionTypes.showError,
})
+5 -3
View File
@@ -1,7 +1,8 @@
import { Action, ActionTypes } from '../reducers/Publish'
import { ActionTypes, Action } from '../reducers/Publish'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events'
import { makePublishEvent } from '../../../events'
import { getRendererEvents } from '../communication'
export const setTopic = (topic?: string): Action => {
return {
@@ -46,7 +47,8 @@ export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, ge
retain: state.publish.retain,
qos: state.publish.qos,
}
rendererEvents.emit(publishEvent, mqttMessage)
getRendererEvents().then(rendererEvents => rendererEvents.emit(publishEvent, mqttMessage))
}
export const toggleRetain = (): Action => {
+15 -76
View File
@@ -1,92 +1,31 @@
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { autoExpandLimitSet } from '../components/Settings'
import { batchActions } from 'redux-batched-actions'
import { default as persistantStorage, StorageIdentifier } from '../PersistantStorage'
import { Action, ActionTypes, TopicOrder } from '../reducers/Settings'
import { ActionTypes as TreeActionTypes } from '../reducers/Tree'
import { Dispatch } from 'redux'
import { showError } from './Global'
import { showTree } from './Tree'
import { AppState } from '../reducers'
import * as q from '../../../backend/src/Model'
import { batchActions } from 'redux-batched-actions'
import { autoExpandLimitSet } from '../components/Settings'
import { TopicViewModel } from '../TopicViewModel'
import {
ActionTypes,
SettingsState,
TopicOrder,
} from '../reducers/Settings'
const settingsIdentifier: StorageIdentifier<Partial<SettingsState>> = {
id: 'Settings',
}
export const loadSettings = () => async (dispatch: Dispatch<any>, _getState: () => AppState) => {
try {
const settings = await persistantStorage.load(settingsIdentifier)
dispatch({
settings,
type: ActionTypes.SETTINGS_DID_LOAD_SETTINGS,
})
} catch (error) {
dispatch(showError(error))
}
}
export const storeSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const settings = {
...getState().settings,
topicFilter: undefined,
visible: undefined,
}
try {
await persistantStorage.store(settingsIdentifier, settings)
} catch (error) {
dispatch(showError(error))
}
}
export const setAutoExpandLimit = (autoExpandLimit: number = 0) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
export const setAutoExpandLimit = (autoExpandLimit: number = 0): Action => {
return {
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
})
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) => {
dispatch({
export const toggleSettingsVisibility = (): Action => {
return {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY,
})
dispatch(storeSettings())
}
}
export const togglehighlightTopicUpdates = () => (dispatch: Dispatch<any>, _getState: () => AppState) => {
dispatch({
type: ActionTypes.SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY,
})
dispatch(storeSettings())
}
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none) => (dispatch: Dispatch<any>, _getState: () => AppState) => {
dispatch({
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none): Action => {
return {
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
})
dispatch(storeSettings())
}
}
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
+8 -31
View File
@@ -1,46 +1,23 @@
import * as q from '../../../backend/src/Model'
import { Action, Dispatch } from 'redux'
import { Dispatch, Action } from 'redux'
import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events'
import { makePublishEvent } from '../../../events'
import { getRendererEvents } from '../communication'
export const clearRetainedTopic = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic } = getState().tree
if (!selectedTopic) {
return
}
dispatch(clearTopic(selectedTopic, false))
}
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId } = getState().connection
if (!connectionId) {
if (!selectedTopic || !connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: topic.path(),
topic: selectedTopic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
console.log('deleting', topic.path())
rendererEvents.emit(publishEvent, mqttMessage)
if (recursive) {
topic.childTopics()
.filter(topic => Boolean(topic.message && topic.message.value))
.slice(0, subtopicClearLimit)
.forEach((topic) => {
console.log('deleting', topic.path())
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
})
}
getRendererEvents().then(rendererEvents => rendererEvents.emit(publishEvent, mqttMessage))
}
+4 -4
View File
@@ -1,10 +1,10 @@
import * as q from '../../../backend/src/Model'
import { ActionTypes } from '../reducers/Tree'
import { AnyAction, Dispatch } from 'redux'
import { AppState } from '../reducers'
import { batchActions } from 'redux-batched-actions'
import { ActionTypes } from '../reducers/Tree'
import * as q from '../../../backend/src/Model'
import { Dispatch, AnyAction } from 'redux'
import { setTopic } from './Publish'
import { TopicViewModel } from '../TopicViewModel'
import { batchActions } from 'redux-batched-actions'
const debounce = require('lodash.debounce')
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
+4 -6
View File
@@ -1,10 +1,8 @@
import * as connectionActions from './Connection'
import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global'
import * as publishActions from './Publish'
import * as settingsActions from './Settings'
import * as sidebarActons from './Sidebar'
import * as publishActions from './Publish'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection'
import * as sidebarActons from './Sidebar'
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons, connectionManagerActions, globalActions }
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons }
+28
View File
@@ -0,0 +1,28 @@
import * as io from 'socket.io-client'
import { EventBusInterface, Event } from '../../../events'
class SocketIoClient implements EventBusInterface {
private client?: any
constructor(url = 'http://localhost:3000') {
this.client = io(url)
}
public subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void) {
this.client.on(event.topic, callback)
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
this.client.removeAllListeners()
}
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
this.client.removeListener(event.topic, callback)
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
this.client.emit(event.topic, msg)
}
}
export const rendererEvents = new SocketIoClient()
+3
View File
@@ -0,0 +1,3 @@
export async function getRendererEvents() {
return (await import('./SocketIO')).rendererEvents
}
+9 -7
View File
@@ -1,11 +1,12 @@
import * as q from '../../../backend/src/Model'
import * as React from 'react'
import { AppState } from '../reducers'
import { connect } from 'react-redux'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../TopicViewModel'
import { Typography } from '@material-ui/core'
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { connect } from 'react-redux'
import { TopicViewModel } from '../TopicViewModel'
const abbreviate = require('number-abbreviate')
interface Stats {
@@ -23,6 +24,7 @@ const styles: StyleRulesCallback = theme => ({
height: '224px',
backgroundColor: 'rebeccapurple',
marginBottom: 0,
marginTop: 'auto',
padding: '8px',
},
})
@@ -40,7 +42,7 @@ class BrokerStatistics extends React.Component<Props, {}> {
public render() {
const { tree, classes } = this.props
if (!tree || !tree.findNode('$SYS/broker/clients/total')) {
if (!tree || !tree.findNode('$SYS')) {
return null
}
-149
View File
@@ -1,149 +0,0 @@
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)
@@ -1,54 +0,0 @@
import * as React from 'react'
import DeviceHubOutlined from '@material-ui/icons/DeviceHubOutlined'
import { AppState } from '../reducers'
import { connect } from 'react-redux'
import { ConnectionHealth } from '../reducers/Connection'
import { green, orange, red } from '@material-ui/core/colors'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { Tooltip } from '@material-ui/core'
const styles: StyleRulesCallback = theme => ({
offline: {
color: red[700],
},
online: {
color: green[500],
},
connecting: {
color: orange[600],
},
})
interface Props {
classes: any
connected: boolean
health?: ConnectionHealth
}
class ConnectionHealthIndicator extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
}
public render() {
const { classes, health, connected } = this.props
if (!health || !connected) {
return null
}
return (
<Tooltip title={`Connection health "${health}"`}>
<DeviceHubOutlined className={classes[health]} />
</Tooltip>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
health: state.connection.health,
connected: state.connection.connected || state.connection.connecting,
}
}
export default connect(mapStateToProps)(withStyles(styles)(ConnectionHealthIndicator))
@@ -1,32 +0,0 @@
import * as React from 'react'
import Add from '@material-ui/icons/Add'
import { Fab } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
const styles = (theme: Theme) => ({
addButton: {
height: theme.spacing(4),
width: theme.spacing(4),
minHeight: '0',
},
addIcon: {
height: theme.spacing(2),
},
})
export const AddButton = withStyles(styles)((props: {
classes: any,
action: any,
}) => {
return (
<Fab
size="small"
color="secondary"
aria-label="Add"
className={props.classes.addButton}
onClick={props.action}
>
<Add className={props.classes.addIcon} />
</Fab>
)
})
@@ -1,204 +0,0 @@
import * as React from 'react'
import Add from '@material-ui/icons/Add'
import Delete from '@material-ui/icons/Delete'
import Undo from '@material-ui/icons/Undo'
import Lock from '@material-ui/icons/Lock'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
Grid,
IconButton,
TextField,
List,
ListItem,
ListItemText,
Tooltip,
Typography,
} from '@material-ui/core'
import ClearAdornment from '../helper/ClearAdornment';
interface Props {
connection: ConnectionOptions
classes: any
managerActions: typeof connectionManagerActions
}
interface State {
subscription: string
}
class ConnectionSettings extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { subscription: '' }
}
private handleChange = (name: string) => (event: any) => {
this.props.managerActions.updateConnection(this.props.connection.id, {
[name]: event.target.value,
})
}
public render() {
const { classes } = this.props
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={10} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="Subscription"
margin="normal"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => this.setState({ subscription: event.target.value })}
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
className={classes.button}
color="secondary"
onClick={() => this.props.managerActions.addSubscription(this.state.subscription, this.props.connection.id)}
variant="contained"
>
<Add /> Add
</Button>
</Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}>
<List
className={`${classes.topicList} advanced-connection-settings-topic-list`}
component="nav"
>
<div className={classes.list}>
{this.renderSubscriptions()}
</div>
</List>
</Grid>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="MQTT Client ID"
margin="normal"
value={this.props.connection.clientId}
onChange={this.handleChange('clientId')}
/>
</Grid>
<Grid item={true} xs={3} className={classes.gridPadding}>
<div>
<Tooltip title="Select certificate to verify authenticity of a self-signed certificate" placement="top">
<Button
variant="contained"
className={classes.button}
onClick={() => this.props.managerActions.selectCertificate(this.props.connection.id)}
>
<Lock /> Certificate
</Button>
</Tooltip>
{this.renderCertificateInfo()}
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
onClick={this.props.managerActions.toggleAdvancedSettings}
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
)
}
private renderCertificateInfo() {
if (!this.props.connection.selfSignedCertificate) {
return null
}
return (
<span>
<Tooltip title={this.props.connection.selfSignedCertificate.name}>
<Typography className={this.props.classes.certificateName}>
<ClearAdornment action={this.clearCertificate} value={this.props.connection.selfSignedCertificate.name} />
{this.props.connection.selfSignedCertificate.name}
</Typography>
</Tooltip>
</span>
)
}
private clearCertificate = () => {
this.props.managerActions.updateConnection(this.props.connection.id, {
selfSignedCertificate: undefined,
})
}
private renderSubscriptions() {
const connection = this.props.connection
return connection.subscriptions.map(subscription => (
<Subscription
deleteAction={() => this.props.managerActions.deleteSubscription(subscription, connection.id)}
subscription={subscription}
key={subscription}
/>
))
}
}
const Subscription = (props: {
subscription: string,
deleteAction: any,
}) => {
return (
<ListItem style={{ padding: '0 0 0 8px' }}>
<ListItemText>
<IconButton onClick={props.deleteAction} style={{ padding: '6px' }}>
<Delete />
</IconButton>
{props.subscription}</ListItemText>
</ListItem>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
fullWidth: {
width: '100%',
},
gridPadding: {
padding: '0 12px !important',
},
topicList: {
height: '180px',
overflowY: 'scroll' as 'scroll',
margin: '8px 16px',
backgroundColor: theme.palette.background.default,
},
button: {
marginTop: theme.spacing(3),
float: 'right',
},
certificateName: {
width: '100%',
height: 'calc(1em + 4px)',
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.hint,
},
}
}
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
@@ -0,0 +1,395 @@
import * as React from 'react'
import {
Button,
CircularProgress,
FormControl,
FormControlLabel,
Grid,
IconButton,
Input,
InputAdornment,
InputLabel,
MenuItem,
Modal,
Paper,
Switch,
TextField,
Toolbar,
Typography,
} from '@material-ui/core'
import { connect } from 'react-redux'
import { MqttOptions } from '../../../../backend/src/DataSource'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import Notification from './Notification'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
const sha1 = require('sha1')
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connectionActions } from '../../actions'
interface Props {
classes: {[s: string]: string}
actions: typeof connectionActions,
visible: boolean
connected: boolean
connecting: boolean
error?: string
}
const protocols = [
'mqtt://',
'ws://',
]
interface State {
showPassword: boolean
connectionSettings: ConnectionSettings
}
interface ConnectionSettings {
host: string
protocol: string
port: number
tls: boolean
certValidation: boolean
clientId: string
connectionId?: string
username: string
password: string
}
declare var window: any
class Connection extends React.Component<Props, State> {
private randomClientId: string
private defaultConnectionSettings: ConnectionSettings = {
host: 'iot.eclipse.org',
protocol: protocols[0],
port: 1883,
tls: false,
certValidation: true,
clientId: '',
username: '',
password: '',
connectionId: undefined,
}
constructor(props: any) {
super(props)
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
this.randomClientId = `mqtt-explorer-${clientIdSha}`
this.state = {
connectionSettings: this.loadConnectionSettings(),
showPassword: false,
}
}
private loadConnectionSettings(): ConnectionSettings {
let storedSettings: ConnectionSettings | undefined
const storedSettingsString = window.localStorage.getItem('connectionSettings')
try {
storedSettings = storedSettingsString ? JSON.parse(storedSettingsString) : undefined
} catch {
window.localStorage.setItem('connectionSettings', undefined)
}
return storedSettings || this.defaultConnectionSettings
}
private saveConnectionSettings() {
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state.connectionSettings))
}
private handleClickShowPassword = () => {
this.setState({ showPassword: !this.state.showPassword })
}
private optionsFromState(): MqttOptions {
const protocol = this.state.connectionSettings.protocol === 'tcp://' ? 'mqtt://' : this.state.connectionSettings.protocol
const url = `${protocol}${this.state.connectionSettings.host}:${this.state.connectionSettings.port}`
return {
url,
username: this.state.connectionSettings.username || undefined,
password: this.state.connectionSettings.password || undefined,
clientId: this.state.connectionSettings.clientId || this.randomClientId,
tls: this.state.connectionSettings.tls,
certValidation: this.state.connectionSettings.certValidation,
}
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
root: {
minWidth: 550,
maxWidth: 650,
backgroundColor: theme.palette.background.default,
margin: '14vh auto auto auto',
padding: `${2 * theme.spacing.unit}px`,
outline: 'none',
},
title: {
color: theme.palette.text.primary,
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
textField: {
width: '100%',
},
switch: {
marginTop: `${1 * theme.spacing.unit}px`,
},
button: {
margin: theme.spacing.unit,
},
inputFormControl: {
marginTop: '16px',
},
}
}
private handleChange = (name: string) => (event: any) => {
this.setState({
connectionSettings: {
...this.state.connectionSettings,
[name]: event.target.value,
},
})
}
public render() {
const { classes } = this.props
const passwordVisibilityButton = (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
>
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
let renderError = null
if (this.props.error) {
renderError = (
<Notification
message={this.props.error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
return (
<div>
{renderError}
<Modal open={this.props.visible} disableAutoFocus={true}>
<Paper className={classes.root}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
</Toolbar>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={24}>
<Grid item={true} xs={2}>
{this.renderProtocols()}
</Grid>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={this.state.connectionSettings.host}
onChange={this.handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={3}>
<TextField
label="Port"
className={classes.textField}
value={this.state.connectionSettings.port}
onChange={this.handleChange('port')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={5}>
<TextField
label="Username"
className={classes.textField}
value={this.state.connectionSettings.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={5}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="adornment-password">Password</InputLabel>
<Input
id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'}
value={this.state.connectionSettings.password}
onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton}
/>
</FormControl>
</Grid>
<Grid item={true} xs={5}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="client-id">Client ID</InputLabel>
<Input
placeholder={this.randomClientId}
className={classes.textField}
value={this.state.connectionSettings.clientId || ''}
onChange={this.handleChange('clientId')}
startAdornment={<span />}
/>
</FormControl>
</Grid>
<Grid item={true} xs={4}>
{this.renderCertValidationSwitch()}
</Grid>
<Grid item={true} xs={3}>
{this.renderTlsSwitch()}
</Grid>
</Grid>
<br />
<div style={{ textAlign: 'right' }}>
<Button variant="contained" color="secondary" className={classes.button} onClick={() => this.saveConnectionSettings()}>
Save
</Button>
{this.renderConnectButton()}
</div>
</form>
</Paper>
</Modal>
</div>
)
}
private renderProtocols() {
const { classes } = this.props
const protocolItems = protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))
return (
<TextField
select={true}
label="Protocol"
className={classes.textField}
value={this.state.connectionSettings.protocol}
onChange={this.handleChange('protocol')}
margin="normal"
>
{protocolItems}
</TextField>
)
}
private renderCertValidationSwitch() {
const { classes } = this.props
const certSwitch = (
<Switch
checked={this.state.connectionSettings.certValidation}
onChange={this.toggleCertValidation}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={certSwitch}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
)
}
private toggleCertValidation = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
certValidation: !this.state.connectionSettings.certValidation,
},
})
private renderTlsSwitch() {
const { classes } = this.props
const tlsSwitch = (
<Switch
checked={this.state.connectionSettings.tls}
onChange={this.toggleTls}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={tlsSwitch}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
)
}
private toggleTls = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
tls: !this.state.connectionSettings.tls,
},
})
private renderConnectButton() {
const { classes, actions } = this.props
if (this.props.connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
<CircularProgress size={22} style={{ marginRight: '10px' }} color="secondary" /> Abort
</Button>
)
}
return (
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
Connect
</Button>
)
}
private onClickConnect = () => {
const connectionId = String(sha1(String(Math.random())).slice(0, 8))
const options = this.optionsFromState()
this.props.actions.connect(options, connectionId)
}
}
const mapStateToProps = (state: AppState) => {
return {
visible: !state.connection.connected,
connected: state.connection.connected,
connecting: state.connection.connecting,
error: state.connection.error,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(Connection.styles)(Connection))
@@ -1,341 +0,0 @@
import * as React from 'react'
import Delete from '@material-ui/icons/Delete'
import Settings from '@material-ui/icons/Settings'
import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew'
import Save from '@material-ui/icons/Save'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
FormControl,
FormControlLabel,
Grid,
IconButton,
Input,
InputAdornment,
InputLabel,
MenuItem,
Switch,
TextField,
} from '@material-ui/core'
import ConnectionHealthIndicator from '../ConnectionHealthIndicator'
interface Props {
connection: ConnectionOptions
classes: {[s: string]: string}
actions: typeof connectionActions,
managerActions: typeof connectionManagerActions
connected: boolean
connecting: boolean
}
const protocols = [
'mqtt',
'ws',
]
interface State {
showPassword: boolean
}
class ConnectionSettings extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = {
showPassword: false,
}
}
private handleClickShowPassword = () => {
this.setState({ showPassword: !this.state.showPassword })
}
private requiresBasePath() {
return this.props.connection.protocol !== 'mqtt'
}
private renderBasePathInput() {
return (
<Grid item={true} xs={4}>
<TextField
label="Basepath"
className={this.props.classes.textField}
value={this.props.connection.basePath}
onChange={this.handleChange('basePath')}
margin="normal"
/>
</Grid>
)
}
private handleChange = (name: string) => (event: any) => {
if (!this.props.connection) {
return
}
this.updateConnection(name, event.target.value)
}
private updateConnection(name: string, value: any) {
this.props.managerActions.updateConnection(this.props.connection.id, {
[name]: value,
})
}
public render() {
const { classes, connection } = this.props
const passwordVisibilityButton = (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
>
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={5}>
<TextField
label="Name"
className={classes.textField}
value={connection.name}
onChange={this.handleChange('name')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={4}>
{this.renderCertValidationSwitch()}
</Grid>
<Grid item={true} xs={3}>
{this.renderTlsSwitch()}
</Grid>
<Grid item={true} xs={2}>
{this.renderProtocols()}
</Grid>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={connection.host}
onChange={this.handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={3}>
<TextField
label="Port"
className={classes.textField}
value={connection.port}
onChange={this.handleChange('port')}
margin="normal"
/>
</Grid>
{this.requiresBasePath() ? this.renderBasePathInput() : null}
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
<TextField
label="Username"
className={classes.textField}
value={connection.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="adornment-password">Password</InputLabel>
<Input
id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'}
value={connection.password}
onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton}
/>
</FormControl>
</Grid>
</Grid>
<br />
<div>
<div style={{ float: 'left' }}>
<Button variant="contained" className={classes.button} onClick={() => this.props.managerActions.deleteConnection(this.props.connection.id)}>
Delete <Delete />
</Button>
<Button variant="contained" className={classes.button} onClick={this.props.managerActions.toggleAdvancedSettings}>
<Settings /> Advanced
</Button>
</div>
<div style={{ float : 'right' }}>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.managerActions.saveConnectionSettings}>
<Save /> Save
</Button>
{this.renderConnectButton()}
</div>
</div>
</form>
</div>
)
}
private renderProtocols() {
const { classes, connection } = this.props
const protocolItems = protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}://
</MenuItem>
))
return (
<TextField
select={true}
label="Protocol"
className={classes.textField}
value={connection.protocol}
onChange={this.updateProtocol}
margin="normal"
>
{protocolItems}
</TextField>
)
}
private updateProtocol = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
this.updateConnection('protocol', value)
if (event.target.value === 'mqtt') {
this.updateConnection('basePath', undefined)
} else {
this.updateConnection('basePath', 'ws')
}
}
private renderCertValidationSwitch() {
const { classes, connection } = this.props
const certSwitch = (
<Switch
checked={connection.certValidation}
onChange={this.toggleCertValidation}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={certSwitch}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
)
}
private toggleCertValidation = () => {
this.props.managerActions.updateConnection(this.props.connection.id, {
certValidation: !this.props.connection.certValidation,
})
}
private renderTlsSwitch() {
const { classes, connection } = this.props
const tlsSwitch = (
<Switch
checked={connection.encryption}
onChange={this.toggleTls}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={tlsSwitch}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
)
}
private toggleTls = () => {
this.props.managerActions.updateConnection(this.props.connection.id, {
encryption: !this.props.connection.encryption,
})
}
private renderConnectButton() {
const { classes, actions } = this.props
if (this.props.connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
<ConnectionHealthIndicator />&nbsp;&nbsp;Abort
</Button>
)
}
return (
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
<PowerSettingsNew /> Connect
</Button>
)
}
private onClickConnect = () => {
if (!this.props.connection) {
return
}
const mqttOptions = toMqttConnection(this.props.connection)
if (mqttOptions) {
this.props.actions.connect(mqttOptions, this.props.connection.id)
}
}
}
const mapStateToProps = (state: AppState) => {
return {
connected: state.connection.connected,
connecting: state.connection.connecting,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionActions, dispatch),
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
textField: {
width: '100%',
},
switch: {
marginTop: 0,
},
button: {
margin: theme.spacing(1),
},
inputFormControl: {
marginTop: '16px',
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
@@ -1,128 +0,0 @@
import * as React from 'react'
import ConnectionSettings from './ConnectionSettings'
import ProfileList from './ProfileList'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import {
Modal,
Paper,
Toolbar,
Typography,
Collapse,
} from '@material-ui/core'
import AdvancedConnectionSettings from './AdvancedConnectionSettings'
interface Props {
actions: any
classes: any
connection?: ConnectionOptions
visible: boolean
showAdvancedSettings: boolean
}
class ConnectionSetup extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
public componentDidMount() {
this.props.actions.loadConnectionSettings()
}
public render() {
const { classes, visible, connection } = this.props
const mqttConnection = connection && toMqttConnection(connection)
return (
<div>
<Modal open={visible} disableAutoFocus={true}>
<Paper className={classes.root}>
<div className={classes.left}><ProfileList /></div>
<div className={classes.right} key={connection && connection.id}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
<Typography className={classes.connectionUri}>
{mqttConnection && mqttConnection.url}
</Typography>
</Toolbar>
{this.renderSettings()}
</div>
</Paper>
</Modal>
</div>
)
}
private renderSettings() {
const { connection, showAdvancedSettings } = this.props
if (!connection) {
return null
}
return (
<div>
<Collapse in={!showAdvancedSettings}><ConnectionSettings connection={connection} /></Collapse>
<Collapse in={showAdvancedSettings}><AdvancedConnectionSettings connection={connection} /></Collapse>
</div>
)
}
}
const connectionHeight = '440px'
const styles = (theme: Theme) => ({
title: {
color: theme.palette.text.primary,
whiteSpace: 'nowrap' as 'nowrap',
},
root: {
margin: `calc((100vh - ${connectionHeight}) / 2) auto 0 auto`,
minWidth: '800px',
maxWidth: '850px',
height: connectionHeight,
outline: 'none' as 'none',
display: 'flex' as 'flex',
},
left: {
borderRightStyle: 'dotted' as 'dotted',
borderRadius: `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`,
paddingTop: theme.spacing(2),
flex: 3,
overflow: 'hidden',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
},
right: {
borderRadius: `0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0`,
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
flex: 10,
},
connectionUri: {
width: '27em',
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
color: theme.palette.text.hint,
fontSize: '0.9em',
marginLeft: theme.spacing(4),
},
})
const mapStateToProps = (state: AppState) => {
return {
visible: !state.connection.connected,
showAdvancedSettings: state.connectionManager.showAdvancedSettings,
connection: state.connectionManager.selected ? state.connectionManager.connections[state.connectionManager.selected] : undefined,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionManagerActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
@@ -1,113 +0,0 @@
import * as React from 'react'
import { AddButton } from './AddButton'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import {
List,
ListItem,
ListSubheader,
Typography,
} from '@material-ui/core'
interface Props {
classes: any
selected?: string
connections: {[s: string]: ConnectionOptions}
actions: any
}
class ProfileList extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
private addConnectionButton() {
return <span id="addProfileButton" style={{ marginRight: '12px' }}><AddButton action={this.props.actions.createConnection} /></span>
}
public render() {
return (
<List
style={{ height: '100%' }}
component="nav"
subheader={<ListSubheader component="div">{this.addConnectionButton()}Connections</ListSubheader>}
>
<div className={this.props.classes.list}>
{Object.values(this.props.connections).map(connection => <ConnectionItem connection={connection} key={connection.id} selected={this.props.selected === connection.id} />)}
</div>
</List>
)
}
}
const styles = (theme: Theme) => ({
list: {
marginTop: theme.spacing(1),
height: `calc(100% - ${theme.spacing(6)})`,
overflowY: 'auto' as 'auto',
},
})
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionManagerActions, dispatch),
}
}
interface ConnectionItemProps {
connection: ConnectionOptions,
actions: any,
selected: boolean,
classes: any
}
const connectionItemStyle = (theme: Theme) => ({
name: {
width: '100%',
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
},
details: {
width: '100%',
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
color: theme.palette.text.hint,
fontSize: '0.7em',
},
})
const connectionItemRenderer = withStyles(connectionItemStyle)((props: ConnectionItemProps) => {
const connection = props.connection.host && toMqttConnection(props.connection)
return (
<ListItem
button={true}
selected={props.selected}
style={{ display: 'block' }}
onClick={() => props.actions.selectConnection(props.connection.id)}
>
<Typography className={props.classes.name}>
{props.connection.name || 'mqtt broker'}
</Typography>
<Typography className={props.classes.details}>
{connection && connection.url}
</Typography>
</ListItem>
)
})
const ConnectionItem = connect(null, mapDispatchToProps)(connectionItemRenderer)
const mapStateToProps = (state: AppState) => {
return {
connections: state.connectionManager.connections,
selected: state.connectionManager.selected,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
+21 -30
View File
@@ -1,10 +1,9 @@
import * as React from 'react'
import Check from '@material-ui/icons/Check'
import CustomIconButton from './CustomIconButton'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import FileCopy from '@material-ui/icons/FileCopy'
import Check from '@material-ui/icons/Check'
import green from '@material-ui/core/colors/green'
import { Snackbar, SnackbarContent, Tooltip } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { withStyles, Theme } from '@material-ui/core/styles'
const copy = require('copy-text-to-clipboard')
@@ -33,36 +32,28 @@ class Copy extends React.Component<Props, State> {
public render() {
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" />
? <FileCopy fontSize="inherit" style={{ cursor: 'pointer' }} onClick={this.handleClick} />
: <Check fontSize="inherit" style={{ cursor: 'default' }} />
return (
return <span>
<span style={{ fontSize: '16px' }}>{icon}</span>
<span>
<Tooltip placement="top" title="Copy to clipboard">
<span style={{ fontSize: '16px' }}>
<CustomIconButton onClick={this.handleClick} >
{icon}
</CustomIconButton>
</span>
</Tooltip>
<span>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.snackBarOpen}
autoHideDuration={2000}
onClose={() => { this.setState({ snackBarOpen: false }) }}
>
<SnackbarContent
className={this.props.classes.snackbar}
message="Copied to clipboard"
/>
</Snackbar>
</span>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.snackBarOpen}
autoHideDuration={2000}
onClose={() => { this.setState({ snackBarOpen: false }) }}
>
<SnackbarContent
className={this.props.classes.snackbar}
message="Copied to clipboard"
/>
</Snackbar>
</span>
)
</span>
}
private handleClick = (event: React.MouseEvent) => {
-34
View File
@@ -1,34 +0,0 @@
import * as React from 'react'
import { IconButton } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
interface Props {
onClick: any,
classes: any
}
const styles = (theme: Theme) => ({
button: {
padding: '6px',
fontSize: '1.2em',
},
})
class CustomIconButton extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
public render() {
return (
<IconButton className={this.props.classes.button} onClick={this.onClick}>{this.props.children}</IconButton>
)
}
private onClick = (event: React.MouseEvent) => {
event.stopPropagation()
this.props.onClick(event)
}
}
export default withStyles(styles)(CustomIconButton)
+15 -88
View File
@@ -1,14 +1,6 @@
import * as React from 'react'
import BrokerStatistics from './BrokerStatistics'
import ChevronRight from '@material-ui/icons/ChevronRight'
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 { AppState } from '../reducers'
import {
Divider,
Drawer,
@@ -18,10 +10,15 @@ import {
MenuItem,
Select,
Typography,
Switch,
Tooltip,
} from '@material-ui/core'
const sha1 = require('sha1')
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import ChevronRight from '@material-ui/icons/ChevronRight'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions, treeActions } from '../actions'
import { TopicOrder } from '../reducers/Settings'
import BrokerStatistics from './BrokerStatistics'
export const autoExpandLimitSet = [{
limit: 0,
@@ -44,39 +41,28 @@ const styles: StyleRulesCallback = theme => ({
drawer: {
backgroundColor: theme.palette.background.default,
flexShrink: 0,
userSelect: 'none' as 'none',
},
paper: {
width: '300px',
},
title: {
color: theme.palette.text.primary,
paddingTop: theme.spacing(1),
paddingTop: `${theme.spacing.unit}px`,
...theme.mixins.toolbar,
},
input: {
minWidth: '150px',
margin: `auto ${theme.spacing(1)} auto ${theme.spacing(2)}px`,
},
author: {
margin: 'auto 8px 8px auto',
color: theme.palette.text.hint,
cursor: 'pointer' as 'pointer',
},
switchBase: {
height: theme.spacing(4),
margin: `auto ${theme.spacing.unit}px auto ${2 * theme.spacing.unit}px`,
},
})
interface Props {
actions: typeof settingsActions
autoExpandLimit: number
classes: any
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
visible: boolean
store?: any
topicOrder: TopicOrder
visible: boolean
classes: any
actions: typeof settingsActions
}
class Settings extends React.Component<Props, {}> {
@@ -110,69 +96,12 @@ class Settings extends React.Component<Props, {}> {
{this.renderAutoExpand()}
{this.renderNodeOrder()}
{this.renderHighlightTopicUpdates()}
{this.selectTopicsOnMouseOver()}
</div>
<Tooltip placement="top" title="App Author">
<Typography className={classes.author} onClick={this.openGithubPage}>
by Thomas Nordquist
</Typography>
</Tooltip>
<BrokerStatistics />
</Drawer>
)
}
private openGithubPage = () => {
shell.openExternal('https://github.com/thomasnordquist')
}
private renderHighlightTopicUpdates() {
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 (
<div style={{ padding: '8px', display: 'flex' }}>
<Tooltip title={tooltip}>
<InputLabel
htmlFor={`toggle-${sha1(title)}`}
onClick={clickHandler}
style={{ flex: '1', paddingTop: '8px' }}
>
{title}
</InputLabel>
</Tooltip>
<Tooltip title={tooltip}>
<Switch
name={`toggle-${sha1(title)}`}
checked={checked}
onChange={action}
color="primary"
classes={{ switchBase: classes.switchBase }}
/>
</Tooltip>
</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() {
const { classes, autoExpandLimit } = this.props
@@ -203,7 +132,7 @@ class Settings extends React.Component<Props, {}> {
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Topic Order</InputLabel>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Topic order</InputLabel>
<Select
value={topicOrder}
onChange={this.onChangeSorting}
@@ -231,8 +160,6 @@ const mapStateToProps = (state: AppState) => {
autoExpandLimit: state.settings.autoExpandLimit,
topicOrder: state.settings.topicOrder,
visible: state.settings.visible,
highlightTopicUpdates: state.settings.highlightTopicUpdates,
selectTopicWithMouseOver: state.settings.selectTopicWithMouseOver,
}
}
+7 -8
View File
@@ -4,16 +4,15 @@ import { Badge, Typography } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
interface HistoryItem {
title: JSX.Element | string
value: string | any
selected?: boolean
title: string
value: string
}
interface Props {
items: HistoryItem[]
onClick?: (index: number, element: EventTarget) => void
classes: any
contentTypeIndicator?: JSX.Element
contentTypeIndicator?: String
}
interface State {
@@ -29,18 +28,18 @@ class MessageHistory extends React.Component<Props, State> {
}
public renderHistory() {
const style = (element: HistoryItem) => ({
backgroundColor: element.selected ? 'rgba(120, 120, 120, 0.6)' : 'rgba(80, 80, 80, 0.6)',
const style = {
backgroundColor: 'rgba(80, 80, 80, 0.6)',
margin: '8px',
padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit',
})
}
const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }
const elements = this.props.items.map((element, index) => (
<div
key={index}
style={style(element)}
style={style}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
>
<div><i>{element.title}</i></div>
@@ -1,17 +1,16 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import BarChart from '@material-ui/icons/BarChart'
import DateFormatter from '../helper/DateFormatter'
import History from './History'
import { TopicViewModel } from '../../TopicViewModel'
const PlotHistory = React.lazy(() => import('./PlotHistory'))
const throttle = require('lodash.throttle')
interface Props {
node?: q.TreeNode<TopicViewModel>
selected?: q.Message
onSelect: (message: q.Message) => void
}
@@ -53,7 +52,6 @@ class MessageHistory extends React.Component<Props, State> {
const historyElements = history.reverse().map(message => ({
title: <DateFormatter date={message.received} />,
value: message.value,
selected: message && message === this.props.selected,
}))
const numericMessages = history.filter(message => !isNaN(parseFloat(message.value)))
@@ -63,7 +61,7 @@ class MessageHistory extends React.Component<Props, State> {
<div>
<History
items={historyElements}
contentTypeIndicator={showPlot ? <BarChart /> : undefined}
contentTypeIndicator={showPlot ? <BarChart /> : null}
onClick={this.displayMessage}
>
{showPlot ? this.renderPlot(numericMessages) : null}
@@ -81,10 +79,8 @@ class MessageHistory extends React.Component<Props, State> {
}
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
if (message) {
this.props.onSelect(message)
}
const message = this.props.node && this.props.node.messageHistory.toArray()[index]
this.props.onSelect(message)
}
}
+3 -2
View File
@@ -1,7 +1,8 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { TopicViewModel } from '../../TopicViewModel'
import * as q from '../../../../backend/src/Model'
import { Typography } from '@material-ui/core'
import { TopicViewModel } from '../../TopicViewModel'
interface Props {
node: q.TreeNode<TopicViewModel>
+5 -4
View File
@@ -1,9 +1,10 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import DateFormatter from '../helper/DateFormatter'
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 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = require('react-vis')
interface Props {
messages: q.Message[]
+2 -31
View File
@@ -19,7 +19,6 @@ import {
Checkbox,
MenuItem,
Tooltip,
Fab,
} from '@material-ui/core'
// tslint:disable-next-line
@@ -28,10 +27,10 @@ import { AppState } from '../../../reducers'
import History from '../History'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import Clear from '@material-ui/icons/Clear'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { publishActions, globalActions } from '../../../actions'
import { publishActions } from '../../../actions'
import ClearAdornment from '../../helper/ClearAdornment'
import { TopicViewModel } from '../../../TopicViewModel'
@@ -41,7 +40,6 @@ interface Props {
topic?: string
payload?: string
actions: typeof publishActions
globalActions: typeof globalActions
retain: boolean
editorMode: string
qos: 0 | 1 | 2
@@ -153,37 +151,11 @@ 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() {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
{this.renderEditorModeSelection()}
{this.renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()}
</div>
@@ -327,7 +299,6 @@ class Publish extends React.Component<Props, State> {
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(publishActions, dispatch),
globalActions: bindActionCreators(globalActions, dispatch),
}
}
+52 -123
View File
@@ -1,48 +1,43 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import Code from '@material-ui/icons/Code'
import Copy from '../Copy'
import CustomIconButton from '../CustomIconButton'
import DateFormatter from '../helper/DateFormatter'
import Delete from '@material-ui/icons/Delete'
import ExpandMore from '@material-ui/icons/ExpandMore'
import MessageHistory from './MessageHistory'
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 { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions, sidebarActons } from '../../actions'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../TopicViewModel'
import { ValueRendererDisplayMode } from '../../reducers/Settings'
import * as q from '../../../../backend/src/Model'
import {
Button,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Fade,
Paper,
Popper,
Typography,
Tooltip,
Badge,
} from '@material-ui/core'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { sidebarActons } from '../../actions'
import { AppState } from '../../reducers'
import Copy from '../Copy'
import DateFormatter from '../helper/DateFormatter'
import ExpandMore from '@material-ui/icons/ExpandMore'
import Clear from '@material-ui/icons/Clear'
import MessageHistory from './MessageHistory'
import NodeStats from './NodeStats'
import Topic from './Topic'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { TopicViewModel } from '../../TopicViewModel'
import { default as ReactResizeDetector } from 'react-resize-detector'
const throttle = require('lodash.throttle')
const Publish = React.lazy(() => import('./Publish/Publish'))
const ValueRenderer = React.lazy(() => import('./ValueRenderer'))
interface Props {
node?: q.TreeNode<TopicViewModel>
actions: typeof sidebarActons
settingsActions: typeof settingsActions
valueRendererDisplayMode: ValueRendererDisplayMode
classes: any
connectionId?: string
node?: q.TreeNode<TopicViewModel>,
actions: typeof sidebarActons,
classes: any,
connectionId?: string,
}
interface State {
@@ -89,7 +84,7 @@ class Sidebar extends React.Component<Props, State> {
public render() {
return (
<div id="Sidebar" className={this.props.classes.drawer}>
<div className={this.props.classes.drawer}>
{this.renderNode()}
</div>
)
@@ -97,105 +92,42 @@ class Sidebar extends React.Component<Props, State> {
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
private renderTopicDeleteButton() {
if (!this.props.node || (!this.props.node.message || !this.props.node.message.value)) {
return null
}
return (
<CustomIconButton onClick={() => this.deleteTopic(this.props.node)}>
<Tooltip title="Clear this topic">
<Delete />
</Tooltip>
</CustomIconButton>
)
}
private renderRecursiveTopicDeleteButton() {
const deleteLimit = 50
const topicCount = this.props.node ? this.props.node.childTopicCount() : 0
if ((!this.props.node || topicCount === 0) || (this.props.node.message && topicCount === 1)) {
return null
}
return (
<CustomIconButton onClick={() => this.deleteTopic(this.props.node, true, deleteLimit)}>
<Tooltip title={`Deletes up to ${deleteLimit} sub-topics with a single click`}>
<Badge
classes={{ badge: this.props.classes.badge }}
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
color="secondary"
>
<Delete color="action" />
</Badge>
</Tooltip>
</CustomIconButton>
)
}
private deleteTopic = (topic?: q.TreeNode<TopicViewModel>, recursive: boolean = false, maxCount = 50) => {
if (!topic) {
return
}
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() {
const { classes, node } = this.props
const copyTopic = node ? <Copy value={node.path()} /> : null
const deleteTopic = this.renderTopicDeleteButton()
const deleteRecursiveTopic = this.renderRecursiveTopicDeleteButton()
const copyValue = node && node.message ? <Copy value={node.message.value} /> : null
const summaryStyle = { minHeight: '0' }
const summeryStyle = { minHeight: '0' }
return (
<div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}</Typography>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Topic {copyTopic}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
{this.messageMetaInfo()}
<div ref={this.valueRef}>
<React.Suspense fallback={<div>Loading...</div>}>
{this.renderValue()}
</React.Suspense>
<React.Suspense fallback={<div>Loading...</div>}>
<ReactResizeDetector handleWidth={true} onResize={this.valueRenderWidthChange} />
<ValueRenderer message={this.props.node && this.props.node.message} />
</React.Suspense>
</div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} selected={this.state.compareMessage} node={this.props.node} /></div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} node={this.props.node} /></div>
<Popper open={Boolean(this.state.compareMessage)} anchorEl={this.valueRef.current} placement="left" transition={true}>
{this.showValueComparison}
</Popper>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Publish</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
@@ -205,7 +137,7 @@ class Sidebar extends React.Component<Props, State> {
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
{this.renderNodeStats()}
@@ -214,20 +146,19 @@ class Sidebar extends React.Component<Props, State> {
)
}
private renderValue() {
const node = this.props.node
if (!node || !node.message) {
return null
}
return (
<ValueRenderer
message={node.message}
messageHistory={node.messageHistory}
compareWith={this.state.compareMessage} />
)
private valueRenderWidthChange = (width: number) => {
console.log(width)
this.setState({ valueRenderWidth: width })
}
private showValueComparison = (a: any) => (
<Fade {...a.TransitionProps} timeout={350}>
<Paper style={{ maxWidth: this.state.valueRenderWidth }}>
<ValueRenderer message={this.state.compareMessage} />
</Paper>
</Fade>
)
private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
return null
@@ -239,6 +170,7 @@ class Sidebar extends React.Component<Props, State> {
size="small"
color="secondary"
variant="contained"
mini={true}
style={{ marginTop: '-3px', padding: '0px 4px', minHeight: '24px' }}
onClick={this.props.actions.clearRetainedTopic}
>
@@ -248,9 +180,8 @@ class Sidebar extends React.Component<Props, State> {
)
return (
<div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}>
<div style={{ width: '100%', display: 'flex' }}>
<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' }}>
{this.props.node.mqttMessage.retain ? retainedButton : null}
</div>
@@ -283,14 +214,12 @@ class Sidebar extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => {
return {
node: state.tree.selectedTopic,
valueRendererDisplayMode: state.settings.valueRendererDisplayMode,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(sidebarActons, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
}
}
@@ -301,7 +230,7 @@ const styles: StyleRulesCallback<string> = (theme: Theme) => {
height: '100%',
},
valuePaper: {
margin: theme.spacing(1),
margin: `${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px`,
},
heading: {
fontSize: theme.typography.pxToRem(15),
+37 -79
View File
@@ -1,108 +1,66 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import CodeDiff from '../CodeDiff'
import { AppState } from '../../reducers'
import { connect } from 'react-redux'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { ValueRendererDisplayMode } from '../../reducers/Settings'
import * as q from '../../../../backend/src/Model'
import { Theme, withTheme } from '@material-ui/core/styles'
import { default as ReactJson } from 'react-json-view'
interface Props {
message: q.Message
messageHistory: q.RingBuffer<q.Message>
compareWith?: q.Message
renderMode: ValueRendererDisplayMode
message?: q.Message
theme: Theme
}
interface State {
width: number
}
class ValueRenderer extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { width: 0 }
}
class ValueRenderer extends React.Component<Props, {}> {
public render() {
return (
<div style={{ padding: '8px 0px 8px 8px' }}>
<ReactResizeDetector handleWidth={true} onResize={this.updateWidth} />
{this.renderValue()}
</div>
)
return <div style={{ padding: '8px 0px 8px 8px' }}>{this.renderValue()}</div>
}
public renderValue() {
const { message, messageHistory, compareWith, renderMode } = this.props
const previousMessages = messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {
compareMessage = message
const { message } = this.props
if (!message) {
return null
}
let json
try {
json = JSON.parse(message.value)
} catch (error) {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
}
if (typeof json === 'string') {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'number') {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'boolean') {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
} else {
const current = this.messageToPrettyJson(message) || message.value
const compare = this.messageToPrettyJson(compareMessage) || compareMessage.value
const language = current && compare ? 'json' : undefined
return this.renderDiff(current, compare, language)
const theme = (this.props.theme.palette.type === 'dark') ? 'monokai' : 'bright:inverted'
return (
<ReactJson
style={{ width: '100%', overflowY: 'scroll', wordBreak: 'break-all' }}
src={json}
name={null}
theme={theme}
/>
)
}
}
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
return (
<CodeDiff
previous={previous}
current={current}
language={language}
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
/>
)
}
private messageToPrettyJson(message?: q.Message): string | undefined {
if (!message || !message.value) {
return undefined
private renderRawValue(value: string) {
const style: React.CSSProperties = {
padding: '8px 12px 8px 12px',
backgroundColor: 'rgba(100, 100, 100, 0.55)',
wordBreak: 'break-all',
width: '100%',
overflow: 'auto hidden',
display: 'block',
lineHeight: '1.2em',
color: this.props.theme.palette.text.primary,
}
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)
return <pre style={style}><code>{value}</code></pre>
}
}
const mapStateToProps = (state: AppState) => {
return {
renderMode: state.settings.valueRendererDisplayMode,
}
}
export default connect(mapStateToProps)(ValueRenderer)
export default withTheme()(ValueRenderer)
+15 -22
View File
@@ -1,23 +1,17 @@
import * as React from 'react'
import ClearAdornment from './helper/ClearAdornment'
import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import CloudOff from '@material-ui/icons/CloudOff'
import ConnectionHealthIndicator from './ConnectionHealthIndicator'
import Menu from '@material-ui/icons/Menu'
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 {
AppBar,
Button,
IconButton,
InputBase,
Toolbar,
Typography,
} from '@material-ui/core'
import { settingsActions, connectionActions } from '../actions'
import { AppState } from '../reducers'
import ClearAdornment from './helper/ClearAdornment'
const styles: StyleRulesCallback = theme => ({
title: {
@@ -33,16 +27,16 @@ const styles: StyleRulesCallback = theme => ({
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing(2),
marginRight: theme.spacing.unit * 2,
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(3),
marginLeft: theme.spacing.unit * 3,
width: 'auto',
},
},
searchIcon: {
width: theme.spacing(8),
width: theme.spacing.unit * 8,
height: '100%',
position: 'absolute',
pointerEvents: 'none',
@@ -55,10 +49,10 @@ const styles: StyleRulesCallback = theme => ({
width: '100%',
},
inputInput: {
paddingTop: theme.spacing(1),
paddingRight: theme.spacing(1),
paddingBottom: theme.spacing(1),
paddingLeft: theme.spacing(10),
paddingTop: theme.spacing.unit,
paddingRight: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit * 10,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
@@ -95,12 +89,11 @@ class TitleBar extends React.Component<Props, {}> {
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={actions.settings.toggleSettingsVisibility}>
<Menu />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Explorer</Typography>
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
{this.renderSearch()}
<Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
<ConnectionHealthIndicator />
</Toolbar>
</AppBar>
)
+5 -9
View File
@@ -1,12 +1,13 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import TreeNode from './TreeNode'
import * as q from '../../../../backend/src/Model'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import TreeNode from './TreeNode'
import { connect } from 'react-redux'
import { TopicOrder } from '../../reducers/Settings'
import { TopicViewModel } from '../../TopicViewModel'
import { treeActions } from '../../actions'
import { bindActionCreators } from 'redux'
const MovingAverage = require('moving-average')
@@ -21,10 +22,9 @@ interface Props {
tree?: q.Tree<TopicViewModel>
filter: string
host?: string
topicOrder: TopicOrder
autoExpandLimit: number
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
}
interface State {
@@ -111,8 +111,6 @@ class Tree extends React.PureComponent<Props, State> {
topicOrder={this.props.topicOrder}
lastUpdate={tree.lastUpdate}
didSelectTopic={this.props.actions.selectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
</div>
)
@@ -130,8 +128,6 @@ const mapStateToProps = (state: AppState) => {
host: state.connection.host,
autoExpandLimit: state.settings.autoExpandLimit,
topicOrder: state.settings.topicOrder,
highlightTopicUpdates: state.settings.highlightTopicUpdates,
selectTopicWithMouseOver: state.settings.selectTopicWithMouseOver,
}
}
+24 -45
View File
@@ -1,11 +1,12 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Theme, withStyles } from '@material-ui/core/styles'
import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle'
import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicOrder } from '../../reducers/Settings'
import { TopicViewModel } from '../../TopicViewModel'
const debounce = require('lodash.debounce')
declare var performance: any
@@ -28,14 +29,11 @@ const styles = (theme: Theme) => {
cursor: 'pointer',
marginTop: '-1px',
},
subnodes: {
marginLeft: theme.spacing(1.5),
},
selected: {
backgroundColor: 'rgba(170, 170, 170, 0.55)',
backgroundColor: 'rgba(120, 120, 120, 0.55)',
},
hover: {
backgroundColor: 'rgba(100, 100, 100, 0.55)',
backgroundColor: 'rgba(80, 80, 80, 0.55)',
},
}
}
@@ -53,8 +51,6 @@ interface Props {
autoExpandLimit: number
lastUpdate: number
didSelectTopic: any
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
}
interface State {
@@ -68,10 +64,12 @@ class TreeNode extends React.Component<Props, State> {
private dirtyEdges: boolean = true
private dirtyMessage: boolean = true
private animationDirty: boolean = false
private lastRenderTime = 0
private cssAnimationWasSetAt?: number
private willUpdateTime: number = performance.now()
private titleRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
private nodeRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
private subnodesDidchange = () => {
@@ -131,6 +129,7 @@ class TreeNode extends React.Component<Props, State> {
public componentWillUnmount() {
const { treeNode } = this.props
this.removeSubscriber(treeNode)
this.titleRef = undefined
this.nodeRef = undefined
}
@@ -183,35 +182,29 @@ class TreeNode extends React.Component<Props, State> {
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot && this.props.highlightTopicUpdates
const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot
const animation = shouldStartAnimation ? { willChange: 'auto', translateZ: 0, animation: 'example 0.5s' } : {}
this.animationDirty = shouldStartAnimation
const highlightClass = this.state.selected ? this.props.classes.selected : (this.state.mouseOver ? this.props.classes.hover : '')
return (
<div>
<div
key={this.props.treeNode.hash()}
className={`${classes.node} ${this.props.className}`}
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
onClick={this.didClickTitle}
ref={this.nodeRef}
>
<div
key={this.props.treeNode.hash()}
className={`${classes.node} ${this.props.className} ${highlightClass}`}
onClick={this.didClickNode}
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
ref={this.nodeRef}
>
<span ref={this.titleRef} style={animation}>
<TreeNodeTitle
style={animation}
toggleCollapsed={this.toggleCollapsed}
didSelectNode={this.didSelectTopic}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
className={highlightClass}
didSelectNode={this.didSelectTopic}
/>
</div>
<div className={classes.subnodes}>
{this.renderNodes()}
</div>
</span>
{this.renderNodes()}
</div>
)
}
@@ -220,18 +213,9 @@ class TreeNode extends React.Component<Props, State> {
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) => {
event.stopPropagation()
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) => {
@@ -243,16 +227,13 @@ class TreeNode extends React.Component<Props, State> {
this.setState({ mouseOver: hover })
}, 45)
private toggleCollapsed = (event: React.MouseEvent) => {
private didClickNode = (event: React.MouseEvent) => {
event.stopPropagation()
this.toggle()
this.didSelectTopic()
}
private renderNodes() {
if (this.collapsed()) {
return null
}
return (
<TreeNodeSubnodes
animateChanges={this.props.animateChages}
@@ -262,8 +243,6 @@ class TreeNode extends React.Component<Props, State> {
topicOrder={this.props.topicOrder}
lastUpdate={this.props.treeNode.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
)
}
+10 -8
View File
@@ -1,7 +1,10 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { AppState } from '../../reducers'
import TreeNode from './TreeNode'
import { connect } from 'react-redux'
import { TopicOrder } from '../../reducers/Settings'
import { Theme, withStyles } from '@material-ui/core'
import { TopicViewModel } from '../../TopicViewModel'
@@ -12,13 +15,13 @@ export interface Props {
filter?: string
collapsed?: boolean | undefined
classes: any
lastUpdate: number
topicOrder: TopicOrder
selectedTopic?: q.TreeNode<TopicViewModel>
autoExpandLimit: number
didSelectTopic: any
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
}
interface State {
@@ -52,13 +55,13 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
}
private renderMore() {
this.renderMoreAnimationFrame = (window as any).requestIdleCallback(() => {
this.renderMoreAnimationFrame = window.requestAnimationFrame(() => {
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
}, { timeout: 500 })
})
}
public componentWillUnmount() {
(window as any).cancelIdleCallback(this.renderMoreAnimationFrame)
window.cancelAnimationFrame(this.renderMoreAnimationFrame)
}
public render() {
@@ -68,6 +71,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
}
if (this.state.alreadyAdded < edges.length) {
const delta = Math.min(this.state.alreadyAdded, edges.length - this.state.alreadyAdded)
this.renderMore()
}
@@ -83,8 +87,6 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
autoExpandLimit={this.props.autoExpandLimit}
lastUpdate={node.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
)
})
@@ -103,7 +105,7 @@ const styles = (theme: Theme) => ({
clear: 'both' as 'both',
},
listItem: {
padding: `0px 0px 0px ${theme.spacing(1)}`,
padding: '3px 0px 0px 8px',
},
})
+16 -18
View File
@@ -11,19 +11,24 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
collapsed?: boolean | undefined
classes: any
didSelectNode: any
toggleCollapsed: any
}
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() {
const { classes, treeNode, style, className } = this.props
return (
<span
className={`${classes.title} ${className}`}
style={style}
>
<span className={classes.expander} onClick={this.props.toggleCollapsed}>{this.renderExpander()}</span>
{this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
<span className={this.props.classes.title} onMouseOver={this.props.treeNode.message ? this.mouseOver : undefined}>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
)
}
@@ -64,24 +69,17 @@ const styles = (theme: Theme) => ({
overflow: 'hidden' as 'hidden',
textOverflow: 'ellipsis' as 'ellipsis',
padding: '0',
marginLeft: '5px',
display: 'inline-block' as 'inline-block',
},
sourceEdge: {
fontWeight: 'bold' as 'bold',
overflow: 'hidden' as 'hidden',
},
expander: {
color: theme.palette.type === 'light' ? '#222' : '#eee',
cursor: 'pointer' as 'pointer',
paddingRight: theme.spacing(0.25),
display: 'inline-block' as 'inline-block',
},
title: {
borderRadius: '4px',
lineHeight: '1em',
display: 'inline-block' as 'inline-block',
whiteSpace: 'nowrap' as 'nowrap',
padding: '1px 4px 0px 4px',
height: '14px',
margin: '1px 0px 2px 0px',
},
collapsedSubnodes: {
color: theme.palette.text.secondary,
-44
View File
@@ -1,44 +0,0 @@
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
@@ -1,78 +0,0 @@
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
@@ -1,91 +0,0 @@
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
@@ -1,14 +0,0 @@
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 -4
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import { IconButton } from '@material-ui/core'
import Clear from '@material-ui/icons/Clear'
interface Props {
value?: string
@@ -8,9 +8,6 @@ interface Props {
style?: React.CSSProperties
}
/**
* Clear button for text input fields
*/
class ClearAdornment extends React.Component<Props, {}> {
public render() {
if (this.props.value) {
+2 -11
View File
@@ -1,22 +1,13 @@
import * as React from 'react'
import * as moment from 'moment'
interface Props {
date: Date
}
class DateFormatter extends React.Component<Props, {}> {
public render() {
const locale = window.navigator.language
return <span>{locale ? this.localizedDate(locale) : this.legacyDate()}</span>
}
private legacyDate() {
return `${this.props.date.toLocaleDateString()} ${this.props.date.toLocaleTimeString()}`
}
private localizedDate(locale: string) {
return moment(this.props.date).locale(locale).format('L LTS')
return <span>{this.props.date.toLocaleDateString()} {this.props.date.toLocaleTimeString()}</span>
}
}
+11 -12
View File
@@ -1,17 +1,16 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom'
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 './tracking'
import * as React from 'react'
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 { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
const composeEnhancers = /*(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || */ compose
const store = createStore(
@@ -28,6 +27,7 @@ const theme = createMuiTheme({
palette: {
type: 'dark',
},
typography: { useNextVariants: true },
})
setTimeout(() => {
@@ -41,8 +41,7 @@ setTimeout(() => {
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<Provider store={store}>
<App />
<Demo />
<App name="" />
</Provider>
</MuiThemeProvider>,
document.getElementById('app'),
-81
View File
@@ -1,81 +0,0 @@
import { MqttOptions } from '../../../backend/src/DataSource'
import { v4 } from 'uuid'
const sha1 = require('sha1')
export interface CertificateParameters {
name: string
/** @property data base64 encoded data */
data: string
}
export interface ConnectionOptions {
type: 'mqtt'
id: string
host: string
protocol: 'mqtt' | 'ws'
basePath?: string
port: number
name: string
username?: string
password?: string
encryption: boolean
certValidation: boolean
selfSignedCertificate?: CertificateParameters
clientId?: string
subscriptions: string[]
}
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
if (options.type !== 'mqtt') {
return
}
return {
url: `${options.protocol}://${options.host}:${options.port}/${options.basePath || ''}`,
username: options.username,
password: options.password,
tls: options.encryption,
clientId: options.clientId,
certValidation: options.certValidation,
subscriptions: options.subscriptions,
certificateAuthority: options.selfSignedCertificate ? options.selfSignedCertificate.data : undefined,
}
}
function generateClientId() {
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
return `mqtt-explorer-${clientIdSha}`
}
export function createEmptyConnection(): ConnectionOptions {
return {
certValidation: true,
clientId: generateClientId(),
id: v4() as string,
name: 'new connection',
encryption: false,
password: undefined,
username: undefined,
subscriptions: ['#', '$SYS/#'],
type: 'mqtt',
host: '',
port: 1883,
protocol: 'mqtt',
}
}
export function makeDefaultConnections() {
return {
'iot.eclipse.org': {
...createEmptyConnection(),
id: 'iot.eclipse.org',
name: 'iot.eclipse.org',
host: 'iot.eclipse.org',
},
'test.mosquitto.org': {
...createEmptyConnection(),
id: 'test.mosquitto.org',
name: 'test.mosquitto.org',
host: 'test.mosquitto.org',
},
}
}
-59
View File
@@ -1,59 +0,0 @@
import { ConnectionOptions, createEmptyConnection } from './ConnectionOptions'
import { v4 } from 'uuid'
interface LegacyConnectionSettings {
host: string
protocol: string
port: number
tls: boolean
certValidation: boolean
clientId: string
connectionId?: string
username: string
password: string
}
export function clearLegacyConnectionOptions() {
window.localStorage.setItem('connectionSettings', '')
}
export function loadLegacyConnectionOptions(): {[s: string]: ConnectionOptions} | {} {
const legacySettingsString = window.localStorage.getItem('connectionSettings')
if (!legacySettingsString) {
return {}
}
let legacyConnection
try {
legacyConnection = JSON.parse(legacySettingsString) as LegacyConnectionSettings
} catch {
return {}
}
const protocolMap: {[s: string]: string} = {
'tcp://': 'mqtt',
'ws://': 'ws',
'mqtt://': 'mqtt',
}
const migratedOptions: Partial<ConnectionOptions> = {
certValidation: legacyConnection.certValidation,
host: legacyConnection.host,
name: legacyConnection.host,
protocol: protocolMap[legacyConnection.protocol] as any,
port: legacyConnection.port,
username: legacyConnection.username,
password: legacyConnection.password,
clientId: legacyConnection.clientId,
encryption: legacyConnection.tls,
}
const emptyConnection = createEmptyConnection()
return {
[emptyConnection.id]: {
...emptyConnection,
...migratedOptions,
},
}
}
+1 -18
View File
@@ -1,10 +1,9 @@
import * as q from '../../../backend/src/Model'
import { Action } from 'redux'
import { createReducer } from './lib'
import * as q from '../../../backend/src/Model'
import { MqttOptions } from '../../../backend/src/DataSource'
import { TopicViewModel } from '../TopicViewModel'
export type ConnectionHealth = 'offline' | 'online' | 'connecting'
export interface ConnectionState {
host?: string
tree?: q.Tree<TopicViewModel>
@@ -13,7 +12,6 @@ export interface ConnectionState {
error?: string
connected: boolean
connecting: boolean
health?: ConnectionHealth
}
export type Action = SetConnecting | SetConnected | SetDisconnected | ShowError
@@ -23,7 +21,6 @@ export enum ActionTypes {
CONNECTION_SET_CONNECTED = 'CONNECTION_SET_CONNECTED',
CONNECTION_SET_DISCONNECTED = 'CONNECTION_SET_DISCONNECTED',
CONNECTION_SET_SHOW_ERROR = 'CONNECTION_SET_SHOW_ERROR',
CONNECTION_SET_HEALTH = 'CONNECTION_SET_HEALTH',
}
export interface SetConnecting {
@@ -41,11 +38,6 @@ export interface SetDisconnected {
type: ActionTypes.CONNECTION_SET_DISCONNECTED
}
export interface SetHealth {
health: ConnectionHealth
type: ActionTypes.CONNECTION_SET_DISCONNECTED
}
export interface ShowError {
type: ActionTypes.CONNECTION_SET_SHOW_ERROR
error?: Error
@@ -54,7 +46,6 @@ export interface ShowError {
const initialState: ConnectionState = {
connected: false,
connecting: false,
health: undefined,
}
export const connectionReducer = createReducer(initialState, {
@@ -62,7 +53,6 @@ export const connectionReducer = createReducer(initialState, {
CONNECTION_SET_CONNECTED: setConnected,
CONNECTION_SET_DISCONNECTED: setDisconnected,
CONNECTION_SET_SHOW_ERROR: showError,
CONNECTION_SET_HEALTH: setHealth,
})
function setConnecting(state: ConnectionState, action: SetConnecting): ConnectionState {
@@ -74,13 +64,6 @@ function setConnecting(state: ConnectionState, action: SetConnecting): Connectio
}
}
function setHealth(state: ConnectionState, action: SetHealth): ConnectionState {
return {
...state,
health: action.health,
}
}
function setConnected(state: ConnectionState, action: SetConnected): ConnectionState {
return {
...state,
-174
View File
@@ -1,174 +0,0 @@
import { Action } from 'redux'
import { ConnectionOptions } from '../model/ConnectionOptions'
import { createReducer } from './lib'
export interface ConnectionManagerState {
connections: {[s:string]: ConnectionOptions},
selected?: string
showAdvancedSettings: boolean
}
const initialState: ConnectionManagerState = {
connections: {},
selected: undefined,
showAdvancedSettings: false,
}
export type Action = SetConnections | SelectConnection | UpdateConnection | AddConnection | DeleteConnection | ToggleAdvancedSettings | DeleteSubscription | AddSubscription
export enum ActionTypes {
CONNECTION_MANAGER_SET_CONNECTIONS = 'CONNECTION_MANAGER_SET_CONNECTIONS',
CONNECTION_MANAGER_SELECT_CONNECTION = 'CONNECTION_MANAGER_SELECT_CONNECTION',
CONNECTION_MANAGER_UPDATE_CONNECTION = 'CONNECTION_MANAGER_UPDATE_CONNECTION',
CONNECTION_MANAGER_ADD_CONNECTION = 'CONNECTION_MANAGER_ADD_CONNECTION',
CONNECTION_MANAGER_DELETE_CONNECTION = 'CONNECTION_MANAGER_DELETE_CONNECTION',
CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS = 'CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS',
CONNECTION_MANAGER_ADD_SUBSCRIPTION = 'CONNECTION_MANAGER_ADD_SUBSCRIPTION',
CONNECTION_MANAGER_DELETE_SUBSCRIPTION = 'CONNECTION_MANAGER_DELETE_SUBSCRIPTION',
}
export interface SetConnections {
type: ActionTypes.CONNECTION_MANAGER_SET_CONNECTIONS
connections: {[s:string]: ConnectionOptions}
}
export interface SelectConnection {
type: ActionTypes.CONNECTION_MANAGER_SELECT_CONNECTION
selected: string
}
export interface AddSubscription {
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION
subscription: string
connectionId: string
}
export interface DeleteSubscription {
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION
subscription: string
connectionId: string
}
export interface UpdateConnection {
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION
connectionId: string
changeSet: any
}
export interface AddConnection {
type: ActionTypes.CONNECTION_MANAGER_ADD_CONNECTION
connection: ConnectionOptions
}
export interface DeleteConnection {
type: ActionTypes.CONNECTION_MANAGER_DELETE_CONNECTION
connectionId: string
}
export interface ToggleAdvancedSettings {
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS
}
export const connectionManagerReducer = createReducer(initialState, {
CONNECTION_MANAGER_SET_CONNECTIONS: setConnections,
CONNECTION_MANAGER_SELECT_CONNECTION: selectConnection,
CONNECTION_MANAGER_UPDATE_CONNECTION: updateConnection,
CONNECTION_MANAGER_ADD_CONNECTION: addConnection,
CONNECTION_MANAGER_DELETE_CONNECTION: deleteConnection,
CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS: toggleAdvancedSettings,
CONNECTION_MANAGER_DELETE_SUBSCRIPTION: deleteSubscription,
CONNECTION_MANAGER_ADD_SUBSCRIPTION: addSubscription,
})
function setConnections(state: ConnectionManagerState, action: SetConnections): ConnectionManagerState {
return {
...state,
connections: action.connections,
}
}
function selectConnection(state: ConnectionManagerState, action: SelectConnection): ConnectionManagerState {
return {
...state,
selected: action.selected,
}
}
function toggleAdvancedSettings(state: ConnectionManagerState, action: ToggleAdvancedSettings): ConnectionManagerState {
return {
...state,
showAdvancedSettings: !state.showAdvancedSettings,
}
}
function addConnection(state: ConnectionManagerState, action: AddConnection): ConnectionManagerState {
return {
...state,
connections: {
...state.connections,
[action.connection.id]: action.connection,
},
}
}
function addSubscription(state: ConnectionManagerState, action: AddSubscription): ConnectionManagerState {
const connection = state.connections[action.connectionId]
const alreadyExists = connection.subscriptions.indexOf(action.subscription) !== -1
if (alreadyExists) {
return state
}
const newSubscriptions = connection.subscriptions.slice()
newSubscriptions.push(action.subscription)
return {
...state,
connections: {
...state.connections,
[action.connectionId]: {
...connection,
subscriptions: newSubscriptions,
},
},
}
}
function deleteSubscription(state: ConnectionManagerState, action: AddSubscription): ConnectionManagerState {
const connection = state.connections[action.connectionId]
const newSubscriptions = connection.subscriptions.filter(s => s !== action.subscription)
return {
...state,
connections: {
...state.connections,
[action.connectionId]: {
...connection,
subscriptions: newSubscriptions,
},
},
}
}
function deleteConnection(state: ConnectionManagerState, action: DeleteConnection): ConnectionManagerState {
const connections = { ...state.connections }
delete connections[action.connectionId]
return {
...state,
connections,
}
}
function updateConnection(state: ConnectionManagerState, action: UpdateConnection): ConnectionManagerState {
let connection = state.connections[action.connectionId]
connection = {
...connection,
...action.changeSet,
}
return {
...state,
connections: {
...state.connections,
[action.connectionId]: connection,
},
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ export interface ToggleRetain {
}
const initialState: PublishState = {
editorMode: 'json',
editorMode: 'text',
retain: false,
qos: 0,
}
+8 -73
View File
@@ -8,39 +8,25 @@ export enum TopicOrder {
topics = '#topics',
}
export type ValueRendererDisplayMode = 'diff' | 'raw'
export interface SettingsState {
autoExpandLimit: number
visible: boolean
topicOrder: TopicOrder
topicFilter?: string
highlightTopicUpdates: boolean
valueRendererDisplayMode: ValueRendererDisplayMode
selectTopicWithMouseOver: boolean
}
export type Action = SetAutoExpandLimit | ToggleVisibility | SetTopicOrder | FilterTopics | TogglehighlightTopicUpdates | SetValueRendererDisplayMode
export type Action = SetAutoExpandLimit | ToggleVisibility | SetTopicOrder | FilterTopics
export enum ActionTypes {
SETTINGS_SET_AUTO_EXPAND_LIMIT = 'SETTINGS_SET_AUTO_EXPAND_LIMIT',
SETTINGS_TOGGLE_VISIBILITY = 'SETTINGS_TOGGLE_VISIBILITY',
SETTINGS_SET_TOPIC_ORDER = 'SETTINGS_SET_TOPIC_ORDER',
SETTINGS_FILTER_TOPICS = 'SETTINGS_FILTER_TOPICS',
SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY = 'SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY',
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 = {
autoExpandLimit: 0,
topicOrder: TopicOrder.none,
visible: false,
highlightTopicUpdates: true,
valueRendererDisplayMode: 'diff',
selectTopicWithMouseOver: false,
}
export const settingsReducer = createReducer(initialState, {
@@ -48,53 +34,8 @@ export const settingsReducer = createReducer(initialState, {
SETTINGS_TOGGLE_VISIBILITY: toggleVisibility,
SETTINGS_SET_TOPIC_ORDER: setTopicOrder,
SETTINGS_FILTER_TOPICS: filterTopics,
SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY: togglehighlightTopicUpdates,
SETTINGS_DID_LOAD_SETTINGS: didLoadSettings,
SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE: setValueRendererDisplayMode,
SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER: setSelectTopicWithMouseOver,
})
export interface DidLoadSettings {
type: ActionTypes.SETTINGS_DID_LOAD_SETTINGS
settings: Partial<SettingsState>
}
function didLoadSettings(state: SettingsState, action: DidLoadSettings) {
return {
...state,
...action.settings,
}
}
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 {
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT
autoExpandLimit: number
}
function setAutoExpandLimit(state: SettingsState, action: SetAutoExpandLimit) {
return {
...state,
@@ -102,19 +43,9 @@ function setAutoExpandLimit(state: SettingsState, action: SetAutoExpandLimit) {
}
}
export interface TogglehighlightTopicUpdates {
type: ActionTypes.SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY
}
function togglehighlightTopicUpdates(state: SettingsState, _action: TogglehighlightTopicUpdates) {
return {
...state,
highlightTopicUpdates: !state.highlightTopicUpdates,
}
}
export interface ToggleVisibility {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY
export interface SetAutoExpandLimit {
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT
autoExpandLimit: number
}
function toggleVisibility(state: SettingsState, action: ToggleVisibility) {
@@ -124,6 +55,10 @@ function toggleVisibility(state: SettingsState, action: ToggleVisibility) {
}
}
export interface ToggleVisibility {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY
}
function setTopicOrder(state: SettingsState, action: SetTopicOrder) {
return {
...state,
+11 -23
View File
@@ -1,45 +1,40 @@
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 { treeReducer, TreeState } from './Tree'
import { Action, Reducer, combineReducers } from 'redux'
import { trackEvent } from '../tracking'
import { PublishState, publishReducer } from './Publish'
import { ConnectionState, connectionReducer } from './Connection'
import { SettingsState, settingsReducer } from './Settings'
import { TreeState, treeReducer } from './Tree'
export enum ActionTypes {
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS',
showError = 'SHOW_ERROR',
}
export interface CustomAction extends Action {
type: ActionTypes,
showUpdateNotification?: boolean
showUpdateDetails?: boolean
error?: string
}
export interface AppState {
globalState: GlobalState
tooBigReducer: TooBigOfState
tree: TreeState
settings: SettingsState,
publish: PublishState
connection: ConnectionState
connectionManager: ConnectionManagerState
}
export interface GlobalState {
export interface TooBigOfState {
showUpdateNotification?: boolean
showUpdateDetails: boolean
error?: string
}
const initialBigState: GlobalState = {
const initialBigState: TooBigOfState = {
showUpdateDetails: false,
}
const globalState: Reducer<GlobalState | undefined, CustomAction> = (state = initialBigState, action) => {
const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => {
if (!state) {
throw Error('No initial state')
}
@@ -52,12 +47,6 @@ const globalState: Reducer<GlobalState | undefined, CustomAction> = (state = ini
showUpdateNotification: action.showUpdateNotification,
}
case ActionTypes.showError:
return {
...state,
error: action.error,
}
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
@@ -73,12 +62,11 @@ const globalState: Reducer<GlobalState | undefined, CustomAction> = (state = ini
}
const reducer = combineReducers({
globalState,
tooBigReducer,
publish: publishReducer,
connection: connectionReducer,
settings: settingsReducer,
tree: treeReducer,
connectionManager: connectionManagerReducer,
})
export default reducer
+651 -581
View File
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
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\*.appx'
-67
View File
@@ -1,67 +0,0 @@
import * as FileAsync from 'lowdb/adapters/FileAsync'
import * as lowdb from 'lowdb'
import { backendEvents } from '../../events'
import {
makeStorageResponseEvent,
storageClearEvent,
storageLoadEvent,
storageStoreEvent,
makeStorageAcknoledgementEvent,
} from '../../events/StorageEvents'
export default class ConfigStorage {
private file: string
private database: any
constructor(file: string) {
this.file = file
}
private async getDb() {
const adapter = new FileAsync(this.file)
if (!this.database) {
this.database = await lowdb(adapter)
}
return this.database
}
public async init() {
backendEvents.subscribe(storageStoreEvent, async (event) => {
const ack = makeStorageAcknoledgementEvent(event.transactionId)
try {
const db = await this.getDb()
await db.set(event.store, event.data).write()
backendEvents.emit(ack, undefined)
} catch (error) {
backendEvents.emit(ack, { error, transactionId: event.transactionId, store: event.store })
throw error
}
})
backendEvents.subscribe(storageLoadEvent, async (event) => {
const responseEvent = makeStorageResponseEvent(event.transactionId)
try {
const db = await this.getDb()
const data = await db.get(event.store).value()
backendEvents.emit(responseEvent, { data, transactionId: event.transactionId, store: event.store })
} catch (error) {
backendEvents.emit(responseEvent, { error, transactionId: event.transactionId, store: event.store })
throw error
}
})
backendEvents.subscribe(storageClearEvent, async (event) => {
try {
const db = await this.getDb()
const keys = await db.keys().value()
for (const key of keys) {
await db.unset(key).write()
}
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), undefined)
} catch (error) {
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), { error, transactionId: event.transactionId })
throw error
}
})
}
}
+3 -4
View File
@@ -25,17 +25,16 @@ export class DataSourceStateMachine {
public setError(error: Error) {
this.state = {
...this.state,
error: error.message,
// connected: false,
// connecting: false,
connected: false,
connecting: false,
}
this.onUpdate.dispatch(this.state)
}
public setConnecting() {
this.state = {
...this.state,
error: undefined,
connected: false,
connecting: true,
}
+10 -13
View File
@@ -11,14 +11,13 @@ export interface MqttOptions {
tls: boolean
certValidation: boolean
clientId?: string
subscriptions: string[]
certificateAuthority?: string
}
export class MqttSource implements DataSource<MqttOptions> {
public stateMachine: DataSourceStateMachine = new DataSourceStateMachine()
private client: Client | undefined
private messageCallback?: (topic: string, message: Buffer, packet: any) => void
private rootSubscription = '#'
public topicSeparator = '/'
public onMessage(messageCallback: (topic: string, message: Buffer, packet: any) => void) {
@@ -43,7 +42,6 @@ export class MqttSource implements DataSource<MqttOptions> {
username: options.username,
password: options.password,
clientId: options.clientId,
ca: options.certificateAuthority ? Buffer.from(options.certificateAuthority, 'base64') : undefined,
})
this.client = client
@@ -57,22 +55,21 @@ export class MqttSource implements DataSource<MqttOptions> {
this.stateMachine.setConnected(false)
})
client.on('end', () => {
this.stateMachine.setConnected(false)
})
client.on('reconnect', () => {
this.stateMachine.setConnecting()
})
client.on('connect', () => {
this.stateMachine.setConnected(true)
options.subscriptions.forEach((subscription) => {
client.subscribe(subscription, (err: Error) => {
if (err) {
this.stateMachine.setError(err)
}
})
client.subscribe(this.rootSubscription, (err: Error) => {
if (err) {
this.stateMachine.setError(err)
}
})
client.subscribe('$SYS/#', (err: Error) => {
if (err) {
console.error('failed to subscribe to sys topic', err)
}
})
})
+4 -6
View File
@@ -6,7 +6,7 @@ export class RingBuffer<T extends Lengthwise> {
private capacity: number
private maxItems: number
private usage: number = 0
private items: T[] = []
private items: (any)[] = []
private start: number = 0
private end: number = 0
@@ -17,7 +17,7 @@ export class RingBuffer<T extends Lengthwise> {
if (ringBuffer) {
this.items = ringBuffer.toArray()
this.end = this.items.length
this.usage = ringBuffer.usage
this.usage = this.items.length
}
}
@@ -62,11 +62,9 @@ export class RingBuffer<T extends Lengthwise> {
}
private dropFirst() {
const firstItem = this.items[this.start]
const freedSpace = this.items[this.start].length
this.usage -= freedSpace
delete this.items[this.start]
this.start += 1
const freedSpace = firstItem.length
this.usage -= freedSpace
}
}
-35
View File
@@ -47,9 +47,6 @@ export class TreeNode<ViewModel> {
this.lastUpdate = Date.now()
})
this.onEdgesChange.subscribe(() => {
this.cachedChildTopics = undefined
this.cachedChildTopicCount = undefined
this.cachedLeafMessageCount = undefined
this.lastUpdate = Date.now()
})
this.onMessage.subscribe(() => {
@@ -105,34 +102,6 @@ export class TreeNode<ViewModel> {
return previous.branch().concat([this])
}
private isTopicEmptyLeaf() {
const hasNoMessage = (!this.message || !this.message.value)
return hasNoMessage && this.isLeaf()
}
private isLeaf() {
return this.edgeArray.length === 0
}
private removeFromParent() {
const previous = this.previous()
if (!previous || !this.sourceEdge) {
return
}
previous.removeEdge(this.sourceEdge)
}
public removeEdge(edge: Edge<any>) {
delete this.edges[edge.name]
this.edgeArray = Object.values(this.edges)
this.onMerge.dispatch()
if (this.isTopicEmptyLeaf()) {
debugger
this.removeFromParent()
}
}
public updateWithNode(node: TreeNode<ViewModel>) {
if (node.message) {
this.setMessage(node.message)
@@ -140,10 +109,6 @@ export class TreeNode<ViewModel> {
this.mqttMessage = node.mqttMessage
}
if (this.isTopicEmptyLeaf()) {
this.removeFromParent()
}
this.mergeEdges(node)
this.onMerge.dispatch()
}
+2 -2
View File
@@ -43,10 +43,10 @@ describe('TreeNode', () => {
it('updateWithNode should add nodes to the tree', () => {
const topics1 = 'foo/bar'.split('/')
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, 'foo')
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined)
const topics2 = 'foo/bar/baz'.split('/')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, 'bar')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined)
leaf1.firstNode().updateWithNode(leaf2.firstNode())
+39
View File
@@ -0,0 +1,39 @@
import * as SocketIo from 'socket.io'
import { EventBusInterface, Event } from '../../../events'
import * as http from 'http'
class SocketIoServerEventBus implements EventBusInterface {
private httpServer: any
private socketIo: SocketIo.Server
constructor(serverPort = 3000) {
this.httpServer = http.createServer()
this.socketIo = SocketIo(this.httpServer)
this.httpServer.listen(serverPort)
}
public subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void) {
console.log('subscribing', event.topic)
Object.keys(this.socketIo.sockets.sockets).forEach((key) => {
const socket = this.socketIo.sockets.sockets[key]
socket.on(event.topic, callback)
})
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
Object.keys(this.socketIo.sockets.sockets).forEach((key) => {
const socket = this.socketIo.sockets.sockets[key]
socket.removeAllListeners()
})
}
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
throw new Error('Not implemented') // Todo: implement
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
this.socketIo.emit(event.topic, msg)
}
}
export const backendEvents = new SocketIoServerEventBus()
+7 -17
View File
@@ -3,7 +3,6 @@ import {
EventDispatcher,
MqttMessage,
addMqttConnectionEvent,
backendEvents,
checkForUpdates,
makeConnectionMessageEvent,
makeConnectionStateEvent,
@@ -11,8 +10,9 @@ import {
removeConnection,
updateAvailable,
} from '../../events'
import { DataSource, MqttSource } from './DataSource'
import { backendEvents } from './communication/SocketIo'
import { UpdateInfo } from 'builder-util-runtime'
export class ConnectionManager {
@@ -20,19 +20,11 @@ export class ConnectionManager {
public manageConnections() {
backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest)
backendEvents.subscribe(removeConnection, (connectionId: string) => {
this.removeConnection(connectionId)
})
backendEvents.subscribe(removeConnection, (connectionId: string) => this.removeConnection(connectionId))
}
private handleConnectionRequest = (event: AddMqttConnection) => {
const connectionId = event.id
// Prevent double connections when reloading
if (this.connections[connectionId]) {
this.removeConnection(connectionId)
}
const options = event.options
const connection = new MqttSource()
this.connections[connectionId] = connection
@@ -61,19 +53,17 @@ export class ConnectionManager {
})
}
public removeConnection(conenctionId: string) {
const connection = this.connections[conenctionId]
public removeConnection(hash: string) {
const connection = this.connections[hash]
if (connection) {
backendEvents.unsubscribeAll(makePublishEvent(conenctionId))
connection.disconnect()
delete this.connections[conenctionId]
connection.stateMachine.onUpdate.removeAllListeners()
delete this.connections[hash]
}
}
public closeAllConnections() {
Object.keys(this.connections)
.forEach(conenctionId => this.removeConnection(conenctionId))
.forEach(hash => this.removeConnection(hash))
}
}
@@ -21,16 +21,15 @@ class IpcMainEventBus implements EventBusInterface {
this.ipc = ipc
}
public subscribe<MessageType>(subscribeEvent: Event<MessageType>, callback:(msg: MessageType) => void) {
console.log('subscribing', subscribeEvent.topic)
this.ipc.on(subscribeEvent.topic, (event: any, arg: any) => {
public subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void) {
console.log('subscribing', event.topic)
this.ipc.on(event.topic, (event: any, arg: any) => {
this.client = event.sender
callback(arg)
})
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
console.log('unsubscribeAll', event.topic)
this.ipc.removeAllListeners(event.topic)
}
+8
View File
@@ -0,0 +1,8 @@
import { Event } from './Events'
export interface EventBusInterface {
subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void): void
unsubscribeAll<MessageType>(event: Event<MessageType>): void
emit<MessageType>(event: Event<MessageType>, msg: MessageType): void
unsubscribe<MessageType>(event: Event<MessageType>, callback: any): void
}
-39
View File
@@ -1,39 +0,0 @@
import { Event } from './'
interface StorageEvent {
transactionId: string
}
export interface StoreCommand extends StorageEvent {
store?: string,
data?: any
error?: any
}
export interface LoadCommand extends StorageEvent {
store: string,
}
export const storageStoreEvent: Event<StoreCommand> = {
topic: 'storage/store',
}
export const storageLoadEvent: Event<LoadCommand> = {
topic: 'storage/load',
}
export function makeStorageAcknoledgementEvent(transactionId: string): Event<StoreCommand> {
return {
topic: `storage/ack/${transactionId}`,
}
}
export function makeStorageResponseEvent(transactionId: string): Event<StoreCommand> {
return {
topic: `storage/response/${transactionId}`,
}
}
export const storageClearEvent: Event<StorageEvent> = {
topic: 'storage/clear',
}
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './Events'
export * from './EventDispatcher'
export * from './EventBus'
export * from './EventBusInterface'
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 47 KiB

BIN
View File
Binary file not shown.
+14 -27
View File
@@ -1,15 +1,15 @@
{
"name": "MQTT-Explorer",
"version": "0.2.4-alpha1",
"version": "0.1.3",
"description": "Explore your message queues",
"main": "dist/src/electron.js",
"scripts": {
"start": "electron .",
"test": "yarn run test-backend",
"install": "cd app && yarn && cd ..",
"install": "cd app; yarn; cd ..",
"build": "tsc && cd app && yarn run build && cd ..",
"test-backend": "cd backend && yarn run test && cd ..",
"prepare-release": "ts-node scripts/prepare-release.ts",
"prepare-release": "./scripts/prepare-release.sh",
"package": "ts-node package.ts",
"ui-test": "./scripts/uiTests.sh",
"upload-video-artifacts": "./scripts/uploadVideoAsset.ts ui-test.mp4 ui-test.gif",
@@ -21,34 +21,22 @@
},
"build": {
"appId": "mqtt-explorer",
"productName": "MQTT Explorer",
"nodeGypRebuild": false,
"mac": {
"appId": "de.t7n.apps.mqtt-explorer",
"category": "public.app-category.developer-tools",
"category": "de.t7n.apps.mqtt-explorer",
"publish": [
"github"
],
"provisioningProfile": "res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile",
"entitlements": "res/entitlements.mas.plist"
]
},
"linux": {
"category": "Development",
"maintainer": "Thomas Nordquist"
},
"appx": {
"applicationId": "mqttexplorer",
"identityName": "51031thomas.nordquist.MQTT-Explorer",
"publisherDisplayName": "Thomas Nordquist",
"publisher": "CN=0A6DE643-4AA2-4FF2-9729-6935C9ED8C13",
"backgroundColor": "transparent"
},
"directories": {
"app": "./",
"buildResources": "res",
"output": "build"
},
"afterAllArtifactBuild": "./dist/scripts/afterAllArtifactBuild.js"
}
},
"author": "Thomas Nordquist",
"email": "xxnerowingerxx@gmail.com",
@@ -56,20 +44,18 @@
"license": "ISC",
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/fs-extra": "^5.0.5",
"@types/lowdb": "^1.0.6",
"@types/mime": "^2.0.0",
"@types/mocha": "^5.2.5",
"@types/mustache": "^0.8.32",
"@types/node": "^10.12.18",
"@types/semver": "^5.5.0",
"@types/sha1": "^1.1.1",
"app-builder-lib": "https://github.com/thomasnordquist/app-builder-lib.git",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"axios": "^0.18.0",
"chai": "^4.2.0",
"electron": "^4.0.6",
"electron-builder": "^20.38.5",
"fs-extra": "^7.0.1",
"electron": "^4.0.2",
"electron-builder": "^20.38.4",
"mime": "^2.4.0",
"mocha": "^5.2.0",
"mustache": "^3.0.1",
@@ -86,13 +72,14 @@
"webdriverio": "5.4"
},
"dependencies": {
"about-window": "^1.12.1",
"electron-debug": "^2.0.0",
"electron-is-dev": "^1.0.1",
"electron-log": "^2.2.17",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"electron-updater": "^4.0.6",
"lowdb": "^1.0.0",
"mqtt": "^2.18.8",
"sha1": "^1.1.1"
"sha1": "^1.1.1",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0"
}
}
+18 -58
View File
@@ -1,12 +1,11 @@
import * as builder from 'electron-builder'
import * as fs from 'fs'
import * as path from 'path'
const linuxAppImage: builder.CliOptions = {
const linux: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: true,
arm64: false,
linux: ['AppImage'],
projectDir: './build/clean',
publish: 'always',
}
@@ -16,94 +15,55 @@ const linuxSnap: builder.CliOptions = {
ia32: false,
armv7l: false,
arm64: false,
linux: ['snap'],
projectDir: './build/clean',
publish: 'always',
}
const winPortable: builder.CliOptions = {
const win: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
win: ['portable', 'nsis'],
projectDir: './build/clean',
publish: 'always',
}
const winNsis: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
projectDir: './build/clean',
publish: 'always',
}
const winAppx: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
projectDir: './build/clean',
publish: 'onTag',
}
const mac: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
mac: ['dmg'],
projectDir: './build/clean',
publish: 'always',
}
async function executeBuild() {
console.log(process.argv[2])
switch (process.argv[2]) {
case 'win':
await buildWithOptions(winPortable, { platform: 'win', package: 'portable' })
await buildWithOptions(winNsis, { platform: 'win', package: 'nsis' })
break
case 'appx':
await buildWithOptions(winAppx, { platform: 'win', package: 'appx' })
await builder.build(win)
break
case 'linux':
await buildWithOptions(linuxAppImage, { platform: 'linux', package: 'AppImage' })
await buildWithOptions(linuxSnap, { platform: 'linux', package: 'snap' })
await builder.build(linux)
break
case 'snap':
try {
await builder.build(linuxSnap)
} catch {
// ignore
}
break
case 'mac':
await buildWithOptions(mac, { platform: 'mac', package: 'mas' })
await buildWithOptions(mac, { platform: 'mac', package: 'dmg' })
await buildWithOptions(mac, { platform: 'mac', package: 'zip' })
await builder.build(mac)
break
default:
await buildWithOptions({ ...mac, projectDir: '' }, { platform: 'mac', package: 'mas-dev' })
await builder.build(mac)
}
}
export interface BuildInfo {
platform: 'win' | 'linux' | 'mac'
package: Packages
}
type Packages = 'portable' | 'nsis' | 'appx' | 'AppImage' | 'snap' | 'dmg' | 'zip' | 'mas' | 'mas-dev'
async function buildWithOptions(options: builder.CliOptions, buildInfo: BuildInfo) {
fs.writeFileSync(path.join(options.projectDir!, 'buildOptions.json'), JSON.stringify(buildInfo))
ensureAppNameForPackage(options, buildInfo.package)
await builder.build({
...options,
[buildInfo.platform]: [buildInfo.package],
})
}
// AppX must hav a different name since the store name is already taken (but not used)
function ensureAppNameForPackage(options: builder.CliOptions, packageOption: Packages) {
const jsonLocation = path.join((options.projectDir as string), 'package.json')
const packageJson = JSON.parse(fs.readFileSync(jsonLocation).toString())
packageJson.build.productName = packageOption === 'appx' ? 'MQTT-Explorer' : 'MQTT Explorer'
fs.writeFileSync(jsonLocation, JSON.stringify(packageJson, undefined, ' '))
}
function build() {
try {
executeBuild()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

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