Compare commits

..
48 Commits
Author SHA1 Message Date
thomasnordquist 8edf5ad7c2 Apply automatic changes 2024-05-15 06:25:08 +00:00
thomasnordquist 51a1f5eef2 Apply automatic changes 2024-05-11 19:58:24 +00:00
Thomas Nordquist 952cb8d946 chore: update readme template 2024-05-11 21:57:26 +02:00
Thomas Nordquist c70bf7f0a2 update readme 2024-05-11 13:10:01 +02:00
Thomas Nordquist d74b4bd8ad chore: update readme 2024-05-11 13:05:53 +02:00
Thomas Nordquist dbeb3a7a18 add new version 2024-05-11 12:56:01 +02:00
Thomas Nordquist 6c6e4df164 Update links to build artifacts 2020-04-27 21:00:43 +02:00
Thomas Nordquist c23f6e2a1a Update changelog 2020-04-27 20:54:45 +02:00
Thomas Nordquist ae9724408d Update release 2020-04-27 20:51:20 +02:00
Thomas Nordquist b36d26ebc6 Merge remote-tracking branch 'origin/gh-pages' into gh-pages 2020-04-27 20:45:52 +02:00
Thomas Nordquist 6e5068207b Update dependecies 2020-01-17 12:46:17 +01:00
Thomas Nordquist aac7bf3696 Document telemetry details in issue #52 2019-07-22 16:13:56 +02:00
Thomas Nordquist a53670d368 Update Readme 2019-07-22 16:09:49 +02:00
Thomas Nordquist 737207297a Update changelog 2019-07-18 12:15:13 +02:00
Thomas Nordquist 318e604169 Update Readme 2019-07-18 01:19:48 +02:00
Thomas Nordquist 024cd2925b Update video 2019-07-17 20:32:44 +02:00
Thomas Nordquist 77c8986920 Update Readme 2019-07-17 20:31:05 +02:00
Thomas Nordquist 31a9e3f6e3 Combat form bots 2019-07-17 16:15:53 +02:00
Thomas Nordquist 5212a8330e Update readme 2019-07-17 16:15:39 +02:00
Thomas Nordquist d0a9b17b68 Add paypal.me link 2019-07-13 20:23:57 +02:00
Thomas Nordquist 4334bd9326 Add ubuntu tweer 2019-07-03 13:04:48 +02:00
Thomas Nordquist a43dc11d98 Add survey 2019-07-02 23:51:45 +02:00
Thomas Nordquist 1200b40f1b Update Readme 2019-06-21 02:10:10 +02:00
Thomas Nordquist fcda705083 Update links 2019-06-19 08:48:49 +02:00
Thomas Nordquist deea820107 Update links 2019-06-19 00:11:28 +02:00
Thomas Nordquist 427abf5647 Update screenshot 2019-06-19 00:09:54 +02:00
Thomas Nordquist 9fe9b08c55 Add version number 2019-06-18 23:53:01 +02:00
Thomas Nordquist a2c77b5d2f Update screenshot 2019-06-18 23:51:32 +02:00
Thomas Nordquist 202a04eea3 Update release 2019-06-18 23:36:49 +02:00
Thomas Nordquist d2b42293c2 Update changelog 2019-06-18 15:03:10 +02:00
Thomas Nordquist 223d4b55aa Update links 2019-05-13 12:50:52 +02:00
Thomas Nordquist b452fac4a5 Fix spacing 2019-05-07 19:34:00 +02:00
Thomas Nordquist c8a20d9d8d Update version 2019-05-07 19:31:09 +02:00
Thomas Nordquist 2126f9a00c Fix github button 2019-05-02 16:26:53 +02:00
Thomas Nordquist 9d2a2271cd Update page 2019-05-02 12:59:37 +02:00
Thomas Nordquist 4add67ec5a Update changelog 2019-04-25 00:30:25 +02:00
Thomas Nordquist 84709b29a1 Update template 2019-04-09 13:20:46 +02:00
Thomas Nordquist 1a4af8dbe5 Add OSX like frame 2019-04-09 13:17:48 +02:00
Thomas Nordquist 14fce24458 Use mp4 instaed of a gif 2019-04-09 12:33:27 +02:00
Thomas Nordquist 50d86478a0 Add jekyll local support 2019-04-09 12:33:04 +02:00
Thomas Nordquist 4ad4472dee Add mp4 video instead of gif 2019-04-09 12:18:25 +02:00
Thomas Nordquist 7687b32b44 Remove duplicate MQTT Explorer titlte 2019-04-09 12:11:55 +02:00
Thomas Nordquist 88ab41875a Update pages 2019-04-09 03:58:51 +02:00
Thomas Nordquist 48ee4dee29 Update page 2019-04-08 14:57:29 +02:00
Thomas Nordquist 3e5130b9d8 Update changelog 2019-04-08 14:42:57 +02:00
Thomas Nordquist 26ac4e0c27 Add changelog 2019-04-08 14:24:58 +02:00
Thomas Nordquist 03b304163d Fix image location 2019-04-08 14:23:42 +02:00
Thomas Nordquist 16ed84193a Initial commit 2019-04-04 20:36:35 +02:00
115 changed files with 1363 additions and 22137 deletions
+2 -7
View File
@@ -1,9 +1,4 @@
node_modules
backend/coverage
backend/.nyc_output
build
/dist
.DS_Store
test.dot
test.png
.awcache
.bundle
_site
-29
View File
@@ -1,29 +0,0 @@
language: node_js
services:
- docker
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
node_js:
- "10"
os:
- linux
- osx
install:
- npm install
script:
- npm run build
- npm run test
- if [[ "$TRAVIS_TAG" != "" ]]; then npm run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then npm run package -- linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then npm run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then npm run package -- win; fi
+1
View File
@@ -0,0 +1 @@
mqtt-explorer.com
+150
View File
@@ -0,0 +1,150 @@
[back](./)
# 0.4.0-beta1
- Fix Copy & Paste
- Allow setting QoS for subscriptions
- Show Meassge ID (for QoS > 0)
- Improve JSON editor
- Allow multiple connections to the same broker from MQTT Explorer (random client ids)
- Remove recursive topic removal limit
- Retain focus on publish input fields when publishing
- Fix editor layout issue
- Fix password input
- Fix charts where json properties contain periods
- Fix publish sidebar width
- Fix plot range selection
- Update Electron
A downgrade to a version prior to 0.4.0 is due to a new configuration format not possible.
# 0.3.5
- Add keyboard support
- Navigate with arrow keys and tab return and escape
- Delete topic topics with the delete key
- Add time range options to chart panel
- Add "Clear Data" option to charts
- Add support for client certificates
- Add support for SNI (encryption-related)
- Make connection profile list scrollable
- Improve the browsability of previous messages
- various small fixes
# 0.3.2
- Ubuntu performance fix
# 0.3.1
- Ubuntu bugfix release
- Add wayland support (for snap builds)
# 0.3.0
- Add functionality to plot JSON data
- Add chart panel, containing multiple plots (bottom panel)
- Chart panel and settings will be restored on restart
- Charts can be paused
- Panels can be hidden/overlap others completely
- Plot range can be customized
- Plot interpolation can be customized
- Migrated public broker from iot.eclipse.org to mqtt.eclipse.org
- Fix unrounded time intervals in the message history
- Fix weird plot curve interpolations
# 0.2.6
- Add a pause function
- Add light / dark themes
- Add localized date formatting
- Allow pinning self-signed-certificates
- Allow copying values from the message history
- Buffer changes while in pause mode
- Improve layout
- Improve numeric plot layout and smooth curves
- Improve scroll behavior message history
- Improve performance and memory footprint by ~30%
- Improve text selection behavior (Ctrl+A) for value preview and history
- Immediately apply changed settings
- Fix memory leak on search and disconnect- Deactivate auto-update for Windows portable releases.
- Fix OSX code-signing and auto-update
# 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.1, 0.2.2
- Release for Mac App Store
- Release for Microsoft Store
# 0.2.0
- Support multiple connection profiles
- Support custom subscriptions
- Delete Topics recursively
- Add option to disable activity indicator
- Add connection health indicator
- Remember settings after exiting the app
- Fixed ws:// connections
- Fixed exception after failed connection
- Rework topic selection indicator
- Fix scrollbar (draggable scroll handle)
- Improve look & feel
- Reduce binary size by ~60%
# 0.1.3
- Add search function
- Fix a crash on windows
- Greatly increase performance
- Show broker statistics (Settings sidebar)
- Show more content in topic preview
- Make right sidebar resizable
- Fixed minor visual imperfections.
# 0.1.0
- Improved error handling


- Fix plot
# 0.0.9
- Plot topics in a graph
- Delete retained topics with click
- Show message retention and QoS
- Set message retain and QoS
- Subscribed to broker statistics: $SYS topic
- Improved error handling


