Compare commits

..
116 Commits
Author SHA1 Message Date
Thomas Nordquist 4e51d7af79 Prepare release 2019-01-20 19:07:54 +01:00
Thomas Nordquist 9bf3973160 Fix plot 2019-01-20 19:05:33 +01:00
Thomas Nordquist 1719876734 Fix state mixup 2019-01-20 18:48:18 +01:00
Thomas Nordquist 82ef5bc4e7 Remove arm64 build
Due to people confusing the arm64 with amd64 release
2019-01-20 18:36:50 +01:00
Thomas Nordquist ce60d1b20a Update Readme 2019-01-20 18:35:15 +01:00
Thomas Nordquist 2316dac809 Update Readme 2019-01-20 16:37:33 +01:00
Thomas Nordquist 3b049d2008 Add topic selection to topic drawer 2019-01-20 15:38:55 +01:00
Thomas Nordquist 9f48301c4b Fix dependencies 2019-01-20 15:15:27 +01:00
Thomas Nordquist 009fb08b33 Bump version 2019-01-20 14:58:15 +01:00
Thomas Nordquist fa24fa9abb Add tooltip to retain checkbox 2019-01-20 14:57:10 +01:00
Thomas Nordquist 1ac1dede98 Add "delete retained topic" button 2019-01-20 14:56:52 +01:00
Thomas Nordquist b0d25b438d Bump version 2019-01-20 13:47:54 +01:00
Thomas Nordquist 4b42c23121 Update dependecies 2019-01-20 13:38:54 +01:00
Thomas Nordquist 0f7ea7c491 Add formatter to plot tooltip 2019-01-20 13:37:13 +01:00
Thomas Nordquist 71c4d1e04b Improve plot 2019-01-20 13:27:29 +01:00
Thomas Nordquist 435fb1f9b9 Supposably fix travis builds 2019-01-20 12:56:35 +01:00
Thomas Nordquist ab83c682ac Remove "empty payload" toggle switch
This function is redundat, simply send an empty message wil suffice
2019-01-20 12:54:54 +01:00
Thomas Nordquist 4d794205d5 Fix publish topic update 2019-01-20 12:47:46 +01:00
Thomas Nordquist fa9def4dd7 Fix mqttMessage merge 2019-01-20 12:29:00 +01:00
Thomas Nordquist f893d5ce60 Refactor communication
Add QoS andd retain flag
Refactor reducer
2019-01-20 05:30:21 +01:00
Thomas Nordquist 1839b551c0 Add webpack bundle analyzer 2019-01-19 19:21:26 +01:00
Thomas Nordquist 6f48f0bd8e Fix dependencies 2019-01-19 19:03:24 +01:00
Thomas Nordquist 07bfde6244 Add numeric history plot
Resolves #31
2019-01-19 18:59:28 +01:00
Thomas Nordquist b23ffd334d Refactor & add error reporing 2019-01-19 16:48:36 +01:00
Thomas Nordquist f3a686b23f Refactor connection 2019-01-19 15:00:26 +01:00
Thomas Nordquist bfcee49e74 Also subscribe to $SYS 2019-01-18 23:54:08 +01:00
Thomas Nordquist 2c4c7bb72e Beautify error boundary 2019-01-18 19:16:37 +01:00
Thomas Nordquist 353081175b Add error boundary to nake frontend more resiliant
Allows users to recover from errors in userStorage

Is related to #23
2019-01-18 13:13:36 +01:00
Thomas Nordquist f462e7a881 Show developer toolbar in menu 2019-01-18 11:18:48 +01:00
Thomas Nordquist 14401fbc7b Reverse publish history order 2019-01-18 10:26:19 +01:00
Thomas Nordquist fb7c27c53f Fix message history order
The latest message should be the first, oldest last.
Fixes #32
2019-01-18 10:23:42 +01:00
Thomas Nordquist 96c7c56f64 Add clear topic button
Resolves #29
2019-01-18 10:21:20 +01:00
Thomas Nordquist 8a72a0224d Always show mqtt client id 2019-01-18 10:04:20 +01:00
Thomas Nordquist 2f2f2842b4 Support custom client ids
Resolves #25
2019-01-18 00:21:07 +01:00
Thomas Nordquist ef9bdad984 Fix peer dependecy 2019-01-17 23:49:35 +01:00
Thomas Nordquist 0250d61dbd Add menus items
Fixes copy paste issue in release
Fixes #27
2019-01-17 23:46:43 +01:00
Thomas Nordquist a06ff100a8 Restructure 2019-01-17 23:31:36 +01:00
Thomas Nordquist ed0942983b Remove dev code 2019-01-17 23:28:17 +01:00
Thomas Nordquist 3777963f8c Switch electron.js to typescript 2019-01-17 23:27:45 +01:00
Thomas Nordquist d3a684d89e Update Readme 2019-01-17 22:30:31 +01:00
Thomas Nordquist 73e83beeb0 Update Readme 2019-01-16 02:39:09 +01:00
Thomas Nordquist 27c23f7432 Fix certificate validation switch 2019-01-16 02:23:20 +01:00
Thomas Nordquist d6335e68de Refactor history badge 2019-01-16 02:19:09 +01:00
Thomas Nordquist 60382e3c80 Update electron 2019-01-16 01:33:53 +01:00
Thomas Nordquist 4192e91cce Revert "Update readme"
This reverts commit 64358e772d.
2019-01-16 01:16:17 +01:00
Thomas Nordquist 64358e772d Update readme 2019-01-16 01:14:07 +01:00
Thomas Nordquist 3ba31de468 Add last message time to sidebar 2019-01-16 00:53:18 +01:00
Thomas Nordquist ab6938c7da Fix debugging 2019-01-16 00:33:03 +01:00
Thomas Nordquist 1eeaee0fe0 Fix update url 2019-01-16 00:29:55 +01:00
Thomas Nordquist 0e7ec90c65 Add electron-debug 2019-01-16 00:08:46 +01:00
Thomas Nordquist 5eb6f41189 Add debug option --debug 2019-01-15 23:54:03 +01:00
Thomas Nordquist 830dd31f06 Update readme 2019-01-15 22:26:16 +01:00
Thomas Nordquist 0de3ce2c41 Update build targets 2019-01-15 22:01:54 +01:00
Thomas Nordquist 9c5140c419 Lazy load js 2019-01-15 22:00:47 +01:00
Thomas Nordquist 61b64a5ac2 Fix authentication 2019-01-15 22:00:35 +01:00
Thomas Nordquist f905ba8e89 Clarify mouse interaction 2019-01-15 19:00:37 +01:00
Thomas Nordquist c6a0a15d95 Print update info 2019-01-15 15:10:29 +01:00
Thomas Nordquist 75230ccb40 Add snap package target 2019-01-15 13:55:06 +01:00
Thomas Nordquist 97a1cf72c5 Fix linux icon 2019-01-15 12:51:21 +01:00
Thomas Nordquist 71c06b3f74 Try fix linux app icon 2019-01-14 18:20:29 +01:00
Thomas Nordquist b0fa4a9f7a Update readme 2019-01-14 17:38:00 +01:00
Thomas Nordquist d8c8799406 Don't send events after window is gone 2019-01-14 17:12:46 +01:00
Thomas Nordquist 7c72344174 Update readme 2019-01-14 17:00:14 +01:00
Thomas Nordquist 7d42ed12b1 Add readme template 2019-01-14 16:48:01 +01:00
Thomas Nordquist 0df5ff13c1 Update publish settings 2019-01-14 15:28:24 +01:00
Thomas Nordquist ac2387ed9b Update CI deploy configuration 2019-01-14 14:20:33 +01:00
Thomas Nordquist d4bb00262e Prepare release 2019-01-14 12:14:04 +01:00
Thomas Nordquist d41a2c2269 Remove root node name from json view 2019-01-14 12:14:04 +01:00
Thomas Nordquist 4d11302b24 Update screenshot 2019-01-14 12:08:00 +01:00
Thomas Nordquist fd386bf1df Fix types in tests 2019-01-14 11:49:57 +01:00
Thomas Nordquist ad78ca03d8 Close connections when window closed
Fixes #8
2019-01-14 11:23:46 +01:00
Thomas Nordquist 611fde13e2 Improve scroll behavior 2019-01-14 11:16:39 +01:00
Thomas Nordquist 4f02e19f65 Only package on tag builds 2019-01-14 11:02:50 +01:00
Thomas Nordquist 2268175a38 Fix performance issue
When the root node was collapsed, it also was selected due to the click event.
The sidebar was updated every time a new message was receivend,
effectivly inhibiting the "render the tree if you got nothing else to do" optimization to render anything at all.

