mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-12 01:23:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba31de468 | ||
|
|
ab6938c7da | ||
|
|
1eeaee0fe0 | ||
|
|
0e7ec90c65 | ||
|
|
5eb6f41189 | ||
|
|
830dd31f06 | ||
|
|
0de3ce2c41 | ||
|
|
9c5140c419 | ||
|
|
61b64a5ac2 | ||
|
|
f905ba8e89 | ||
|
|
c6a0a15d95 | ||
|
|
75230ccb40 | ||
|
|
97a1cf72c5 | ||
|
|
71c06b3f74 | ||
|
|
b0fa4a9f7a | ||
|
|
d8c8799406 | ||
|
|
7c72344174 | ||
|
|
7d42ed12b1 | ||
|
|
0df5ff13c1 | ||
|
|
ac2387ed9b | ||
|
|
d4bb00262e | ||
|
|
d41a2c2269 | ||
|
|
4d11302b24 | ||
|
|
fd386bf1df | ||
|
|
ad78ca03d8 | ||
|
|
611fde13e2 | ||
|
|
4f02e19f65 | ||
|
|
2268175a38 | ||
|
|
bd3b81273d | ||
|
|
322c8e67aa |
+8
-5
@@ -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
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# MQTT-Explorer
|
||||
[](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
|
||||
|
||||
### Version 0.0.6
|
||||
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.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)).
|
||||
|
||||

|
||||
More architectures and package types: [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
|
||||
|
||||

|
||||
|
||||
## 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."
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# MQTT-Explorer
|
||||
[](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)
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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>
|
||||
|
||||
@@ -42,6 +42,13 @@ class UpdateNotifier extends React.Component<Props, {}> {
|
||||
rendererEvents.subscribe(updateAvailable, this.handleUpdate)
|
||||
}
|
||||
|
||||
private fixUrl(url: string, version: string) {
|
||||
if (!/^http/.test(url)) {
|
||||
return `https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v${version}/${url}`
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
rendererEvents.unsubscribeAll(updateAvailable)
|
||||
}
|
||||
@@ -156,7 +163,7 @@ class UpdateNotifier extends React.Component<Props, {}> {
|
||||
<div key={index}>
|
||||
<Button
|
||||
className={this.props.classes.download}
|
||||
href={file.url}
|
||||
href={this.fixUrl(file.url, updateInfo.version)}
|
||||
>
|
||||
<IconButton><CloudDownload /></IconButton>{this.urlToFilename(file.url)}
|
||||
</Button>
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react'
|
||||
|
||||
interface Props {
|
||||
date: Date
|
||||
}
|
||||
|
||||
class DateFormatter extends React.Component<Props, {}> {
|
||||
|
||||
public render() {
|
||||
return <span>{this.props.date.toLocaleDateString()} {this.props.date.toLocaleTimeString()}</span>
|
||||
}
|
||||
}
|
||||
|
||||
export default DateFormatter
|
||||
@@ -3,11 +3,10 @@ import * as q from '../../../../backend/src/Model'
|
||||
|
||||
import { Theme, withTheme } from '@material-ui/core/styles'
|
||||
|
||||
import Fade from '@material-ui/core/Fade'
|
||||
import DateFormatter from '../DateFormatter'
|
||||
import History from './History'
|
||||
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
|
||||
@@ -26,9 +25,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)
|
||||
@@ -51,7 +50,7 @@ class MessageHistory extends React.Component<Props, State> {
|
||||
|
||||
const history = node.messageHistory.toArray()
|
||||
const historyElements = history.map(message => ({
|
||||
title: message.received.toGMTString(),
|
||||
title: <DateFormatter date={message.received} />,
|
||||
value: message.value,
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
|
||||
import { Typography } from '@material-ui/core'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
|
||||
|
||||
import { AppState } from '../../reducers'
|
||||
import Copy from '../Copy'
|
||||
import DateFormatter from '../DateFormatter'
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||
import MessageHistory from './MessageHistory'
|
||||
import NodeStats from './NodeStats'
|
||||
@@ -22,6 +23,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 +39,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)
|
||||
@@ -113,7 +116,12 @@ class Sidebar extends React.Component<Props, State> {
|
||||
<Typography className={classes.heading}>Value {copyValue}</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails style={this.detailsStyle}>
|
||||
<div ref={this.valueRef}><ValueRenderer message={this.props.node && this.props.node.message} /></div>
|
||||
<div style={{ width: '100%', textAlign:'right' }}>
|
||||
{this.props.node && this.props.node.message && <i><DateFormatter date={this.props.node.message.received} /></i>}
|
||||
</div>
|
||||
<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 }) => (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import * as q from '../../../backend/src/Model'
|
||||
|
||||
import { AppBar, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
|
||||
import Search from '@material-ui/icons/Search'
|
||||
import Menu from '@material-ui/icons/Menu'
|
||||
import { withStyles, StyleRulesCallback } from '@material-ui/core/styles'
|
||||
import { fade } from '@material-ui/core/styles/colorManipulator'
|
||||
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
|
||||
|
||||
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'
|
||||
|
||||
const styles: StyleRulesCallback = theme => ({
|
||||
@@ -87,14 +87,16 @@ 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 />
|
||||
</IconButton>
|
||||
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Xplorer</Typography>
|
||||
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
}
|
||||
|
||||
private renderSearch() {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-3
@@ -2,6 +2,9 @@ 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'});
|
||||
|
||||
autoUpdater.logger = log;
|
||||
autoUpdater.logger.transports.file.level = 'info';
|
||||
@@ -15,20 +18,25 @@ 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 () {
|
||||
@@ -48,10 +56,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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+333
-462
File diff suppressed because it is too large
Load Diff
Executable
+10
@@ -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 $@
|
||||
+18
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "MQTT-Explorer",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.7",
|
||||
"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,25 +41,29 @@
|
||||
"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-debug": "^2.0.0",
|
||||
"electron-log": "^2.2.17",
|
||||
"electron-updater": "^4.0.6",
|
||||
"mqtt": "^2.18.8",
|
||||
|
||||
+32
-6
@@ -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()
|
||||
|
||||
@@ -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 |
@@ -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()
|
||||
Reference in New Issue
Block a user