- Fixed Copy&Paste
- Crash on launch
# 0.0.7
- Fix auto-updater fallback
- Add debugging capability
- Show receive date next to message value
- Fix certificate validation switch
# 0.0.6
- Fix issue with mqtt authentication
# 0.0.5
- Fix performance bottleneck
- Fix missing icons
# 0.0.3
- Add splash screen
# 0.0.2
- Fixed bugs
- Added bugs
# 0.0.1
- Initial Release
+2
View File
@@ -0,0 +1,2 @@
source 'https://rubygems.org'
gem 'github-pages', group: :jekyll_plugins
+248
View File
@@ -0,0 +1,248 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.10)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.5)
dnsruby (1.61.2)
addressable (~> 2.5)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
ethon (0.12.0)
ffi (>= 1.3.0)
eventmachine (1.2.7)
execjs (2.7.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
ffi (1.10.0)
forwardable-extended (2.6.0)
gemoji (3.0.0)
github-pages (197)
activesupport (= 4.2.10)
github-pages-health-check (= 1.16.1)
jekyll (= 3.7.4)
jekyll-avatar (= 0.6.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.5)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.11.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.12.1)
jekyll-mentions (= 1.4.1)
jekyll-optional-front-matter (= 0.3.0)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.2.0)
jekyll-redirect-from (= 0.14.0)
jekyll-relative-links (= 0.6.0)
jekyll-remote-theme (= 0.3.1)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.5.0)
jekyll-sitemap (= 1.2.0)
jekyll-swiss (= 0.4.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
jekyll-theme-hacker (= 0.1.1)
jekyll-theme-leap-day (= 0.1.1)
jekyll-theme-merlot (= 0.1.1)
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.3)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.1)
jemoji (= 0.10.2)
kramdown (= 1.17.0)
liquid (= 4.0.0)
listen (= 3.1.5)
mercenary (~> 0.3)
minima (= 2.5.0)
nokogiri (>= 1.8.5, < 2.0)
rouge (= 2.2.1)
terminal-table (~> 1.4)
github-pages-health-check (1.16.1)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (~> 3.0)
typhoeus (~> 1.3)
html-pipeline (2.11.0)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.7.4)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (~> 1.14)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.6.0)
jekyll (~> 3.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.3.1)
commonmarker (~> 0.14)
jekyll (>= 3.7, < 5.0)
jekyll-commonmark-ghpages (0.1.5)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1)
rouge (~> 2)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.11.0)
jekyll (~> 3.3)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.12.1)
jekyll (~> 3.4)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.4.1)
html-pipeline (~> 2.3)
jekyll (~> 3.0)
jekyll-optional-front-matter (0.3.0)
jekyll (~> 3.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.2.0)
jekyll (~> 3.0)
jekyll-redirect-from (0.14.0)
jekyll (~> 3.3)
jekyll-relative-links (0.6.0)
jekyll (~> 3.3)
jekyll-remote-theme (0.3.1)
jekyll (~> 3.5)
rubyzip (>= 1.2.1, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.5.0)
jekyll (~> 3.3)
jekyll-sitemap (1.2.0)
jekyll (~> 3.3)
jekyll-swiss (0.4.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.3)
jekyll (~> 3.5)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.1)
jekyll (~> 3.3)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.10.2)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (~> 3.0)
kramdown (1.17.0)
liquid (4.0.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
mercenary (0.3.6)
mini_portile2 (2.4.0)
minima (2.5.0)
jekyll (~> 3.5)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.11.3)
multipart-post (2.0.0)
nokogiri (1.10.2)
mini_portile2 (~> 2.4.0)
octokit (4.14.0)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (3.0.3)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rouge (2.2.1)
ruby-enum (0.7.2)
i18n
ruby_dep (1.5.0)
rubyzip (1.2.2)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.3.1)
ethon (>= 0.9.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.5.0)
PLATFORMS
ruby
DEPENDENCIES
github-pages
BUNDLED WITH
2.0.1
+122 -28
View File
@@ -1,41 +1,135 @@
# MQTT-Explorer
[![Downloads](https://github-basic-badges.herokuapp.com/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)
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://github.com/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://github.com/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)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/47b26e03fce543ceac7914214482334a)](https://app.codacy.com/app/thomasnordquist/MQTT-Explorer?utm_source=github.com&utm_medium=referral&utm_content=thomasnordquist/MQTT-Explorer&utm_campaign=Badge_Grade_Dashboard)
<a href="#" onClick="a=window;a.location='ht'+'tps:'+'/'+'/pay'+'pal.me/'+'ThomasNordquist'"><img src="https://img.shields.io/badge/Sponsor-PayPal-Flat.svg?color=0070ba&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgICB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKICAgIHZlcnNpb249IjEuMSIgYmFzZVByb2ZpbGU9ImZ1bGwiIHZpZXdCb3g9IjAgMCAyNCAzMiIgd2lkdGg9IjI0IiBoZWlnaHQ9IjMyIiA+CiAgICA8cGF0aCB0cmFuc2Zvcm09InNjYWxlKDIpIiBmaWxsPSIjZWE0YWFhIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05IDJjLS45NyAwLTEuNjkuNDItMi4yIDEtLjUxLjU4LS43OC45Mi0uOCAxLS4wMi0uMDgtLjI4LS40Mi0uOC0xLS41Mi0uNTgtMS4xNy0xLTIuMi0xLTEuNjMyLjA4Ni0yLjk1NCAxLjMzMy0zIDMgMCAuNTIuMDkgMS41Mi42NyAyLjY3QzEuMjUgOC44MiAzLjAxIDEwLjYxIDYgMTNjMi45OC0yLjM5IDQuNzctNC4xNyA1LjM0LTUuMzNDMTEuOTEgNi41MSAxMiA1LjUgMTIgNWMtLjA0Ny0xLjY5LTEuMzQyLTIuOTEzLTMtM3oiPjwvcGF0aD4KPC9zdmc+Cgo=" /></a>
### Version 0.1.0
See the whole picture of your message queue.
The perfect tool to integrate new services, IoT devices in your network.
This application subscribes to all topics on your MQTT-Server and displays your message queue hierarchy, allowing you to drill-down to the topics that are of interest.
<div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel" data-interval="2500">
<ol class="carousel-indicators">
<li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
<li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
<li data-target="#carouselExampleIndicators" data-slide-to="2"></li>
</ol>
<div class="carousel-inner">
<div class="carousel-item active">
<a href="./img/screen2.png"><img src="./img/screen2_small.png" /></a>
</div>
<div class="carousel-item">
<a href="./img/screen3.png"><img src="./img/screen3_small.png" /></a>
</div>
<div class="carousel-item">
<a href="./img/screen-composite.png"><img src="./img/screen-composite_small.png" /></a>
</div>
</div>
<a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
<span class="slider">
<a href="./img/screen2.png"><img src="./img/screen2_small.png" /></a>
<a href="./img/screen3.png"><img src="./img/screen3_small.png" /></a>
<a href="./img/screen-composite.png"><img src="./img/screen-composite_small.png" /></a>
</span>
## Version 0.4.0-beta.6
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.
### Features
- Visualize topics and topic activity
- Delete retained topics
- Search/filter topics
- Delete topics recursively
- Diff view of current and previous received messages
- Publish topics
- Plot numeric topics
- Retain a history of each topic
- Dark/Light Themes
- ... [See Changelog to see all features](./Changelog)
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.
## Download
The app is prebuilt for Windows ([portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.0&#x2F;MQTT-Explorer-0.1.0.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.0&#x2F;MQTT-Explorer-Setup-0.1.0.exe)), Linux ([AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.0&#x2F;MQTT-Explorer-0.1.0-x86_64.AppImage)) and Mac ([dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.0&#x2F;MQTT-Explorer-0.1.0.dmg)).
<center>
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.
| | | |
|:----------|:-------------:|:------:|
| ![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.0&#x2F;MQTT-Explorer-0.1.0.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.0&#x2F;MQTT-Explorer-Setup-0.1.0.exe) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | [AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.0&#x2F;MQTT-Explorer-0.1.0-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.0&#x2F;MQTT-Explorer-0.1.0.dmg) |
| 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://get.microsoft.com/images/en-us%20dark.svg" width="165" /></a><br />**[portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.4.0-beta.6&#x2F;MQTT-Explorer-0.4.0-beta.6.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.4.0-beta.6&#x2F;MQTT-Explorer-Setup-0.4.0-beta.6.exe)** |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | **Mac** | <a href="https://itunes.apple.com/app/apple-store/id1455214828?pt=118225860&ct=mqtt-explorer.com&mt=8"><img src="https://developer.apple.com/app-store/marketing/guidelines/images/badge-download-on-the-mac-app-store.svg" height="40"/></a><br />**[Intel](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.4.0-beta.6&#x2F;MQTT-Explorer-0.4.0-beta.6.dmg), [ARM](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.4.0-beta.6&#x2F;MQTT-Explorer-0.4.0-beta.6-arm64.dmg)** <br /> *[How to start untrusted apps](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac)*|
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | **Ubuntu**<br />*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.4.0-beta.6&#x2F;MQTT-Explorer-0.4.0-beta.6.AppImage), [deb](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.4.0-beta.6&#x2F;MQTT-Explorer_0.4.0-beta.6_amd64.deb)**<br />*Run AppImage:<br />Make it executable and double-click it.* |
</center>
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
More architectures and package types: [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Video
![screen1](https://user-images.githubusercontent.com/7721625/51441563-41418180-1cd3-11e9-893b-c22f6a6c3695.png)
<div class="osx-frame">
<div class="titlebar">
<div class="buttons">
<div class="close"></div>
<div class="minimize"></div>
<div class="zoom"></div>
</div>
MQTT Explorer
</div>
<div class="content">
<video style="width: 100%" autoplay muted loop>
<source type="video/mp4" src="./video.mp4">
![screencast](https://user-images.githubusercontent.com/7721625/53954172-b7f4db80-40d5-11e9-852c-f8d85e511a00.gif)
</video></div>
</div>
<br>
![screen2](https://user-images.githubusercontent.com/7721625/51441470-59fd6780-1cd2-11e9-925f-bc491027300c.png)
## Performance
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
## Feedback
<form onsubmit="if(!this.elements.noshow.value){ this.action='https://docs.google.com/forms/d/e/1FAIpQLSePeIHoZxpbB_-ku4H97go8JPhfdrlxNc-nHPGQuWB6g-U3HQ/formResponse?embedded=true' }">
### What do you like?
<textarea style="width: 100%; border-radius: 5px; box-shadow: 5px 5px;" name="entry.1944371594"></textarea>
### What can be improved?
<textarea style="width: 100%; border-radius: 5px; box-shadow: 5px 5px;" name="entry.22814098"></textarea>
### Email (optional)
<span style="display: flex"><input style="padding: 8px; flex: 2; border-radius: 5px; box-shadow: 5px 5px; border: 1px solid" type="email" name="entry.2055444782" /><button type="submit" style="padding: 8px; margin-left: 16px; border: 1px solid #2879d0; border-radius: 5px; background-color: #eee; box-shadow: 5px 5px #ccc; text-align: right; color: #2879d0;">Submit</button></span>
<input class="noshow" name="noshow" value="">
</form>
## Develop
PRs and issues are welcome
<div style="text-align: center; width: 100%">
Ideas and bugs can also be reported on [github](https://github.com/thomasnordquist/MQTT-Explorer/issues).
</div>
Install with `npm run install`, build with `npm run build`
## Telemetry
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## License
Not yet decided which license exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
App telemetry has been removed, we rely on your bug reports to [GitHub issues](https://github.com/thomasnordquist/MQTT-Explorer/issues)
+122 -28
View File
@@ -1,41 +1,135 @@
# MQTT-Explorer
[![Downloads](https://github-basic-badges.herokuapp.com/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)
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://github.com/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://github.com/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)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/47b26e03fce543ceac7914214482334a)](https://app.codacy.com/app/thomasnordquist/MQTT-Explorer?utm_source=github.com&utm_medium=referral&utm_content=thomasnordquist/MQTT-Explorer&utm_campaign=Badge_Grade_Dashboard)
<a href="#" onClick="a=window;a.location='ht'+'tps:'+'/'+'/pay'+'pal.me/'+'ThomasNordquist'"><img src="https://img.shields.io/badge/Sponsor-PayPal-Flat.svg?color=0070ba&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgICB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKICAgIHZlcnNpb249IjEuMSIgYmFzZVByb2ZpbGU9ImZ1bGwiIHZpZXdCb3g9IjAgMCAyNCAzMiIgd2lkdGg9IjI0IiBoZWlnaHQ9IjMyIiA+CiAgICA8cGF0aCB0cmFuc2Zvcm09InNjYWxlKDIpIiBmaWxsPSIjZWE0YWFhIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05IDJjLS45NyAwLTEuNjkuNDItMi4yIDEtLjUxLjU4LS43OC45Mi0uOCAxLS4wMi0uMDgtLjI4LS40Mi0uOC0xLS41Mi0uNTgtMS4xNy0xLTIuMi0xLTEuNjMyLjA4Ni0yLjk1NCAxLjMzMy0zIDMgMCAuNTIuMDkgMS41Mi42NyAyLjY3QzEuMjUgOC44MiAzLjAxIDEwLjYxIDYgMTNjMi45OC0yLjM5IDQuNzctNC4xNyA1LjM0LTUuMzNDMTEuOTEgNi41MSAxMiA1LjUgMTIgNWMtLjA0Ny0xLjY5LTEuMzQyLTIuOTEzLTMtM3oiPjwvcGF0aD4KPC9zdmc+Cgo=" /></a>
### Version {{ version }}
See the whole picture of your message queue.
The perfect tool to integrate new services, IoT devices in your network.
This application subscribes to all topics on your MQTT-Server and displays your message queue hierarchy, allowing you to drill-down to the topics that are of interest.
<div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel" data-interval="2500">
<ol class="carousel-indicators">
<li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
<li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
<li data-target="#carouselExampleIndicators" data-slide-to="2"></li>
</ol>
<div class="carousel-inner">
<div class="carousel-item active">
<a href="./img/screen2.png"><img src="./img/screen2_small.png" /></a>
</div>
<div class="carousel-item">
<a href="./img/screen3.png"><img src="./img/screen3_small.png" /></a>
</div>
<div class="carousel-item">
<a href="./img/screen-composite.png"><img src="./img/screen-composite_small.png" /></a>
</div>
</div>
<a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
<span class="slider">
<a href="./img/screen2.png"><img src="./img/screen2_small.png" /></a>
<a href="./img/screen3.png"><img src="./img/screen3_small.png" /></a>
<a href="./img/screen-composite.png"><img src="./img/screen-composite_small.png" /></a>
</span>
## Version {{ version }}
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.
### Features
- Visualize topics and topic activity
- Delete retained topics
- Search/filter topics
- Delete topics recursively
- Diff view of current and previous received messages
- Publish topics
- Plot numeric topics
- Retain a history of each topic
- Dark/Light Themes
- ... [See Changelog to see all features](./Changelog)
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.
## Download
The app is prebuilt for Windows ({{windowsTargets}}), Linux ({{linuxTargets}}) and Mac ({{macTargets}}).
<center>
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.
| | | |
|:----------|:-------------:|:------:|
| ![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}} |
| 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://get.microsoft.com/images/en-us%20dark.svg" width="165" /></a><br />**{{windowsTargets}}** |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | **Mac** | <a href="https://itunes.apple.com/app/apple-store/id1455214828?pt=118225860&ct=mqtt-explorer.com&mt=8"><img src="https://developer.apple.com/app-store/marketing/guidelines/images/badge-download-on-the-mac-app-store.svg" height="40"/></a><br />**{{macTargets}}** <br /> *[How to start untrusted apps](https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac)*|
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | **Ubuntu**<br />*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.* |
</center>
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
More architectures and package types: [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Video
![screen1](https://user-images.githubusercontent.com/7721625/51441563-41418180-1cd3-11e9-893b-c22f6a6c3695.png)
<div class="osx-frame">
<div class="titlebar">
<div class="buttons">
<div class="close"></div>
<div class="minimize"></div>
<div class="zoom"></div>
</div>
MQTT Explorer
</div>
<div class="content">
<video style="width: 100%" autoplay muted loop>
<source type="video/mp4" src="./video.mp4">
![screencast](https://user-images.githubusercontent.com/7721625/53954172-b7f4db80-40d5-11e9-852c-f8d85e511a00.gif)
</video></div>
</div>
<br>
![screen2](https://user-images.githubusercontent.com/7721625/51441470-59fd6780-1cd2-11e9-925f-bc491027300c.png)
## Performance
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
## Feedback
<form onsubmit="if(!this.elements.noshow.value){ this.action='https://docs.google.com/forms/d/e/1FAIpQLSePeIHoZxpbB_-ku4H97go8JPhfdrlxNc-nHPGQuWB6g-U3HQ/formResponse?embedded=true' }">
### What do you like?
<textarea style="width: 100%; border-radius: 5px; box-shadow: 5px 5px;" name="entry.1944371594"></textarea>
### What can be improved?
<textarea style="width: 100%; border-radius: 5px; box-shadow: 5px 5px;" name="entry.22814098"></textarea>
### Email (optional)
<span style="display: flex"><input style="padding: 8px; flex: 2; border-radius: 5px; box-shadow: 5px 5px; border: 1px solid" type="email" name="entry.2055444782" /><button type="submit" style="padding: 8px; margin-left: 16px; border: 1px solid #2879d0; border-radius: 5px; background-color: #eee; box-shadow: 5px 5px #ccc; text-align: right; color: #2879d0;">Submit</button></span>
<input class="noshow" name="noshow" value="">
</form>
## Develop
PRs and issues are welcome
<div style="text-align: center; width: 100%">
Ideas and bugs can also be reported on [github](https://github.com/thomasnordquist/MQTT-Explorer/issues).
</div>
Install with `npm run install`, build with `npm run build`
## Telemetry
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## License
Not yet decided which license exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
App telemetry has been removed, we rely on your bug reports to [GitHub issues](https://github.com/thomasnordquist/MQTT-Explorer/issues)
+15
View File
@@ -0,0 +1,15 @@
theme: jekyll-theme-architect
title: MQTT Explorer
description: An all-round MQTT client that provides a structured topic overview
author:
name: Thomas Nordquist
picture: https://avatars1.githubusercontent.com/u/7721625?s=460&v=4
github_username: thomasnordquist
logo: https://user-images.githubusercontent.com/7721625/52227945-5280b580-28b1-11e9-804c-75a842537393.png
plugins:
- jekyll-seo-tag
google_analytics: UA-133696837-1
markdown: kramdown
kramdown:
parse_block_html: true
+192
View File
@@ -0,0 +1,192 @@
<!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">
<script
src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8="
crossorigin="anonymous"></script>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<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">
<link rel="stylesheet" href="./osx-frame.css">
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
<style>
.noshow {display: none}
@media (min-width: 768px) {
.slider {
display: flex;
}
.carousel {
display: none;
}
}
@media (max-width: 768px) {
.slider {
display: none;
}
.carousel {
display: block;
}
}
form>h3::before {
content: '' !important;
margin-left: 0 !important;
}
.carousel-control-next-icon, .carousel-control-prev-icon {
display: inline-block;
width: 32px;
height: 32px;
background-size: 70%;
background-repeat: no-repeat;
background-color: grey;
border-radius: 5px;
}
.inner img {
border-radius: 8px;
}
.slider a {
padding: 4px;
}
.carousel-item, .slider img {
box-shadow: 0px 0px 5px #6b6b6b;
}
</style>
{% seo %}
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-TRN5K6H');</script>
<!-- End Google Tag Manager -->
<script>
function trackConversion(url) {
const conversions = {
'snap://mqtt-explorer': 'snap',
'https://snapcraft.io/mqtt-explorer': 'snap',
'https://geo.itunes.apple.com/us/app/mqtt-explorer/id1455214828?mt=12&app=apps': 'Mac App Store',
'https://www.microsoft.com/store/apps/9PP8SFM082WD?ocid=badge': 'Microsoft Store',
'https://github.com/thomasnordquist/MQTT-Explorer/releases/download/': 'binary download'
}
const conversion = Object.keys(conversions).find(function(conversionUrlPrefix) { return url.startsWith(conversionUrlPrefix) })
if (conversion) {
ga('send', 'event', 'outbound', 'download', conversions[conversion], {
'transport': 'beacon',
'hitCallback': function(){ document.location = url; }
});
} else {
ga('send', 'event', 'outbound', 'click', url, {
'transport': 'beacon',
'hitCallback': function(){ document.location = url; }
});
}
}
function didLoad() {
if (!Array.prototype.filter || !Array.prototype.find || !String.prototype.startsWith) {
return
}
Object.values(document.getElementsByTagName('a'))
.filter(function(a) { return !a.href.startsWith(a.baseURI)})
.forEach(function(a) { a.onclick = function() { trackConversion(a.href) }})
}
</script>
</head>
<body onload="didLoad()">
<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 }}" style="box-sizing: initial" 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 %}
<br />
<h3>In the media</h3>
<blockquote class="twitter-tweet" data-width="200">
<p lang="en" dir="ltr">If youre seeking a comprehensive and easy to use
<a href="https://twitter.com/hashtag/MQTT?src=hash&amp;ref_src=twsrc%5Etfw">#MQTT</a> client for
<a href="https://twitter.com/hashtag/Ubuntu?src=hash&amp;ref_src=twsrc%5Etfw">#Ubuntu</a> then look no further than MQTT Explorer.
<br>
<br>snap install mqtt-explorer
<a href="https://t.co/1nd0Z8VQeR">https://t.co/1nd0Z8VQeR</a>
<a href="https://t.co/8XKenpQu0p">pic.twitter.com/8XKenpQu0p</a>
</p>
&mdash; Ubuntu (@ubuntu)
<a href="https://twitter.com/ubuntu/status/1125747632910077953?ref_src=twsrc%5Etfw">May 7, 2019</a>
</blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</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>
+3
View File
@@ -0,0 +1,3 @@
require 'webrick'
include WEBrick
WEBrick::HTTPUtils::DefaultMimeTypes.store 'mp4', 'video/mpeg'
-134
View File
@@ -1,134 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>MQTT-Explorer</title>
<script src="./build/bugtracking.bundle.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
}
@keyframes example {
0% {background-color: none;}
25% {background-color: #3f51b5;}
50% {background-color: #3f51b5;}
100% {background-color: none;}
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0.0);
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(30,30,30,0.3);
}
::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8);
}
</style>
<style>
#splash {
z-index: 1000000;
background-color: #303030;
display: block;
width: 100vw;
height: 100vh;
position: fixed;
opacity: 1;
}
#splash1 {
margin: 37vh auto 0 auto;
height: 25vh;
width: 25vh;
/* background-image:url('../rings.svg'); */
background-size: cover;
}
#splash2 {
margin: 0 auto;
}
@keyframes unsplash {
0% {opacity: 1;}
100% {opacity: 0;}
}
.Resizer {
background: #eee;
opacity: .2;
z-index: 1;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.Resizer.horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
.Resizer.vertical {
width: 11px;
margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
}
.Resizer.vertical:hover {
border-left: 5px solid rgba(255, 255, 255, 0.5);
border-right: 5px solid rgba(255, 255, 255, 0.5);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
</style>
<script src="http://localhost:35729/livereload.js"></script>
</head>
<body>
<div id="splash"><div id="splash1"></div></div>
<div id="app" style="font:-webkit-control"></div>
<script>
function loadScript(path) {
var script = document.createElement("script");
script.src = path
document.head.appendChild(script);
}
document.addEventListener('DOMContentLoaded', onLoad(), false);
function onLoad() {
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script><% }); %>
</body>
</html>
-8424
View File
File diff suppressed because it is too large Load Diff
-57
View File
@@ -1,57 +0,0 @@
{
"name": "mqtt-explorer-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@material-ui/icons": "^3.0.1",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.12",
"css-loader": "^2.1.0",
"electron-nucleus": "^1.11.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git",
"html-webpack-plugin": "^4.0.0-beta.5",
"number-abbreviate": "^2.0.0",
"react": "^16.8.0-alpha.1",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"style-loader": "^0.23.1",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-livereload-plugin": "^2.2.0"
},
"dependencies": {
"@material-ui/core": "^3.9.0",
"@material-ui/styles": "^3.0.0-alpha.8",
"@types/node": "^10.12.18",
"@types/react-resize-detector": "^3.1.0",
"@types/react-split-pane": "^0.1.67",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^1.4.32",
"@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1",
"copy-text-to-clipboard": "^1.0.4",
"jquery": "^3.3.1",
"lodash.throttle": "^4.1.1",
"moving-average": "^1.0.0",
"react-ace": "^6.3.2",
"react-json-view": "^1.19.1",
"react-resize-detector": "^3.4.0",
"react-split-pane": "^0.1.85",
"redux-batched-actions": "^0.4.1",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4",
"typescript": "^3.2.2",
"webpack": "^4.28.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
}
}
-42
View File
@@ -1,42 +0,0 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="45" height="45" viewBox="0 0 45 45" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd" transform="translate(1 1)" stroke-width="2">
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="1.5s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="1.5s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="1.5s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="3s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="3s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="3s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="8">
<animate attributeName="r"
begin="0s" dur="1.5s"
values="6;1;2;3;4;5;6"
calcMode="linear"
repeatCount="indefinite" />
</circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-123
View File
@@ -1,123 +0,0 @@
import * as React from 'react'
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'
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 { connect } from 'react-redux'
import ErrorBoundary from './ErrorBoundary'
import { default as SplitPane } from 'react-split-pane'
interface Props {
name: string
connectionId: string
classes: any
settingsVisible: boolean
}
class App extends React.PureComponent<Props, {}> {
constructor(props: any) {
super(props)
this.state = { }
}
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes
return (
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<Settings />
</React.Suspense>
<div className={centerContent}>
<div className={`${settingsVisible ? contentShift : content}`}>
<TitleBar />
</div>
<div>
<SplitPane
step={48}
primary="second"
className={`${settingsVisible ? contentShift : content} ${heightProperty}`}
split="vertical"
minSize={250}
defaultSize={500}
allowResize={true}
pane1Style={{ overflow: 'hidden' }}
>
<div className={paneDefaults}>
<Tree />
</div>
<div className={paneDefaults}>
<Sidebar connectionId={this.props.connectionId} />
</div>
</SplitPane>
</div>
</div>
<UpdateNotifier />
<Connection />
</ErrorBoundary>
</div >
)
}
}
const mapStateToProps = (state: AppState) => {
return {
settingsVisible: state.settings.visible,
connectionId: state.connection.connectionId,
}
}
const styles = (theme: Theme) => {
const drawerWidth = 300
return {
heightProperty: {
height: 'calc(100vh - 64px) !important',
},
paneDefaults: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
overflowY: 'scroll' as 'scroll',
overflowX: 'hidden' as 'hidden',
display: 'block' as 'block',
height: 'calc(100vh - 64px)',
},
centerContent: {
width: '100vw',
overflow: 'hidden' as 'hidden',
},
content: {
width: '100vw',
overflowX: 'hidden',
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
transform: 'translateX(0px)',
},
contentShift: {
overflowX: 'hidden',
width: '100vw',
padding: 0,
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
transform: `translateX(${drawerWidth}px)`,
},
}
}
export default withStyles(styles)(connect(mapStateToProps)(App))
-112
View File
@@ -1,112 +0,0 @@
import * as React from 'react'
import { electronRendererTelementry } from 'electron-telemetry'
import {
Button,
Modal,
Paper,
Toolbar,
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
}
interface Props {
classes: any
}
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {}
}
public componentDidCatch(error: Error, errorInfo: any) {
electronRendererTelementry.trackError(error)
console.log('did catch', error)
}
public static getDerivedStateFromError(error: Error) {
return { error }
}
private restart = () => {
window.location = window.location
}
private clearStorage = () => {
localStorage.clear()
window.location = window.location
}
public render() {
if (!this.state.error) {
return this.props.children
}
const { classes } = this.props
return (
<Modal open={true} disableAutoFocus={true}>
<Paper className={classes.root}>
<Toolbar style={{ padding: '0' }}>
<Typography className={classes.title} variant="h6" color="inherit"><Warning /> Oooooops!</Typography>
</Toolbar>
<Typography className={classes.centered}>I hoped that you would never see this window, but MQTT-Explorer had an unexpected error.</Typography>
<Typography className={classes.centered}><SentimentDissatisfied /></Typography>
<pre className={classes.textColor} style={{ maxHeight: '35vh', overflow: 'scroll' }}>
<code className={classes.textColor}>
{this.state.error.stack}
</code>
</pre>
<Typography>
Please report this issue with a short description of what happened to
<span> <a className={classes.textColor} href="https://github.com/thomasnordquist/MQTT-Explorer/issues">https://github.com/thomasnordquist/MQTT-Explorer/issues</a></span>
</Typography>
<div>
<div className={classes.buttonPositioning}>
<Button className={classes.button} variant="contained" color="secondary" onClick={this.clearStorage}>Start Fresh</Button>
<Button className={classes.button} variant="contained" color="primary" onClick={this.restart}>Restart</Button>
</div>
</div>
</Paper>
</Modal>
)
}
}
const styles = (theme: Theme) => ({
button: {
margin: '0 8px 0 8px',
},
root: {
minWidth: 550,
maxWidth: 650,
backgroundColor: theme.palette.background.default,
margin: '10vh auto auto auto',
padding: `${2 * theme.spacing.unit}px`,
outline: 'none',
},
title: {
color: theme.palette.text.primary,
margin: '0',
textAlign: 'center' as 'center',
},
textColor: {
color: theme.palette.text.primary,
},
centered: {
textAlign: 'center' as 'center',
},
buttonPositioning: {
textAlign: 'center' as 'center',
marginTop: `${theme.spacing.unit * 2}px`,
},
})
export default withStyles(styles)(ErrorBoundary)
-19
View File
@@ -1,19 +0,0 @@
import { EventDispatcher } from '../../events'
export class TopicViewModel {
private selected: boolean
public change = new EventDispatcher<void, TopicViewModel>(this)
public constructor() {
this.selected = false
}
public isSelected() {
return this.selected
}
public setSelected(selected: boolean) {
this.selected = selected
this.change.dispatch()
}
}
-226
View File
@@ -1,226 +0,0 @@
import * as React from 'react'
import {
Button,
IconButton,
Modal,
Paper,
Snackbar,
SnackbarContent,
Typography,
} from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { UpdateInfo, checkForUpdates, rendererEvents, updateAvailable } from '../../events'
import { green, red } from '@material-ui/core/colors'
import { AppState } from './reducers'
import Close from '@material-ui/icons/Close'
import CloudDownload from '@material-ui/icons/CloudDownload'
import { UpdateFileInfo } from 'builder-util-runtime'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { updateNotifierActions } from './actions'
interface Props {
showUpdateNotification: boolean
showUpdateDetails: boolean
classes: any
actions: any
}
class UpdateNotifier extends React.Component<Props, {}> {
private updateInfo?: UpdateInfo
constructor(props: any) {
super(props)
this.state = {
selectedNode: undefined,
}
}
public componentDidMount() {
rendererEvents.emit(checkForUpdates, undefined)
rendererEvents.subscribe(updateAvailable, this.handleUpdate)
}
public componentWillUnmount() {
rendererEvents.unsubscribeAll(updateAvailable)
}
private fixUrl(url: string, version: string) {
if (!/^http/.test(url)) {
return `https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v${version}/${url}`
}
return url
}
private handleUpdate = (updateInfo: UpdateInfo) => {
this.updateInfo = updateInfo
this.props.actions.showUpdateNotification(true)
}
private onCloseNotification = (event: React.SyntheticEvent<any>, reason: string) => {
if (reason === 'clickaway') {
return
}
this.props.actions.showUpdateNotification(false)
}
private closeNotification = () => {
this.props.actions.showUpdateNotification(false)
}
private showDetails = () => {
this.props.actions.showUpdateNotification(false)
this.props.actions.showUpdateDetails(true)
}
private hideDetails = () => {
this.props.actions.showUpdateDetails(false)
}
public render() {
return (
<div>
{this.renderUpdateNotification()}
{this.renderUpdateDetails()}
</div>
)
}
private renderUpdateNotification() {
const snackbarAnchor: any = {
vertical: 'top',
horizontal: 'right',
}
return (
<Snackbar
anchorOrigin={snackbarAnchor}
open={this.props.showUpdateNotification}
autoHideDuration={7000}
onClose={this.onCloseNotification}
>
<SnackbarContent
className={this.props.classes.success}
message="Update available"
action={this.notificationActions()}
/>
</Snackbar>
)
}
private notificationActions() {
return [(
<Button key="undo" size="small" onClick={this.showDetails}>
Download
</Button>
), (
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={this.props.classes.close}
onClick={this.closeNotification}
>
<Close />
</IconButton>
),
]
}
private renderUpdateDetails() {
if (!this.updateInfo) {
return null
}
const releaseNotes = (this.updateInfo.releaseNotes as string) || ''
return (
<Modal
open={this.props.showUpdateDetails}
disableAutoFocus={true}
onClose={this.hideDetails}
>
<Paper className={this.props.classes.root}>
<Typography variant="h6" className={this.props.classes.title}>Version {this.updateInfo.version}</Typography>
<Typography className={this.props.classes.title}>Changelog</Typography>
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: releaseNotes }} />
{this.renderDownloads(this.updateInfo)}
<Button className={this.props.classes.closeButton} color="secondary" onClick={this.hideDetails}>Close</Button>
</Paper>
</Modal>
)
}
private urlToFilename(url: string) {
const parts = url.split('/')
return parts[parts.length - 1]
}
private renderDownloads(updateInfo: UpdateInfo) {
return updateInfo.files
.map((file: UpdateFileInfo, index: number) => (
<div key={index}>
<Button
className={this.props.classes.download}
href={this.fixUrl(file.url, updateInfo.version)}
>
<IconButton><CloudDownload /></IconButton>{this.urlToFilename(file.url)}
</Button>
</div>
))
}
}
const styles = (theme: Theme) => ({
success: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
close: {
padding: theme.spacing.unit / 2,
},
root: {
minWidth: '350px',
maxWidth: '500px',
backgroundColor: theme.palette.background.default,
margin: '20vh auto auto auto',
padding: `${2 * theme.spacing.unit}px`,
outline: 'none',
},
title: {
color: theme.palette.text.primary,
},
releaseNotes: {
overflow: 'auto scroll',
color: theme.palette.text.secondary,
backgroundColor: 'rgba(60, 60, 60, 0.6)',
maxHeight: '28vh',
},
paper: {
padding: `${theme.spacing.unit * 2}px`,
color: theme.palette.text.secondary,
},
download: {
width: '100%',
},
closeButton: {
display: 'block',
margin: '0 0 0 auto',
},
})
const mapStateToProps = (state: AppState) => {
return {
showUpdateNotification: state.tooBigReducer.showUpdateNotification,
showUpdateDetails: state.tooBigReducer.showUpdateDetails,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(updateNotifierActions, dispatch),
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
-55
View File
@@ -1,55 +0,0 @@
import { ActionTypes, Action, ConnectionState } from '../reducers/Connection'
import { MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { rendererEvents, 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'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
rendererEvents.subscribe(event, (dataSourceState) => {
if (dataSourceState.connected) {
const 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())
}
})
}
export const connected: (tree: q.Tree<TopicViewModel>, host: string) => Action = (tree: q.Tree<TopicViewModel>, host: string) => ({
tree,
host,
type: ActionTypes.CONNECTION_SET_CONNECTED,
})
export const connecting: (connectionId: string) => Action = (connectionId: string) => ({
connectionId,
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
rendererEvents.emit(removeConnection, connectionId)
tree && tree.stopUpdating()
dispatch(showTree(undefined))
dispatch({
type: ActionTypes.CONNECTION_SET_DISCONNECTED,
})
}
-56
View File
@@ -1,56 +0,0 @@
import { ActionTypes, Action } from '../reducers/Publish'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { rendererEvents, makePublishEvent } from '../../../events'
export const setTopic = (topic?: string): Action => {
return {
topic,
type: ActionTypes.PUBLISH_SET_TOPIC,
}
}
export const setPayload = (payload?: string): Action => {
return {
payload,
type: ActionTypes.PUBLISH_SET_PAYLOAD,
}
}
export const setQoS = (qos: 0 | 1 | 2): Action => {
return {
qos,
type: ActionTypes.PUBLISH_SET_QOS,
}
}
export const setEditorMode = (editorMode: string): Action => {
return {
editorMode,
type: ActionTypes.PUBLISH_SET_EDITOR_MODE,
}
}
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const state = getState()
const topic = state.publish.topic
if (!topic) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic,
payload: state.publish.payload,
retain: state.publish.retain,
qos: state.publish.qos,
}
rendererEvents.emit(publishEvent, mqttMessage)
}
export const toggleRetain = (): Action => {
return {
type: ActionTypes.PUBLISH_TOGGLE_RETAIN,
}
}
-89
View File
@@ -1,89 +0,0 @@
import { Action, ActionTypes, TopicOrder } from '../reducers/Settings'
import { ActionTypes as TreeActionTypes } from '../reducers/Tree'
import { Dispatch } from 'redux'
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'
export const setAutoExpandLimit = (autoExpandLimit: number = 0): Action => {
return {
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
}
}
export const toggleSettingsVisibility = (): Action => {
return {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY,
}
}
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none): Action => {
return {
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
}
}
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { tree } = getState().connection
dispatch({
topicFilter: filterStr,
type: ActionTypes.SETTINGS_FILTER_TOPICS,
})
if (!filterStr || !tree) {
dispatch(batchActions([setAutoExpandLimit(0), (showTree(tree) as any)]))
return
}
const topicFilter = filterStr.toLowerCase()
const nodeFilter = (node: q.TreeNode<TopicViewModel>): boolean => {
const topicMatches = node.path().toLowerCase().indexOf(topicFilter) !== -1
if (topicMatches) {
return true
}
const messageMatches = (node.message && typeof node.message.value === 'string' && node.message.value.toLowerCase().indexOf(filterStr) !== -1)
return Boolean(messageMatches)
}
const resultTree = tree.childTopics()
.filter(nodeFilter)
.map((node: q.TreeNode<TopicViewModel>) => {
const clone = node.unconnectedClone()
q.TreeNodeFactory.insertNodeAtPosition(node.path().split('/'), clone)
return clone.firstNode()
})
.reduce((a: q.TreeNode<TopicViewModel>, b: q.TreeNode<TopicViewModel>) => {
a.updateWithNode(b)
return a
}, new q.Tree<TopicViewModel>())
const nextTree: q.Tree<TopicViewModel> = resultTree as q.Tree<TopicViewModel>
if (tree.updateSource && tree.connectionId) {
nextTree.updateWithConnection(tree.updateSource, tree.connectionId, nodeFilter)
}
dispatch(batchActions([setAutoExpandLimit(autoExpandLimitForTree(nextTree)), (showTree(nextTree) as any)]))
}
function autoExpandLimitForTree(tree: q.Tree<TopicViewModel>) {
if (!tree) {
return 0
}
function closestExistingLimit(i: number): number {
const sorted = autoExpandLimitSet.sort((a, b) => Math.abs(a.limit - i) - Math.abs(b.limit - i))
return sorted[0]!.limit
}
const count = tree.childTopicCount()
const calculatedLimit = Math.max(7 - Math.log(count), 0) * 2
return closestExistingLimit(calculatedLimit)
}
-21
View File
@@ -1,21 +0,0 @@
import { Dispatch, Action } from 'redux'
import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events'
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic } = getState().tree
const { connectionId } = getState().connection
if (!selectedTopic || !connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: selectedTopic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
}
-54
View File
@@ -1,54 +0,0 @@
import { AppState } from '../reducers'
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'
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { selectedTopic } = getState().tree
if (selectedTopic === topic) {
return
}
// Update publish topic
let setTopicDispatch: any | undefined
if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) {
setTopicDispatch = setTopic(topic.path())
}
if (selectedTopic && selectedTopic.viewModel) {
selectedTopic.viewModel.setSelected(false)
}
if (topic.viewModel) {
topic.viewModel.setSelected(true)
}
const selectTreeTopicDispatch = {
selectedTopic: topic,
type: ActionTypes.TREE_SELECT_TOPIC,
}
if (setTopicDispatch) {
dispatch(batchActions([selectTreeTopicDispatch, setTopicDispatch]))
} else {
dispatch(selectTreeTopicDispatch)
}
}
export const showTree = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
const visibleTree = getState().tree.tree
const connectionTree = getState().connection.tree
// Stop updates of old tree
if (visibleTree !== connectionTree && visibleTree) {
visibleTree.stopUpdating()
}
return dispatch({
tree,
type: ActionTypes.TREE_SHOW_TREE,
})
}
-15
View File
@@ -1,15 +0,0 @@
import { ActionTypes, CustomAction } from '../reducers'
export const showUpdateNotification = (show: boolean): CustomAction => {
return {
type: ActionTypes.showUpdateNotification,
showUpdateNotification: show,
}
}
export const showUpdateDetails = (show: boolean): CustomAction => {
return {
type: ActionTypes.showUpdateDetails,
showUpdateDetails: show,
}
}
-8
View File
@@ -1,8 +0,0 @@
import * as settingsActions from './Settings'
import * as publishActions from './Publish'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection'
import * as sidebarActons from './Sidebar'
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons }
-4
View File
@@ -1,4 +0,0 @@
import { electronRendererTelementry } from 'electron-telemetry'
const spareMeFromGc = electronRendererTelementry
electronRendererTelementry.registerErrorHandler()
-132
View File
@@ -1,132 +0,0 @@
import * as React from 'react'
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 {
topic: string
title: string
}
const styles: StyleRulesCallback = theme => ({
flex: {
display: 'flex',
width: '100%',
},
container: {
width: '100%',
height: '224px',
backgroundColor: 'rebeccapurple',
marginBottom: 0,
marginTop: 'auto',
padding: '8px',
},
})
interface Props {
classes: any
tree?: q.Tree<TopicViewModel>
}
class BrokerStatistics extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
}
public render() {
const { tree, classes } = this.props
if (!tree) {
return null
}
const stats: any = {
broker: {
topic: '$SYS/broker/version',
title: 'Broker',
},
clients: {
topic: '$SYS/broker/clients/total',
title: 'Clients',
},
subscriptions: {
topic: '$SYS/broker/subscriptions/count',
title: 'Subscriptions',
},
received: {
topic: '$SYS/broker/messages/received',
title: 'Received',
},
sent: {
topic: '$SYS/broker/messages/sent',
title: 'Sent',
},
received5m: {
topic: '$SYS/broker/load/messages/received/5min',
title: 'Received last 5min',
},
sent5m: {
topic: '$SYS/broker/load/messages/sent/5min',
title: 'Sent 5m',
},
heap: {
topic: '$SYS/broker/heap/current',
title: 'Memory',
},
heapMax: {
topic: '$SYS/broker/heap/maximum',
title: 'Memory (max)',
},
}
return (
<div className={classes.container}>
{this.renderStat(tree, stats.broker)}
{this.renderPair(tree, stats.sent, stats.received)}
{this.renderPair(tree, stats.clients, stats.subscriptions)}
{this.renderPair(tree, stats.sent5m, stats.received5m)}
{this.renderPair(tree, stats.heap, stats.heapMax)}
</div>
)
}
private renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div className={this.props.classes.flex}>
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{this.renderStat(tree, b)}</div>
</div>
)
}
public renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
const node = tree.findNode(stat.topic)
if (!node) {
return null
}
let value = node.message && node.message.value
value = !isNaN(value) ? abbreviate(value) : value
return (
<div key={stat.title}>
<Typography><b>{stat.title}</b></Typography>
<Typography style={{ paddingLeft: '8px' }}><i>{value}</i></Typography>
</div>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
tree: state.connection.tree,
}
}
export default withStyles(styles)(connect(mapStateToProps)(BrokerStatistics))
@@ -1,395 +0,0 @@
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,51 +0,0 @@
import * as React from 'react'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { green, red } from '@material-ui/core/colors'
interface Props {
message?: string
onClose: () => void
classes: any
}
class Notification extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
public static styles = (theme: Theme) => ({
success: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
error: {
backgroundColor: red[600],
color: theme.typography.button.color,
},
})
public render() {
const snackbarAnchor = {
vertical: 'bottom' as 'bottom',
horizontal: 'left' as 'left',
}
return (
<Snackbar
anchorOrigin={snackbarAnchor}
open={Boolean(this.props.message)}
autoHideDuration={10000}
onClose={this.props.onClose}
>
<SnackbarContent
className={this.props.classes.error}
message={this.props.message}
/>
</Snackbar>
)
}
}
export default withStyles(Notification.styles)(Notification)
-70
View File
@@ -1,70 +0,0 @@
import * as React from 'react'
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 { withStyles, Theme } from '@material-ui/core/styles'
const copy = require('copy-text-to-clipboard')
interface Props {
value: string
classes: any
}
interface State {
didCopy: boolean
snackBarOpen: boolean
}
const styles = (theme: Theme) => ({
snackbar: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
})
class Copy extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { didCopy: false, snackBarOpen: false }
}
public render() {
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" style={{ cursor: 'pointer' }} onClick={this.handleClick} />
: <Check fontSize="inherit" style={{ cursor: 'default' }} />
return <span>
<span style={{ fontSize: '16px' }}>{icon}</span>
<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) => {
event.stopPropagation()
copy(this.props.value)
this.setState({ didCopy: true, snackBarOpen: true })
setTimeout(() => {
this.setState({ didCopy: false })
}, 1500)
}
}
export default withStyles(styles)(Copy)
-172
View File
@@ -1,172 +0,0 @@
import * as React from 'react'
import { AppState } from '../reducers'
import {
Divider,
Drawer,
IconButton,
Input,
InputLabel,
MenuItem,
Select,
Typography,
} from '@material-ui/core'
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,
name: 'Collapsed',
}, {
limit: 2,
name: 'Few',
}, {
limit: 3,
name: 'Some',
}, {
limit: 10,
name: 'Most',
}, {
limit: 1E6,
name: 'All',
}]
const styles: StyleRulesCallback = theme => ({
drawer: {
backgroundColor: theme.palette.background.default,
flexShrink: 0,
},
paper: {
width: '300px',
},
title: {
color: theme.palette.text.primary,
paddingTop: `${theme.spacing.unit}px`,
...theme.mixins.toolbar,
},
input: {
minWidth: '150px',
margin: `auto ${theme.spacing.unit}px auto ${2 * theme.spacing.unit}px`,
},
})
interface Props {
autoExpandLimit: number
visible: boolean
store?: any
topicOrder: TopicOrder
classes: any
actions: typeof settingsActions
}
class Settings extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
}
public render() {
const { classes, actions, visible } = this.props
return (
<Drawer
anchor="left"
className={classes.drawer}
open={visible}
variant="persistent"
>
<div
className={classes.paper}
tabIndex={0}
role="button"
>
<Typography className={classes.title} variant="h6" color="inherit">
<IconButton onClick={actions.toggleSettingsVisibility}>
<ChevronRight />
</IconButton>
Settings
</Typography>
<Divider />
{this.renderAutoExpand()}
{this.renderNodeOrder()}
</div>
<BrokerStatistics />
</Drawer>
)
}
private renderAutoExpand() {
const { classes, autoExpandLimit } = this.props
const limits = autoExpandLimitSet.map(limit => <MenuItem key={limit.limit} value={limit.limit}>{limit.name}</MenuItem>)
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Auto Expand</InputLabel>
<Select
value={autoExpandLimit}
onChange={this.onChangeAutoExpand}
input={<Input name="auto-expand" id="auto-expand-label-placeholder" />}
name="auto-expand"
className={classes.input}
style={{ flex: '1' }}
>
{limits}
</Select>
</div>
)
}
private onChangeAutoExpand = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setAutoExpandLimit(parseInt(e.target.value, 10))
}
private renderNodeOrder() {
const { classes, topicOrder } = this.props
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Topic order</InputLabel>
<Select
value={topicOrder}
onChange={this.onChangeSorting}
input={<Input name="node-order" id="node-order-label-placeholder" />}
displayEmpty={true}
name="node-order"
className={classes.input}
style={{ flex: '1' }}
>
<MenuItem value={TopicOrder.none}><em>default</em></MenuItem>
<MenuItem value={TopicOrder.abc}>a-z</MenuItem>
<MenuItem value={TopicOrder.messages}>{TopicOrder.messages}</MenuItem>
<MenuItem value={TopicOrder.topics}>{TopicOrder.topics}</MenuItem>
</Select>
</div>)
}
private onChangeSorting = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setTopicOrder(e.target.value as TopicOrder)
}
}
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
topicOrder: state.settings.topicOrder,
visible: state.settings.visible,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(settingsActions, dispatch),
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Settings))
-98
View File
@@ -1,98 +0,0 @@
import * as React from 'react'
import { Badge, Typography } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
interface HistoryItem {
title: string
value: string
}
interface Props {
items: HistoryItem[]
onClick?: (index: number, element: EventTarget) => void
classes: any
contentTypeIndicator?: String
}
interface State {
collapsed: boolean
}
class MessageHistory extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = {
collapsed: true,
}
}
public renderHistory() {
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}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
>
<div><i>{element.title}</i></div>
<div style={messageStyle}>
<span><pre>{element.value}</pre></span>
</div>
</div>
))
const visible = this.props.items.length > 0 && this.state.collapsed
return (
<div style={{ backgroundColor: 'rgba(60, 60, 60, 0.6)', marginTop: '16px' }}>
<Typography
component={'span'}
onClick={this.toggle}
style={{ cursor: 'pointer' }}
>
<Badge
classes={{ badge: this.props.classes.badge }}
invisible={!visible}
badgeContent={this.props.items.length}
color="primary"
>
{this.state.collapsed ? '▶' : '▼'} History
</Badge>
<div style={{ float: 'right' }}>{this.state.collapsed ? this.props.contentTypeIndicator : null}</div>
</Typography>
<div style={{ maxHeight: '230px', overflowY: 'scroll' }}>
{this.state.collapsed ? null : this.props.children}
{this.state.collapsed ? null : elements}
</div>
</div>
)
}
public render() {
return (
<div
style={{ display: 'block', width: '100%' }}
>
{this.renderHistory()}
</div>
)
}
private toggle = () => {
this.setState({ collapsed: !this.state.collapsed })
}
}
const styles = (theme: Theme) => ({
badge: { right:'-25px' },
})
export default withStyles(styles)(MessageHistory)
@@ -1,87 +0,0 @@
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>
onSelect: (message: q.Message) => void
}
interface State {
displayMessage?: q.Message,
anchorEl?: HTMLElement
}
class MessageHistory extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { }
}
private updateNode = throttle(() => {
this.setState(this.state)
}, 300)
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode)
}
public componentDidMount() {
this.props.node && this.props.node.onMessage.subscribe(this.updateNode)
}
public componentWillUnMount() {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
}
public render() {
const { node } = this.props
if (!node) {
return null
}
const history = node.messageHistory.toArray()
const historyElements = history.reverse().map(message => ({
title: <DateFormatter date={message.received} />,
value: message.value,
}))
const numericMessages = history.filter(message => !isNaN(parseFloat(message.value)))
const showPlot = numericMessages.length >= 2
return (
<div>
<History
items={historyElements}
contentTypeIndicator={showPlot ? <BarChart /> : null}
onClick={this.displayMessage}
>
{showPlot ? this.renderPlot(numericMessages) : null}
</History>
</div>
)
}
public renderPlot(numericMessages: q.Message[]) {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<PlotHistory messages={numericMessages} />
</React.Suspense>
)
}
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray()[index]
this.props.onSelect(message)
}
}
export default MessageHistory
-29
View File
@@ -1,29 +0,0 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Typography } from '@material-ui/core'
import { TopicViewModel } from '../../TopicViewModel'
interface Props {
node: q.TreeNode<TopicViewModel>
}
class NodeStats extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
}
public render() {
const { node } = this.props
return (
<div>
<Typography>Messages: #{node.messages}</Typography>
<Typography>Subtopics: {node.childTopicCount()}</Typography>
<Typography>Messages Subtopics: #{node.leafMessageCount()}</Typography>
</div>
)
}
}
export default NodeStats
@@ -1,73 +0,0 @@
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'
interface Props {
messages: q.Message[]
}
interface Stats {
width: number
value?: any
}
class PlotHistory extends React.Component<Props, Stats> {
constructor(props: Props) {
super(props)
this.state = { width: 300 }
}
private resize = (width: number, height: number) => {
this.setState({ width: width - 12 })
}
public render() {
const data = this.props.messages.map((message) => {
return {
x: message.received.getTime(),
y: parseFloat(message.value),
}
})
return (
<div>
<XYPlot width={this.state.width} height={150}>
<HorizontalGridLines />
<YAxis />
<LineMarkSeries
onValueMouseOver={this._rememberValue}
onValueMouseOut={this._forgetValue}
size={3}
data={data}
curve="curveCardinal"
/>
{this.state.value ? <Hint format={this.hintFormatter} value={this.state.value} /> : null}
</XYPlot>
<ReactResizeDetector handleWidth={true} onResize={this.resize} />
</div>
)
}
private hintFormatter = (point: any) => {
return [
{ title: <b>Time</b>, value: <DateFormatter date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: point.y },
]
}
private _forgetValue = () => {
this.setState({
value: undefined,
})
}
private _rememberValue = (value: any) => {
this.setState({ value })
}
}
export default PlotHistory
@@ -1,7 +0,0 @@
interface Message {
topic: string
payload?: string
sent: Date
}
export default Message
@@ -1,314 +0,0 @@
import 'react-ace'
import 'brace/mode/json'
import 'brace/mode/text'
import 'brace/mode/xml'
import 'brace/theme/monokai'
import * as React from 'react'
import * as q from '../../../../../backend/src/Model'
import {
Button,
FormControlLabel,
Radio,
RadioGroup,
TextField,
FormControl,
InputLabel,
Input,
Checkbox,
MenuItem,
Tooltip,
} from '@material-ui/core'
// tslint:disable-next-line
import { default as AceEditor } from 'react-ace'
import { AppState } from '../../../reducers'
import History from '../History'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import Clear from '@material-ui/icons/Clear'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { publishActions } from '../../../actions'
import ClearAdornment from '../../helper/ClearAdornment'
import { TopicViewModel } from '../../../TopicViewModel'
interface Props {
node?: q.TreeNode<TopicViewModel>
connectionId?: string
topic?: string
payload?: string
actions: typeof publishActions
retain: boolean
editorMode: string
qos: 0 | 1 | 2
}
interface State {
history: Message[]
}
class Publish extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { history: [] }
}
private updatePayload = (payload: string, event?: any) => {
this.props.actions.setPayload(payload)
}
private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.actions.setTopic(e.target.value)
}
private updateMode = (e: React.ChangeEvent<{}>, value: string) => {
this.props.actions.setEditorMode(value)
}
private publish = (e: React.MouseEvent) => {
e.stopPropagation()
if (!this.props.connectionId) {
return
}
this.props.actions.publish(this.props.connectionId)
const topic = this.props.topic || ''
const payload = this.props.payload
if (this.props.connectionId && topic) {
this.addMessageToHistory(topic, payload)
}
}
private addMessageToHistory(topic: string, payload?: string) {
// Remove duplicates
let filteredHistory = this.state.history.filter(e => e.payload !== payload || e.topic !== topic)
filteredHistory = filteredHistory.slice(-7)
const history: Message[] = [...filteredHistory, { topic, payload, sent: new Date() }]
this.setState({ history })
}
public render() {
return (
<div style={{ flexGrow: 1, marginLeft: '8px' }}>
{this.topic()}
{this.editor()}
{this.history()}
</div>
)
}
private clearTopic = () => {
this.props.actions.setTopic('')
}
private topic() {
const topicStr = this.props.topic || ''
return (
<div>
<FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
<Input
id="publish-topic"
value={topicStr}
startAdornment={<span/>}
endAdornment={<ClearAdornment action={this.clearTopic} value={topicStr} />}
onBlur={this.onTopicBlur}
onChange={this.updateTopic}
multiline={true}
placeholder="example/topic"
/>
</FormControl>
</div>
)
}
private onTopicBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) {
this.props.actions.setTopic(undefined)
}
}
private editorOptions = {
showLineNumbers: false,
tabSize: 2,
}
private publishButton() {
return (
<Button
variant="contained"
size="small"
color="primary"
onClick={this.publish}
>
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
}
private editorMode() {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
{this.renderEditorModeSelection()}
<div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()}
</div>
</div>
</div>
)
}
private renderEditorModeSelection() {
const labelStyle = { margin: '0 8px 0 8px' }
return (
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={this.props.editorMode}
onChange={this.updateMode}
row={true}
>
<FormControlLabel
value="text"
style={labelStyle}
control={<Radio color="primary" />}
label="raw"
labelPlacement="top"
/>
<FormControlLabel
value="xml"
style={labelStyle}
control={<Radio color="primary" />}
label="xml"
labelPlacement="top"
/>
<FormControlLabel
value="json"
style={labelStyle}
control={<Radio color="primary" />}
label="json"
labelPlacement="top"
/>
</RadioGroup>
)
}
private onChangeQoS = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
if (value !== 0 && value !== 1 && value !== 2) {
return
}
this.props.actions.setQoS(value)
}
private publishMode() {
const labelStyle = { margin: '0 8px 0 8px' }
const itemStyle = { padding: '0' }
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
const qosSelect = (
<TextField
select={true}
value={this.props.qos}
margin="normal"
style={{ margin: '8px 0 8px 8px' }}
onChange={this.onChangeQoS}
>
<MenuItem key={0} value={0} style={itemStyle}>
<Tooltip title="At most once"><div style={tooltipStyle}>0</div></Tooltip>
</MenuItem>
<MenuItem key={1} value={1} style={itemStyle}>
<Tooltip title="At least once"><div style={tooltipStyle}>1</div></Tooltip>
</MenuItem>
<MenuItem key={2} value={2} style={itemStyle}>
<Tooltip title="Exactly once"><div style={tooltipStyle}>2</div></Tooltip>
</MenuItem>
</TextField>
)
return (
<div style={{ marginTop: '8px', clear: 'both' }}>
<div style={{ width: '100%', textAlign: 'right' }}>
<FormControlLabel
style={labelStyle}
control={qosSelect}
label="QoS"
labelPlacement="start"
/>
<Tooltip title="Retained messages only appear to be retained, when client subscribes after the initial publish." placement="top">
<FormControlLabel
value="retain"
style={labelStyle}
control={<Checkbox color="primary" checked={this.props.retain} onChange={this.props.actions.toggleRetain} />}
label="retain"
labelPlacement="end"
/>
</Tooltip>
</div>
</div>
)
}
private history() {
const items = this.state.history.reverse().map(message => ({
title: message.topic,
value: message.payload,
}))
return <History items={items} onClick={this.didSelectHistoryEntry} />
}
private didSelectHistoryEntry = (index: number) => {
const message = this.state.history[index]
this.props.actions.setTopic(message.topic)
this.props.actions.setPayload(message.payload)
}
private editor() {
return (
<div style={{ width: '100%', display: 'block' }}>
{this.editorMode()}
{this.renderEditor()}
{this.publishMode()}
</div>
)
}
private renderEditor() {
return (
<AceEditor
mode={this.props.editorMode}
theme="monokai"
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
showGutter={true}
value={this.props.payload}
onChange={this.updatePayload}
setOptions={this.editorOptions}
editorProps={{ $blockScrolling: true }}
/>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(publishActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
topic: state.publish.topic,
payload: state.publish.payload,
editorMode: state.publish.editorMode,
retain: state.publish.retain,
qos: state.publish.qos,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Publish)
-234
View File
@@ -1,234 +0,0 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import {
Button,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Fade,
Paper,
Popper,
Typography,
Tooltip,
} 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'
const Publish = React.lazy(() => import('./Publish/Publish'))
import Topic from './Topic'
const ValueRenderer = React.lazy(() => import('./ValueRenderer'))
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { TopicViewModel } from '../../TopicViewModel'
const throttle = require('lodash.throttle')
interface Props {
node?: q.TreeNode<TopicViewModel>,
actions: typeof sidebarActons,
classes: any,
connectionId?: string,
}
interface State {
node: q.TreeNode<TopicViewModel>
compareMessage?: q.Message
}
class Sidebar extends React.Component<Props, State> {
private valueRef: any = React.createRef<HTMLDivElement>()
private updateNode = throttle(() => {
this.setState(this.state)
}, 300)
constructor(props: any) {
super(props)
console.error('Find and fix me #state')
this.state = { node: new q.Tree() }
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.removeUpdateListener(this.props.node)
nextProps.node && this.registerUpdateListener(nextProps.node)
this.props.node && this.setState({ node: this.props.node })
if (this.props.node !== nextProps.node) {
this.setState({ compareMessage: undefined })
}
}
public componentWillUnmount() {
this.props.node && this.removeUpdateListener(this.props.node)
}
private registerUpdateListener(node: q.TreeNode<TopicViewModel>) {
node.onMerge.subscribe(this.updateNode)
node.onMessage.subscribe(this.updateNode)
}
private removeUpdateListener(node: q.TreeNode<TopicViewModel>) {
node.onMerge.unsubscribe(this.updateNode)
node.onMessage.unsubscribe(this.updateNode)
}
public render() {
return (
<div className={this.props.classes.drawer}>
{this.renderNode()}
</div>
)
}
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
private renderNode() {
const { classes, node } = this.props
const copyTopic = node ? <Copy value={node.path()} /> : null
const copyValue = node && node.message ? <Copy value={node.message.value} /> : null
const summeryStyle = { minHeight: '0' }
return (
<div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<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={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>}>
<ValueRenderer message={this.props.node && this.props.node.message} />
</React.Suspense>
</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={summeryStyle}>
<Typography className={classes.heading}>Publish</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<React.Suspense fallback={<div>Loading...</div>}>
<Publish node={this.props.node} connectionId={this.props.connectionId} />
</React.Suspense>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
{this.renderNodeStats()}
</ExpansionPanel>
</div>
)
}
private showValueComparison = (a: any) => (
<Fade {...a.TransitionProps} timeout={350}>
<Paper>
<ValueRenderer message={this.state.compareMessage} />
</Paper>
</Fade>
)
private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
return null
}
const retainedButton = (
<Tooltip title="Delete retained topic" placement="top">
<Button
size="small"
color="secondary"
variant="contained"
mini={true}
style={{ marginTop: '-3px', padding: '0px 4px', minHeight: '24px' }}
onClick={this.props.actions.clearRetainedTopic}
>
retained <Clear style={{ fontSize: '16px', marginLeft: '2px' }} />
</Button>
</Tooltip>
)
return (
<div style={{ width: '100%', display: 'flex' }}>
<div style={{ flex: 6 }}><Typography>QoS: {this.props.node.mqttMessage.qos}</Typography></div>
<div style={{ flex: 8, textAlign: 'center' }}>
{this.props.node.mqttMessage.retain ? retainedButton : null}
</div>
<div style={{ flex: 8, textAlign: 'right' }}><Typography><i><DateFormatter date={this.props.node.message.received} /></i></Typography></div>
</div>
)
}
private handleMessageHistorySelect = (message: q.Message) => {
if (message !== this.state.compareMessage) {
this.setState({ compareMessage: message })
} else {
this.setState({ compareMessage: undefined })
}
}
private renderNodeStats() {
if (!this.props.node) {
return null
}
return (
<ExpansionPanelDetails style={this.detailsStyle}>
<NodeStats node={this.props.node} />
</ExpansionPanelDetails>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
node: state.tree.selectedTopic,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(sidebarActons, dispatch),
}
}
const styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
drawer: {
display: 'block',
height: '100%',
},
valuePaper: {
margin: `${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px`,
},
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
-70
View File
@@ -1,70 +0,0 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import Button from '@material-ui/core/Button'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
import { treeActions } from '../../actions'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { TopicViewModel } from '../../TopicViewModel'
interface Props {
classes: any
theme: Theme
node?: q.TreeNode<TopicViewModel>
selected?: q.TreeNode<TopicViewModel>
actions: typeof treeActions
didSelectNode: (node: q.TreeNode<TopicViewModel>) => void
}
const styles: StyleRulesCallback<string> = (theme: Theme) => ({
button: {
textTransform: 'none',
padding: '3px 5px 3px 5px',
minWidth: '30px',
},
})
class Topic extends React.Component<Props, {}> {
public render() {
const { node } = this.props
if (!node) {
return null
}
let key = 0
const breadCrumps = node.branch()
.map(node => node.sourceEdge)
.filter(edge => Boolean(edge))
.map(edge =>
[(
<Button
onClick={() => this.props.actions.selectTopic(edge!.target)}
size="small"
color="secondary"
className={this.props.classes.button}
key={edge!.hash()}
>
{edge!.name}
</Button>
)],
)
if (breadCrumps.length === 0) {
return null
}
const joinedBreadCrumps = breadCrumps.reduce((prev, current) =>
prev.concat([<span key={key += 1}>/</span>]).concat(current),
)
return <span style={{ lineHeight: '2.2em' }}>{joinedBreadCrumps}</span>
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(treeActions, dispatch),
}
}
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic))
@@ -1,66 +0,0 @@
import * as React from 'react'
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
theme: Theme
}
class ValueRenderer extends React.Component<Props, {}> {
public render() {
return <div>{this.renderValue()}</div>
}
public renderValue() {
const { message } = this.props
if (!message) {
return null
}
let json
try {
json = JSON.parse(message.value)
} catch (error) {
return this.renderRawValue(message.value)
}
if (typeof json === 'string') {
return this.renderRawValue(message.value)
} else if (typeof json === 'number') {
return this.renderRawValue(message.value)
} else if (typeof json === 'boolean') {
return this.renderRawValue(message.value)
} else {
const theme = (this.props.theme.palette.type === 'dark') ? 'monokai' : 'bright:inverted'
return (
<ReactJson
style={{ width: '100%' }}
src={json}
name={null}
theme={theme}
/>
)
}
}
private renderRawValue(value: string) {
const style: React.CSSProperties = {
backgroundColor: 'rgba(80, 80, 80, 0.6)',
wordBreak: 'break-all',
width: '100%',
overflow: 'auto hidden',
display: 'block',
lineHeight: '1.2em',
padding: '12px 5px 12px 5px',
color: this.props.theme.palette.text.primary,
}
return <pre style={style}><code>{value}</code></pre>
}
}
export default withTheme()(ValueRenderer)
-3
View File
@@ -1,3 +0,0 @@
import Sidebar from './Sidebar'
export { Sidebar }
-145
View File
@@ -1,145 +0,0 @@
import * as React from 'react'
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 Menu from '@material-ui/icons/Menu'
import Search from '@material-ui/icons/Search'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { settingsActions, connectionActions } from '../actions'
import { AppState } from '../reducers'
import ClearAdornment from './helper/ClearAdornment'
const styles: StyleRulesCallback = theme => ({
title: {
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'block',
},
},
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing.unit * 2,
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing.unit * 3,
width: 'auto',
},
},
searchIcon: {
width: theme.spacing.unit * 8,
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit',
width: '100%',
},
inputInput: {
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')]: {
width: 200,
},
},
menuButton: {
marginLeft: -12,
marginRight: 20,
},
})
interface Props {
classes: any
actions: {
settings: typeof settingsActions,
connection: typeof connectionActions,
}
topicFilter?: string
}
class TitleBar extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = { }
}
public render() {
const { actions, classes } = this.props
return (
<AppBar position="static">
<Toolbar>
<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>
{this.renderSearch()}
<Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
</Toolbar>
</AppBar>
)
}
private renderSearch() {
const { classes, topicFilter } = this.props
return (
<div className={classes.search}>
<div className={classes.searchIcon}>
<Search />
</div>
<InputBase
value={topicFilter}
onChange={this.onFilterChange}
placeholder="Search…"
endAdornment={<div style={{ width: '24px', paddingRight: '8px' }}><ClearAdornment action={this.clearFilter} value={topicFilter} /></div>}
classes={{ root: classes.inputRoot, input: classes.inputInput }}
/>
</div>
)
}
private clearFilter = () => {
this.props.actions.settings.filterTopics('')
}
private onFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.props.actions.settings.filterTopics(event.target.value)
}
}
const mapStateToProps = (state: AppState) => {
return {
topicFilter: state.settings.topicFilter,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
settings: bindActionCreators(settingsActions, dispatch),
connection: bindActionCreators(connectionActions, dispatch),
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
-140
View File
@@ -1,140 +0,0 @@
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 { TopicViewModel } from '../../TopicViewModel'
import { treeActions } from '../../actions'
import { bindActionCreators } from 'redux'
const MovingAverage = require('moving-average')
const averagingTimeInterval = 10 * 1000
const average = MovingAverage(averagingTimeInterval)
declare var window: any
interface Props {
actions: typeof treeActions
connectionId?: string
tree?: q.Tree<TopicViewModel>
filter: string
host?: string
topicOrder: TopicOrder
autoExpandLimit: number
}
interface State {
lastUpdate: number
}
class Tree extends React.PureComponent<Props, State> {
private updateTimer?: any
private perf: number = 0
private renderTime = 0
constructor(props: any) {
super(props)
this.state = { lastUpdate: 0 }
}
public time(): number {
const time = performance.now() - this.perf
this.perf = performance.now()
return time
}
public componentWillReceiveProps(nextProps: Props) {
if (this.props.tree !== nextProps.tree) {
if (this.props.tree) {
this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
}
if (nextProps.tree) {
nextProps.tree.didReceive.subscribe(this.throttledTreeUpdate)
}
this.setState(this.state)
}
}
public componentWillUnmount() {
this.props.tree && this.props.tree.didReceive.unsubscribe(this.throttledTreeUpdate)
}
public throttledTreeUpdate = () => {
if (this.updateTimer) {
return
}
const expectedRenderTime = average.forecast()
const updateInterval = Math.max(expectedRenderTime * 7, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
this.updateTimer = setTimeout(() => {
window.requestIdleCallback(() => {
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.renderTime = performance.now()
this.props.tree && this.props.tree.applyUnmergedChanges()
window.requestIdleCallback(() => {
this.setState({ lastUpdate: this.renderTime })
}, { timeout: 100 })
}, { timeout: 500 })
}, Math.max(0, timeUntilNextUpdate))
}
public render() {
const { tree, filter } = this.props
if (!tree) {
return null
}
const style: React.CSSProperties = {
lineHeight: '1.1',
cursor: 'default',
}
return (
<div style={style}>
<TreeNode
key={tree.hash()}
animateChages={true}
isRoot={true}
treeNode={tree}
name={this.props.host}
collapsed={false}
performanceCallback={this.performanceCallback}
autoExpandLimit={this.props.autoExpandLimit}
topicOrder={this.props.topicOrder}
lastUpdate={tree.lastUpdate}
didSelectTopic={this.props.actions.selectTopic}
/>
</div>
)
}
private performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
}
const mapStateToProps = (state: AppState) => {
return {
tree: state.tree.tree,
filter: state.tree.filter,
host: state.connection.host,
autoExpandLimit: state.settings.autoExpandLimit,
topicOrder: state.settings.topicOrder,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(treeActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Tree)
-251
View File
@@ -1,251 +0,0 @@
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 { TopicOrder } from '../../reducers/Settings'
import { TopicViewModel } from '../../TopicViewModel'
const debounce = require('lodash.debounce')
declare var performance: any
const styles = (theme: Theme) => {
return {
collapsedSubnodes: {
color: theme.palette.text.secondary,
},
displayBlock: {
display: 'block',
},
node: {
display: 'block',
marginLeft: '10px',
},
topicSelect: {
float: 'right' as 'right',
opacity: 0,
cursor: 'pointer',
marginTop: '-1px',
},
selected: {
backgroundColor: 'rgba(120, 120, 120, 0.55)',
},
hover: {
backgroundColor: 'rgba(80, 80, 80, 0.55)',
},
}
}
interface Props {
animateChages: boolean
isRoot?: boolean
treeNode: q.TreeNode<TopicViewModel>
name?: string | undefined
collapsed?: boolean | undefined
performanceCallback?: ((ms: number) => void) | undefined
classes: any
className?: string
topicOrder: TopicOrder
autoExpandLimit: number
lastUpdate: number
didSelectTopic: any
}
interface State {
collapsedOverride: boolean | undefined
mouseOver: boolean
selected: boolean
}
class TreeNode extends React.Component<Props, State> {
private dirtySubnodes: boolean = true
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 = () => {
this.dirtySubnodes = true
}
private messageDidChange = () => {
this.dirtyMessage = true
}
private edgesDidChange = () => {
this.dirtyMessage = true
}
constructor(props: Props) {
super(props)
this.state = {
collapsedOverride: props.collapsed,
mouseOver: false,
selected: false,
}
}
public componentDidMount() {
const { treeNode } = this.props
this.addSubscriber(treeNode)
}
private addSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
treeNode.viewModel = new TopicViewModel()
treeNode.viewModel.change.subscribe(this.viewStateHasChanged)
treeNode.onMerge.subscribe(this.subnodesDidchange)
treeNode.onEdgesChange.subscribe(this.edgesDidChange)
treeNode.onMessage.subscribe(this.messageDidChange)
}
private viewStateHasChanged = (msg: void, viewModel: TopicViewModel) => {
this.setState({ selected: viewModel.isSelected() })
}
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
treeNode.viewModel && treeNode.viewModel.change.unsubscribe(this.viewStateHasChanged)
treeNode.viewModel = undefined
treeNode.onMerge.unsubscribe(this.subnodesDidchange)
treeNode.onEdgesChange.unsubscribe(this.edgesDidChange)
treeNode.onMessage.unsubscribe(this.messageDidChange)
}
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.treeNode !== this.props.treeNode) {
this.removeSubscriber(this.props.treeNode)
this.addSubscriber(nextProps.treeNode)
}
}
public componentWillUnmount() {
const { treeNode } = this.props
this.removeSubscriber(treeNode)
this.titleRef = undefined
this.nodeRef = undefined
}
private stateHasChanged(newState: State) {
return this.state.collapsedOverride !== newState.collapsedOverride
|| this.state.mouseOver !== newState.mouseOver
|| this.state.selected !== newState.selected
}
private propsHasChanged(newProps: Props) {
return this.props.autoExpandLimit !== newProps.autoExpandLimit
}
public shouldComponentUpdate(nextProps: Props, nextState: State) {
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
return this.stateHasChanged(nextState)
|| this.propsHasChanged(nextProps)
|| (this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes)
|| this.animationDirty
|| shouldRenderToRemoveCssAnimation
}
public componentDidUpdate() {
if (this.props.performanceCallback) {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
}
}
public componentWillUpdate() {
if (this.props.performanceCallback) {
this.willUpdateTime = performance.now()
}
}
private toggle() {
this.setState({ collapsedOverride: !this.collapsed() })
}
private collapsed() {
if (this.state.collapsedOverride !== undefined) {
return this.state.collapsedOverride
}
return this.props.treeNode.edgeCount() > this.props.autoExpandLimit
}
public render() {
const { classes } = this.props
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
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
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
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
didSelectNode={this.didSelectTopic}
/>
</span>
{this.renderNodes()}
</div>
)
}
private didSelectTopic = () => {
this.props.didSelectTopic(this.props.treeNode)
}
private mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(true)
}
private mouseOut = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(false)
}
private setHover = debounce((hover: boolean) => {
this.setState({ mouseOver: hover })
}, 5)
private didClickNode = (event: React.MouseEvent) => {
event.stopPropagation()
this.toggle()
this.didSelectTopic()
}
private renderNodes() {
return (
<TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
autoExpandLimit={this.props.autoExpandLimit}
topicOrder={this.props.topicOrder}
lastUpdate={this.props.treeNode.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
/>
)
}
}
export default withStyles(styles)(TreeNode)
@@ -1,112 +0,0 @@
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'
export interface Props {
animateChanges: boolean
treeNode: q.TreeNode<TopicViewModel>
filter?: string
collapsed?: boolean | undefined
classes: any
lastUpdate: number
topicOrder: TopicOrder
selectedTopic?: q.TreeNode<TopicViewModel>
autoExpandLimit: number
didSelectTopic: any
}
interface State {
alreadyAdded: number
}
class TreeNodeSubnodes extends React.Component<Props, State> {
private renderMoreAnimationFrame?: any
constructor(props: Props) {
super(props)
this.state = { alreadyAdded: 10 }
}
private sortedNodes(): q.TreeNode<TopicViewModel>[] {
const { topicOrder, treeNode } = this.props
let edges = treeNode.edgeArray
if (topicOrder === TopicOrder.abc) {
edges = edges.sort((a, b) => a.name.localeCompare(b.name))
}
let nodes = edges.map(edge => edge.target)
if (topicOrder === TopicOrder.messages) {
nodes = nodes.sort((a, b) => b.leafMessageCount() - a.leafMessageCount())
}
if (topicOrder === TopicOrder.topics) {
nodes = nodes.sort((a, b) => b.childTopicCount() - a.childTopicCount())
}
return nodes
}
private renderMore() {
this.renderMoreAnimationFrame = window.requestAnimationFrame(() => {
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
})
}
public componentWillUnmount() {
window.cancelAnimationFrame(this.renderMoreAnimationFrame)
}
public render() {
const edges = this.props.treeNode.edgeArray
if (edges.length === 0 || this.props.collapsed) {
return null
}
if (this.state.alreadyAdded < edges.length) {
const delta = Math.min(this.state.alreadyAdded, edges.length - this.state.alreadyAdded)
this.renderMore()
}
const nodes = this.sortedNodes().slice(0, this.state.alreadyAdded)
const listItems = nodes.map((node) => {
return (
<TreeNode
key={`${node.hash()}-${this.props.filter}`}
animateChages={this.props.animateChanges}
treeNode={node}
className={this.props.classes.listItem}
topicOrder={this.props.topicOrder}
autoExpandLimit={this.props.autoExpandLimit}
lastUpdate={node.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
/>
)
})
return (
<span className={this.props.classes.list}>
{listItems}
</span>
)
}
}
const styles = (theme: Theme) => ({
list: {
display: 'block' as 'block',
clear: 'both' as 'both',
},
listItem: {
padding: '3px 0px 0px 8px',
},
})
export default withStyles(styles)(TreeNodeSubnodes)
-89
View File
@@ -1,89 +0,0 @@
import * as React from 'react'
import { connect } from 'react-redux'
import * as q from '../../../../backend/src/Model'
import { withStyles, Theme } from '@material-ui/core'
import { TopicViewModel } from '../../TopicViewModel'
const debounce = require('lodash.debounce')
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode<TopicViewModel>
name?: string | undefined
collapsed?: boolean | undefined
classes: any
didSelectNode: 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() {
return (
<span className={this.props.classes.title} onMouseOver={this.props.treeNode.message ? this.mouseOver : undefined}>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
)
}
private renderSourceEdge() {
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
return <span className={this.props.classes.sourceEdge}>{name}</span>
}
private renderValue() {
return this.props.treeNode.message && this.props.treeNode.message.length > 0
? <span className={this.props.classes.value}> = {this.props.treeNode.message.value.toString().slice(0, 120)}</span>
: null
}
private renderExpander() {
if (this.props.treeNode.edgeCount() === 0) {
return null
}
return this.props.collapsed ? '▶' : '▼'
}
private renderCollapsedSubnodes() {
if (this.props.treeNode.edgeCount() === 0 || !this.props.collapsed) {
return null
}
const messages = this.props.treeNode.leafMessageCount()
return <span className={this.props.classes.collapsedSubnodes}>({this.props.treeNode.childTopicCount()} topics, {messages} messages)</span>
}
}
const styles = (theme: Theme) => ({
value: {
whiteSpace: 'nowrap' as 'nowrap',
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',
display: 'inline-block' as 'inline-block',
},
title: {
lineHeight: '1em',
whiteSpace: 'nowrap' as 'nowrap',
},
collapsedSubnodes: {
color: theme.palette.text.secondary,
},
})
export default withStyles(styles)(TreeNodeTitle)
@@ -1,25 +0,0 @@
import * as React from 'react'
import { IconButton } from '@material-ui/core'
import Clear from '@material-ui/icons/Clear'
interface Props {
value?: string
action: any
style?: React.CSSProperties
}
class ClearAdornment extends React.Component<Props, {}> {
public render() {
if (this.props.value) {
return (
<IconButton style={{ ...this.props.style, padding: '1px' }} onClick={this.props.action}>
<Clear style={{ fontSize: '16px' }} />
</IconButton>
)
}
return null
}
}
export default ClearAdornment
@@ -1,14 +0,0 @@
import * as React from 'react'
interface Props {
date: Date
}
class DateFormatter extends React.Component<Props, {}> {
public render() {
return <span>{this.props.date.toLocaleDateString()} {this.props.date.toLocaleTimeString()}</span>
}
}
export default DateFormatter
@@ -1,12 +0,0 @@
declare const window: any
declare const document: any
export function isElementInViewport(el: any) {
const rect = el.getBoundingClientRect()
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
)
}
-48
View File
@@ -1,48 +0,0 @@
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(
reducers,
composeEnhancers(
applyMiddleware(
reduxThunk,
batchDispatchMiddleware,
),
),
)
const theme = createMuiTheme({
palette: {
type: 'dark',
},
typography: { useNextVariants: true },
})
setTimeout(() => {
const splash = document.getElementById('splash')
if (splash) {
splash.style.animation = 'unsplash 0.5s ease-in 0s 1 normal forwards'
setTimeout(() => splash.remove(), 600)
}
}, 300)
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<Provider store={store}>
<App name="" />
</Provider>
</MuiThemeProvider>,
document.getElementById('app'),
)
-93
View File
@@ -1,93 +0,0 @@
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 interface ConnectionState {
host?: string
tree?: q.Tree<TopicViewModel>
connectionOptions?: MqttOptions
connectionId?: string
error?: string
connected: boolean
connecting: boolean
}
export type Action = SetConnecting | SetConnected | SetDisconnected | ShowError
export enum ActionTypes {
CONNECTION_SET_CONNECTING = 'CONNECTION_SET_CONNECTING',
CONNECTION_SET_CONNECTED = 'CONNECTION_SET_CONNECTED',
CONNECTION_SET_DISCONNECTED = 'CONNECTION_SET_DISCONNECTED',
CONNECTION_SET_SHOW_ERROR = 'CONNECTION_SET_SHOW_ERROR',
}
export interface SetConnecting {
type: ActionTypes.CONNECTION_SET_CONNECTING,
connectionId: string
}
export interface SetConnected {
type: ActionTypes.CONNECTION_SET_CONNECTED
host: string
tree: q.Tree<TopicViewModel>
}
export interface SetDisconnected {
type: ActionTypes.CONNECTION_SET_DISCONNECTED
}
export interface ShowError {
type: ActionTypes.CONNECTION_SET_SHOW_ERROR
error?: Error
}
const initialState: ConnectionState = {
connected: false,
connecting: false,
}
export const connectionReducer = createReducer(initialState, {
CONNECTION_SET_CONNECTING: setConnecting,
CONNECTION_SET_CONNECTED: setConnected,
CONNECTION_SET_DISCONNECTED: setDisconnected,
CONNECTION_SET_SHOW_ERROR: showError,
})
function setConnecting(state: ConnectionState, action: SetConnecting): ConnectionState {
return {
...state,
connecting: true,
connected: false,
connectionId: action.connectionId,
}
}
function setConnected(state: ConnectionState, action: SetConnected): ConnectionState {
return {
...state,
host: action.host,
connecting: false,
connected: true,
tree: action.tree,
}
}
function setDisconnected(state: ConnectionState, action: SetDisconnected): ConnectionState {
return {
...state,
host: undefined,
connecting: false,
connected: false,
connectionId: undefined,
tree: undefined,
}
}
function showError(state: ConnectionState, action: ShowError) {
return {
...state,
error: action.error,
}
}
-93
View File
@@ -1,93 +0,0 @@
import { Action } from 'redux'
import { createReducer } from './lib'
export interface PublishState {
topic?: string
payload?: string
retain: boolean
editorMode: string
qos: 0 | 1 | 2
}
export type Action = SetPayload | SetTopic | ToggleRetain | SetEditorMode | SetQoS
export enum ActionTypes {
PUBLISH_SET_TOPIC = 'PUBLISH_SET_TOPIC',
PUBLISH_SET_PAYLOAD = 'PUBLISH_SET_PAYLOAD',
PUBLISH_TOGGLE_RETAIN = 'PUBLISH_TOGGLE_RETAIN',
PUBLISH_SET_EDITOR_MODE = 'PUBLISH_SET_EDITOR_MODE',
PUBLISH_SET_QOS = 'PUBLISH_SET_QOS',
}
export interface SetPayload {
type: ActionTypes.PUBLISH_SET_PAYLOAD
payload?: string
}
export interface SetTopic {
type: ActionTypes.PUBLISH_SET_TOPIC
topic?: string
}
export interface SetQoS {
type: ActionTypes.PUBLISH_SET_QOS
qos: 0 | 1 | 2
}
export interface SetEditorMode {
type: ActionTypes.PUBLISH_SET_EDITOR_MODE
editorMode: string
}
export interface ToggleRetain {
type: ActionTypes.PUBLISH_TOGGLE_RETAIN
}
const initialState: PublishState = {
editorMode: 'text',
retain: false,
qos: 0,
}
export const publishReducer = createReducer(initialState, {
PUBLISH_SET_TOPIC: setTopic,
PUBLISH_SET_PAYLOAD: setPayload,
PUBLISH_TOGGLE_RETAIN: toggleRetain,
PUBLISH_SET_EDITOR_MODE: setEditorMode,
PUBLISH_SET_QOS: setQoS,
})
function setTopic(state: PublishState, action: SetTopic) {
return {
...state,
topic: action.topic,
}
}
function setPayload(state: PublishState, action: SetPayload) {
return {
...state,
payload: action.payload,
}
}
function setQoS(state: PublishState, action: SetQoS) {
return {
...state,
qos: action.qos,
}
}
function setEditorMode(state: PublishState, action: SetEditorMode) {
return {
...state,
editorMode: action.editorMode,
}
}
function toggleRetain(state: PublishState) {
return {
...state,
retain: !state.retain,
}
}
-84
View File
@@ -1,84 +0,0 @@
import { Action } from 'redux'
import { createReducer } from './lib'
export enum TopicOrder {
none = 'none',
messages = '#messages',
abc = 'abc',
topics = '#topics',
}
export interface SettingsState {
autoExpandLimit: number
visible: boolean
topicOrder: TopicOrder
topicFilter?: string
}
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',
}
const initialState: SettingsState = {
autoExpandLimit: 0,
topicOrder: TopicOrder.none,
visible: false,
}
export const settingsReducer = createReducer(initialState, {
SETTINGS_SET_AUTO_EXPAND_LIMIT: setAutoExpandLimit,
SETTINGS_TOGGLE_VISIBILITY: toggleVisibility,
SETTINGS_SET_TOPIC_ORDER: setTopicOrder,
SETTINGS_FILTER_TOPICS: filterTopics,
})
function setAutoExpandLimit(state: SettingsState, action: SetAutoExpandLimit) {
return {
...state,
autoExpandLimit: action.autoExpandLimit,
}
}
export interface SetAutoExpandLimit {
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT
autoExpandLimit: number
}
function toggleVisibility(state: SettingsState, action: ToggleVisibility) {
return {
...state,
visible: !state.visible,
}
}
export interface ToggleVisibility {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY
}
function setTopicOrder(state: SettingsState, action: SetTopicOrder) {
return {
...state,
topicOrder: action.topicOrder,
}
}
export interface SetTopicOrder {
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER
topicOrder: string
}
function filterTopics(state: SettingsState, action: FilterTopics) {
return {
...state,
topicFilter: action.topicFilter,
}
}
export interface FilterTopics {
type: ActionTypes.SETTINGS_FILTER_TOPICS
topicFilter: string
}
-11
View File
@@ -1,11 +0,0 @@
import { createReducer } from './lib'
import { createMuiTheme, Theme } from '@material-ui/core'
const initialState: Theme = createMuiTheme({
palette: {
type: 'dark',
},
typography: { useNextVariants: true },
})
export const theme = createReducer(initialState, {})
-50
View File
@@ -1,50 +0,0 @@
import * as q from '../../../backend/src/Model'
import { Action } from 'redux'
import { createReducer } from './lib'
import { TopicViewModel } from '../TopicViewModel'
export interface TreeState {
tree?: q.Tree<TopicViewModel>
selectedTopic?: q.TreeNode<TopicViewModel>
filter?: string
}
export type Action = ShowTree | SelectTopic
export enum ActionTypes {
TREE_SHOW_TREE = 'TREE_SHOW_TREE',
TREE_SELECT_TOPIC = 'TREE_SELECT_TOPIC',
}
export interface ShowTree {
type: ActionTypes.TREE_SHOW_TREE
tree?: q.Tree<TopicViewModel>
filter?: string
}
export interface SelectTopic {
type: ActionTypes.TREE_SELECT_TOPIC
selectedTopic?: q.TreeNode<TopicViewModel>
}
const initialState: TreeState = { }
export const treeReducer = createReducer(initialState, {
TREE_SHOW_TREE: showTree,
TREE_SELECT_TOPIC: selectTopic,
})
function showTree(state: TreeState, action: ShowTree) {
return {
...state,
tree: action.tree,
filter: action.filter,
}
}
function selectTopic(state: TreeState, action: SelectTopic) {
return {
...state,
selectedTopic: action.selectedTopic,
}
}
-74
View File
@@ -1,74 +0,0 @@
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'
import { theme } from './theme'
export enum ActionTypes {
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS',
}
export interface CustomAction extends Action {
type: ActionTypes,
showUpdateNotification?: boolean
showUpdateDetails?: boolean
}
export interface AppState {
tooBigReducer: TooBigOfState
tree: TreeState
settings: SettingsState,
publish: PublishState
connection: ConnectionState
}
export interface TooBigOfState {
showUpdateNotification?: boolean
showUpdateDetails: boolean
}
const initialBigState: TooBigOfState = {
showUpdateDetails: false,
}
const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => {
if (!state) {
throw Error('No initial state')
}
trackEvent(action.type)
switch (action.type) {
case ActionTypes.showUpdateNotification:
return {
...state,
showUpdateNotification: action.showUpdateNotification,
}
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
}
return {
...state,
showUpdateDetails: action.showUpdateDetails,
}
default:
return state
}
}
const reducer = combineReducers({
theme,
tooBigReducer,
publish: publishReducer,
connection: connectionReducer,
settings: settingsReducer,
tree: treeReducer,
})
export default reducer
-9
View File
@@ -1,9 +0,0 @@
export const createReducer = (initialState: any, handlers: any) => {
return (state = initialState, action: any) => {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
-24
View File
@@ -1,24 +0,0 @@
let userId = window.localStorage.getItem('userId')
const sha1 = require('sha1')
import { electronRendererTelementry } from 'electron-telemetry'
if (!userId) {
userId = sha1(sha1(Math.random()) + sha1(performance.now()) + sha1(Date.now())).slice(0, 8) as string
window.localStorage.setItem('userId', userId)
}
setInterval(() => {
try {
electronRendererTelementry.trackCustomEvent({ name: 'heapStatistics', payload: process.getHeapStatistics() })
electronRendererTelementry.trackCustomEvent({ name: 'cpuUsage', payload: process.getCPUUsage() })
} catch (error) {
console.error(error)
}
}, 30 * 1000)
export function trackEvent(name: string) {
if (name.match(/^@@redux/)) {
return
}
electronRendererTelementry.trackEvent(name)
}
-26
View File
@@ -1,26 +0,0 @@
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strict": true,
"lib": ["es2017", "dom"],
"moduleResolution": "node",
"outDir": "./build/",
"sourceMap": true,
"module": "esnext",
"target": "es2017",
"jsx": "react"
},
"include": [
"./src/**/*"
],
"exclude": [
"**/*.d.ts"
],
"awesomeTypescriptLoaderOptions": {
"useCache": true,
"transpileModule": true,
"errorsAsWarnings": true
}
}
-80
View File
@@ -1,80 +0,0 @@
const LiveReloadPlugin = require('webpack-livereload-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
app: "./src/index.tsx",
bugtracking: "./src/bugtracking.ts",
},
output: {
chunkFilename: '[name].bundle.js',
filename: "[name].bundle.js",
path: __dirname + "/build"
},
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
},
target: 'electron-renderer',
mode: 'production',
// Enable sourcemaps for debugging webpack's output.
devtool: "source-map",
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: [".ts", ".tsx", ".js", ".json"]
},
module: {
rules: [
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
{ test: /\.tsx?$/, loader: "awesome-typescript-loader" },
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" },
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
]
},
plugins: [
new LiveReloadPlugin({}),
new HtmlWebpackPlugin({ template: './index.html', file: './build/index.html', inject: false }),
new BundleAnalyzerPlugin(),
],
// When importing a module whose path matches one of the following, just
// assume a corresponding global variable exists and use that instead.
// This is important because it allows us to avoid bundling all of our
// dependencies, which allows browsers to cache those libraries between builds.
externals: {
// "react": "React",
// "react-dom": "ReactDOM"
}
};
-2677
View File
File diff suppressed because it is too large Load Diff
-38
View File
@@ -1,38 +0,0 @@
{
"name": "mqtt-explorer-backend",
"version": "1.0.0",
"description": "",
"main": "build/index.js",
"scripts": {
"test": "mocha",
"build": "tsc",
"test-inspect": "mocha --inspect-brk",
"coverage": "nyc mocha",
"debug": "ts-node --inspect ./src/index.ts",
"postinstall": "npm run build"
},
"author": "",
"license": "ISC",
"nyc": {
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"src/**/spec/*.spec.ts"
],
"extension": [
".ts",
".tsx"
],
"require": [
"ts-node/register"
],
"reporter": [
"text-summary",
"html"
],
"sourceMap": true,
"instrument": true
}
}
-16
View File
@@ -1,16 +0,0 @@
import { DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
type MessageCallback = (topic: string, payload: Buffer) => void
// A DataSource should automatically reconnect if connection was broken
interface DataSource<DataSourceOptions> {
connect(options: DataSourceOptions): DataSourceStateMachine
disconnect(): void
onMessage(messageCallback: MessageCallback): void
publish(msg: MqttMessage): void
topicSeparator: string
stateMachine: DataSourceStateMachine
}
export { DataSource, MessageCallback }
-47
View File
@@ -1,47 +0,0 @@
import { EventDispatcher } from '../../../events'
export interface DataSourceState {
connecting: boolean
connected: boolean
error?: string
}
export class DataSourceStateMachine {
public onUpdate = new EventDispatcher<DataSourceState, DataSourceStateMachine>(this)
private state: DataSourceState = {
error: undefined,
connected: false,
connecting: false,
}
public setConnected(connected: boolean) {
this.state = {
connected,
error: undefined,
connecting: false,
}
this.onUpdate.dispatch(this.state)
}
public setError(error: Error) {
this.state = {
error: error.message,
connected: false,
connecting: false,
}
this.onUpdate.dispatch(this.state)
}
public setConnecting() {
this.state = {
error: undefined,
connected: false,
connecting: true,
}
this.onUpdate.dispatch(this.state)
}
public toJSON() {
return this.state
}
}
-90
View File
@@ -1,90 +0,0 @@
import * as Url from 'url'
import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource, DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
export interface MqttOptions {
url: string
username?: string
password?: string
tls: boolean
certValidation: boolean
clientId?: 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) {
this.messageCallback = messageCallback
}
public connect(options: MqttOptions): DataSourceStateMachine {
this.stateMachine.setConnecting()
const urlStr = options.tls ? options.url.replace(/^(mqtt|ws):/, '$1s:') : options.url
let url
try {
url = Url.parse(urlStr)
} catch (error) {
this.stateMachine.setError(error)
throw error
}
const client = mqttConnect(url, {
resubscribe: false,
rejectUnauthorized: options.certValidation,
username: options.username,
password: options.password,
clientId: options.clientId,
})
this.client = client
client.on('error', (error: Error) => {
console.log(error)
this.stateMachine.setError(error)
})
client.on('close', () => {
this.stateMachine.setConnected(false)
})
client.on('reconnect', () => {
this.stateMachine.setConnecting()
})
client.on('connect', () => {
this.stateMachine.setConnected(true)
client.subscribe(this.rootSubscription, (err: Error) => {
if (err) {
throw new Error('mqtt subscription failed')
}
})
client.subscribe('$SYS/#', (err: Error) => {
if (err) {
throw new Error('mqtt subscription failed')
}
})
})
client.on('message', (topic, message, packet) => {
this.messageCallback && this.messageCallback(topic, message, packet)
})
return this.stateMachine
}
public publish(msg: MqttMessage) {
this.client && this.client.publish(msg.topic, msg.payload, { qos: msg.qos, retain: msg.retain })
}
public disconnect() {
this.client && this.client.end()
}
}
-3
View File
@@ -1,3 +0,0 @@
export { DataSource } from './DataSource'
export { DataSourceState, DataSourceStateMachine } from './DataSourceState'
export { MqttOptions, MqttSource } from './MqttSource'
-42
View File
@@ -1,42 +0,0 @@
import { Hashable, TreeNode } from './'
const sha1 = require('sha1')
export class Edge<ViewModel> implements Hashable {
public name: string
public target!: TreeNode<ViewModel>
public source?: TreeNode<ViewModel> | undefined
private cachedHash?: string
constructor(name: string) {
this.name = name
}
public edges() {
return this.target ? this.target.edgeArray : []
}
public hash(): string {
if (!this.cachedHash) {
let previousHash
if (this.source && this.source.sourceEdge) {
previousHash = this.source.sourceEdge.hash()
} else {
// Use the tree hash to distinguish between different trees
previousHash = this.source && this.source.isTree ? (this.source as any).hash() : ''
}
this.cachedHash = `H${sha1(previousHash + this.name)}`
}
return this.cachedHash
}
public firstEdge(): Edge<ViewModel> {
if (this.source && this.source.sourceEdge) {
return this.source.sourceEdge.firstEdge()
}
return this
}
}
-3
View File
@@ -1,3 +0,0 @@
export interface Hashable {
hash(): string
}
-5
View File
@@ -1,5 +0,0 @@
export interface Message {
value?: any | undefined
length: number
received: Date
}
-70
View File
@@ -1,70 +0,0 @@
interface Lengthwise {
length: number
}
export class RingBuffer<T extends Lengthwise> {
private capacity: number
private maxItems: number
private usage: number = 0
private items: (any)[] = []
private start: number = 0
private end: number = 0
constructor(capacity: number, maxItems = Infinity, ringBuffer?: RingBuffer<T>) {
this.capacity = capacity
this.maxItems = maxItems
if (ringBuffer) {
this.items = ringBuffer.toArray()
this.end = this.items.length
this.usage = this.items.length
}
}
public clone(): RingBuffer<T> {
return new RingBuffer(this.capacity, this.maxItems, this)
}
public toArray() {
return this.items.slice(this.start, this.end)
}
public add(item: T) {
const size = item.length
this.enforceCapacityConstraints(size)
this.usage += size
this.items[this.end] = item
this.end += 1
}
private enforceCapacityConstraints(addedItemSize: number) {
const remainingSize = this.capacity - (this.usage + addedItemSize)
if (remainingSize < 0) {
this.freeSomeSpace(Math.abs(remainingSize))
}
while ((this.end - this.start) >= this.maxItems) {
this.dropFirst()
}
}
private freeSpace(): number {
return this.capacity - this.usage
}
private isEmpty(): boolean {
return this.usage === 0
}
private freeSomeSpace(requiredSpace: number) {
while (this.freeSpace() < requiredSpace && !this.isEmpty()) {
this.dropFirst()
}
}
private dropFirst() {
const freedSpace = this.items[this.start].length
this.usage -= freedSpace
delete this.items[this.start]
this.start += 1
}
}
-55
View File
@@ -1,55 +0,0 @@
import { TreeNode } from './'
import { EventBusInterface, makeConnectionMessageEvent, MqttMessage, EventDispatcher } from '../../../events'
import { TreeNodeFactory } from './TreeNodeFactory'
export class Tree<ViewModel> extends TreeNode<ViewModel> {
public connectionId?: string
public updateSource?: EventBusInterface
public nodeFilter?: (node: TreeNode<ViewModel>) => boolean
private subscriptionEvent?: any
public isTree = true
private cachedHash = `${Math.random()}`
private unmergedMessages: MqttMessage[] = []
public didReceive = new EventDispatcher<void, Tree<ViewModel>>(this)
constructor() {
super(undefined, undefined)
}
public updateWithConnection(emitter: EventBusInterface, connectionId: string, nodeFilter?:(node: TreeNode<ViewModel>) => boolean) {
this.updateSource = emitter
this.connectionId = connectionId
this.nodeFilter = nodeFilter
this.subscriptionEvent = makeConnectionMessageEvent(connectionId)
this.updateSource.subscribe(this.subscriptionEvent, this.handleNewData)
}
public hash() {
return this.cachedHash
}
public applyUnmergedChanges() {
this.unmergedMessages.forEach((msg) => {
const edges = msg.topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue<ViewModel, any>(edges, msg.payload)
node.mqttMessage = msg
if (!this.nodeFilter || this.nodeFilter(node)) {
this.updateWithNode(node.firstNode())
}
})
this.unmergedMessages = []
}
private handleNewData = (msg: MqttMessage) => {
this.unmergedMessages.push(msg)
this.didReceive.dispatch()
}
public stopUpdating() {
if (this.subscriptionEvent && this.updateSource) {
this.updateSource.unsubscribe(this.subscriptionEvent, this.handleNewData)
}
}
}
-189
View File
@@ -1,189 +0,0 @@
import { Edge, Message, RingBuffer } from './'
import { EventDispatcher, MqttMessage } from '../../../events'
export class TreeNode<ViewModel> {
public sourceEdge?: Edge<ViewModel>
public message?: Message
public mqttMessage?: MqttMessage
public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100)
public viewModel?: ViewModel
public edges: {[s: string]: Edge<ViewModel>} = {}
public edgeArray: Edge<ViewModel>[] = []
public collapsed = false
public messages: number = 0
public lastUpdate: number = Date.now()
public onMerge = new EventDispatcher<void, TreeNode<ViewModel>>(this)
public onEdgesChange = new EventDispatcher<void, TreeNode<ViewModel>>(this)
public onMessage = new EventDispatcher<Message, TreeNode<ViewModel>>(this)
public isTree = false
private cachedPath?: string
private cachedChildTopics?: TreeNode<ViewModel>[]
private cachedLeafMessageCount?: number
private cachedChildTopicCount?: number
public unconnectedClone() {
const node = new TreeNode<ViewModel>()
node.message = this.message
node.mqttMessage = this.mqttMessage
node.messageHistory = this.messageHistory.clone()
node.messages = this.messages
node.lastUpdate = this.lastUpdate
return node
}
constructor(sourceEdge?: Edge<ViewModel>, message?: Message) {
if (sourceEdge) {
this.sourceEdge = sourceEdge
sourceEdge.target = this
}
message && this.setMessage(message)
this.onMerge.subscribe(() => {
this.cachedChildTopics = undefined
this.cachedChildTopicCount = undefined
this.cachedLeafMessageCount = undefined
this.lastUpdate = Date.now()
})
this.onEdgesChange.subscribe(() => {
this.lastUpdate = Date.now()
})
this.onMessage.subscribe(() => {
this.lastUpdate = Date.now()
})
}
public setMessage(message: Message) {
this.messageHistory.add(message)
this.message = message
this.messages += 1
}
public hash(): string {
return `N${(this.sourceEdge ? this.sourceEdge.hash() : '')}`
}
public firstNode(): TreeNode<ViewModel> {
return this.sourceEdge && this.sourceEdge.source ? this.sourceEdge.source.firstNode() : this
}
public path(): string {
if (!this.cachedPath) {
return this.branch()
.map(node => (node.sourceEdge && node.sourceEdge.name))
.filter(name => name !== undefined)
.join('/')
}
return this.cachedPath
}
private previous(): TreeNode<ViewModel> | undefined {
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
}
public addEdge(edge: Edge<ViewModel>, emitUpdate: boolean = false) {
this.edges[edge.name] = edge
this.edgeArray.push(edge)
edge.source = this
if (emitUpdate) {
this.onEdgesChange.dispatch()
}
}
public branch(): TreeNode<ViewModel>[] {
const previous = this.previous()
if (!previous) {
return [this]
}
return previous.branch().concat([this])
}
public updateWithNode(node: TreeNode<ViewModel>) {
if (node.message) {
this.setMessage(node.message)
this.onMessage.dispatch(node.message)
this.mqttMessage = node.mqttMessage
}
this.mergeEdges(node)
this.onMerge.dispatch()
}
public leafMessageCount(): number {
if (this.cachedLeafMessageCount === undefined) {
this.cachedLeafMessageCount = this.edgeArray
.map(edge => edge.target.leafMessageCount())
.reduce((a, b) => a + b, 0) + this.messages
}
return this.cachedLeafMessageCount as number
}
public childTopicCount(): number {
if (this.cachedChildTopicCount === undefined) {
this.cachedChildTopicCount = this.edgeArray
.map(e => e.target.childTopicCount())
.reduce((a, b) => a + b, this.edgeArray.length === 0 ? 1 : 0)
}
return this.cachedChildTopicCount as number
}
public edgeCount(): number {
return this.edgeArray.length
}
public childTopics(): TreeNode<ViewModel>[] {
if (this.cachedChildTopics === undefined) {
const initialValue = this.message && this.message.value ? [this] : []
this.cachedChildTopics = this.edgeArray
.map(e => e.target.childTopics())
.reduce((a, b) => a.concat(b), initialValue)
}
return this.cachedChildTopics as TreeNode<ViewModel>[]
}
public findNode (path: String): TreeNode<ViewModel> | undefined {
const topics = path.split('/')
return this.findChild(topics)
}
private findChild(edges: string[]): TreeNode<ViewModel> | undefined {
if (edges.length === 0) {
return this
}
const nextEdge = this.edges[edges[0]]
if (!nextEdge) {
return undefined
}
return nextEdge.target.findChild(edges.slice(1))
}
private mergeEdges(node: TreeNode<ViewModel>) {
const edgeKeys = Object.keys(node.edges)
let edgesDidUpdate = false
for (const edgeKey of edgeKeys) {
const matchingEdge = this.edges[edgeKey]
if (matchingEdge) {
matchingEdge.target.updateWithNode(node.edges[edgeKey].target)
} else {
this.addEdge(node.edges[edgeKey], false)
edgesDidUpdate = true
}
}
if (edgesDidUpdate) {
this.onEdgesChange.dispatch()
}
}
}
-33
View File
@@ -1,33 +0,0 @@
import { Edge, Tree, TreeNode } from './'
interface HasLength {
length: number
}
export abstract class TreeNodeFactory {
public static insertNodeAtPosition<ViewModel>(edgeNames: string[], node: TreeNode<ViewModel>) {
let currentNode: TreeNode<ViewModel> = new Tree()
let edge
for (const edgeName of edgeNames) {
edge = new Edge<ViewModel>(edgeName)
currentNode.addEdge(edge)
currentNode = new TreeNode(edge)
edge.target = currentNode
}
node.sourceEdge = edge
node.sourceEdge!.target = node
}
public static fromEdgesAndValue<ViewModel, T extends HasLength>(edgeNames: string[], value?: T): TreeNode<ViewModel> {
const node = new TreeNode<ViewModel>()
node.setMessage({
value,
length: value ? value.length : 0,
received: new Date(),
})
this.insertNodeAtPosition<ViewModel>(edgeNames, node)
return node
}
}
-7
View File
@@ -1,7 +0,0 @@
export { Edge } from './Edge'
export { TreeNode } from './TreeNode'
export { Message } from './Message'
export { TreeNodeFactory } from './TreeNodeFactory'
export { Tree } from './Tree'
export { Hashable } from './Hashable'
export * from './RingBuffer'
-51
View File
@@ -1,51 +0,0 @@
import 'mocha'
import { Edge, TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai'
describe('Edge', () => {
it('should contain a name', () => {
const e = new Edge('foo')
expect(e.name).to.equal('foo')
})
it('firstEdge should retireve the first edge', () => {
const topics = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
const bazEdge = leaf.sourceEdge
if (!bazEdge) {
expect.fail('should not be undefined')
return
}
expect(bazEdge.name).to.eq('baz')
expect(bazEdge.firstEdge().name).to.eq('foo')
})
it('hash should not be empty', () => {
const e = new Edge('bar')
expect(e.hash().length).to.be.gt(0)
})
it('hash should be stable', () => {
const e = new Edge('bar')
const previousHash = e.hash()
expect(e.hash()).to.eq(previousHash)
})
it('hash should include change if parents are different', () => {
const topics1 = 'foo/bar/baz'.split('/')
const bazEdge1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined).sourceEdge
const topics2 = 'foo/foo/baz'.split('/')
const bazEdge2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined).sourceEdge
if (!bazEdge1 || !bazEdge2) {
throw Error('should not happen')
}
expect(bazEdge1.hash()).to.not.eq(bazEdge2.hash())
})
})
@@ -1,42 +0,0 @@
import 'mocha'
import { EventDispatcher } from '../../../../events'
import { expect } from 'chai'
describe('EventDispatcher', async () => {
it('should dispatch', async function () {
const dispatcher = new EventDispatcher<string, string>('me')
this.timeout(300)
setTimeout(() => dispatcher.dispatch('hello'), 5)
const response = await new Promise((resolve) => {
dispatcher.subscribe((msg) => {
resolve(msg)
})
})
expect(response).to.eq('hello')
})
it('should unsubscribe', async function () {
const dispatcher = new EventDispatcher<string, string>('me')
this.timeout(300)
let incrementee = 0
const callback = (msg: any) => {
incrementee += 1
}
dispatcher.subscribe(callback)
dispatcher.subscribe(callback)
dispatcher.unsubscribe(callback)
// Should increment to 1
dispatcher.dispatch('hello')
dispatcher.unsubscribe(callback)
// should not cause any increment
dispatcher.dispatch('hello')
expect(incrementee).to.eq(1)
})
})
-46
View File
@@ -1,46 +0,0 @@
import { RingBuffer } from '../'
import { expect } from 'chai'
import 'mocha'
describe('RingBuffer', () => {
it('should add a new value', () => {
const b = new RingBuffer(30)
b.add('hello')
expect(b.toArray()[0]).to.eq('hello')
})
it('should add a new value after the first', () => {
const b = new RingBuffer(30)
b.add('hello')
b.add('world')
expect(b.toArray()[1]).to.eq('world')
})
it('should remove old values if buffer size is reached', () => {
const b = new RingBuffer(6)
b.add('hello')
b.add('world')
expect(b.toArray()[0]).to.eq('world')
})
it('items bigger then the buffer should be stored anyway', () => {
const b = new RingBuffer(3)
b.add('hello')
expect(b.toArray()[0]).to.eq('hello')
b.add('world')
expect(b.toArray()[0]).to.eq('world')
})
it('max item count should be respected', () => {
const b = new RingBuffer(100, 3)
b.add('a')
b.add('b')
b.add('c')
b.add('d')
b.add('e')
expect(b.toArray().length).to.eq(3)
expect(b.toArray()[0]).to.eq('c')
expect(b.toArray()[1]).to.eq('d')
expect(b.toArray()[2]).to.eq('e')
})
})
-18
View File
@@ -1,18 +0,0 @@
import 'mocha'
import { Tree, TreeNodeFactory } from '../'
import { expect } from 'chai'
describe('Tree', () => {
it('node can be merged into a tree', () => {
const tree = new Tree()
const topics = 'foo/bar'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
tree.updateWithNode(leaf.firstNode())
const expectedNode = tree.findNode('foo/bar')
expect(expectedNode).to.eq(leaf)
})
})
@@ -1,28 +0,0 @@
import 'mocha'
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
describe('TreeNode.findNode', () => {
it('findNode should retrieve node', () => {
const topics = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
const root = leaf.firstNode()
expect(root.sourceEdge).to.eq(undefined)
const barNode = root.findNode('foo/bar')
if (!barNode) {
expect.fail('did not find node')
return
}
expect(barNode.sourceEdge && barNode.sourceEdge.name).to.eq('bar')
const bazNode = root.findNode('foo/bar/baz')
if (!bazNode) {
expect.fail('did not find node')
return
}
expect(bazNode.sourceEdge && bazNode.sourceEdge.name).to.eq('baz')
})
})
-59
View File
@@ -1,59 +0,0 @@
import 'mocha'
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
describe('TreeNode', () => {
it('firstNode should retrieve first node', () => {
const topics = 'foo/bar'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
expect(leaf.firstNode().edges['foo']).to.not.eq(undefined)
})
it('updateWithNode should update value', () => {
const topics = 'foo/bar'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, '3')
expect(leaf.message && leaf.message.value).to.eq('3')
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics, '5')
const root = leaf.firstNode()
root.updateWithNode(updateLeave.firstNode())
expect(root.sourceEdge).to.eq(undefined)
expect(leaf.message && leaf.message.value).to.eq('5')
})
it('updateWithNode should update intermediate nodes', () => {
const topics1 = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics1, '3')
expect(leaf.message && leaf.message.value).to.eq('3')
const topics2 = 'foo/bar'.split('/')
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics2, '5')
leaf.firstNode().updateWithNode(updateLeave.firstNode())
const barNode = leaf.firstNode().findNode('foo/bar')
expect(barNode && barNode.sourceEdge && barNode.sourceEdge.name).to.eq('bar')
expect(barNode && barNode.message && barNode.message.value).to.eq('5')
expect(leaf.sourceEdge && leaf.sourceEdge.name).to.eq('baz')
expect(leaf.message && leaf.message.value).to.eq('3')
})
it('updateWithNode should add nodes to the tree', () => {
const topics1 = 'foo/bar'.split('/')
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined)
const topics2 = 'foo/bar/baz'.split('/')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined)
leaf1.firstNode().updateWithNode(leaf2.firstNode())
const expectedNode = leaf1.firstNode().findNode('foo/bar/baz')
if (!expectedNode) {
expect.fail('merge seems to have failed')
return
}
})
})
@@ -1,60 +0,0 @@
import 'mocha'
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
describe('TreeNodeFactory', () => {
it('root node must not have a sourceEdge', () => {
const topic = 'foo/bar'
const edges = topic.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(edges, undefined)
expect(leaf.firstNode().sourceEdge).to.eq(undefined)
})
it('should create node', () => {
const topic = 'foo/bar'
const edges = topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue(edges, '5')
if (!node.sourceEdge || !node.sourceEdge.source || !node.message) {
expect.fail('should not happen')
return
}
expect(node).to.not.eq(undefined)
expect(node.sourceEdge.name).to.eq('bar')
expect(node.message.value).to.eq('5')
const foo = node.firstNode().findNode('foo')
expect(foo && foo.sourceEdge && foo.sourceEdge.name).to.eq('foo')
})
it('node should contain edges in order', () => {
const topic = 'foo/bar/baz'
const edges = topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue(edges, '5')
if (!node.sourceEdge || !node.sourceEdge.source || !node.message) {
expect.fail('should not happen')
return
}
expect(node.message.value).to.eq('5')
expect(node.sourceEdge.name).to.eq('baz')
const barNode = node.sourceEdge.source
if (!barNode || !barNode.sourceEdge) {
expect.fail('should not fail')
return
}
expect(barNode.sourceEdge.name).to.eq('bar')
const fooNode = barNode.sourceEdge.source
if (!fooNode || !fooNode.sourceEdge) {
expect.fail('should not fail')
return
}
expect(fooNode.sourceEdge.name).to.eq('foo')
})
})
-80
View File
@@ -1,80 +0,0 @@
import {
AddMqttConnection,
EventDispatcher,
MqttMessage,
addMqttConnectionEvent,
backendEvents,
checkForUpdates,
makeConnectionMessageEvent,
makeConnectionStateEvent,
makePublishEvent,
removeConnection,
updateAvailable,
} from '../../events'
import { DataSource, MqttSource } from './DataSource'
import { UpdateInfo } from 'builder-util-runtime'
export class ConnectionManager {
private connections: {[s: string]: DataSource<any>} = {}
public manageConnections() {
backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest)
backendEvents.subscribe(removeConnection, (connectionId: string) => this.removeConnection(connectionId))
}
private handleConnectionRequest = (event: AddMqttConnection) => {
const connectionId = event.id
const options = event.options
const connection = new MqttSource()
this.connections[connectionId] = connection
const connectionStateEvent = makeConnectionStateEvent(connectionId)
connection.stateMachine.onUpdate.subscribe((state) => {
backendEvents.emit(connectionStateEvent, state)
})
connection.connect(options)
this.handleNewMessagesForConnection(connectionId, connection)
backendEvents.subscribe(makePublishEvent(connectionId), (msg: MqttMessage) => {
this.connections[connectionId].publish(msg)
})
}
private handleNewMessagesForConnection(connectionId: string, connection: MqttSource) {
const messageEvent = makeConnectionMessageEvent(connectionId)
connection.onMessage((topic: string, payload: Buffer, packet: any) => {
let buffer = payload
if (buffer.length > 10000) {
buffer = buffer.slice(0, 10000)
}
backendEvents.emit(messageEvent, { topic, payload: buffer.toString(), qos: packet.qos, retain: packet.retain })
})
}
public removeConnection(hash: string) {
const connection = this.connections[hash]
connection.disconnect()
delete this.connections[hash]
}
public closeAllConnections() {
Object.keys(this.connections)
.forEach(hash => this.removeConnection(hash))
}
}
class UpdateNotifier {
public onCheckUpdateRequest = new EventDispatcher<void, UpdateNotifier>(this)
constructor() {
backendEvents.subscribe(checkForUpdates, () => {
this.onCheckUpdateRequest.dispatch()
})
}
public notify(updateInfo: UpdateInfo) {
backendEvents.emit(updateAvailable, updateInfo)
}
}
export const updateNotifier = new UpdateNotifier()
-3
View File
@@ -1,3 +0,0 @@
--require ts-node/register
--require source-map-support/register
--recursive ./src/**/*.spec.ts
-21
View File
@@ -1,21 +0,0 @@
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"outDir": "./build",
"strict": true,
"lib": ["es2017", "dom"],
"sourceMap": true
},
"includes": [
"src/**/*.ts"
],
"exclude": [
"app",
"node_modules",
"src/**/*.spec.ts",
"**/*.d.ts",
"typings"
]
}
-88
View File
@@ -1,88 +0,0 @@
import { IpcMain, IpcRenderer, ipcMain, ipcRenderer } from 'electron'
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
}
interface CallbackStore {
wrappedCallback: any
callback: any
}
class IpcMainEventBus implements EventBusInterface {
private ipc: IpcMain
private client: any
constructor(ipc: IpcMain) {
this.ipc = ipc
}
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>) {
this.ipc.removeAllListeners(event.topic)
}
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
throw new Error('Not implemented') // Todo: implement
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
if (!this.client.isDestroyed()) {
this.client.send(event.topic, msg)
}
}
}
class IpcRendererEventBus implements EventBusInterface {
private ipc: IpcRenderer
private callbacks: CallbackStore[] = []
constructor(ipc: IpcRenderer) {
this.ipc = ipc
}
public subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void) {
const wrappedCallback = (_event: any, arg: any) => {
callback(arg)
}
this.ipc.on(event.topic, wrappedCallback)
this.callbacks.push({
callback,
wrappedCallback,
})
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
this.ipc.removeAllListeners(event.topic)
}
public unsubscribe<MessageType>(event: Event<MessageType>, callback: any) {
const item = this.callbacks.find(store => store.callback === callback)
if (!item) {
return
}
this.ipc.removeListener(event.topic, item.wrappedCallback)
this.callbacks = this.callbacks.filter(a => a !== item)
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
this.ipc.send(event.topic, msg)
}
}
export const rendererEvents = new IpcRendererEventBus(ipcRenderer)
export const backendEvents = new IpcMainEventBus(ipcMain)
-47
View File
@@ -1,47 +0,0 @@
import { EventEmitter } from 'events'
interface CallbackStore {
wrappedCallback: any
callback: any
}
export class EventDispatcher<Message, Dispatcher> {
private emitter = new EventEmitter()
private dispatcher: Dispatcher
private callbacks: CallbackStore[] = []
constructor(dispatcher: Dispatcher) {
this.dispatcher = dispatcher
}
public dispatch(msg: Message) {
this.emitter.emit('event', msg)
}
public subscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
const wrappedCallback = (msg: Message) => {
callback(msg, this.dispatcher)
}
this.emitter.on('event', wrappedCallback)
this.callbacks.push({
callback,
wrappedCallback,
})
}
public unsubscribe(callback: (msg: Message, dispatcher: Dispatcher) => void) {
const item = this.callbacks.find(store => store.callback === callback)
if (!item) {
return
}
this.emitter.removeListener('event', item.wrappedCallback)
this.callbacks = this.callbacks.filter(a => a !== item)
}
public removeAllListeners() {
this.emitter.removeAllListeners()
this.callbacks = []
}
}
-55
View File
@@ -1,55 +0,0 @@
import { DataSourceState, MqttOptions } from '../backend/src/DataSource'
import { UpdateInfo } from 'builder-util-runtime'
export { UpdateInfo } from 'builder-util-runtime'
export interface Event<MessageType> {
topic: string
}
export interface AddMqttConnection {
id: string,
options: MqttOptions
}
export const addMqttConnectionEvent: Event<AddMqttConnection> = {
topic: 'connection/add/mqtt',
}
export const removeConnection: Event<string> = {
topic: 'connection/remove',
}
export function makeConnectionStateEvent(connectionId: string): Event<DataSourceState> {
return {
topic: `conn/state/${connectionId}`,
}
}
export const checkForUpdates: Event<void> = {
topic: 'app/update/check',
}
export const updateAvailable: Event<UpdateInfo> = {
topic: 'app/update/available',
}
export interface MqttMessage {
topic: string,
payload: any,
qos: 0 | 1 | 2,
retain: boolean
}
export function makePublishEvent(connectionId: string): Event<MqttMessage> {
return {
topic: `conn/publish/${connectionId}`,
}
}
export function makeConnectionMessageEvent(connectionId: string): Event<MqttMessage> {
return {
topic: `conn/${connectionId}`,
}
}
View File
-3
View File
@@ -1,3 +0,0 @@
export * from './Events'
export * from './EventDispatcher'
export * from './EventBus'
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 81 KiB

