Compare commits

..
23 Commits
Author SHA1 Message Date
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
32 changed files with 795 additions and 695 deletions
+8 -5
View File
@@ -1,5 +1,8 @@
language: node_js
services:
- docker
cache:
directories:
- node_modules
@@ -16,11 +19,11 @@ os:
install:
- npm install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install snapd && sudo snap install snapcraft --classic; fi
script:
- npm run build
- npm run prepare-release
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then npm run package -- linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then npm run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then npm run package -- win; fi
- 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" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then npm run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then npm run package-with-docker -- win; fi
+6 -4
View File
@@ -1,14 +1,17 @@
# MQTT-Explorer
[![Build Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
### Version v0.0.5
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, Linux and Mac. [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
The app is prebuilt for Windows ([portable](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.5/MQTT-Explorer-0.0.5.exe), [installer](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.5/MQTT-Explorer-Setup-0.0.5.exe)), Linux ([AppImage](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.5/MQTT-Explorer-0.0.5-x86_64.AppImage)) and Mac ([dmg](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.0.5/MQTT-Explorer-0.0.5.dmg)).
![screenshot](https://user-images.githubusercontent.com/7721625/51086078-340c1c00-1742-11e9-83a9-07f932055467.png)
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
@@ -20,5 +23,4 @@ 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 licens exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
Not yet decided which license exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
+26
View File
@@ -0,0 +1,26 @@
# MQTT-Explorer
[![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."
+11 -9
View File
@@ -17,6 +17,7 @@
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-corner {
@@ -58,17 +59,18 @@
}
</style>
<script src="http://localhost:35729/livereload.js"></script>
</head>
<body>
<div id="splash"><div id="splash1"></div>
<!-- <div id="splash1">
<div id="splash2" style="color: #ddd" class="la-square-jelly-box la-3x">
<div>a</div>
<div>ads</div>
</div> -->
</div>
</div>
<div id="splash"><div id="splash1"></div></div>
<div id="app" style="font:-webkit-control"></div>
<script src="./build/bundle.js"></script>
<script>
function onLoad() {
var script = document.createElement("script");
script.src = "./build/bundle.js"
document.head.appendChild(script);
}
document.addEventListener('DOMContentLoaded', onLoad(), false);
</script>
</body>
</html>
+149 -102
View File
@@ -3,8 +3,13 @@ import * as React from 'react'
import {
Button,
CircularProgress,
FormControl,
FormControlLabel,
Grid,
IconButton,
Input,
InputAdornment,
InputLabel,
MenuItem,
Modal,
Paper,
@@ -17,6 +22,10 @@ import { DataSourceState, 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')
interface Props {
@@ -34,7 +43,7 @@ const protocols = [
interface State {
connecting: boolean
connectionId?: string
error?: Error
error?: string
visible: boolean
host: string
protocol: string
@@ -44,6 +53,7 @@ interface State {
clientId: string
username: string
password: string
showPassword: boolean
}
declare var window: any
@@ -71,6 +81,7 @@ class Connection extends React.Component<Props, State> {
password: '',
connecting: false,
connectionId: undefined,
showPassword: false,
}
this.state = Object.assign({}, defaultState, storedSettings)
@@ -80,6 +91,10 @@ class Connection extends React.Component<Props, State> {
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state))
}
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}`
@@ -87,7 +102,7 @@ class Connection extends React.Component<Props, State> {
return {
url,
username: this.state.username || undefined,
password: this.state.username || undefined,
password: this.state.password || undefined,
tls: this.state.tls,
certValidation: this.state.certValidation,
}
@@ -110,8 +125,8 @@ class Connection extends React.Component<Props, State> {
this.props.onConnection(connectionId)
this.setState({ visible: false })
} else if (state.error) {
console.log('error', state.error)
this.setState({ error: state.error })
this.disconnect()
}
})
}
@@ -150,6 +165,9 @@ class Connection extends React.Component<Props, State> {
button: {
margin: theme.spacing.unit,
},
passwordFormControl: {
marginTop: '16px',
},
}
}
@@ -163,108 +181,137 @@ class Connection extends React.Component<Props, State> {
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.state.error) {
renderError = (
<Notification
message={this.state.error}
type="error"
onClose={() => { this.setState({ error: undefined }) }}
/>
)
}
return (
<div>
{renderError}
<Modal open={this.state.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}>
<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>
</Grid>
<Grid item xs={7}>
<TextField
label="Host"
className={classes.textField}
value={this.state.host}
onChange={this.handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={3}>
<TextField
label="Port"
className={classes.textField}
value={this.state.port}
onChange={this.handleChange('port')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={5}>
<TextField
label="Username"
className={classes.textField}
value={this.state.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={5}>
<FormControl className={`${classes.textField} ${classes.passwordFormControl}`}>
<InputLabel htmlFor="adornment-password">Password</InputLabel>
<Input
id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'}
value={this.state.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={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"
/>
)}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
</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>
</Grid>
<Grid item={true} xs={4} />
</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() {
@@ -0,0 +1,55 @@
import * as React from 'react'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { green, red } from '@material-ui/core/colors'
enum MessageType {
success = 'success',
error = 'error',
}
interface Props {
message?: string
type: MessageType
onClose: () => void
classes: any
}
class Notification extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
public static styles = (theme: Theme) => ({
success: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
error: {
backgroundColor: red[600],
color: theme.typography.button.color,
},
})
public render() {
return (
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={Boolean(this.props.message)}
autoHideDuration={10000}
onClose={this.props.onClose}
>
<SnackbarContent
className={this.props.classes[this.props.type]}
message={this.props.message}
/>
</Snackbar>
)
}
}
export default withStyles(Notification.styles)(Notification)
@@ -9,6 +9,8 @@ import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper'
import ValueRenderer from './ValueRenderer'
const throttle = require('lodash.throttle')
interface Props {
node?: q.TreeNode
theme: Theme
@@ -26,9 +28,9 @@ class MessageHistory extends React.Component<Props, State> {
this.state = { }
}
private updateNode = () => {
private updateNode = throttle(() => {
this.setState(this.state)
}
}, 300)
public componentWillReceiveProps(nextProps: Props) {
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
+1
View File
@@ -1,5 +1,6 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Typography } from '@material-ui/core'
interface Props {
+4 -2
View File
@@ -22,6 +22,8 @@ import Topic from './Topic'
import ValueRenderer from './ValueRenderer'
import { connect } from 'react-redux'
const throttle = require('lodash.throttle')
interface Props {
node?: q.TreeNode,
classes: any,
@@ -36,9 +38,9 @@ interface State {
class Sidebar extends React.Component<Props, State> {
private valueRef: any = React.createRef<HTMLDivElement>()
private updateNode = () => {
private updateNode = throttle(() => {
this.setState(this.state)
}
}, 300)
constructor(props: any) {
super(props)
+2 -2
View File
@@ -40,6 +40,7 @@ class ValueRenderer extends React.Component<Props, {}> {
<ReactJson
style={{ width: '100%' }}
src={json}
name={null}
theme={theme}
/>
)
@@ -51,8 +52,7 @@ class ValueRenderer extends React.Component<Props, {}> {
backgroundColor: 'rgba(80, 80, 80, 0.6)',
wordBreak: 'break-all',
width: '100%',
overflowX: 'scroll',
overflowY: 'clip',
overflow: 'auto hidden',
display: 'block',
lineHeight: '1.2em',
padding: '12px 5px 12px 5px',
+3 -1
View File
@@ -87,7 +87,8 @@ class TitleBar extends React.Component<Props, State> {
public render() {
const { 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}>
<Menu />
@@ -95,6 +96,7 @@ class TitleBar extends React.Component<Props, State> {
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
</Toolbar>
</AppBar>
)
}
private renderSearch() {
+10 -10
View File
@@ -9,12 +9,11 @@ import { connect } from 'react-redux'
const MovingAverage = require('moving-average')
declare const performance: any
declare const window: any
const timeInterval = 10 * 1000
const average = MovingAverage(timeInterval)
declare var window: any
interface Props {
autoExpandLimit: number
didSelectNode?: (node: q.TreeNode) => void
@@ -44,22 +43,26 @@ class Tree extends React.Component<Props, TreeState> {
return time
}
private performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
public throttledStateUpdate(state: any) {
if (this.updateTimer) {
return
}
const expectedRenderTime = average.forecast()
const updateInterval = Math.max(expectedRenderTime * 20, 300)
const updateInterval = Math.max(expectedRenderTime * 7, 300)
const timeUntilNextUpdate = updateInterval - (performance.now() - this.lastUpdate)
this.updateTimer = setTimeout(() => {
window.requestAnimationFrame(() => {
window.requestIdleCallback(() => {
this.lastUpdate = performance.now()
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.setState(state)
})
}, { timeout: 500 })
}, Math.max(0, timeUntilNextUpdate))
}
@@ -104,10 +107,6 @@ class Tree extends React.Component<Props, TreeState> {
cursor: 'default',
}
const performanceCallback = (ms: number) => {
average.push(Date.now(), ms)
}
return (
<div style={style}>
<TreeNode
@@ -120,6 +119,7 @@ class Tree extends React.Component<Props, TreeState> {
collapsed={false}
key="rootNode"
lastUpdate={this.state.tree.lastUpdate}
performanceCallback={this.performanceCallback}
/>
</div>
)
+46 -26
View File
@@ -1,13 +1,15 @@
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 { 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
@@ -28,6 +30,12 @@ const styles = (theme: Theme) => {
backgroundColor: 'rgba(80, 80, 80, 0.35)',
},
},
topicSelect: {
float: 'right' as 'right',
opacity: 0,
cursor: 'pointer',
marginTop: '-1px',
},
}
}
@@ -60,6 +68,7 @@ class TreeNode extends React.Component<Props, State> {
private willUpdateTime: number = performance.now()
private titleRef = React.createRef<HTMLDivElement>()
private topicSelectRef = React.createRef<HTMLDivElement>()
private subnodesDidchange = () => {
this.dirtySubnodes = true
@@ -119,12 +128,6 @@ class TreeNode extends React.Component<Props, State> {
const renderTime = performance.now() - this.willUpdateTime
this.props.performanceCallback(renderTime)
}
// setTimeout(() => {
// this.setState(this.state)
// }, 500)
// this.addCssAnimation()
// setTimeout(this.removeCssAnimation, 500)
}
public componentWillUpdate() {
@@ -145,20 +148,6 @@ class TreeNode extends React.Component<Props, State> {
return this.props.treeNode.edgeCount() > this.props.autoExpandLimit
}
// private addCssAnimation = () => {
// const element = this.titleRef.current
// if ((this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes) && element && isElementInViewport(element)) {
// element.style.animation = 'example 0.5s'
// }
// }
//
// private removeCssAnimation = () => {
// const element = this.titleRef.current
// if (element && element.style.animation) {
// element.style.animation = ''
// }
// }
public render() {
const { classes } = this.props
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
@@ -174,6 +163,8 @@ class TreeNode extends React.Component<Props, State> {
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
@@ -183,11 +174,40 @@ class TreeNode extends React.Component<Props, State> {
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()
+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,
}
+4 -1
View File
@@ -1,6 +1,7 @@
import * as Url from 'url'
import { Client, connect as mqttConnect } from 'mqtt'
import { DataSource, DataSourceStateMachine } from './'
import * as Url from 'url'
export interface MqttOptions {
url: string
@@ -36,6 +37,8 @@ export class MqttSource implements DataSource<MqttOptions> {
const client = mqttConnect(url, {
resubscribe: false,
rejectUnauthorized: !options.certValidation,
username: options.username,
password: options.password,
})
this.client = client
+2 -2
View File
@@ -5,7 +5,7 @@ interface HasLength {
}
export abstract class TreeNodeFactory {
public static fromEdgesAndValue<T extends HasLength>(edgeNames: string[], value: T): 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)
@@ -17,7 +17,7 @@ export abstract class TreeNodeFactory {
currentNode.setMessage({
value,
length: value.length,
length: value ? value.length : 0,
received: new Date(),
})
return currentNode
+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')
+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
+5
View File
@@ -57,6 +57,11 @@ export class ConnectionManager {
connection.disconnect()
delete this.connections[hash]
}
public closeAllConnections() {
Object.keys(this.connections)
.forEach(hash => this.removeConnection(hash))
}
}
class UpdateNotifier {
+14 -1
View File
@@ -2,6 +2,8 @@ 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')
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
@@ -15,21 +17,29 @@ connectionManager.manageConnections()
let mainWindow
function createWindow () {
const icon = path.join(__dirname, 'icon.png')
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 700,
webPreferences: {
nodeIntegration: true
}
},
icon
})
console.log(icon)
// 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('close', function () {
connectionManager.closeAllConnections()
})
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
@@ -48,10 +58,13 @@ app.on('ready', () => {
let updateInfo
autoUpdater.on('update-available', (info) => {
console.log('there is an update')
updateInfo = info
})
autoUpdater.on('error', () => {
console.log('could not update due to error')
if (updateInfo) {
updateNotifier.notify(updateInfo)
}
+3 -1
View File
@@ -28,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)
}
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+278 -462
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 $@
+17 -13
View File
@@ -1,6 +1,6 @@
{
"name": "MQTT-Explorer",
"version": "0.0.3",
"version": "0.0.5",
"description": "Explore your message queues",
"main": "electron.js",
"scripts": {
@@ -10,7 +10,8 @@
"build": "cd app; npm run build; cd ..; cd backend; npm run build; cd ..",
"test-backend": "cd backend && npm run test && cd ..",
"prepare-release": "./prepare-release.sh",
"package": "ts-node package.ts"
"package": "ts-node package.ts",
"package-with-docker": "./package-with-docker.sh"
},
"repository": {
"type": "git",
@@ -40,23 +41,26 @@
"homepage": "https://github.com",
"license": "ISC",
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"@types/mustache": "^0.8.32",
"@types/node": "^10.12.18",
"@types/sha1": "^1.1.1",
"@types/socket.io": "^2.1.2",
"axios": "^0.18.0",
"chai": "^4.2.0",
"electron": "^4.0.0",
"electron-builder": "^20.38.4",
"mocha": "^5.2.0",
"mustache": "^3.0.1",
"nyc": "^13.1.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": {
"electron-log": "^2.2.17",
+32 -6
View File
@@ -5,9 +5,19 @@ const linux: builder.CliOptions = {
ia32: true,
armv7l: true,
arm64: true,
linux: ['AppImage', 'deb', 'snap'],
linux: ['AppImage'],
projectDir: './build/clean',
publish: 'onTag',
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 = {
@@ -17,7 +27,7 @@ const win: builder.CliOptions = {
arm64: false,
win: ['portable', 'nsis'],
projectDir: './build/clean',
publish: 'onTag',
publish: 'always',
}
const mac: builder.CliOptions = {
@@ -27,10 +37,10 @@ const mac: builder.CliOptions = {
arm64: false,
mac: ['dmg'],
projectDir: './build/clean',
publish: 'onTag',
publish: 'always',
}
async function buildAll() {
async function executeBuild() {
console.log(process.argv[2])
switch (process.argv[2]) {
case 'win':
@@ -39,6 +49,13 @@ async function buildAll() {
case 'linux':
await builder.build(linux)
break
case 'snap':
try {
await builder.build(linuxSnap)
} catch {
// ignore
}
break
case 'mac':
await builder.build(mac)
break
@@ -47,4 +64,13 @@ async function buildAll() {
}
}
buildAll()
function build() {
try {
executeBuild()
} catch (error) {
console.error(error)
process.exit(1)
}
}
build()
-10
View File
@@ -26,13 +26,3 @@ cd ..
npm run build
rm -rf app/node_modules
cd "$ORIGINAL_DIR"
exit 0
docker run --rm -ti \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
--env GH_TOKEN="$GH_TOKEN" \
-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

+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()