Fixes #7
2019-01-14 10:45:15 +01:00
Thomas Nordquist bd3b81273d Clean up 2019-01-14 00:15:53 +01:00
Thomas Nordquist 322c8e67aa Fix product name 2019-01-13 23:58:06 +01:00
Thomas Nordquist 906eadf08b Add splash screen 2019-01-13 21:55:55 +01:00
Thomas Nordquist 225f536cd1 Tweak update notification settings 2019-01-13 21:09:20 +01:00
Thomas Nordquist 87f0f06a07 Remove debug code 2019-01-13 20:51:41 +01:00
Thomas Nordquist e294d9700f Add manual auto-update fallback 2019-01-13 20:49:36 +01:00
Thomas Nordquist fdece7ae91 Notify user if auto update failed 2019-01-13 17:37:13 +01:00
Thomas Nordquist d2bb098772 Track redux actions 2019-01-13 17:13:34 +01:00
Thomas Nordquist 490678d7b4 Fix sort topic function
Fixes #4
2019-01-13 15:26:32 +01:00
Thomas Nordquist 2d649b3f51 Refactor 2019-01-13 15:26:19 +01:00
Thomas Nordquist 0de67a48a6 Fix auto-expand selection
Fixes #5
2019-01-13 15:19:34 +01:00
Thomas Nordquist 1267bb27f7 Clean up 2019-01-13 15:19:09 +01:00
Thomas Nordquist 31b3077743 Remove unnecessary json editor options 2019-01-13 15:12:57 +01:00
Thomas Nordquist 7015c31a91 Remove unused code 2019-01-13 15:11:11 +01:00
Thomas Nordquist f486f5888c Change default server to iot.eclipse.org 2019-01-13 15:10:30 +01:00
Thomas Nordquist 33bd2b7340 Update Readme.md 2019-01-13 15:05:07 +01:00
Thomas Nordquist 241a99207d Add readme 2019-01-13 14:40:47 +01:00
Thomas Nordquist fa9f321d88 Add crash reporter 2019-01-13 14:38:31 +01:00
Thomas Nordquist 683a6a7245 Update build settings 2019-01-13 05:31:41 +01:00
Thomas Nordquist 37bb0b6ea1 Prepare travis builds 2019-01-13 04:41:58 +01:00
Thomas Nordquist 7e28865a73 Improve build 2019-01-13 03:13:00 +01:00
Thomas Nordquist c40949f38d Set scroll corners background 2019-01-13 02:27:05 +01:00
Thomas Nordquist 5cf5029530 Try catch autoupdater errors 2019-01-13 02:25:50 +01:00
Thomas Nordquist ab87b13eed Fix scrollbars for windows 2019-01-13 01:48:43 +01:00
Thomas Nordquist e83fe78755 Add "diff" for historic messages 2019-01-12 21:39:18 +01:00
Thomas Nordquist 8fa8ea5aa7 Update tslint.json 2019-01-12 20:26:56 +01:00
Thomas Nordquist d75122d44d Fix app style 2019-01-12 20:26:36 +01:00
Thomas Nordquist a677fb7a0c Add message history to frontend 2019-01-12 20:25:52 +01:00
Thomas Nordquist 6fbf04cd00 Improve node selection via MouseOver 2019-01-12 14:29:56 +01:00
Thomas Nordquist 48b7754a2b Add message history to TreeNode 2019-01-12 14:29:21 +01:00
Thomas Nordquist a8e082f084 Refactor 2019-01-11 19:07:56 +01:00
Thomas Nordquist b52b2d7696 Refactor 2019-01-11 18:52:12 +01:00
Thomas Nordquist 1586d0121c Fix layout 2019-01-11 10:32:38 +01:00
Thomas Nordquist 5425a80ada Fix topic selection 2019-01-11 10:32:29 +01:00
Thomas Nordquist f7843bc348 Add publish history & fix topic input 2019-01-11 10:17:02 +01:00
Thomas Nordquist c956afdee0 Fix node title updates 2019-01-11 09:10:41 +01:00
Thomas Nordquist 4e8727432d Fix button handler 2019-01-11 09:10:19 +01:00
Thomas Nordquist ea87c24fb5 Fix import 2019-01-11 00:42:00 +01:00
Thomas Nordquist c3ef9335c0 Add topic publishing sidebar 2019-01-11 00:40:05 +01:00
Thomas Nordquist eb375073f9 Fix Sidebar & Mouse event target area
Fix clipboard
Fix invalid state in sidebar due to missing event termination
2019-01-10 10:34:09 +01:00
Thomas Nordquist 269061bdc8 Work in progress 2019-01-08 23:58:24 +01:00
Thomas Nordquist b44f352804 WiP #broken 2019-01-08 16:39:18 +01:00
77 changed files with 5647 additions and 1297 deletions
+1
View File
@@ -2,6 +2,7 @@ node_modules
backend/coverage
backend/.nyc_output
build
/dist
.DS_Store
test.dot
test.png
+29
View File
@@ -0,0 +1,29 @@
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
+31
View File
@@ -0,0 +1,31 @@
# 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)
### Version 0.0.9
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.
## Download
The app is prebuilt for Windows ([portable](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.9/MQTT-Explorer-0.0.9.exe), [installer](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.9/MQTT-Explorer-Setup-0.0.9.exe)), Linux ([AppImage](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.9/MQTT-Explorer-0.0.9-x86_64.AppImage)) and Mac ([dmg](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.9/MQTT-Explorer-0.0.9.dmg)).
More architectures and package types: [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
![screen1](https://user-images.githubusercontent.com/7721625/51441563-41418180-1cd3-11e9-893b-c22f6a6c3695.png)
![screen2](https://user-images.githubusercontent.com/7721625/51441470-59fd6780-1cd2-11e9-925f-bc491027300c.png)
## Develop
PRs and issues are welcome
Install with `npm run install`, build with `npm run build`
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## License
Not yet decided which license exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
+28
View File
@@ -0,0 +1,28 @@
# MQTT-Explorer
[![Downloads](https://github-basic-badges.herokuapp.com/release/thomasnordquist/MQTT-Explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://github-basic-badges.herokuapp.com/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)
### 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.
## Download
The app is prebuilt for Windows ({{windowsTargets}}), Linux ({{linuxTargets}}) and Mac ({{macTargets}}).
More architectures and package types: [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
![screen2](https://user-images.githubusercontent.com/7721625/51109225-dbe22200-17f4-11e9-8f6b-c6a27c07c90e.png)
## Develop
PRs and issues are welcome
Install with `npm run install`, build with `npm run build`
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## License
Not yet decided which license exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
+56 -9
View File
@@ -2,7 +2,9 @@
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<title>MQTT-Explorer</title>
<script src="./build/bugtracking.bundle.js"></script>
<style>
body, html {
margin: 0;
@@ -14,17 +16,62 @@
50% {background-color: #3f51b5;}
100% {background-color: inherit;}
}
::-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;}
}
</style>
<script src="http://localhost:35729/livereload.js"></script>
</head>
<body>
<div id="example"></div>
<!-- Dependencies -->
<!-- <script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script> -->
<!-- Main -->
<script src="./build/bundle.js"></script>
<div id="splash"><div id="splash1"></div></div>
<div id="app" style="font:-webkit-control"></div>
<script>
function onLoad() {
var script = document.createElement("script");
script.src = "./build/app.bundle.js"
document.head.appendChild(script);
}
document.addEventListener('DOMContentLoaded', onLoad(), false);
</script>
</body>
</html>
+2057 -10
View File
File diff suppressed because it is too large Load Diff
+12 -5
View File
@@ -4,8 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --mode production",
"postinstall": "npm run build"
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
@@ -15,14 +14,22 @@
"@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",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"react-transition-group": "^2.5.2",
"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.7.1",
"@types/node": "^10.12.18",
"@types/react-resize-detector": "^3.1.0",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^1.4.32",
"@types/vis": "^4.21.9",
@@ -31,9 +38,9 @@
"jquery": "^3.3.1",
"lodash.throttle": "^4.1.1",
"moving-average": "^1.0.0",
"react": "^16.3.2",
"react-dom": "^16.3.3",
"react-ace": "^6.3.2",
"react-json-view": "^1.19.1",
"react-resize-detector": "^3.4.0",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4",
+42
View File
@@ -0,0 +1,42 @@
<!-- 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>

After

Width:  |  Height:  |  Size: 1.7 KiB

+38 -35
View File
@@ -1,32 +1,30 @@
import * as React from 'react'
import { connect } from 'react-redux'
import * as q from '../../backend/src/Model'
import CssBaseline from '@material-ui/core/CssBaseline'
import { withStyles, Theme } from '@material-ui/core/styles'
import Tree from './components/Tree/Tree'
import TitleBar from './components/TitleBar'
import Sidebar from './components/Sidebar/Sidebar'
import Connection from './components/ConnectionSetup/Connection'
import Settings from './components/Settings'
import { AppState } from './reducers'
interface State {
selectedNode?: q.TreeNode,
connectionId?: string
}
import { Theme, withStyles } from '@material-ui/core/styles'
import { AppState } from './reducers'
import Connection from './components/ConnectionSetup/Connection'
import CssBaseline from '@material-ui/core/CssBaseline'
import Settings from './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'
interface Props {
name: string
connectionId: string
theme: Theme
settingsVisible: boolean
}
class App extends React.Component<Props, State> {
class App extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {
selectedNode: undefined,
}
this.state = { }
}
private getStyles(): {[s: string]: React.CSSProperties} {
@@ -79,30 +77,35 @@ class App extends React.Component<Props, State> {
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent } = this.getStyles()
return <div style={centerContent}>
<CssBaseline />
<Settings />
<div style={settingsVisible ? contentShift : content}>
<TitleBar />
<div style={centerContent}>
<div style={this.getStyles().left}>
<Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node })
}} />
</div>
<div style={this.getStyles().right}>
<Sidebar node={this.state.selectedNode} />
</div>
return (
<div style={centerContent}>
<CssBaseline />
<ErrorBoundary>
<Settings />
<div style={settingsVisible ? contentShift : content}>
<TitleBar />
<div style={centerContent}>
<div style={this.getStyles().left}>
<Tree connectionId={this.props.connectionId} />
</div>
<div style={this.getStyles().right}>
<Sidebar connectionId={this.props.connectionId} />
</div>
</div>
</div>
</div>
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
</div >
<UpdateNotifier />
<Connection />
</ErrorBoundary>
</div >
)
}
}
const mapStateToProps = (state: AppState) => {
return {
settingsVisible: state.settings.visible,
settingsVisible: state.tooBigReducer.settings.visible,
connectionId: state.tooBigReducer.connectionId,
}
}
+112
View File
@@ -0,0 +1,112 @@
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)
+226
View File
@@ -0,0 +1,226 @@
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)
}
private fixUrl(url: string, version: string) {
if (!/^http/.test(url)) {
return `https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v${version}/${url}`
}
return url
}
public componentWillUnmount() {
rendererEvents.unsubscribeAll(updateAvailable)
}
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) {
console.log(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, { withTheme: true })(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
+40
View File
@@ -0,0 +1,40 @@
import { ActionTypes, NodeOrder, CustomAction, AppState } from '../reducers'
import { MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
rendererEvents.subscribe(event, (dataSourceState) => {
if (dataSourceState.connected) {
dispatch(connected())
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
})
}
export const connected: () => CustomAction = () => ({
type: ActionTypes.connected,
})
export const connecting: (connectionId: string) => CustomAction = (connectionId: string) => ({
connectionId,
type: ActionTypes.connecting,
})
export const showError = (error?: string) => ({
error,
type: ActionTypes.showError,
})
export const disconnect = () => (dispatch: Dispatch<CustomAction>, getState: () => AppState) => {
rendererEvents.emit(removeConnection, getState().tooBigReducer.connectionId)
dispatch({
type: ActionTypes.disconnect,
})
}
+56
View File
@@ -0,0 +1,56 @@
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,
}
}
+10 -3
View File
@@ -1,14 +1,21 @@
import { ActionTypes } from '../reducers'
import { ActionTypes, NodeOrder, CustomAction } from '../reducers'
export const setAutoExpandLimit = (autoExpandLimit: number = 0) => {
export const setAutoExpandLimit = (autoExpandLimit: number = 0): CustomAction => {
return {
autoExpandLimit,
type: ActionTypes.setAutoExpandLimit,
}
}
export const toggleSettingsVisibility = () => {
export const toggleSettingsVisibility = (): CustomAction => {
return {
type: ActionTypes.toggleSettingsVisibility,
}
}
export const setNodeOrder = (nodeOrder: NodeOrder = NodeOrder.none): CustomAction => {
return {
nodeOrder,
type: ActionTypes.setNodeOrder,
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Dispatch, Action } from 'redux'
import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events'
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic, connectionId } = getState().tooBigReducer
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)
}
+18
View File
@@ -0,0 +1,18 @@
import { ActionTypes, CustomAction, AppState } from '../reducers'
import * as q from '../../../backend/src/Model'
import { Dispatch } from 'redux'
import { setTopic } from './Publish'
export const selectTopic = (topic: q.TreeNode) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { selectedTopic } = getState().tooBigReducer
// Update publish topic
if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) {
dispatch(setTopic(topic.path()))
}
dispatch({
selectedTopic: topic,
type: ActionTypes.selectTopic,
})
}
+15
View File
@@ -0,0 +1,15 @@
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,
}
}
+6 -1
View File
@@ -1,3 +1,8 @@
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 }
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons }
+4
View File
@@ -0,0 +1,4 @@
import { electronRendererTelementry } from 'electron-telemetry'
const spareMeFromGc = electronRendererTelementry
electronRendererTelementry.registerErrorHandler()
+294 -182
View File
@@ -1,21 +1,44 @@
import * as React from 'react'
import {
Typography, Toolbar, Modal,
MenuItem, Button, Grid, Paper,
TextField, Switch, FormControlLabel,
Button,
CircularProgress,
FormControl,
FormControlLabel,
Grid,
IconButton,
Input,
InputAdornment,
InputLabel,
MenuItem,
Modal,
Paper,
Switch,
TextField,
Toolbar,
Typography,
} from '@material-ui/core'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
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 { MqttOptions, DataSourceState } from '../../../../backend/src/DataSource'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../../events'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
import sha1 = require('sha1')
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connectionActions } from '../../actions'
interface Props {
classes: {[s: string]: string}
theme: Theme
onAbort: () => void,
onConnection: (connectionId: string) => void
actions: typeof connectionActions,
visible: boolean
connected: boolean
connecting: boolean
error?: string
}
const protocols = [
@@ -24,16 +47,18 @@ const protocols = [
]
interface State {
connecting: boolean
connectionId?: string
error?: Error
visible: boolean
showPassword: boolean
connectionSettings: ConnectionSettings
}
interface ConnectionSettings {
host: string
protocol: string
port: number
tls: boolean
certValidation: boolean
clientId: string
connectionId?: string
username: string
password: string
}
@@ -41,80 +66,65 @@ interface State {
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')
let storedSettings
try {
storedSettings = storedSettingsString ? JSON.parse(storedSettingsString) : undefined
} catch {
window.localStorage.setItem('connectionSettings', undefined)
}
const defaultState = {
visible: true,
host: 'nodered',
protocol: protocols[0],
port: 1883,
tls: false,
certValidation: true,
clientId: '',
username: '',
password: '',
connecting: false,
connectionId: undefined,
}
this.state = Object.assign({}, defaultState, storedSettings)
return storedSettings || this.defaultConnectionSettings
}
private saveConnectionSettings() {
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state))
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state.connectionSettings))
}
private handleClickShowPassword = () => {
this.setState({ showPassword: !this.state.showPassword })
}
private optionsFromState(): MqttOptions {
const protocol = this.state.protocol === 'tcp://' ? 'mqtt://' : this.state.protocol
const url = `${protocol}${this.state.host}:${this.state.port}`
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.username || undefined,
password: this.state.username || undefined,
tls: this.state.tls,
certValidation: this.state.certValidation,
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,
}
}
private connect() {
this.setState({
connecting: true,
})
const options = this.optionsFromState()
const connectionId = (sha1(Math.random() + JSON.stringify(options)).slice(0, 8)) as string
this.setState({ connectionId })
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
rendererEvents.subscribe(event, (state: DataSourceState) => {
console.log(state)
if (state.connected) {
this.props.onConnection(connectionId)
this.setState({ visible: false })
} else if (state.error) {
console.log('error', state.error)
this.setState({ error: state.error })
}
})
}
private disconnect() {
this.setState({
connecting: false,
})
rendererEvents.emit(removeConnection, this.state.connectionId)
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
root: {
@@ -142,143 +152,245 @@ class Connection extends React.Component<Props, State> {
button: {
margin: theme.spacing.unit,
},
inputFormControl: {
marginTop: '16px',
},
}
}
private handleChange = (name: string) => (event: any) => {
const state: any = {
[name]: event.target.value,
}
this.setState(state)
this.setState({
connectionSettings: {
...this.state.connectionSettings,
[name]: event.target.value,
},
})
}
public render() {
const { classes } = this.props
return <Modal open={this.state.visible} disableAutoFocus={true} onClose={() => { console.log('close') }}>
<Paper className={classes.root}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
</Toolbar>
<form className={classes.container} noValidate autoComplete="off">
<Grid container spacing={24}>
<Grid item xs={2}>
<TextField
select
label="Protocol"
className={classes.textField}
value={this.state.protocol}
onChange={this.handleChange('protocol')}
margin="normal"
>
{protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={7}>
<TextField
label="Host"
className={classes.textField}
value={this.state.host}
onChange={this.handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item xs={3}>
<TextField
label="Port"
className={classes.textField}
value={this.state.port}
onChange={this.handleChange('port')}
margin="normal"
/>
</Grid>
<Grid item xs={5}>
<TextField
label="Username"
className={classes.textField}
value={this.state.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item xs={5}>
<TextField
label="Password"
type="type"
className={classes.textField}
value={this.state.password}
onChange={this.handleChange('password')}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.certValidation}
onChange={() => this.setState({ certValidation: !this.state.certValidation })}
color="primary"
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}
/>
)}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
</Grid>
<Grid item xs={4}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.tls}
onChange={() => this.setState({ tls: !this.state.tls })}
color="primary"
</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 />}
/>
)}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</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>
</Grid>
<Grid item xs={4}></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>
</form>
</Paper>
</Modal>
</div>
)
}
private renderConnectButton() {
private renderProtocols() {
const { classes } = this.props
const protocolItems = protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))
if (this.state.connecting) {
return <Button variant="contained" color="primary" className={classes.button} onClick={this.onClickAbort}>
<CircularProgress size={22} style={{ marginRight: '10px' }} color="secondary" /> Abort
</Button>
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>
return (
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
Connect
</Button>
)
}
private onClickConnect = () => {
this.connect()
}
private onClickAbort = () => {
this.disconnect()
const connectionId = String(sha1(String(Math.random())).slice(0, 8))
const options = this.optionsFromState()
this.props.actions.connect(options, connectionId)
}
}
export default withStyles(Connection.styles, { withTheme: true })(Connection)
const mapStateToProps = (state: AppState) => {
return {
visible: !state.tooBigReducer.connected,
connected: state.tooBigReducer.connected,
connecting: state.tooBigReducer.connecting,
error: state.tooBigReducer.error,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(Connection.styles, { withTheme: true })(Connection))
@@ -1,28 +1,18 @@
import * as React from 'react'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import { red, green } from '@material-ui/core/colors'
import { withStyles, Theme } from '@material-ui/core/styles'
enum MessageType {
success = 'success',
error = 'error',
}
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
type: MessageType
message?: string
onClose: () => void
classes: any
}
interface State {
snackBarOpen: boolean
}
class Notification extends React.Component<Props, State> {
class Notification extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
this.state = { snackBarOpen: false }
}
public static styles = (theme: Theme) => ({
@@ -37,20 +27,24 @@ class Notification extends React.Component<Props, State> {
})
public render() {
return <Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open = {this.state.snackBarOpen}
autoHideDuration = {6000}
onClose = {() => { this.setState({ snackBarOpen: false }) }}
>
<SnackbarContent
className = {this.props.classes[this.props.type]}
message = {this.props.message}
/>
</Snackbar>
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>
)
}
}
+2 -2
View File
@@ -32,8 +32,8 @@ class Copy extends React.Component<Props, State> {
public render() {
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" style={ { cursor: 'pointer' } } onClick={this.handleClick} />
: <Check fontSize="inherit" style={ { cursor: 'default' } } />
? <FileCopy fontSize="inherit" style={{ cursor: 'pointer' }} onClick={this.handleClick} />
: <Check fontSize="inherit" style={{ cursor: 'default' }} />
return <span>
<span style={{ fontSize: '16px' }}>{icon}</span>
+14
View File
@@ -0,0 +1,14 @@
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
+90 -60
View File
@@ -1,23 +1,23 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import Drawer from '@material-ui/core/Drawer'
import IconButton from '@material-ui/core/IconButton'
import Paper from '@material-ui/core/Paper'
import Divider from '@material-ui/core/Divider'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import { AppState, NodeOrder } 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 { Typography, InputBase, Input, InputLabel } from '@material-ui/core'
import { withStyles, StyleRulesCallback } from '@material-ui/core/styles'
import { settingsActions } from '../actions'
import { AppState } from '../reducers'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions } from '../actions'
const styles: StyleRulesCallback = theme => ({
drawer: {
@@ -40,16 +40,14 @@ const styles: StyleRulesCallback = theme => ({
interface Props {
actions?: any
autoExpandLimit: number,
visible: boolean,
store?: any,
autoExpandLimit: number
visible: boolean
store?: any
classes: any
nodeOrder: NodeOrder
}
interface State {
}
class Settings extends React.Component<Props, State> {
class Settings extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
@@ -57,60 +55,92 @@ class Settings extends React.Component<Props, 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"
onClick = {(e: React.MouseEvent) => e.stopPropagation()}
onKeyDown = {(e: React.KeyboardEvent) => e.stopPropagation()}
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 />
<Typography className={classes.title} variant="h6" color="inherit">
<IconButton onClick={actions.toggleSettingsVisibility}>
<ChevronRight />
</IconButton>
Settings
</Typography>
<Divider />
{this.renderAutoExpand()}
</div>
</Drawer>
{this.renderAutoExpand()}
{this.renderNodeOrder()}
</div>
</Drawer>
)
}
private renderAutoExpand() {
const { classes, actions, autoExpandLimit } = this.props
const { classes, autoExpandLimit } = this.props
return (
<div style={{ padding: '8px' }}>
<InputLabel htmlFor="auto-expand">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}
>
<MenuItem value={0}><em>Collapsed</em></MenuItem>
<MenuItem value={2}>Few</MenuItem>
<MenuItem value={3}>Some</MenuItem>
<MenuItem value={10}>Most</MenuItem>
<MenuItem value={1E6}>All</MenuItem>
</Select>
</div>
)
}
return <div style={{ padding: '8px' }}>
<InputLabel htmlFor="auto-expand">Auto Expand</InputLabel>
private onChangeAutoExpand = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setAutoExpandLimit(e.target.value)
}
private renderNodeOrder() {
const { classes, nodeOrder } = this.props
return (
<div style={{ padding: '8px' }}>
<InputLabel htmlFor="auto-expand">Topic order</InputLabel>
<Select
value = { autoExpandLimit }
onChange = { (e: React.ChangeEvent<HTMLSelectElement>) => actions.setAutoExpandLimit(e.target.value) }
input = {<Input name="auto-expand" id="auto-expand-label-placeholder" />}
displayEmpty
name = "auto-expand"
className = {classes.input}
value={nodeOrder}
onChange={this.onChangeSorting}
input={<Input name="node-order" id="node-order-label-placeholder" />}
displayEmpty={true}
name="node-order"
className={classes.input}
>
<MenuItem value = {0}><em>Disabled</em></MenuItem>
<MenuItem value = {2}>Few</MenuItem>
<MenuItem value = {3}>Some</MenuItem>
<MenuItem value = {10}>Most</MenuItem>
<MenuItem value = {1E6}>All</MenuItem>
<MenuItem value={NodeOrder.none}><em>default</em></MenuItem>
<MenuItem value={NodeOrder.abc}>a-z</MenuItem>
<MenuItem value={NodeOrder.messages}>{NodeOrder.messages}</MenuItem>
<MenuItem value={NodeOrder.topics}>{NodeOrder.topics}</MenuItem>
</Select>
</div>
</div>)
}
private onChangeSorting = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.setNodeOrder(e.target.value)
}
}
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
visible: state.settings.visible,
autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
nodeOrder: state.tooBigReducer.settings.nodeOrder,
visible: state.tooBigReducer.settings.visible,
}
}
+98
View File
@@ -0,0 +1,98 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
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
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)
@@ -0,0 +1,84 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Theme, withTheme } from '@material-ui/core/styles'
import BarChart from '@material-ui/icons/BarChart'
import DateFormatter from '../DateFormatter'
import History from './History'
import PlotHistory from './PlotHistory'
const throttle = require('lodash.throttle')
interface Props {
node?: q.TreeNode
theme: Theme
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 componentWillMount() {
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 <PlotHistory messages={numericMessages} />
}
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray()[index]
this.props.onSelect(message)
}
}
export default withTheme()(MessageHistory)
+8 -6
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
// import Drawer from '@material-ui/core/Drawer'
import { Typography } from '@material-ui/core'
interface Props {
@@ -15,11 +15,13 @@ class NodeStats extends React.Component<Props, {}> {
public render() {
const { node } = this.props
return <div>
<Typography>Messages: #{node.messages}</Typography>
<Typography>Subtopics: {node.leafCount()}</Typography>
<Typography>Messages Subtopics: #{node.leafMessageCount()}</Typography>
</div>
return (
<div>
<Typography>Messages: #{node.messages}</Typography>
<Typography>Subtopics: {node.leafCount()}</Typography>
<Typography>Messages Subtopics: #{node.leafMessageCount()}</Typography>
</div>
)
}
}
@@ -0,0 +1,74 @@
const { XYPlot, XAxis, LineMarkSeries, Hint, YAxis, HorizontalGridLines, LineSeries } = require('react-vis')
import { default as ReactResizeDetector } from 'react-resize-detector'
import DateFormatter from '../DateFormatter'
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
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 withStyles({}, { withTheme: true })(PlotHistory)
@@ -0,0 +1,7 @@
interface Message {
topic: string
payload?: string
sent: Date
}
export default Message
@@ -0,0 +1,313 @@
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,
IconButton,
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'
interface Props {
node?: q.TreeNode
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={<IconButton style={{ padding: '4px' }} onClick={this.clearTopic}><Clear style={{ fontSize: '14px' }} /></IconButton>}
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)
+160 -38
View File
@@ -1,32 +1,60 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
import ExpandMore from '@material-ui/icons/ExpandMore'
import {
Button,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Fade,
Fab,
Paper,
Popper,
Typography,
IconButton,
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 ValueRenderer from './ValueRenderer'
import DateFormatter from '../DateFormatter'
import ExpandMore from '@material-ui/icons/ExpandMore'
import Clear from '@material-ui/icons/Clear'
import MessageHistory from './MessageHistory'
import NodeStats from './NodeStats'
import Publish from './Publish/Publish'
import Topic from './Topic'
import ValueRenderer from './ValueRenderer'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
const throttle = require('lodash.throttle')
interface Props {
node?: q.TreeNode,
actions: typeof sidebarActons,
classes: any,
theme: Theme,
connectionId?: string,
}
interface State {
node: q.TreeNode
node: q.TreeNode,
compareMessage?: q.Message
}
class Sidebar extends React.Component<Props, State> {
private updateNode = () => {
private valueRef: any = React.createRef<HTMLDivElement>()
private updateNode = throttle(() => {
this.setState(this.state)
}
}, 300)
constructor(props: any) {
super(props)
console.warn('Find and fix me #state')
console.error('Find and fix me #state')
this.state = { node: new q.Tree() }
}
@@ -50,6 +78,10 @@ class Sidebar extends React.Component<Props, State> {
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 })
}
}
private registerUpdateListener(node: q.TreeNode) {
@@ -63,44 +95,134 @@ class Sidebar extends React.Component<Props, State> {
}
public render() {
return <div className={this.props.classes.drawer}>
{this.renderNode()}
</div>
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}>
<ValueRenderer message={this.props.node && this.props.node.message} />
</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}>
<Publish node={this.props.node} connectionId={this.props.connectionId} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
{this.renderNodeStats()}
</ExpansionPanel>
</div>
)
}
return <div>
<ExpansionPanel key="topic" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Topic {copyTopic}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<ValueRenderer node={this.state.node} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<NodeStats node={this.state.node} />
</ExpansionPanelDetails>
</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: '-2px', 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>
)
}
}
export default withStyles(Sidebar.styles, { withTheme: true })(Sidebar)
const mapStateToProps = (state: AppState) => {
return {
node: state.tooBigReducer.selectedTopic,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(sidebarActons, dispatch),
}
}
export default withStyles(Sidebar.styles, { withTheme: true })(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
+16 -4
View File
@@ -2,12 +2,16 @@ 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'
interface Props {
classes: any
theme: Theme
node?: q.TreeNode
selected?: q.TreeNode
actions: typeof treeActions
didSelectNode: (node: q.TreeNode) => void
}
@@ -31,15 +35,17 @@ class Topic extends React.Component<Props, {}> {
.map(node => node.sourceEdge)
.filter(edge => Boolean(edge))
.map(edge =>
[<Button
onClick={() => this.setState({ node: edge!.target })}
[(
<Button
onClick={() => this.props.actions.selectTopic(edge!.target)}
size="small"
color="secondary"
className={this.props.classes.button}
key={edge!.hash()}
>
{edge!.name}
</Button>],
</Button>
)],
)
if (breadCrumps.length === 0) {
@@ -54,4 +60,10 @@ class Topic extends React.Component<Props, {}> {
}
}
export default withStyles(styles, { withTheme: true })(Topic)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(treeActions, dispatch),
}
}
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic))
+29 -37
View File
@@ -1,71 +1,63 @@
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'
import { withTheme, Theme } from '@material-ui/core/styles'
interface Props {
node?: q.TreeNode | undefined
message?: q.Message
theme: Theme
}
interface State {
node?: q.TreeNode | undefined
}
class ValueRenderer extends React.Component<Props, State> {
private updateNode: () => void
constructor(props: any) {
super(props)
this.state = {}
this.updateNode = () => {
this.setState(this.state)
}
}
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode)
}
class ValueRenderer extends React.Component<Props, {}> {
public render() {
const node = this.props.node
if (!node || !node.message) {
return <div>{this.renderValue()}</div>
}
public renderValue() {
const { message } = this.props
if (!message) {
return null
}
let json
try {
json = JSON.parse(node.message.value)
json = JSON.parse(message.value)
} catch (error) {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
}
if (typeof json === 'string') {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'number') {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'boolean') {
return this.renderRawValue(node.message.value)
return this.renderRawValue(message.value)
} else {
const theme = this.props.theme.palette.type === 'dark' ? 'monokai' : 'bright:inverted'
return <ReactJson
style={{ width: '100%' }}
src={json}
theme={theme}
onEdit={(val) => {
console.log(val)
}} />
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: 'scroll',
overflow: 'auto hidden',
display: 'block',
lineHeight: '1.2em',
padding: '12px 5px 12px 5px',
maxWidth: '40vw',
color: this.props.theme.palette.text.primary,
}
return <pre style={style}><code>{value}</code></pre>
+37 -26
View File
@@ -1,15 +1,16 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as q from '../../../backend/src/Model'
import { AppBar, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import Search from '@material-ui/icons/Search'
import Menu from '@material-ui/icons/Menu'
import { withStyles, StyleRulesCallback } from '@material-ui/core/styles'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { settingsActions } from '../actions'
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'
const styles: StyleRulesCallback = theme => ({
title: {
@@ -65,7 +66,10 @@ const styles: StyleRulesCallback = theme => ({
interface Props {
classes: any
actions: any
actions: {
settings: typeof settingsActions,
connection: typeof connectionActions,
}
}
interface State {
@@ -85,40 +89,47 @@ class TitleBar extends React.Component<Props, State> {
}
public render() {
const { classes } = this.props
const { actions, classes } = this.props
return <AppBar position="static">
return (
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={this.props.actions.toggleSettingsVisibility}>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={actions.settings.toggleSettingsVisibility}>
<Menu />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Xplorer</Typography>
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
<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 } = this.props
return <div className={classes.search}>
<div className={classes.searchIcon}>
<Search />
return (
<div className={classes.search}>
<div className={classes.searchIcon}>
<Search />
</div>
<InputBase
placeholder="Search…"
classes={{ root: classes.inputRoot, input: classes.inputInput }}
/>
</div>
<InputBase
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
/>
</div>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(settingsActions, dispatch),
actions: {
settings: bindActionCreators(settingsActions, dispatch),
connection: bindActionCreators(connectionActions, dispatch),
},
}
}
export default withStyles(styles)(connect(null, mapDispatchToProps)(TitleBar))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(TitleBar))
+55 -42
View File
@@ -1,22 +1,24 @@
import * as React from 'react'
import { connect } from 'react-redux'
import * as q from '../../../../backend/src/Model'
import { Typography } from '@material-ui/core'
import TreeNode from './TreeNode'
import { makeConnectionMessageEvent, rendererEvents } from '../../../../events'
import { makeConnectionMessageEvent, rendererEvents, MqttMessage } from '../../../../events'
import { AppState } from '../../reducers'
import TreeNode from './TreeNode'
import { connect } from 'react-redux'
const MovingAverage = require('moving-average')
declare const performance: any
const timeInterval = 10 * 1000
const average = MovingAverage(timeInterval)
declare var window: any
interface Props {
autoExpandLimit: number
didSelectNode?: (node: q.TreeNode) => void
connectionId?: string
connected: boolean
}
interface TreeState {
@@ -31,8 +33,7 @@ class Tree extends React.Component<Props, TreeState> {
constructor(props: any) {
super(props)
const tree = new q.Tree()
this.state = { tree, msg: {} }
this.state = { tree: new q.Tree(), msg: {} }
}
public time(): number {
@@ -42,38 +43,43 @@ class Tree extends React.Component<Props, TreeState> {
return time
}
public throttledStateUpdate(state: any) {
private performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
public throttledTreeUpdate() {
if (this.updateTimer) {
return
}
const expectedRenderTime = average.forecast()
const updateInterval = Math.max(expectedRenderTime * 5, 300)
const updateInterval = Math.max(expectedRenderTime * 7, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
this.updateTimer = setTimeout(() => {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(state)
window.requestIdleCallback(() => {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(this.state)
}, { timeout: 500 })
}, Math.max(0, timeUntilNextUpdate))
}
public componentWillReceiveProps(nextProps: Props) {
if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId)
rendererEvents.unsubscribeAll(event)
}
if (nextProps.connectionId) {
const event = makeConnectionMessageEvent(nextProps.connectionId)
rendererEvents.subscribe(event, this.handleNewData)
}
this.registerAndUnregisterEventSubscriptionsForNewProps(nextProps)
}
public componentDidMount() {
if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId)
rendererEvents.subscribe(event, this.handleNewData)
private registerAndUnregisterEventSubscriptionsForNewProps(nextProps: Props) {
if (this.props.connectionId !== nextProps.connectionId) {
if (this.props.connectionId) {
this.setState({ tree: new q.Tree() })
rendererEvents.unsubscribeAll(makeConnectionMessageEvent(this.props.connectionId))
this.updateTimer && clearTimeout(this.updateTimer)
}
if (nextProps.connectionId) {
rendererEvents.subscribe(makeConnectionMessageEvent(nextProps.connectionId), this.handleNewData)
}
}
}
@@ -84,41 +90,48 @@ class Tree extends React.Component<Props, TreeState> {
}
}
private handleNewData = (msg: any) => {
private handleNewData = (msg: MqttMessage) => {
const edges = msg.topic.split('/')
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString())
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, msg.payload)
node.mqttMessage = msg
this.state.tree.updateWithNode(node.firstNode())
this.throttledStateUpdate({ msg, tree: this.state.tree })
this.throttledTreeUpdate()
}
public render() {
if (!this.props.connected) {
return null
}
const style: React.CSSProperties = {
lineHeight: '1.1',
cursor: 'default',
}
return <Typography style={ style }>
return (
<div style={style}>
<TreeNode
animateChages = { true }
autoExpandLimit = { this.props.autoExpandLimit }
isRoot = { true }
didSelectNode = { this.props.didSelectNode }
treeNode = { this.state.tree }
name = "/"
collapsed = { false }
animateChages={true}
autoExpandLimit={this.props.autoExpandLimit}
isRoot={true}
didSelectNode={this.props.didSelectNode}
treeNode={this.state.tree}
name="/"
collapsed={false}
key="rootNode"
performanceCallback={(ms: number) => {
average.push(Date.now(), ms)
}}
lastUpdate={this.state.tree.lastUpdate}
performanceCallback={this.performanceCallback}
/>
</Typography>
</div>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
connected: state.tooBigReducer.connected,
}
}
+97 -54
View File
@@ -1,10 +1,15 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { withStyles, Theme } from '@material-ui/core/styles'
import { isElementInViewport } from '../helper/isElementInViewport'
import TreeNodeTitle from './TreeNodeTitle'
import { Theme, withStyles } from '@material-ui/core/styles'
import LabelImportant from '@material-ui/icons/LabelImportant'
import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { isElementInViewport } from '../helper/isElementInViewport'
import { treeActions } from '../../actions'
declare var performance: any
@@ -25,10 +30,18 @@ const styles = (theme: Theme) => {
backgroundColor: 'rgba(80, 80, 80, 0.35)',
},
},
topicSelect: {
float: 'right' as 'right',
opacity: 0,
cursor: 'pointer',
marginTop: '-1px',
},
}
}
interface Props {
actions: any
lastUpdate: number
animateChages: boolean
isRoot?: boolean
treeNode: q.TreeNode
@@ -38,6 +51,7 @@ interface Props {
didSelectNode?: (node: q.TreeNode) => void
classes: any
autoExpandLimit: number
style?: React.CSSProperties
}
interface State {
@@ -48,11 +62,13 @@ class TreeNode extends React.Component<Props, State> {
private dirtySubnodes: boolean = true
private dirtyEdges: boolean = true
private dirtyMessage: boolean = true
private animationDirty: boolean = false
private cssAnimationWasSetAt?: number
private willUpdateTime: number = performance.now()
private titleRef = React.createRef<HTMLDivElement>()
private topicSelectRef = React.createRef<HTMLDivElement>()
private subnodesDidchange = () => {
this.dirtySubnodes = true
@@ -103,6 +119,7 @@ class TreeNode extends React.Component<Props, State> {
|| this.dirtyEdges
|| this.dirtyMessage
|| this.dirtySubnodes
|| this.animationDirty
|| shouldRenderToRemoveCssAnimation
}
@@ -132,63 +149,89 @@ class TreeNode extends React.Component<Props, State> {
}
public render() {
const animationStyle = this.indicatingChangeAnimationStyle()
const { classes } = this.props
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
return <div
key={this.props.treeNode.hash()}
className={`${classes.node} ${!this.props.isRoot ? classes.hover : ''}`}
onClick={(event) => {
event.stopPropagation()
this.toggle()
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}}
>
<span ref={this.titleRef} style={animationStyle}>
<TreeNodeTitle
onClick={() => this.toggle()}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
didSelectNode={this.props.didSelectNode}
toggleCollapsed={() => this.toggle()}
/>
</span>
{ this.renderNodes() }
</div>
const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot
const animation = shouldStartAnimation ? { animation: 'example 0.5s' } : {}
this.animationDirty = shouldStartAnimation
return (
<div
key={this.props.treeNode.hash()}
className={`${classes.node} ${!this.props.isRoot ? classes.hover : ''}`}
onClick={this.didClickNode}
style={this.props.style}
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
>
<span ref={this.titleRef} style={animation}>
<TreeNodeTitle
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
lastUpdate={this.props.treeNode.lastUpdate}
/>
</span>
<div
className={this.props.classes.topicSelect}
ref={this.topicSelectRef}
onClick={this.didSelectNode}
title="Select topic"
>
<LabelImportant style={{ fontSize: '14px' }} />
</div>
{this.renderNodes()}
</div>
)
}
private mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
if (this.topicSelectRef.current) {
this.topicSelectRef.current.style.opacity = '1'
}
}
private mouseOut = (event: React.MouseEvent) => {
event.stopPropagation()
if (this.topicSelectRef.current) {
this.topicSelectRef.current.style.opacity = '0'
}
}
private didSelectNode = (event: React.MouseEvent) => {
event.stopPropagation()
if (this.topicSelectRef.current) {
this.topicSelectRef.current.style.opacity = '1'
}
this.props.actions.selectTopic(this.props.treeNode)
}
private didClickNode = (event: React.MouseEvent) => {
event.stopPropagation()
this.toggle()
this.props.actions.selectTopic(this.props.treeNode)
}
private renderNodes() {
return <TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
autoExpandLimit={this.props.autoExpandLimit}
didSelectNode={this.props.didSelectNode}
toggleCollapsed={() => this.toggle()}
treeNode={this.props.treeNode}
/>
}
private indicatingChangeAnimationStyle(): React.CSSProperties {
if (this.props.isRoot) {
return {}
}
if (this.cssAnimationWasSetAt && (performance.now() - this.cssAnimationWasSetAt) > 500) {
this.cssAnimationWasSetAt = undefined
return {}
}
const isInViewPort = this.titleRef.current && isElementInViewport(this.titleRef.current)
const isDirty = this.dirtyMessage || this.dirtyEdges
if (this.props.animateChages && isDirty && isInViewPort) {
if (!this.cssAnimationWasSetAt) {
this.cssAnimationWasSetAt = performance.now()
}
return { animation: 'example 0.5s' }
}
return {}
return (
<TreeNodeSubnodes
animateChanges={this.props.animateChages}
collapsed={this.collapsed()}
autoExpandLimit={this.props.autoExpandLimit}
didSelectNode={this.props.didSelectNode}
treeNode={this.props.treeNode}
lastUpdate={this.props.treeNode.lastUpdate}
/>
)
}
}
export default withStyles(styles)(TreeNode)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(treeActions, dispatch),
}
}
export default withStyles(styles)(connect(null, mapDispatchToProps)(TreeNode))
+53 -26
View File
@@ -1,52 +1,79 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { withTheme, Theme } from '@material-ui/core/styles'
import { AppState, NodeOrder } from '../../reducers'
import { Theme, withTheme } from '@material-ui/core/styles'
import TreeNode from './TreeNode'
import { connect } from 'react-redux'
export interface Props {
lastUpdate: number
nodeOrder?: NodeOrder
animateChanges: boolean
treeNode: q.TreeNode
autoExpandLimit: number
collapsed?: boolean | undefined
didSelectNode?: (node: q.TreeNode) => void
toggleCollapsed: () => void
theme: Theme
}
class TreeNodeSubnodes extends React.Component<Props, {}> {
private sortedNodes(): q.TreeNode[] {
const { nodeOrder, treeNode } = this.props
let edges = Object.values(treeNode.edges)
if (nodeOrder === NodeOrder.abc) {
edges = edges.sort((a, b) => a.name.localeCompare(b.name))
}
let nodes = edges.map(edge => edge.target)
if (nodeOrder === NodeOrder.messages) {
nodes = nodes.sort((a, b) => b.leafMessageCount() - a.leafMessageCount())
}
if (nodeOrder === NodeOrder.topics) {
nodes = nodes.sort((a, b) => b.leafCount() - a.leafCount())
}
return nodes
}
public render() {
const edges = Object.values(this.props.treeNode.edges)
if (edges.length === 0 || this.props.collapsed) {
return null
}
const listItemStyle = {
padding: '3px 8px 0px 8px',
padding: '3px 0px 0px 8px',
}
if (edges.length > 0 && !this.props.collapsed) {
const listItems = edges
.map(edge => edge.target)
.map(node => (
<div
key={node.hash()}
const nodes = this.sortedNodes()
const listItems = nodes.map(node => (
<div key={node.hash()}>
<TreeNode
animateChages={this.props.animateChanges}
treeNode={node}
didSelectNode={this.props.didSelectNode}
autoExpandLimit={this.props.autoExpandLimit}
lastUpdate={node.lastUpdate}
style={listItemStyle}
>
<TreeNode
animateChages={this.props.animateChanges}
treeNode={node}
didSelectNode={this.props.didSelectNode}
autoExpandLimit={this.props.autoExpandLimit}
/>
</div>
))
/>
</div>
))
return <span
style={{ display: 'block', clear: 'both' }}
>
{this.props.collapsed ? null : listItems}
return (
<span style={{ display: 'block', clear: 'both' }} >
{listItems}
</span>
}
return null
)
}
}
export default withTheme()(TreeNodeSubnodes)
const mapStateToProps = (state: AppState) => {
return {
nodeOrder: state.tooBigReducer.settings.nodeOrder,
}
}
export default withTheme()(connect(mapStateToProps)(TreeNodeSubnodes))
+29 -27
View File
@@ -1,14 +1,17 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { treeActions } from '../../actions'
import * as q from '../../../../backend/src/Model'
import { withTheme, Theme } from '@material-ui/core/styles'
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode
actions: any
name?: string | undefined
collapsed?: boolean | undefined
toggleCollapsed: () => void
didSelectNode?: (node: q.TreeNode) => void
theme: Theme
lastUpdate: number
}
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
@@ -24,25 +27,23 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
}
}
private didSelectNode = (event: React.MouseEvent) => {
event.stopPropagation()
if (this.props.treeNode.message) {
this.props.actions.selectTopic(this.props.treeNode)
}
}
public render() {
const style: React.CSSProperties = {
lineHeight: '1em',
whiteSpace: 'nowrap',
}
return <span
style={style}
onClick={() => {
this.toggle()
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}}
onMouseOver={() => {
if (this.props.treeNode.message) {
this.props.didSelectNode && this.props.didSelectNode(this.props.treeNode)
}
}}
>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
return (
<span style={style} onMouseOver={this.didSelectNode}>
{this.renderExpander()} {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
)
}
private renderSourceEdge() {
@@ -58,23 +59,18 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
private renderValue() {
const style: React.CSSProperties = {
width: '15em',
maxWidth: '15em',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '0',
paddingLeft: '5px',
marginLeft: '5px',
display: 'inline-block',
}
return this.props.treeNode.message
? <span
style={style}
> = {this.props.treeNode.message.value.toString()}</span>
: null
}
private toggle() {
this.props.toggleCollapsed()
return this.props.treeNode.message && this.props.treeNode.message.length > 0
? <span style={style}> = {this.props.treeNode.message.value.toString()}</span>
: null
}
private renderExpander() {
@@ -95,4 +91,10 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
}
}
export default withTheme()(TreeNodeTitle)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(treeActions, dispatch),
}
}
export default withTheme()(connect(null, mapDispatchToProps)(TreeNodeTitle))
+23 -13
View File
@@ -1,21 +1,25 @@
import './tracking'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import reduxThunk from 'redux-thunk'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers, { AppState, NodeOrder } from './reducers'
import reducers from './reducers'
import App from './App'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
declare var document: any
const initialAppState = {
settings: {
autoExpandLimit: 0,
visible: false,
},
}
const store = createStore(reducers, initialAppState)
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
reducers,
composeEnhancers(
applyMiddleware(
reduxThunk,
),
),
)
const theme = createMuiTheme({
palette: {
@@ -24,11 +28,17 @@ const theme = createMuiTheme({
typography: { useNextVariants: true },
})
const splash = document.getElementById('splash')
if (splash) {
splash.style.animation = 'unsplash 1s ease-out 0s 1 normal forwards'
setTimeout(() => splash.remove(), 1100)
}
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<Provider store={store}>
<App name="" />
</Provider>
</MuiThemeProvider>,
document.getElementById('example'),
document.getElementById('app'),
)
+93
View File
@@ -0,0 +1,93 @@
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,
}
}
+139 -10
View File
@@ -1,50 +1,179 @@
import { Reducer, Action } from 'redux'
import * as q from '../../../backend/src/Model'
import { Action, Reducer, combineReducers } from 'redux'
import { trackEvent } from '../tracking'
import { MqttOptions } from '../../../backend/src/DataSource'
import { PublishState, publishReducer } from './Publish'
export enum ActionTypes {
disconnect = 'DISCONNECT',
showError = 'SHOW_ERROR',
setAutoExpandLimit = 'SET_AUTO_EXPAND_LIMIT',
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
setNodeOrder = 'SET_NODE_ORDER',
selectTopic = 'SELECT_TOPIC',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS',
connecting = 'CONNECTING',
connected = 'CONNECTED',
}
interface SettingsAction extends Action {
export interface CustomAction extends Action {
type: ActionTypes,
autoExpandLimit: number
autoExpandLimit?: number
nodeOrder?: NodeOrder
selectedTopic?: q.TreeNode
showUpdateNotification?: boolean
showUpdateDetails?: boolean
connectionOptions?: MqttOptions
connectionId?: string
error?: string
}
export interface AppState {
settings: SettingsModel
tooBigReducer: TooBigOfState
publish: PublishState
}
export interface SettingsModel {
export interface TooBigOfState {
settings: SettingsState,
selectedTopic?: q.TreeNode
showUpdateNotification?: boolean
showUpdateDetails: boolean
connecting: boolean
connected: boolean
error?: string
connectionId?: string
}
export interface SettingsState {
autoExpandLimit: number
visible: boolean
nodeOrder: NodeOrder
}
const reducer: Reducer<AppState | undefined, SettingsAction> = (state, action) => {
export enum NodeOrder {
none = 'none',
messages = '#messages',
abc = 'abc',
topics = '#topics',
}
const initialBigState: TooBigOfState = {
settings: {
autoExpandLimit: 0,
nodeOrder: NodeOrder.none,
visible: false,
},
selectedTopic: undefined,
showUpdateDetails: false,
connected: false,
connecting: false,
error: undefined,
}
const tooBigReducer: Reducer<TooBigOfState | undefined, CustomAction> = (state = initialBigState, action) => {
if (!state) {
throw Error('No initial state')
}
console.log(action)
trackEvent(action.type)
console.log(action, state)
switch (action.type) {
case ActionTypes.setAutoExpandLimit:
if (action.autoExpandLimit === undefined) {
return state
}
return {
...state,
settings: {
visible: state.settings.visible,
...state.settings,
autoExpandLimit: action.autoExpandLimit,
},
}
case ActionTypes.toggleSettingsVisibility:
return {
...state,
settings: {
...state.settings,
visible: !state.settings.visible,
autoExpandLimit: state.settings.autoExpandLimit,
},
}
case ActionTypes.selectTopic:
if (!action.selectedTopic) {
return state
}
return {
...state,
selectedTopic: action.selectedTopic,
}
case ActionTypes.setNodeOrder:
if (!action.nodeOrder) {
return state
}
return {
...state,
settings: { ...state.settings, nodeOrder: action.nodeOrder },
}
case ActionTypes.showUpdateNotification:
return {
...state,
showUpdateNotification: action.showUpdateNotification,
}
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
}
return {
...state,
showUpdateDetails: action.showUpdateDetails,
}
case ActionTypes.connecting:
if (!action.connectionId) {
return state
}
return {
...state,
connected: false,
connecting: true,
connectionId: action.connectionId,
}
case ActionTypes.disconnect:
return {
...state,
connected: false,
connecting: false,
connectionId: undefined,
}
case ActionTypes.connected:
return {
...state,
connected: true,
connecting: false,
}
case ActionTypes.showError:
return {
...state,
error: action.error,
}
default:
return state
}
}
const reducer = combineReducers({
tooBigReducer,
publish: publishReducer,
})
export default reducer
+9
View File
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,24 @@
import { rendererEvents } from '../../events'
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)
}
const Nucleus = require('electron-nucleus')('5c3b3e0443b7cc00eec3782b', {
userId,
disableInDev: true,
})
export default Nucleus
export function trackEvent(name: string) {
if (name.match(/^@@redux/)) {
return
}
Nucleus.track(name)
electronRendererTelementry.trackEvent(name)
}
+22 -6
View File
@@ -1,15 +1,25 @@
var LiveReloadPlugin = require('webpack-livereload-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
entry: "./src/index.tsx",
entry: {
app: "./src/index.tsx",
bugtracking: "./src/bugtracking.ts",
},
output: {
filename: "bundle.js",
filename: "[name].bundle.js",
path: __dirname + "/build"
},
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
splitChunks: {
cacheGroups: {
vendors: {
filename: '[name].bundle.js'
}
}
}
},
target: 'electron-renderer',
@@ -30,12 +40,18 @@ module.exports = {
{ 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" }
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" },
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
]
},
plugins: [
new LiveReloadPlugin({})
new LiveReloadPlugin({}),
// new BundleAnalyzerPlugin(),
],
// When importing a module whose path matches one of the following, just
+2
View File
@@ -1,4 +1,5 @@
import { DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
type MessageCallback = (topic: string, payload: Buffer) => void
@@ -7,6 +8,7 @@ interface DataSource<DataSourceOptions> {
connect(options: DataSourceOptions): DataSourceStateMachine
disconnect(): void
onMessage(messageCallback: MessageCallback): void
publish(msg: MqttMessage): void
topicSeparator: string
stateMachine: DataSourceStateMachine
}
+2 -2
View File
@@ -3,7 +3,7 @@ import { EventDispatcher } from '../../../events'
export interface DataSourceState {
connecting: boolean
connected: boolean
error?: Error
error?: string
}
export class DataSourceStateMachine {
@@ -25,7 +25,7 @@ export class DataSourceStateMachine {
public setError(error: Error) {
this.state = {
error,
error: error.message,
connected: false,
connecting: false,
}
+21 -6
View File
@@ -1,6 +1,8 @@
import * as Url from 'url'
import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource, DataSourceStateMachine } from './'
import * as Url from 'url'
import { MqttMessage } from '../../../events'
export interface MqttOptions {
url: string
@@ -8,16 +10,17 @@ export interface MqttOptions {
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) => void
private messageCallback?: (topic: string, message: Buffer, packet: any) => void
private rootSubscription = '#'
public topicSeparator = '/'
public onMessage(messageCallback: (topic: string, message: Buffer) => void) {
public onMessage(messageCallback: (topic: string, message: Buffer, packet: any) => void) {
this.messageCallback = messageCallback
}
@@ -35,7 +38,10 @@ export class MqttSource implements DataSource<MqttOptions> {
const client = mqttConnect(url, {
resubscribe: false,
rejectUnauthorized: !options.certValidation,
rejectUnauthorized: options.certValidation,
username: options.username,
password: options.password,
clientId: options.clientId,
})
this.client = client
@@ -60,15 +66,24 @@ export class MqttSource implements DataSource<MqttOptions> {
throw new Error('mqtt subscription failed')
}
})
client.subscribe('$SYS/#', (err: Error) => {
if (err) {
throw new Error('mqtt subscription failed')
}
})
})
client.on('message', (topic, message) => {
this.messageCallback && this.messageCallback(topic, message)
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()
}
+2
View File
@@ -1,3 +1,5 @@
export interface Message {
value?: any | undefined
length: number
received: Date
}
+60
View File
@@ -0,0 +1,60 @@
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) {
this.capacity = capacity
this.maxItems = maxItems
}
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
}
}
+17 -6
View File
@@ -1,13 +1,15 @@
import { Edge, Message } from './'
import { EventDispatcher } from '../../../events'
import { Edge, Message, RingBuffer } from './'
import { EventDispatcher, MqttMessage } from '../../../events'
export class TreeNode {
public sourceEdge?: Edge
public message?: Message
public mqttMessage?: MqttMessage
public messageHistory: RingBuffer<Message> = new RingBuffer<Message>(3000, 100)
public edges: {[s: string]: Edge} = {}
public collapsed = false
public messages: number = 0
public lastUpdate: number = Date.now()
public onMerge = new EventDispatcher<void, TreeNode>(this)
public onEdgesChange = new EventDispatcher<void, TreeNode>(this)
public onMessage = new EventDispatcher<Message, TreeNode>(this)
@@ -20,15 +22,23 @@ export class TreeNode {
sourceEdge.target = this
}
this.setMessage(message)
message && this.setMessage(message)
this.onMerge.subscribe(() => {
this.cachedLeafes = undefined
this.cachedLeafMessageCount = undefined
this.lastUpdate = Date.now()
})
this.onEdgesChange.subscribe(() => {
this.lastUpdate = Date.now()
})
this.onMessage.subscribe(() => {
this.lastUpdate = Date.now()
})
}
public setMessage(value: any) {
this.message = value
public setMessage(message: Message) {
this.messageHistory.add(message)
this.message = message
this.messages += 1
}
@@ -73,6 +83,7 @@ export class TreeNode {
if (node.message) {
this.setMessage(node.message)
this.onMessage.dispatch(node.message)
this.mqttMessage = node.mqttMessage
}
this.mergeEdges(node)
+11 -3
View File
@@ -1,7 +1,11 @@
import { Edge, Message, Tree, TreeNode } from './'
import { Edge, Tree, TreeNode } from './'
interface HasLength {
length: number
}
export abstract class TreeNodeFactory {
public static fromEdgesAndValue(edgeNames: string[], value: any): TreeNode {
public static fromEdgesAndValue<T extends HasLength>(edgeNames: string[], value?: T): TreeNode {
let currentNode: TreeNode = new Tree()
for (const edgeName of edgeNames) {
const edge = new Edge(edgeName)
@@ -11,7 +15,11 @@ export abstract class TreeNodeFactory {
currentNode = newNode
}
currentNode.setMessage({ value })
currentNode.setMessage({
value,
length: value ? value.length : 0,
received: new Date(),
})
return currentNode
}
}
+1
View File
@@ -4,3 +4,4 @@ export { Message } from './Message'
export { TreeNodeFactory } from './TreeNodeFactory'
export { Tree } from './Tree'
export { Hashable } from './Hashable'
export * from './RingBuffer'
+7 -5
View File
@@ -1,7 +1,9 @@
import { Edge, TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai'
import 'mocha'
import { Edge, TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai'
describe('Edge', () => {
it('should contain a name', () => {
const e = new Edge('foo')
@@ -10,7 +12,7 @@ describe('Edge', () => {
it('firstEdge should retireve the first edge', () => {
const topics = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 5)
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
const bazEdge = leaf.sourceEdge
if (!bazEdge) {
@@ -35,10 +37,10 @@ describe('Edge', () => {
it('hash should include change if parents are different', () => {
const topics1 = 'foo/bar/baz'.split('/')
const bazEdge1 = TreeNodeFactory.fromEdgesAndValue(topics1, 5).sourceEdge
const bazEdge1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined).sourceEdge
const topics2 = 'foo/foo/baz'.split('/')
const bazEdge2 = TreeNodeFactory.fromEdgesAndValue(topics2, 5).sourceEdge
const bazEdge2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined).sourceEdge
if (!bazEdge1 || !bazEdge2) {
throw Error('should not happen')
+46
View File
@@ -0,0 +1,46 @@
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')
})
})
+5 -4
View File
@@ -1,15 +1,16 @@
import { Tree, TreeNodeFactory } from '../'
import { expect } from 'chai'
import 'mocha'
import './TreeNode.findNode'
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, 3)
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
tree.updateWithNode(leaf.firstNode())
const expectedNode = tree.findNode('foo/bar')
@@ -1,13 +1,13 @@
import 'mocha'
import './TreeNode.findNode'
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
import 'mocha'
import './TreeNode.findNode'
describe('TreeNode.findNode', () => {
it('findNode should retrieve node', () => {
const topics = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, 5)
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
const root = leaf.firstNode()
expect(root.sourceEdge).to.eq(undefined)
+15 -14
View File
@@ -1,52 +1,53 @@
import { TreeNodeFactory } from '../'
import './TreeNode.findNode'
import { expect } from 'chai'
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, 3)
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 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)
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 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)
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(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)
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, 3)
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined)
const topics2 = 'foo/bar/baz'.split('/')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, 5)
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined)
leaf1.firstNode().updateWithNode(leaf2.firstNode())
@@ -1,13 +1,14 @@
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
import 'mocha'
import './TreeNode.findNode'
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, 5)
const leaf = TreeNodeFactory.fromEdgesAndValue(edges, undefined)
expect(leaf.firstNode().sourceEdge).to.eq(undefined)
})
@@ -15,7 +16,7 @@ describe('TreeNodeFactory', () => {
it('should create node', () => {
const topic = 'foo/bar'
const edges = topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue(edges, 5)
const node = TreeNodeFactory.fromEdgesAndValue(edges, '5')
if (!node.sourceEdge || !node.sourceEdge.source || !node.message) {
expect.fail('should not happen')
@@ -24,7 +25,7 @@ describe('TreeNodeFactory', () => {
expect(node).to.not.eq(undefined)
expect(node.sourceEdge.name).to.eq('bar')
expect(node.message.value).to.eq(5)
expect(node.message.value).to.eq('5')
const foo = node.firstNode().findNode('foo')
expect(foo && foo.sourceEdge && foo.sourceEdge.name).to.eq('foo')
@@ -33,14 +34,14 @@ describe('TreeNodeFactory', () => {
it('node should contain edges in order', () => {
const topic = 'foo/bar/baz'
const edges = topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue(edges, 5)
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.message.value).to.eq('5')
expect(node.sourceEdge.name).to.eq('baz')
const barNode = node.sourceEdge.source
+39 -9
View File
@@ -1,11 +1,21 @@
import {
addMqttConnectionEvent, backendEvents,
makeConnectionStateEvent, removeConnection,
makeConnectionMessageEvent, AddMqttConnection,
AddMqttConnection,
EventDispatcher,
MqttMessage,
addMqttConnectionEvent,
backendEvents,
checkForUpdates,
makeConnectionMessageEvent,
makeConnectionStateEvent,
makePublishEvent,
removeConnection,
updateAvailable,
} from '../../events'
import { MqttSource, DataSource } from './DataSource'
import { DataSource, MqttSource } from './DataSource'
class ConnectionManager {
import { UpdateInfo } from 'builder-util-runtime'
export class ConnectionManager {
private connections: {[s: string]: DataSource<any>} = {}
public manageConnections() {
@@ -26,16 +36,20 @@ class ConnectionManager {
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) => {
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('base64') })
backendEvents.emit(messageEvent, { topic, payload: buffer.toString(), qos: packet.qos, retain: packet.retain })
})
}
@@ -44,7 +58,23 @@ class ConnectionManager {
connection.disconnect()
delete this.connections[hash]
}
public closeAllConnections() {
Object.keys(this.connections)
.forEach(hash => this.removeConnection(hash))
}
}
const connectionManager = new ConnectionManager()
connectionManager.manageConnections()
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()
-39
View File
@@ -1,39 +0,0 @@
import * as builder from 'electron-builder'
const linux: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: true,
arm64: true,
linux: ['snap', 'AppImage', 'deb', 'pacman'],
projectDir: './build/clean',
publish: 'onTag',
}
const win: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
win: ['portable'],
projectDir: './build/clean',
publish: 'onTag',
}
const mac: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
mac: ['dmg'],
projectDir: './build/clean',
publish: 'onTag',
}
async function buildAll() {
await builder.build(linux)
await builder.build(mac)
await builder.build(win)
}
buildAll()
-71
View File
@@ -1,71 +0,0 @@
const { autoUpdater } = require("electron-updater")
const log = require('electron-log');
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
log.info('App starting...');
// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')
try {
require('./backend/build/backend/src/index.js')
} catch (err) {
console.error(err)
}
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 700,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
mainWindow.loadFile('app/index.html')
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
app.quit()
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
createWindow()
autoUpdater.checkForUpdatesAndNotify()
})
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
+5 -3
View File
@@ -1,5 +1,6 @@
import { IpcMain, IpcRenderer, ipcMain, ipcRenderer } from 'electron'
import { Event } from './Events'
import { ipcMain, ipcRenderer, IpcRenderer, IpcMain } from 'electron'
interface EventBusInterface {
subscribe<MessageType>(event: Event<MessageType>, callback:(msg: MessageType) => void): void
@@ -27,7 +28,9 @@ class IpcMainEventBus implements EventBusInterface {
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
this.client.send(event.topic, msg)
if (!this.client.isDestroyed()) {
this.client.send(event.topic, msg)
}
}
}
@@ -46,7 +49,6 @@ class IpcRendererEventBus implements EventBusInterface {
}
public emit<MessageType>(event: Event<MessageType>, msg: MessageType) {
console.log(event.topic, msg)
this.ipc.send(event.topic, msg)
}
}
+25 -5
View File
@@ -1,4 +1,8 @@
import { MqttOptions, DataSourceState } from '../backend/src/DataSource'
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
@@ -23,12 +27,28 @@ export function makeConnectionStateEvent(connectionId: string): Event<DataSource
}
}
interface Message {
topic: string,
payload: any
export const checkForUpdates: Event<void> = {
topic: 'app/update/check',
}
export function makeConnectionMessageEvent(connectionId: string): Event<Message> {
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}`,
}
-9
View File
@@ -1,9 +0,0 @@
const { EventEmitter } = require('events');
const a = new EventEmitter()
a.on('test', () => console.log('test'))
a.on('test2', () => console.log('test2'))
a.removeAllListeners('test')
a.emit('test')
a.emit('test2')
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+388 -429
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
docker run --rm -ti \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine node_modules/.bin/ts-node package.ts $@
+32 -26
View File
@@ -1,14 +1,17 @@
{
"name": "MQ(TT)-Explorer",
"version": "0.0.1",
"name": "MQTT-Explorer",
"version": "0.1.0",
"description": "Explore your message queues",
"main": "electron.js",
"main": "dist/src/electron.js",
"scripts": {
"start": "electron .",
"install": "cd app; npm install; cd ..",
"test": "npm run test-backend",
"build": "cd app; npm run build; cd ..; cd backend; npm run build; cd ..",
"build": "tsc && cd app && npm run build && cd ..",
"test-backend": "cd backend && npm run test && cd ..",
"release": "npm run test && ./release.sh"
"prepare-release": "./prepare-release.sh",
"package": "ts-node package.ts",
"package-with-docker": "./package-with-docker.sh"
},
"repository": {
"type": "git",
@@ -16,12 +19,11 @@
},
"build": {
"appId": "mqtt-explorer",
"nodeGypRebuild": false,
"mac": {
"category": "de.t7n.apps.mq-explorer",
"files": [
"**/*",
"!**/*.ts",
"!app/node_modules"
"category": "de.t7n.apps.mqtt-explorer",
"publish": [
"github"
]
},
"linux": {
@@ -39,31 +41,35 @@
"homepage": "https://github.com",
"license": "ISC",
"devDependencies": {
"electron": "^4.0.0",
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"@types/mustache": "^0.8.32",
"@types/node": "^10.12.18",
"@types/semver": "^5.5.0",
"@types/sha1": "^1.1.1",
"@types/socket.io": "^2.1.2",
"axios": "^0.18.0",
"chai": "^4.2.0",
"electron": "^4.0.1",
"electron-builder": "^20.38.4",
"mocha": "^5.2.0",
"mustache": "^3.0.1",
"nyc": "^13.1.0",
"redux-thunk": "^2.3.0",
"source-map-support": "^0.5.9",
"ts-node": "^7.0.1",
"tslint": "^5.12.0",
"tslint-config-airbnb": "^5.11.1",
"tslint-react": "^3.6.0",
"typescript": "^3.2.2",
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.18",
"@types/sha1": "^1.1.1",
"@types/socket.io": "^2.1.2",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",
"source-map-support": "^0.5.9",
"tslint-strict-null-checks": "^1.0.1"
"tslint-strict-null-checks": "^1.0.1",
"typescript": "^3.2.2"
},
"dependencies": {
"@types/electron": "^1.6.10",
"@types/socket.io": "^2.1.2",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git",
"electron-debug": "^2.0.0",
"electron-log": "^2.2.17",
"electron-updater": "^4.0.6",
"mqtt": "^2.18.8",
"sha1": "^1.1.1",
"socket.io": "^2.2.0"
"sha1": "^1.1.1"
}
}
+76
View File
@@ -0,0 +1,76 @@
import * as builder from 'electron-builder'
const linux: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: true,
arm64: false,
linux: ['AppImage'],
projectDir: './build/clean',
publish: 'always',
}
const linuxSnap: builder.CliOptions = {
x64: true,
ia32: false,
armv7l: false,
arm64: false,
linux: ['snap'],
projectDir: './build/clean',
publish: 'always',
}
const win: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
win: ['portable', 'nsis'],
projectDir: './build/clean',
publish: 'always',
}
const mac: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
mac: ['dmg'],
projectDir: './build/clean',
publish: 'always',
}
async function executeBuild() {
console.log(process.argv[2])
switch (process.argv[2]) {
case 'win':
await builder.build(win)
break
case 'linux':
await builder.build(linux)
break
case 'snap':
try {
await builder.build(linuxSnap)
} catch {
// ignore
}
break
case 'mac':
await builder.build(mac)
break
default:
await builder.build(mac)
}
}
function build() {
try {
executeBuild()
} catch (error) {
console.error(error)
process.exit(1)
}
}
build()
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
set -e
ORIGINAL_DIR=`pwd`
DIR=build/clean
rm -rf "$DIR" || echo "Directory did not exist"
mkdir -p "$DIR"
git clone .git "$DIR"
cd $DIR
# App
cd app
npm install
cd ..
# Backend
cd backend
npm install;
#npm run test
cd ..
# Build
npm run build
rm -rf app/node_modules
cd "$ORIGINAL_DIR"
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash
DIR=build/clean
rm -rf "$DIR"
mkdir -p "$DIR"
git clone .git "$DIR"
ORIGINAL_DIR=`pwd`
cd $DIR
cd app && npm install; cd ..
cd backend && npm install; cd ..
npm run build
rm -rf app/node_modules
cd "$ORIGINAL_DIR"
node_modules/.bin/ts-node build.ts
exit 0
docker run --rm -ti \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine node_modules/.bin/ts-node build.ts
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+72
View File
@@ -0,0 +1,72 @@
import { Menu, app, BrowserWindow, webContents } from 'electron'
const applicationMenu = {
label: 'Application',
submenu: [
{
label: 'About Application',
selector: 'orderFrontStandardAboutPanel:',
},
{
type: 'separator' as 'separator',
},
{
label: 'Dev Tools',
accelerator: 'CmdOrCtrl+Alt+I',
role: 'toggleDevTools',
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => {
app.quit()
},
},
],
}
const editMenu = {
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
selector: 'undo:',
},
{
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
selector: 'redo:',
},
{
type: 'separator' as 'separator',
},
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
selector: 'cut:',
},
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
selector: 'copy:',
},
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
selector: 'paste:',
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
selector: 'selectAll:',
},
],
}
const template = [
applicationMenu,
editMenu,
]
export const menuTemplate = Menu.buildFromTemplate(template)
+105
View File
@@ -0,0 +1,105 @@
import { UpdateInfo } from '../events'
import { BrowserWindow, app, Menu } from 'electron'
import * as path from 'path'
import { menuTemplate } from './MenuTemplate'
import { autoUpdater } from 'electron-updater'
import * as log from 'electron-log'
import { ConnectionManager, updateNotifier } from '../backend/src/index'
import { electronTelemetryFactory } from 'electron-telemetry'
const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5')
const isDebugEnabled = Boolean(process.argv.find(arg => arg === 'debug'))
require('electron-debug')({ enabled: isDebugEnabled })
autoUpdater.logger = log
log.info('App starting...')
const connectionManager = new ConnectionManager()
connectionManager.manageConnections()
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow: BrowserWindow | undefined
function createWindow() {
const iconPath = path.join(__dirname, 'icon.png')
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 700,
webPreferences: {
nodeIntegration: true,
devTools: true,
},
icon: iconPath,
})
console.log('icon path', iconPath)
// and load the index.html of the app.
mainWindow.loadFile('app/index.html')
// Emitted when the window is closed.
mainWindow.on('close', () => {
connectionManager.closeAllConnections()
})
// Emitted when the window is closed.
mainWindow.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = undefined
app.quit()
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
Menu.setApplicationMenu(menuTemplate)
createWindow()
let updateInfo: UpdateInfo
autoUpdater.on('update-available', (info: UpdateInfo) => {
console.log('there is an update')
updateInfo = info
})
autoUpdater.on('error', () => {
console.log('could not update due to error')
if (updateInfo) {
updateNotifier.notify(updateInfo)
}
})
updateNotifier.onCheckUpdateRequest.subscribe(() => {
try {
autoUpdater.checkForUpdatesAndNotify()
} catch (error) {
console.error(error)
}
})
})
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
+5 -3
View File
@@ -3,9 +3,11 @@
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"outDir": "./build",
"outDir": "./dist",
"strict": true,
"lib": ["es2017"],
"sourceRoot": "src/",
"lib": ["es2017", "dom"],
"sourceMap": true
}
},
"include": ["src/electron.ts"]
}
+4 -2
View File
@@ -1,11 +1,13 @@
{
"extends": "tslint-config-airbnb",
"extends": ["tslint-config-airbnb", "tslint-react"],
"rules": {
"semicolon": [true, "never"],
"max-line-length": [true, 200],
"member-access": true,
"no-else-after-return": false,
"align": false,
"variable-name": false
"variable-name": false,
"jsx-no-lambda": false,
"indent": [true, "spaces", 2]
}
}
+64
View File
@@ -0,0 +1,64 @@
import * as mustache from 'mustache'
import { readFileSync, writeFileSync } from 'fs'
import axios from 'axios'
interface Release {
name: string
assets: Asset[]
}
interface Asset {
name: string
browser_download_url: string
}
function createUrl(title: string, path: string) {
return `[${title}](${path})`
}
function fileExtension(file: string): string {
const match = file.match(/\.([a-zA-Z]+)$/)
return (match && match[1]) || ''
}
async function createReadme(): Promise<void> {
const release: Release = (await axios.get('https://api.github.com/repos/thomasnordquist/mqtt-explorer/releases/latest')).data
const linux64 = release.assets.filter(asset => /(x86_64|amd64)\.(AppImage|deb|rpm)$/.test(asset.name))
const windowsInstaller = release.assets.find(asset => /Setup-.+\.exe$/.test(asset.name))
const windowsPortable = release.assets.find(asset => /-(?!Setup).+\.exe$/.test(asset.name))
const macDmg = release.assets.find(asset => /\.dmg$/.test(asset.name))
if (linux64.length === 0 || !windowsInstaller || !windowsPortable || !macDmg) {
console.error('failed retrieving')
process.exit(1)
return
}
const linuxTargets = linux64
.map(item => createUrl(fileExtension(item.browser_download_url), item.browser_download_url))
.join(', ')
const windowsTargets = [
createUrl('portable', windowsPortable.browser_download_url),
createUrl('installer', windowsInstaller.browser_download_url),
].join(', ')
const macTargets = [
createUrl('dmg', macDmg.browser_download_url),
].join(', ')
const template = readFileSync('./Readme.tpl.md').toString()
const rendered = mustache.render(template, {
linuxTargets,
windowsTargets,
macTargets,
version: release.name,
})
writeFileSync('./Readme.md', rendered)
}
createReadme()