BIN
View File
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
convert screen1.png -resize 33% screen1_small.png
convert screen2.png -resize 33% screen2_small.png
convert screen3.png -resize 33% screen3_small.png
convert screen_dark_mode.png -resize 33% screen_dark_mode_small.png
convert -size 2560x1600 xc:black -fill white -draw "polygon 0,0 0,2560 2200,0" framemask.png
convert screen_dark_mode.png framemask.png -alpha off -compose copy_opacity -composite first_half.png
convert -respect-parenthesis screen4.png \( framemask.png -negate \) -alpha off -compose copy_opacity -composite second_half.png
convert *_half.png -compose over -composite screen-composite.png
convert screen-composite.png -resize 33% screen-composite_small.png
rm framemask.png *_half.png
optipng *.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+152
View File
@@ -0,0 +1,152 @@
/* ©2015 Johannes Jakob
Made with <3 in Germany */
/* window BEGIN */
.osx-frame {
background: #fff;
width: 100%;
margin: auto;
border: 1px solid #acacac;
border-radius: 6px;
box-shadow: 0px 0px 20px #acacac;
}
.titlebar {
background: -webkit-gradient(linear, left top, left bottom, color-stop(0.0, #ebebeb, color-stop(1.0, #d5d5d5)));
background: -webkit-linear-gradient(top, #ebebeb, #d5d5d5);
background: -moz-linear-gradient(top, #ebebeb, #d5d5d5);
background: -ms-linear-gradient(top, #ebebeb, #d5d5d5);
background: -o-linear-gradient(top, #ebebeb, #d5d5d5);
background: linear-gradient(top, #ebebeb, #d5d5d5);
color: #4d494d;
font-size: 11pt;
line-height: 20px;
text-align: center;
width: 100%;
height: 20px;
border-top: 1px solid #f3f1f3;
border-bottom: 1px solid #b1aeb1;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
cursor: default;
}
.titlebar p {
margin-left: -61px;
display: inline-block;
}
.buttons {
padding-left: 10px;
padding-top: 3px;
float: left;
line-height: 0px;
}
.buttons div {
float: left;
}
.close {
background: #ff5c5c;
font-size: 9pt;
width: 11px;
height: 11px;
border: 1px solid #e33e41;
border-radius: 50%;
display: inline-block;
opacity: 1;
}
.close:active {
background: #c14645;
border: 1px solid #b03537;
}
.close:active .closebutton {
color: #4e0002;
}
.closebutton {
color: #820005;
visibility: hidden;
cursor: default;
}
.minimize {
background: #ffbd4c;
font-size: 9pt;
line-height: 11px;
margin-left: 6px;
width: 11px;
height: 11px;
border: 1px solid #e09e3e;
border-radius: 50%;
display: inline-block;
}
.minimize:active {
background: #c08e38;
border: 1px solid #af7c33;
}
.minimize:active .minimizebutton {
color: #5a2607;
}
.minimizebutton {
color: #9a5518;
visibility: hidden;
cursor: default;
}
.zoom {
background: #00ca56;
font-size: 9pt;
line-height: 11px;
margin-left: 6px;
width: 11px;
height: 11px;
border: 1px solid #14ae46;
border-radius: 50%;
display: inline-block;
}
.zoom:active {
background: #029740;
border: 1px solid #128435;
}
.zoom:active .zoombutton {
color: #003107;
}
.zoombutton {
color: #006519;
visibility: hidden;
cursor: default;
}
.content {
padding: 0;
margin: 0;
line-height: 0;
overflow: hidden;
border-radius: 0px 0px 5px 5px;
}
/* window END */
/* content BEGIN */
h3 {
margin-top: 0px;
}
/* content END */

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