Compare commits

..
46 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
48 changed files with 2886 additions and 473 deletions
+1
View File
@@ -2,6 +2,7 @@ node_modules
backend/coverage
backend/.nyc_output
build
/dist
.DS_Store
test.dot
test.png
+2 -2
View File
@@ -24,6 +24,6 @@ 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-with-docker -- linux; 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" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then npm run package-with-docker -- win; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then npm run package -- win; fi
+8 -3
View File
@@ -1,17 +1,22 @@
# 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.6
### 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.6/MQTT-Explorer-0.0.6.exe), [installer](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.6/MQTT-Explorer-Setup-0.0.6.exe)), Linux ([AppImage](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.6/MQTT-Explorer-0.0.6-x86_64.AppImage)) and Mac ([dmg](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.6/MQTT-Explorer-0.0.6.dmg)).
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)
![screen2](https://user-images.githubusercontent.com/7721625/51109225-dbe22200-17f4-11e9-8f6b-c6a27c07c90e.png)
![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
+2
View File
@@ -1,4 +1,6 @@
# 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 }}
+3 -2
View File
@@ -3,6 +3,8 @@
<head>
<meta charset="UTF-8" />
<title>MQTT-Explorer</title>
<script src="./build/bugtracking.bundle.js"></script>
<style>
body, html {
margin: 0;
@@ -30,7 +32,6 @@
::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8);
#-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
}
</style>
<style>
@@ -67,7 +68,7 @@
<script>
function onLoad() {
var script = document.createElement("script");
script.src = "./build/bundle.js"
script.src = "./build/app.bundle.js"
document.head.appendChild(script);
}
document.addEventListener('DOMContentLoaded', onLoad(), false);
+1570
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -14,16 +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-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",
@@ -34,6 +40,7 @@
"moving-average": "^1.0.0",
"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",
+22 -25
View File
@@ -12,24 +12,19 @@ import TitleBar from './components/TitleBar'
import Tree from './components/Tree/Tree'
import UpdateNotifier from './UpdateNotifier'
import { connect } from 'react-redux'
interface State {
selectedNode?: q.TreeNode,
connectionId?: string
}
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} {
@@ -82,25 +77,26 @@ 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 })
}} />
<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 style={this.getStyles().right}>
<Sidebar connectionId={this.state.connectionId} />
</div>
</div>
</div>
<UpdateNotifier />
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/>
</div>
<UpdateNotifier />
<Connection />
</ErrorBoundary>
</div >
)
}
@@ -108,7 +104,8 @@ class App extends React.Component<Props, State> {
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)
+2 -2
View File
@@ -212,8 +212,8 @@ const styles = (theme: Theme) => ({
const mapStateToProps = (state: AppState) => {
return {
showUpdateNotification: state.showUpdateNotification,
showUpdateDetails: state.showUpdateDetails,
showUpdateNotification: state.tooBigReducer.showUpdateNotification,
showUpdateDetails: state.tooBigReducer.showUpdateDetails,
}
}
+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,
}
}
+14 -10
View File
@@ -1,15 +1,19 @@
import { ActionTypes, CustomAction } from '../reducers'
import { Dispatch, Action } from 'redux'
import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events'
export const setPublishTopic = (topic: string): CustomAction => {
return {
publishTopic: topic,
type: ActionTypes.setPublishTopic,
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic, connectionId } = getState().tooBigReducer
if (!selectedTopic || !connectionId) {
return
}
}
export const setPublishPayload = (payload: string): CustomAction => {
return {
publishPayload: payload,
type: ActionTypes.setPublishPayload,
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: selectedTopic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
}
+13 -4
View File
@@ -1,9 +1,18 @@
import { ActionTypes, CustomAction } from '../reducers'
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): CustomAction => {
return {
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,
}
})
}
+4 -2
View File
@@ -1,6 +1,8 @@
import * as settingsActions from './Settings'
import * as sidebarActions from './Sidebar'
import * as publishActions from './Publish'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection'
import * as sidebarActons from './Sidebar'
export { settingsActions, treeActions, sidebarActions, updateNotifierActions }
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons }
+4
View File
@@ -0,0 +1,4 @@
import { electronRendererTelementry } from 'electron-telemetry'
const spareMeFromGc = electronRendererTelementry
electronRendererTelementry.registerErrorHandler()
+191 -134
View File
@@ -18,21 +18,27 @@ import {
Toolbar,
Typography,
} from '@material-ui/core'
import { DataSourceState, MqttOptions } from '../../../../backend/src/DataSource'
import { connect } from 'react-redux'
import { MqttOptions } from '../../../../backend/src/DataSource'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../../events'
import Notification from './Notification'
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 = [
@@ -41,54 +47,64 @@ const protocols = [
]
interface State {
connecting: boolean
connectionId?: string
error?: string
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
showPassword: boolean
}
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: 'iot.eclipse.org',
protocol: protocols[0],
port: 1883,
tls: false,
certValidation: true,
clientId: '',
username: '',
password: '',
connecting: false,
connectionId: undefined,
showPassword: false,
}
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 = () => {
@@ -96,48 +112,19 @@ class Connection extends React.Component<Props, State> {
}
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.password || 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) {
this.setState({ error: state.error })
this.disconnect()
}
})
}
private disconnect() {
this.setState({
connecting: false,
})
rendererEvents.emit(removeConnection, this.state.connectionId)
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
root: {
@@ -165,17 +152,19 @@ class Connection extends React.Component<Props, State> {
button: {
margin: theme.spacing.unit,
},
passwordFormControl: {
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() {
@@ -193,12 +182,11 @@ class Connection extends React.Component<Props, State> {
)
let renderError = null
if (this.state.error) {
if (this.props.error) {
renderError = (
<Notification
message={this.state.error}
type="error"
onClose={() => { this.setState({ error: undefined }) }}
message={this.props.error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
@@ -206,7 +194,7 @@ class Connection extends React.Component<Props, State> {
return (
<div>
{renderError}
<Modal open={this.state.visible} disableAutoFocus={true}>
<Modal open={this.props.visible} disableAutoFocus={true}>
<Paper className={classes.root}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
@@ -214,26 +202,13 @@ class Connection extends React.Component<Props, State> {
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={24}>
<Grid item={true} xs={2}>
<TextField
select={true}
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>
{this.renderProtocols()}
</Grid>
<Grid item xs={7}>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={this.state.host}
value={this.state.connectionSettings.host}
onChange={this.handleChange('host')}
margin="normal"
/>
@@ -242,7 +217,7 @@ class Connection extends React.Component<Props, State> {
<TextField
label="Port"
className={classes.textField}
value={this.state.port}
value={this.state.connectionSettings.port}
onChange={this.handleChange('port')}
margin="normal"
/>
@@ -251,54 +226,41 @@ class Connection extends React.Component<Props, State> {
<TextField
label="Username"
className={classes.textField}
value={this.state.username}
value={this.state.connectionSettings.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={5}>
<FormControl className={`${classes.textField} ${classes.passwordFormControl}`}>
<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.password}
value={this.state.connectionSettings.password}
onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton}
/>
</FormControl>
</Grid>
<Grid item={true} xs={4}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.certValidation}
onChange={() => this.setState({ certValidation: !this.state.certValidation })}
color="primary"
/>
)}
label="Validate certificate"
labelPlacement="bottom"
<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 />}
/>
</div>
</FormControl>
</Grid>
<Grid item={true} xs={4}>
<div className={classes.switch}>
<FormControlLabel
control={(
<Switch
checked={this.state.tls}
onChange={() => this.setState({ tls: !this.state.tls })}
color="primary"
/>
)}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
{this.renderCertValidationSwitch()}
</Grid>
<Grid item={true} xs={3}>
{this.renderTlsSwitch()}
</Grid>
<Grid item={true} xs={4} />
</Grid>
<br />
<div style={{ textAlign: 'right' }}>
@@ -314,26 +276,121 @@ class Connection extends React.Component<Props, State> {
)
}
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))
@@ -4,14 +4,8 @@ import { Snackbar, SnackbarContent } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { green, red } from '@material-ui/core/colors'
enum MessageType {
success = 'success',
error = 'error',
}
interface Props {
message?: string
type: MessageType
onClose: () => void
classes: any
}
@@ -33,18 +27,20 @@ class Notification extends React.Component<Props, {}> {
})
public render() {
const snackbarAnchor = {
vertical: 'bottom' as 'bottom',
horizontal: 'left' as 'left',
}
return (
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
anchorOrigin={snackbarAnchor}
open={Boolean(this.props.message)}
autoHideDuration={10000}
onClose={this.props.onClose}
>
<SnackbarContent
className={this.props.classes[this.props.type]}
className={this.props.classes.error}
message={this.props.message}
/>
</Snackbar>
+3 -3
View File
@@ -138,9 +138,9 @@ class Settings extends React.Component<Props, {}> {
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
nodeOrder: state.settings.nodeOrder,
visible: state.settings.visible,
autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
nodeOrder: state.tooBigReducer.settings.nodeOrder,
visible: state.tooBigReducer.settings.visible,
}
}
+24 -15
View File
@@ -13,6 +13,7 @@ interface Props {
items: HistoryItem[]
onClick?: (index: number, element: EventTarget) => void
classes: any
contentTypeIndicator?: String
}
interface State {
@@ -28,16 +29,18 @@ class MessageHistory extends React.Component<Props, State> {
}
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={{
backgroundColor: 'rgba(80, 80, 80, 0.6)',
margin: '8px',
padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit',
}}
style={style}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
>
<div><i>{element.title}</i></div>
@@ -47,15 +50,26 @@ class MessageHistory extends React.Component<Props, State> {
</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' }}
>
{this.state.collapsed ? '▶' : '▼'} History
<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>
@@ -63,17 +77,12 @@ class MessageHistory extends React.Component<Props, State> {
}
public render() {
const visible = this.props.items.length > 0 && this.state.collapsed
return (
<Badge
<div
style={{ display: 'block', width: '100%' }}
invisible={!visible}
badgeContent={this.props.items.length}
color="primary"
classes={{ badge: this.props.classes.badge }}
>
{this.renderHistory()}
</Badge>
</div>
)
}
@@ -83,7 +92,7 @@ class MessageHistory extends React.Component<Props, State> {
}
const styles = (theme: Theme) => ({
badge: { top: '-8px', left:'64px' },
badge: { right:'-25px' },
})
export default withStyles(styles)(MessageHistory)
+14 -3
View File
@@ -2,9 +2,10 @@ 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')
@@ -49,21 +50,31 @@ class MessageHistory extends React.Component<Props, State> {
}
const history = node.messageHistory.toArray()
const historyElements = history.map(message => ({
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)
@@ -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)
+162 -83
View File
@@ -1,4 +1,3 @@
// tslint:disable-next-line
import 'react-ace'
import 'brace/mode/json'
import 'brace/mode/text'
@@ -6,69 +5,78 @@ import 'brace/mode/xml'
import 'brace/theme/monokai'
import * as React from 'react'
import * as brace from 'brace'
import * as q from '../../../../../backend/src/Model'
import {
Button,
Fab,
FormControlLabel,
InputAdornment,
Radio,
RadioGroup,
TextField,
Typography,
IconButton,
FormControl,
InputLabel,
Input,
Checkbox,
MenuItem,
Tooltip,
} from '@material-ui/core'
import { makePublishEvent, rendererEvents } from '../../../../../events'
// 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 { sidebarActions } from '../../../actions'
import { publishActions } from '../../../actions'
interface Props {
node?: q.TreeNode
connectionId?: string
topic?: string
payload?: string
actions: any
actions: typeof publishActions
retain: boolean
editorMode: string
qos: 0 | 1 | 2
}
interface State {
mode: string
history: Message[]
}
class Publish extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { mode: 'json', history: [] }
this.state = { history: [] }
}
private updatePayload = (value: string, event?: any) => {
this.props.actions.setPublishPayload(value)
private updatePayload = (payload: string, event?: any) => {
this.props.actions.setPayload(payload)
}
private updateTopic = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
this.props.actions.setPublishTopic(e.target.value)
this.props.actions.setTopic(e.target.value)
}
private updateMode = (e: React.ChangeEvent<{}>, value: string) => {
this.setState({ mode: value })
this.props.actions.setEditorMode(value)
}
private publish = (e: React.MouseEvent) => {
e.stopPropagation()
const topic = this.currentTopic() || ''
const payload = this.props.payload
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) {
rendererEvents.emit(makePublishEvent(this.props.connectionId), { topic, payload })
this.addMessageToHistory(topic, payload)
}
}
@@ -91,35 +99,35 @@ class Publish extends React.Component<Props, State> {
)
}
private currentTopic(): string | undefined {
const { node, topic } = this.props
const selectedNodePath = (node ? node.path() : undefined)
return (topic !== undefined) ? topic : selectedNodePath
private clearTopic = () => {
this.props.actions.setTopic('')
}
private topic() {
const topicStr = this.currentTopic() || ''
const topicStr = this.props.topic || ''
return (
<div>
<Typography>Topic</Typography>
<TextField
placeholder="example/topic"
style={{ width: '100%' }}
margin="normal"
value={topicStr}
multiline={true}
onChange={this.updateTopic}
InputProps={{ onBlur: this.onTopicBlur }}
/>
<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.setPublishTopic(undefined)
this.props.actions.setTopic(undefined)
}
}
@@ -142,38 +150,10 @@ class Publish extends React.Component<Props, State> {
}
private editorMode() {
const labelStyle = { margin: '0 8px 0 8px' }
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={this.state.mode}
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>
{this.renderEditorModeSelection()}
<div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()}
</div>
@@ -182,8 +162,97 @@ class Publish extends React.Component<Props, State> {
)
}
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.map(message => ({
const items = this.state.history.reverse().map(message => ({
title: message.topic,
value: message.payload,
}))
@@ -192,42 +261,52 @@ class Publish extends React.Component<Props, State> {
}
private didSelectHistoryEntry = (index: number) => {
let message = this.state.history[index]
this.props.actions.setPublishTopic(message.topic)
this.props.actions.setPublishPayload(message.payload)
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()}
<AceEditor
mode={this.state.mode}
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 }}
/>
{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(sidebarActions, dispatch),
actions: bindActionCreators(publishActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
topic: state.sidebar.publishTopic,
payload: state.sidebar.publishPayload,
topic: state.publish.topic,
payload: state.publish.payload,
editorMode: state.publish.editorMode,
retain: state.publish.retain,
qos: state.publish.qos,
}
}
+58 -12
View File
@@ -2,31 +2,40 @@ import * as React from 'react'
import * as q from '../../../../backend/src/Model'
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 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,
@@ -116,21 +125,13 @@ class Sidebar extends React.Component<Props, State> {
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<div style={{ width: '100%', textAlign:'right' }}>
{this.props.node && this.props.node.message && <i><DateFormatter date={this.props.node.message.received} /></i>}
</div>
{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}>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper>
<ValueRenderer message={this.state.compareMessage} />
</Paper>
</Fade>
)}
{this.showValueComparison}
</Popper>
</ExpansionPanelDetails>
</ExpansionPanel>
@@ -152,6 +153,45 @@ class Sidebar extends React.Component<Props, State> {
)
}
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 })
@@ -175,8 +215,14 @@ class Sidebar extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => {
return {
node: state.selectedTopic,
node: state.tooBigReducer.selectedTopic,
}
}
export default withStyles(Sidebar.styles, { withTheme: true })(connect(mapStateToProps)(Sidebar))
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(sidebarActons, dispatch),
}
}
export default withStyles(Sidebar.styles, { withTheme: true })(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
+12 -2
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
}
@@ -33,7 +37,7 @@ class Topic extends React.Component<Props, {}> {
.map(edge =>
[(
<Button
onClick={() => this.setState({ node: edge!.target })}
onClick={() => this.props.actions.selectTopic(edge!.target)}
size="small"
color="secondary"
className={this.props.classes.button}
@@ -56,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))
+27 -18
View File
@@ -1,15 +1,16 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import { AppBar, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import CloudOff from '@material-ui/icons/CloudOff'
import Menu from '@material-ui/icons/Menu'
import Search from '@material-ui/icons/Search'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { settingsActions } from '../actions'
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,15 +89,18 @@ class TitleBar extends React.Component<Props, State> {
}
public render() {
const { classes } = this.props
const { actions, classes } = this.props
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-Explorer</Typography>
<Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
</Toolbar>
</AppBar>
)
@@ -102,25 +109,27 @@ class TitleBar extends React.Component<Props, State> {
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))
+25 -22
View File
@@ -1,7 +1,7 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { makeConnectionMessageEvent, rendererEvents } from '../../../../events'
import { makeConnectionMessageEvent, rendererEvents, MqttMessage } from '../../../../events'
import { AppState } from '../../reducers'
import TreeNode from './TreeNode'
@@ -18,6 +18,7 @@ interface Props {
autoExpandLimit: number
didSelectNode?: (node: q.TreeNode) => void
connectionId?: string
connected: boolean
}
interface TreeState {
@@ -32,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 {
@@ -47,7 +47,7 @@ class Tree extends React.Component<Props, TreeState> {
average.push(Date.now(), ms)
}
public throttledStateUpdate(state: any) {
public throttledTreeUpdate() {
if (this.updateTimer) {
return
}
@@ -61,26 +61,25 @@ class Tree extends React.Component<Props, TreeState> {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(state)
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)
}
}
}
@@ -91,16 +90,19 @@ 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() {
console.log('render called')
if (!this.props.connected) {
return null
}
const style: React.CSSProperties = {
lineHeight: '1.1',
@@ -128,7 +130,8 @@ class Tree extends React.Component<Props, TreeState> {
const mapStateToProps = (state: AppState) => {
return {
autoExpandLimit: state.settings.autoExpandLimit,
autoExpandLimit: state.tooBigReducer.settings.autoExpandLimit,
connected: state.tooBigReducer.connected,
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ class TreeNodeSubnodes extends React.Component<Props, {}> {
const mapStateToProps = (state: AppState) => {
return {
nodeOrder: state.settings.nodeOrder,
nodeOrder: state.tooBigReducer.settings.nodeOrder,
}
}
+2 -1
View File
@@ -67,7 +67,8 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
marginLeft: '5px',
display: 'inline-block',
}
return this.props.treeNode.message
return this.props.treeNode.message && this.props.treeNode.message.length > 0
? <span style={style}> = {this.props.treeNode.message.value.toString()}</span>
: null
}
+11 -12
View File
@@ -2,25 +2,24 @@ import './tracking'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import reduxThunk from 'redux-thunk'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers, { AppState, NodeOrder } from './reducers'
import App from './App'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { createStore, applyMiddleware, compose } from 'redux'
const initialAppState: AppState = {
settings: {
autoExpandLimit: 0,
nodeOrder: NodeOrder.none,
visible: false,
},
sidebar: {},
selectedTopic: undefined,
showUpdateDetails: 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: {
+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,
}
}
+76 -22
View File
@@ -1,18 +1,22 @@
import * as q from '../../../backend/src/Model'
import { Action, Reducer } from 'redux'
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',
setPublishTopic = 'SET_PUBLISH_TOPIC',
setPublishPayload = 'SET_PUBLISH_PAYLOAD',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS',
connecting = 'CONNECTING',
connected = 'CONNECTED',
}
export interface CustomAction extends Action {
@@ -20,23 +24,27 @@ export interface CustomAction extends Action {
autoExpandLimit?: number
nodeOrder?: NodeOrder
selectedTopic?: q.TreeNode
publishTopic?: string
publishPayload?: string
showUpdateNotification?: boolean
showUpdateDetails?: boolean
}
export interface SidebarState {
publishTopic?: string
publishPayload?: string
connectionOptions?: MqttOptions
connectionId?: string
error?: string
}
export interface AppState {
tooBigReducer: TooBigOfState
publish: PublishState
}
export interface TooBigOfState {
settings: SettingsState,
selectedTopic?: q.TreeNode
sidebar: SidebarState
showUpdateNotification?: boolean
showUpdateDetails: boolean
connecting: boolean
connected: boolean
error?: string
connectionId?: string
}
export interface SettingsState {
@@ -52,7 +60,20 @@ export enum NodeOrder {
topics = '#topics',
}
const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) => {
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')
}
@@ -70,16 +91,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) =>
autoExpandLimit: action.autoExpandLimit,
},
}
case ActionTypes.setPublishTopic:
return {
...state,
sidebar: { ...state.sidebar, publishTopic: action.publishTopic },
}
case ActionTypes.setPublishPayload:
return {
...state,
sidebar: { ...state.sidebar, publishPayload: action.publishPayload },
}
case ActionTypes.toggleSettingsVisibility:
return {
...state,
@@ -88,6 +100,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) =>
visible: !state.settings.visible,
},
}
case ActionTypes.selectTopic:
if (!action.selectedTopic) {
return state
@@ -96,6 +109,7 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) =>
...state,
selectedTopic: action.selectedTopic,
}
case ActionTypes.setNodeOrder:
if (!action.nodeOrder) {
return state
@@ -104,11 +118,13 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) =>
...state,
settings: { ...state.settings, nodeOrder: action.nodeOrder },
}
case ActionTypes.showUpdateNotification:
return {
...state,
showUpdateNotification: action.showUpdateNotification,
}
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
@@ -117,9 +133,47 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) =>
...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
}
}
}
+3
View File
@@ -1,5 +1,7 @@
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
@@ -18,4 +20,5 @@ export function trackEvent(name: string) {
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 -1
View File
@@ -1,4 +1,5 @@
import { DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
type MessageCallback = (topic: string, payload: Buffer) => void
@@ -7,7 +8,7 @@ interface DataSource<DataSourceOptions> {
connect(options: DataSourceOptions): DataSourceStateMachine
disconnect(): void
onMessage(messageCallback: MessageCallback): void
publish(topic: string, payload: any): void
publish(msg: MqttMessage): void
topicSeparator: string
stateMachine: DataSourceStateMachine
}
+15 -7
View File
@@ -2,6 +2,7 @@ import * as Url from 'url'
import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource, DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
export interface MqttOptions {
url: string
@@ -9,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
}
@@ -36,9 +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
@@ -63,17 +66,22 @@ 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(topic: string, payload: any) {
this.client && this.client.publish(topic, payload)
public publish(msg: MqttMessage) {
this.client && this.client.publish(msg.topic, msg.payload, { qos: msg.qos, retain: msg.retain })
}
public disconnect() {
+3 -2
View File
@@ -1,15 +1,15 @@
import { Edge, Message, RingBuffer } from './'
import { EventDispatcher } from '../../../events'
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)
@@ -83,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)
+6 -5
View File
@@ -1,7 +1,7 @@
import {
AddMqttConnection,
EventDispatcher,
Message,
MqttMessage,
addMqttConnectionEvent,
backendEvents,
checkForUpdates,
@@ -36,19 +36,20 @@ export class ConnectionManager {
connection.connect(options)
this.handleNewMessagesForConnection(connectionId, connection)
backendEvents.subscribe(makePublishEvent(connectionId), (msg: Message) => {
this.connections[connectionId].publish(msg.topic, msg.payload)
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 })
})
}
+6 -4
View File
@@ -35,18 +35,20 @@ export const updateAvailable: Event<UpdateInfo> = {
topic: 'app/update/available',
}
export interface Message {
export interface MqttMessage {
topic: string,
payload: any
payload: any,
qos: 0 | 1 | 2,
retain: boolean
}
export function makePublishEvent(connectionId: string): Event<Message> {
export function makePublishEvent(connectionId: string): Event<MqttMessage> {
return {
topic: `conn/publish/${connectionId}`,
}
}
export function makeConnectionMessageEvent(connectionId: string): Event<Message> {
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')
+63 -18
View File
@@ -1,6 +1,6 @@
{
"name": "MQTT-Explorer",
"version": "0.0.6",
"version": "0.0.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -208,6 +208,18 @@
"integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==",
"dev": true
},
"@types/pako": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.0.tgz",
"integrity": "sha1-6q6DZNG391LiY7w/1o3+yY5hNsU=",
"dev": true
},
"@types/semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==",
"dev": true
},
"@types/sha1": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/sha1/-/sha1-1.1.1.tgz",
@@ -226,6 +238,15 @@
"@types/node": "*"
}
},
"@types/uuid": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz",
"integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"ajv": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
@@ -1160,22 +1181,14 @@
"dev": true
},
"electron": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-4.0.0.tgz",
"integrity": "sha512-3XPG/3IXlvnT1oe1K6zEushoD0SKbP8xwdrL10EWGe6k2iOV4hSHqJ8vWnR8yZ7VbSXmBRfomEFDNAo/q/cwKw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-4.0.1.tgz",
"integrity": "sha512-kBWDLn1Vq8Tm6+/HpQc8gkjX7wJyQI8v/lf2kAirfi0Q4cXh6vBjozFvV1U/9gGCbyKnIDM+m8/wpyJIjg4w7g==",
"dev": true,
"requires": {
"@types/node": "^8.0.24",
"@types/node": "^10.12.18",
"electron-download": "^4.1.0",
"extract-zip": "^1.0.3"
},
"dependencies": {
"@types/node": {
"version": "8.10.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.39.tgz",
"integrity": "sha512-rE7fktr02J8ybFf6eysife+WF+L4sAHWzw09DgdCebEu+qDwMvv4zl6Bc+825ttGZP73kCKxa3dhJOoGJ8+5mA==",
"dev": true
}
}
},
"electron-builder": {
@@ -1312,6 +1325,32 @@
"mime": "^2.4.0"
}
},
"electron-telemetry": {
"version": "git+https://github.com/thomasnordquist/electron-telemetry.git#cafe92ff8197589bc5c2f9cfdef00a1195204ba3",
"from": "git+https://github.com/thomasnordquist/electron-telemetry.git",
"dev": true,
"requires": {
"@types/node": "^10.12.18",
"@types/pako": "^1.0.0",
"@types/uuid": "^3.4.4",
"axios": "^0.18.0",
"electron": "4.x",
"pako": "^1.0.8",
"ts-node": "^7.0.1",
"tslint": "^5.12.0",
"tslint-config-airbnb": "^5.11.1",
"typescript": "^3.2.2",
"uuid": "^3.3.2"
},
"dependencies": {
"pako": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz",
"integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==",
"dev": true
}
}
},
"electron-updater": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.0.6.tgz",
@@ -2247,7 +2286,7 @@
},
"load-json-file": {
"version": "1.1.0",
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
"dev": true,
"requires": {
@@ -2356,7 +2395,7 @@
},
"meow": {
"version": "3.7.0",
"resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
"dev": true,
"requires": {
@@ -4043,7 +4082,7 @@
},
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
@@ -4081,7 +4120,7 @@
},
"pretty-bytes": {
"version": "1.0.4",
"resolved": "http://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz",
"integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=",
"dev": true,
"requires": {
@@ -4229,6 +4268,12 @@
"strip-indent": "^1.0.1"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==",
"dev": true
},
"reflect-metadata": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",
@@ -4660,7 +4705,7 @@
},
"through2": {
"version": "0.2.3",
"resolved": "http://registry.npmjs.org/through2/-/through2-0.2.3.tgz",
"resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz",
"integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=",
"dev": true,
"requires": {
+7 -4
View File
@@ -1,13 +1,13 @@
{
"name": "MQTT-Explorer",
"version": "0.0.7",
"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 ..",
"prepare-release": "./prepare-release.sh",
"package": "ts-node package.ts",
@@ -45,15 +45,17 @@
"@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.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",
@@ -63,6 +65,7 @@
"typescript": "^3.2.2"
},
"dependencies": {
"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",
+1 -1
View File
@@ -4,7 +4,7 @@ const linux: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: true,
arm64: true,
arm64: false,
linux: ['AppImage'],
projectDir: './build/clean',
publish: 'always',
+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)
+30 -23
View File
@@ -1,49 +1,55 @@
const { app, BrowserWindow } = require('electron')
const { autoUpdater } = require("electron-updater")
const log = require('electron-log');
const { ConnectionManager, updateNotifier } = require('./backend/build/backend/src/index.js')
const fs = require('fs')
const path = require('path')
require('electron-debug')({enabled: process.argv[2] === '--debug'});
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'
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
log.info('App starting...');
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
let mainWindow: BrowserWindow | undefined
function createWindow () {
const icon = path.join(__dirname, 'icon.png')
function createWindow() {
const iconPath = path.join(__dirname, 'icon.png')
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 700,
webPreferences: {
nodeIntegration: true
nodeIntegration: true,
devTools: true,
},
icon
icon: iconPath,
})
console.log(icon)
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', function () {
mainWindow.on('close', () => {
connectionManager.closeAllConnections()
})
// Emitted when the window is closed.
mainWindow.on('closed', function () {
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 = null
mainWindow = undefined
app.quit()
})
}
@@ -52,10 +58,11 @@ function createWindow () {
// 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
autoUpdater.on('update-available', (info) => {
let updateInfo: UpdateInfo
autoUpdater.on('update-available', (info: UpdateInfo) => {
console.log('there is an update')
updateInfo = info
})
@@ -78,7 +85,7 @@ app.on('ready', () => {
})
// Quit when all windows are closed.
app.on('window-all-closed', function () {
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') {
@@ -86,7 +93,7 @@ app.on('window-all-closed', function () {
}
})
app.on('activate', function () {
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) {
+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"]
}
+2 -1
View File
@@ -7,6 +7,7 @@
"no-else-after-return": false,
"align": false,
"variable-name": false,
"jsx-no-lambda": false
"jsx-no-lambda": false,
"indent": [true, "spaces", 2]
}
}