Compare commits

..
11 Commits
Author SHA1 Message Date
Thomas Nordquist ac2387ed9b Update CI deploy configuration 2019-01-14 14:20:33 +01:00
Thomas Nordquist d4bb00262e Prepare release 2019-01-14 12:14:04 +01:00
Thomas Nordquist d41a2c2269 Remove root node name from json view 2019-01-14 12:14:04 +01:00
Thomas Nordquist 4d11302b24 Update screenshot 2019-01-14 12:08:00 +01:00
Thomas Nordquist fd386bf1df Fix types in tests 2019-01-14 11:49:57 +01:00
Thomas Nordquist ad78ca03d8 Close connections when window closed
Fixes #8
2019-01-14 11:23:46 +01:00
Thomas Nordquist 611fde13e2 Improve scroll behavior 2019-01-14 11:16:39 +01:00
Thomas Nordquist 4f02e19f65 Only package on tag builds 2019-01-14 11:02:50 +01:00
Thomas Nordquist 2268175a38 Fix performance issue
When the root node was collapsed, it also was selected due to the click event.
The sidebar was updated every time a new message was receivend,
effectivly inhibiting the "render the tree if you got nothing else to do" optimization to render anything at all.

Fixes #7
2019-01-14 10:45:15 +01:00
Thomas Nordquist bd3b81273d Clean up 2019-01-14 00:15:53 +01:00
Thomas Nordquist 322c8e67aa Fix product name 2019-01-13 23:58:06 +01:00
22 changed files with 370 additions and 569 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
+1 -3
View File
@@ -7,8 +7,7 @@ This application subscribes to all topics on your MQTT-Server and displays your
## Download
The app is prebuilt for Windows, Linux and Mac. [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
![screenshot](https://user-images.githubusercontent.com/7721625/51086078-340c1c00-1742-11e9-83a9-07f932055467.png)
![screen2](https://user-images.githubusercontent.com/7721625/51109225-dbe22200-17f4-11e9-8f6b-c6a27c07c90e.png)
## Develop
PRs and issues are welcome
@@ -21,4 +20,3 @@ The `app` directory contains all the rendering logic, the `backend` directory cu
## License
Not yet decided which licens exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
+1
View File
@@ -17,6 +17,7 @@
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-corner {
@@ -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',
+10 -8
View File
@@ -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() {
+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>
)
+7 -26
View File
@@ -1,13 +1,14 @@
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 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
@@ -119,12 +120,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 +140,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
+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 {
+5
View File
@@ -30,6 +30,11 @@ function createWindow () {
// 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
+232 -470
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
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 package.ts $@
+3 -2
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",
+27 -3
View File
@@ -5,7 +5,17 @@ const linux: builder.CliOptions = {
ia32: true,
armv7l: true,
arm64: true,
linux: ['AppImage', 'deb', 'snap'],
linux: ['AppImage', 'deb', 'rpm', 'pacman', 'tar.gz'],
projectDir: './build/clean',
publish: 'onTag',
}
const linuxSnap: builder.CliOptions = {
x64: false,
ia32: false,
armv7l: false,
arm64: true,
linux: ['snap'],
projectDir: './build/clean',
publish: 'onTag',
}
@@ -30,7 +40,7 @@ const mac: builder.CliOptions = {
publish: 'onTag',
}
async function buildAll() {
async function executeBuild() {
console.log(process.argv[2])
switch (process.argv[2]) {
case 'win':
@@ -38,6 +48,11 @@ async function buildAll() {
break
case 'linux':
await builder.build(linux)
try {
await builder.build(linuxSnap)
} catch {
// ignore
}
break
case 'mac':
await builder.build(mac)
@@ -47,4 +62,13 @@ async function buildAll() {
}
}
buildAll()
function build() {
try {
executeBuild()
} catch (error) {
console.error(error)
process.exit(1)
}
}
build()