Compare commits

..
Author SHA1 Message Date
Thomas Nordquist 623303349b Work in progress 2019-02-17 22:34:46 +01:00
86 changed files with 1262 additions and 2387 deletions
+1 -2
View File
@@ -24,7 +24,6 @@ services:
install:
- yarn install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update && sudo apt-get -y install snap squashfs-tools && sudo snap install snapcraft --classic; fi;
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker build docker --tag uitest; fi;
script:
@@ -32,6 +31,6 @@ script:
- yarn test
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app uitest sh -c "cd app && docker/testMounted.sh"; fi
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package linux; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- win; fi
-12
View File
@@ -1,12 +0,0 @@
# 0.2.1
- Highlight differences in the last received message with a "diff" view
- Magnify app content with "Ctrl +", "Ctrl -", "Ctrl 0"
- Make user interactions more predictable by removing "select topic on mouse over"
- Add "Quick Preview" setting, to enable topic selection on mouse over
- Add JSON formatter
- Show error when connection to the mqtt broker is lost
- Fix bug where mqtt explorer resets the tree after a reconnect
- Fix MQTT-Client id
# 0.2.0
+20 -25
View File
@@ -1,47 +1,42 @@
# [MQTT Explorer](https://thomasnordquist.github.io/MQTT-Explorer/)
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
[![Build status](https://ci.appveyor.com/api/projects/status/c35tkm29rm4m5364/branch/master?svg=true)](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
### Version
### Version 0.1.3
A MQTT Tool to get a quick overview of your MQTT topics and integrate new devices/services more efficiently.
- Visualize topics and topic activity
- Delete retained topics
- Search/filter topics
- Delete topic recursively
- Publish topics
- Plot numeric topics
- Keep a history of each topic
See the whole picture of your broker.
- What topics are active?
- Which retained topics are abandoned?
- Where is this new device / service I connected to the broker?
MQTT-Explorer strives to be THE swiss-army-knife tool.
The perfect tool to integrate new services, IoT devices in your network.
The hierarchical view allows for a quick understanding what is going on on your broker.
## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | [portable](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.2.0/MQTT-Explorer-0.2.0.exe), [installer](https://github.com/thomasnordquist/MQTT-Explorer/releases/download/v0.2.0/MQTT-Explorer-Setup-0.2.0.exe) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | [AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.0&#x2F;MQTT-Explorer-0.2.0-x86_64.AppImage)<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | Ubuntu | [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/mqtt-explorer)<br>[Ubuntu Store](snap://mqtt-explorer) |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | [dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.2.0&#x2F;MQTT-Explorer-0.2.0.dmg) |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
The app is available for Windows ([portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-Setup-0.1.3.exe)), Linux ([AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3-x86_64.AppImage)) and Mac ([dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.dmg)).
## Screenshots
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | [portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-Setup-0.1.3.exe) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | [AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3-x86_64.AppImage)<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | [dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.dmg) |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Demo
![screencast](https://user-images.githubusercontent.com/7721625/52979302-3f073b80-33d5-11e9-9953-b70ebb349439.gif)
![screencast](https://user-images.githubusercontent.com/7721625/51978075-5bc0eb00-2489-11e9-953a-0965b154557b.gif)
## Performance
This App is optimized to handle thousands of topics and at least hundreds of thousands messages per minute.
So don't worry if you got >=30k topics and >=100.000 messages per minute.
So don't worry if you got >=10k topics and >=100k messages per minute.
One can also subscribe to custom topics insted of the "root wildcard #", therefore limiting the amount of messages.
Future releases will allow for a more granular selection of subscribed topics to handle
## Develop
PRs and issues are welcome
+19 -24
View File
@@ -1,47 +1,42 @@
# [MQTT Explorer](https://thomasnordquist.github.io/MQTT-Explorer/)
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
[![Build status](https://ci.appveyor.com/api/projects/status/c35tkm29rm4m5364/branch/master?svg=true)](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
### Version {{ version }}
A MQTT Tool to get a quick overview of your MQTT topics and integrate new devices/services more efficiently.
- Visualize topics and topic activity
- Delete retained topics
- Search/filter topics
- Delete topic recursively
- Publish topics
- Plot numeric topics
- Keep a history of each topic
See the whole picture of your broker.
- What topics are active?
- Which retained topics are abandoned?
- Where is this new device / service I connected to the broker?
MQTT-Explorer strives to be THE swiss-army-knife tool.
The perfect tool to integrate new services, IoT devices in your network.
The hierarchical view allows for a quick understanding what is going on on your broker.
## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | <a href="https://www.microsoft.com/store/apps/9PP8SFM082WD?ocid=badge"><img src="https://assets.windowsphone.com/85864462-9c82-451e-9355-a3d5f874397a/English_get-it-from-MS_InvariantCulture_Default.png" width="150" /></a><br/>{{windowsTargets}} |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | {{linuxTargets}}<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![ubuntu](https://user-images.githubusercontent.com/7721625/51445401-a5306e00-1d04-11e9-9b9b-20e196b82142.png) | Ubuntu | <a href="https://snapcraft.io/mqtt-explorer" title="Get it from the Snap Store"><img src="https://snapcraft.io/static/images/badges/en/snap-store-black.svg" width="150" /><br/>[Ubuntu Store](snap://mqtt-explorer) |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | {{macTargets}} |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
The app is available for Windows ({{windowsTargets}}), Linux ({{linuxTargets}}) and Mac ({{macTargets}}).
## Screenshots
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
## Download
| Platform | | Downloads |
|:----------:|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | {{windowsTargets}} |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | {{linuxTargets}}<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | {{macTargets}} |
[More Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
## Demo
![screencast](https://user-images.githubusercontent.com/7721625/52979302-3f073b80-33d5-11e9-9953-b70ebb349439.gif)
![screencast](https://user-images.githubusercontent.com/7721625/51978075-5bc0eb00-2489-11e9-953a-0965b154557b.gif)
## Performance
This App is optimized to handle thousands of topics and at least hundreds of thousands messages per minute.
So don't worry if you got >=30k topics and >=100.000 messages per minute.
So don't worry if you got >=10k topics and >=100k messages per minute.
One can also subscribe to custom topics insted of the "root wildcard #", therefore limiting the amount of messages.
Future releases will allow for a more granular selection of subscribed topics to handle
## Develop
PRs and issues are welcome
+1 -1
View File
@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>MQTT Explorer</title>
<title>MQTT-Explorer</title>
<script src="./bugtracking.bundle.js"></script>
<style>
+5 -9
View File
@@ -9,13 +9,10 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@material-ui/core": "^4.0.0-alpha.1",
"@material-ui/icons": "^3.0.2",
"@material-ui/lab": "^3.0.0-alpha.30",
"@material-ui/styles": "^3.0.0-alpha.10",
"@types/diff": "^4.0.1",
"@material-ui/core": "^4.0.0-alpha.0",
"@material-ui/icons": "^3.0.1",
"@material-ui/styles": "^3.0.0-alpha.8",
"@types/node": "^10.12.18",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.12",
@@ -29,7 +26,6 @@
"compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^1.0.4",
"css-loader": "^2.1.0",
"diff": "^4.0.1",
"electron-nucleus": "^1.11.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"html-webpack-plugin": "^4.0.0-beta.5",
@@ -38,17 +34,17 @@
"lodash.throttle": "^4.1.1",
"moving-average": "^1.0.0",
"number-abbreviate": "^2.0.0",
"prismjs": "^1.15.0",
"react": "16.8",
"react-ace": "^6.3.2",
"react-dom": "^16.7.0",
"react-json-view": "^1.19.1",
"react-keyboard-event-handler": "^1.4.1",
"react-redux": "^6.0.0",
"react-resize-detector": "^3.4.0",
"react-split-pane": "^0.1.85",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "^0.4.1",
"redux-thunk": "^2.3.0",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4",
+4 -10
View File
@@ -11,18 +11,18 @@ import { AppState } from './reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { default as SplitPane } from 'react-split-pane'
import { globalActions, settingsActions } from './actions'
import { globalActions } from './actions'
import { Theme, withStyles } from '@material-ui/core/styles'
const Settings = React.lazy(() => import('./components/Settings'))
interface Props {
name: string
connectionId: string
classes: any
settingsVisible: boolean
error?: string
actions: typeof globalActions
settingsActions: typeof settingsActions
actions: any
}
class App extends React.PureComponent<Props, {}> {
@@ -31,10 +31,6 @@ class App extends React.PureComponent<Props, {}> {
this.state = { }
}
public componentDidMount() {
this.props.settingsActions.loadSettings()
}
private renderError() {
if (this.props.error) {
const error = typeof this.props.error === 'string' ? this.props.error : JSON.stringify(this.props.error)
@@ -88,7 +84,7 @@ class App extends React.PureComponent<Props, {}> {
<UpdateNotifier />
<ConnectionSetup />
</ErrorBoundary>
</div>
</div >
)
}
}
@@ -98,7 +94,6 @@ const mapStateToProps = (state: AppState) => {
settingsVisible: state.settings.visible,
connectionId: state.connection.connectionId,
error: state.globalState.error,
highlightTopicUpdates: state.settings.highlightTopicUpdates,
}
}
@@ -147,7 +142,6 @@ const styles = (theme: Theme) => {
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(globalActions, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
}
}
+4 -1
View File
@@ -31,6 +31,7 @@ class RemoteStorage implements PersistantStorage {
const ack = makeStorageAcknoledgementEvent(transactionId)
return new Promise<void>((resolve, reject) => {
const callback = (msg: any) => {
console.log(msg)
if (msg && msg.error) {
reject(msg.error)
} else {
@@ -43,7 +44,7 @@ class RemoteStorage implements PersistantStorage {
})
}
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
const transactionId = v4()
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageStoreEvent, { data, transactionId, store: identifier.id })
@@ -56,6 +57,8 @@ class RemoteStorage implements PersistantStorage {
const promise = new Promise<Model>((resolve, reject) => {
const callback = (msg: any) => {
console.log(msg)
if (msg.error) {
reject(msg.error)
} else {
+1
View File
@@ -3,6 +3,7 @@ import { EventDispatcher } from '../../events'
export class TopicViewModel {
private selected: boolean
public change = new EventDispatcher<void, TopicViewModel>(this)
public attached = true // When the viewmodel is attached it's always visible
public constructor() {
this.selected = false
+11 -41
View File
@@ -1,19 +1,18 @@
import * as q from '../../../backend/src/Model'
import * as url from 'url'
import { Action, ActionTypes } from '../reducers/Connection'
import { AppState } from '../reducers'
import { DataSourceState, MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { globalActions } from '.'
import { showError } from './Global'
import { showTree } from './Tree'
import { TopicViewModel } from '../TopicViewModel'
import {
addMqttConnectionEvent,
makeConnectionStateEvent,
removeConnection,
rendererEvents,
} from '../../../events'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { MqttOptions } from '../../../backend/src/DataSource'
import { showTree } from './Tree'
import { TopicViewModel } from '../TopicViewModel'
import { showError } from './Global'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
@@ -24,39 +23,14 @@ export const connect = (options: MqttOptions, connectionId: string) => (dispatch
rendererEvents.subscribe(event, (dataSourceState) => {
console.log(dataSourceState)
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
}
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(connected(tree, host!))
dispatch(showTree(tree))
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
dispatch(updateHealth(dataSourceState))
})
}
const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<any>, getState: () => AppState) => {
let state
if (dataSourceState.connecting) {
state = 'connecting'
} else if (!dataSourceState.connected) {
state = 'offline'
dispatch(globalActions.showError('Disconnected from server'))
} else if (dataSourceState.connected) {
state = 'online'
} else {
state = undefined
}
console.log(state)
dispatch({
type: ActionTypes.CONNECTION_SET_HEALTH,
health: state,
})
}
@@ -73,11 +47,7 @@ export const connecting: (connectionId: string) => Action = (connectionId: strin
export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId, tree } = getState().connection
if (connectionId) {
rendererEvents.emit(removeConnection, connectionId)
rendererEvents.unsubscribeAll(makeConnectionStateEvent(connectionId))
}
rendererEvents.emit(removeConnection, connectionId)
tree && tree.stopUpdating()
dispatch(showTree(undefined))
-1
View File
@@ -35,7 +35,6 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
console.log('store settings')
await persistantStorage.store(storedConnectionsIdentifier, getState().connectionManager.connections)
} catch (error) {
dispatch(showError(error))
+2 -2
View File
@@ -1,7 +1,7 @@
import { Action, ActionTypes } from '../reducers/Publish'
import { ActionTypes, Action } from '../reducers/Publish'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events'
import { rendererEvents, makePublishEvent } from '../../../events'
export const setTopic = (topic?: string): Action => {
return {
+14 -76
View File
@@ -1,92 +1,30 @@
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { autoExpandLimitSet } from '../components/Settings'
import { batchActions } from 'redux-batched-actions'
import { default as persistantStorage, StorageIdentifier } from '../PersistantStorage'
import { Action, ActionTypes, TopicOrder } from '../reducers/Settings'
import { Dispatch } from 'redux'
import { showError } from './Global'
import { showTree } from './Tree'
import { AppState } from '../reducers'
import * as q from '../../../backend/src/Model'
import { batchActions } from 'redux-batched-actions'
import { autoExpandLimitSet } from '../components/Settings'
import { TopicViewModel } from '../TopicViewModel'
import {
ActionTypes,
SettingsState,
TopicOrder,
} from '../reducers/Settings'
const settingsIdentifier: StorageIdentifier<Partial<SettingsState>> = {
id: 'Settings',
}
export const loadSettings = () => async (dispatch: Dispatch<any>, _getState: () => AppState) => {
try {
const settings = await persistantStorage.load(settingsIdentifier)
dispatch({
settings,
type: ActionTypes.SETTINGS_DID_LOAD_SETTINGS,
})
} catch (error) {
dispatch(showError(error))
}
}
export const storeSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const settings = {
...getState().settings,
topicFilter: undefined,
visible: undefined,
}
try {
await persistantStorage.store(settingsIdentifier, settings)
} catch (error) {
dispatch(showError(error))
}
}
export const setAutoExpandLimit = (autoExpandLimit: number = 0) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
export const setAutoExpandLimit = (autoExpandLimit: number = 0): Action => {
return {
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
})
dispatch(storeSettings())
}
}
export const selectTopicWithMouseOver = (selectTopicWithMouseOver: boolean) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
selectTopicWithMouseOver,
type: ActionTypes.SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER,
})
dispatch(storeSettings())
}
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
})
dispatch(storeSettings())
}
export const toggleSettingsVisibility = () => (dispatch: Dispatch<any>, _getState: () => AppState) => {
dispatch({
export const toggleSettingsVisibility = (): Action => {
return {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY,
})
dispatch(storeSettings())
}
}
export const togglehighlightTopicUpdates = () => (dispatch: Dispatch<any>, _getState: () => AppState) => {
dispatch({
type: ActionTypes.SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY,
})
dispatch(storeSettings())
}
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none) => (dispatch: Dispatch<any>, _getState: () => AppState) => {
dispatch({
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none): Action => {
return {
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
})
dispatch(storeSettings())
}
}
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
+5 -30
View File
@@ -1,46 +1,21 @@
import * as q from '../../../backend/src/Model'
import { Action, Dispatch } from 'redux'
import { Dispatch, Action } from 'redux'
import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events'
export const clearRetainedTopic = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic } = getState().tree
if (!selectedTopic) {
return
}
dispatch(clearTopic(selectedTopic, false))
}
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId } = getState().connection
if (!connectionId) {
if (!selectedTopic || !connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: topic.path(),
topic: selectedTopic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
console.log('deleting', topic.path())
rendererEvents.emit(publishEvent, mqttMessage)
if (recursive) {
topic.childTopics()
.filter(topic => Boolean(topic.message && topic.message.value))
.slice(0, subtopicClearLimit)
.forEach((topic) => {
console.log('deleting', topic.path())
const mqttMessage = {
topic: topic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
})
}
}
+25 -4
View File
@@ -1,10 +1,11 @@
import * as q from '../../../backend/src/Model'
import { ActionTypes } from '../reducers/Tree'
import { AnyAction, Dispatch } from 'redux'
import { AppState } from '../reducers'
import { batchActions } from 'redux-batched-actions'
import { ActionTypes } from '../reducers/Tree'
import * as q from '../../../backend/src/Model'
import { Dispatch, AnyAction } from 'redux'
import { setTopic } from './Publish'
import { TopicViewModel } from '../TopicViewModel'
import { batchActions } from 'redux-batched-actions'
import { treeActions } from '.';
const debounce = require('lodash.debounce')
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
@@ -43,6 +44,26 @@ const debouncedSelectTopic = debounce((topic: q.TreeNode<TopicViewModel>, dispat
}
}, 70)
export const handleKeyEvent = (key: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const state = getState()
const { tree, selectedTopic } = state.tree
if (!tree) {
return
}
if (!selectedTopic) {
return dispatch(selectTopic(tree.firstNode()))
}
const visibleTopics: q.TreeNode<TopicViewModel>[] = tree.childTopics().filter(topic => topic.viewModel && topic.viewModel.attached)
if (key === 'down') {
debugger
const selectedIndex = visibleTopics.indexOf(selectedTopic)
const nextIndex = (visibleTopics.length - 1) % (selectedIndex + 1)
return dispatch(selectTopic(visibleTopics[nextIndex]))
}
}
export const showTree = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
const visibleTree = getState().tree.tree
const connectionTree = getState().connection.tree
+5 -5
View File
@@ -1,10 +1,10 @@
import * as connectionActions from './Connection'
import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global'
import * as publishActions from './Publish'
import * as settingsActions from './Settings'
import * as sidebarActons from './Sidebar'
import * as publishActions from './Publish'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection'
import * as sidebarActons from './Sidebar'
import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global'
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons, connectionManagerActions, globalActions }
+8 -6
View File
@@ -1,11 +1,12 @@
import * as q from '../../../backend/src/Model'
import * as React from 'react'
import { AppState } from '../reducers'
import { connect } from 'react-redux'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../TopicViewModel'
import { Typography } from '@material-ui/core'
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { connect } from 'react-redux'
import { TopicViewModel } from '../TopicViewModel'
const abbreviate = require('number-abbreviate')
interface Stats {
@@ -23,6 +24,7 @@ const styles: StyleRulesCallback = theme => ({
height: '224px',
backgroundColor: 'rebeccapurple',
marginBottom: 0,
marginTop: 'auto',
padding: '8px',
},
})
-149
View File
@@ -1,149 +0,0 @@
import * as diff from 'diff'
import * as Prism from 'prismjs'
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json'
import 'prismjs/themes/prism-tomorrow.css'
interface Props {
previous: string
current: string
nameOfCompareMessage: string
language?: 'json'
classes: any
}
class CodeDiff extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
public render() {
const changes = diff.diffLines(this.props.previous, this.props.current)
const styledLines = Prism.highlight(this.props.current, Prism.languages.json).split('\n')
let lineNumber = 0
const code = changes.map((change, key) => {
const hasStyledCode = change.removed !== true
const changedLines = change.count || 0
if (hasStyledCode && this.props.language === 'json') {
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
const lines = currentLines.map((l, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)} dangerouslySetInnerHTML={{ __html: l }} /></div>
})
lineNumber += changedLines
return <div key={key}>{lines}</div>
}
return this.trimNewlineRight(change.value)
.split('\n')
.map((line, idx) => {
return <div key={`${key}-${idx}`} className={this.props.classes.line}><span className={this.cssClassForChange(change)}>{line}</span></div>
})
})
return (
<div>
<pre className={`language-json ${this.props.classes.codeBlock}`}>
{code}
</pre>
{this.renderChangeAmount(changes)}
</div>
)
}
private renderChangeAmount(changes: Diff.Change[]) {
const additions = changes.map(change => (change.added === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
const deletions = changes.map(change => (change.removed === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
if (additions === 0 && deletions === 0) {
return null
}
return (
<span style={{ display: 'block', marginBottom: '8px', float: 'right' }}>
<span>
Comparing with <b>{this.props.nameOfCompareMessage}</b> message:&nbsp;
<span className={this.props.classes.additions}>+ {additions} line{additions === 1 ? '' : 's'}</span>
, <span className={this.props.classes.deletions}>- {deletions} line{deletions === 1 ? '' : 's'}</span>
</span>
</span>
)
}
private trimNewlineRight(str: string) {
if (str.slice(-1) === '\n') {
return str.slice(0, -1)
}
return str
}
private cssClassForChange(change: diff.Change) {
if (change.added === true) {
return this.props.classes.addition
}
if (change.removed === true) {
return this.props.classes.deletion
}
return this.props.classes.noChange
}
}
const style = (theme: Theme) => {
const baseStyle = {
width: '100%',
}
const before = {
margin: '0 2px 0 -9px',
}
return {
additions: {
color: 'rgb(10, 255, 10)',
},
deletions: {
color: 'rgb(255, 10, 10)',
},
line: {
lineHeight: 'normal',
},
codeBlock: {
fontSize: '12px',
maxHeight: '200px',
marginLeft: '-16px',
},
noChange: {
...baseStyle,
'&:before': {
...before,
},
},
deletion: {
...baseStyle,
backgroundColor: 'rgba(255, 10, 10, 0.3)',
'&:before': {
...before,
content: "'-'",
},
'&:hover': {
backgroundColor: 'rgba(255, 10, 10, 0.6)',
},
},
addition: {
...baseStyle,
backgroundColor: 'rgba(10, 255, 10, 0.3)',
'&:hover': {
backgroundColor: 'rgba(10, 255, 10, 0.5)',
},
'&:before': {
...before,
content: "'+'",
},
},
}
}
export default withStyles(style)(CodeDiff)
@@ -1,54 +0,0 @@
import * as React from 'react'
import DeviceHubOutlined from '@material-ui/icons/DeviceHubOutlined'
import { AppState } from '../reducers'
import { connect } from 'react-redux'
import { ConnectionHealth } from '../reducers/Connection'
import { green, orange, red } from '@material-ui/core/colors'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { Tooltip } from '@material-ui/core'
const styles: StyleRulesCallback = theme => ({
offline: {
color: red[700],
},
online: {
color: green[500],
},
connecting: {
color: orange[600],
},
})
interface Props {
classes: any
connected: boolean
health?: ConnectionHealth
}
class ConnectionHealthIndicator extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
}
public render() {
const { classes, health, connected } = this.props
if (!health || !connected) {
return null
}
return (
<Tooltip title={`Connection health "${health}"`}>
<DeviceHubOutlined className={classes[health]} />
</Tooltip>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
health: state.connection.health,
connected: state.connection.connected || state.connection.connecting,
}
}
export default connect(mapStateToProps)(withStyles(styles)(ConnectionHealthIndicator))
@@ -66,10 +66,10 @@ class ConnectionSettings extends React.Component<Props, State> {
</Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}>
<List
className={`${classes.topicList} advanced-connection-settings-topic-list`}
className={classes.topicList}
component="nav"
>
<div className={classes.list}>
<div className={this.props.classes.list}>
{this.renderSubscriptions()}
</div>
</List>
@@ -79,7 +79,6 @@ class ConnectionSettings extends React.Component<Props, State> {
className={classes.fullWidth}
label="MQTT Client ID"
margin="normal"
value={this.props.connection.clientId}
onChange={this.handleChange('clientId')}
/>
</Grid>
@@ -14,6 +14,7 @@ import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
CircularProgress,
FormControl,
FormControlLabel,
Grid,
@@ -25,7 +26,6 @@ import {
Switch,
TextField,
} from '@material-ui/core'
import ConnectionHealthIndicator from '../ConnectionHealthIndicator'
interface Props {
connection: ConnectionOptions
@@ -284,7 +284,7 @@ class ConnectionSettings extends React.Component<Props, State> {
if (this.props.connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
<ConnectionHealthIndicator />&nbsp;&nbsp;Abort
<CircularProgress size={22} style={{ marginRight: '10px' }} color="secondary" /> Abort
</Button>
)
}
@@ -71,17 +71,16 @@ class ConnectionSetup extends React.Component<Props, {}> {
}
}
const connectionHeight = '440px'
const styles = (theme: Theme) => ({
title: {
color: theme.palette.text.primary,
whiteSpace: 'nowrap' as 'nowrap',
},
root: {
margin: `calc((100vh - ${connectionHeight}) / 2) auto 0 auto`,
margin: '13vw auto 0 auto',
minWidth: '800px',
maxWidth: '850px',
height: connectionHeight,
height: '440px',
outline: 'none' as 'none',
display: 'flex' as 'flex',
},
+21 -30
View File
@@ -1,10 +1,9 @@
import * as React from 'react'
import Check from '@material-ui/icons/Check'
import CustomIconButton from './CustomIconButton'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import FileCopy from '@material-ui/icons/FileCopy'
import Check from '@material-ui/icons/Check'
import green from '@material-ui/core/colors/green'
import { Snackbar, SnackbarContent, Tooltip } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { withStyles, Theme } from '@material-ui/core/styles'
const copy = require('copy-text-to-clipboard')
@@ -33,36 +32,28 @@ class Copy extends React.Component<Props, State> {
public render() {
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" />
? <FileCopy fontSize="inherit" style={{ cursor: 'pointer' }} onClick={this.handleClick} />
: <Check fontSize="inherit" style={{ cursor: 'default' }} />
return (
return <span>
<span style={{ fontSize: '16px' }}>{icon}</span>
<span>
<Tooltip placement="top" title="Copy to clipboard">
<span style={{ fontSize: '16px' }}>
<CustomIconButton onClick={this.handleClick} >
{icon}
</CustomIconButton>
</span>
</Tooltip>
<span>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.snackBarOpen}
autoHideDuration={2000}
onClose={() => { this.setState({ snackBarOpen: false }) }}
>
<SnackbarContent
className={this.props.classes.snackbar}
message="Copied to clipboard"
/>
</Snackbar>
</span>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.snackBarOpen}
autoHideDuration={2000}
onClose={() => { this.setState({ snackBarOpen: false }) }}
>
<SnackbarContent
className={this.props.classes.snackbar}
message="Copied to clipboard"
/>
</Snackbar>
</span>
)
</span>
}
private handleClick = (event: React.MouseEvent) => {
-34
View File
@@ -1,34 +0,0 @@
import * as React from 'react'
import { IconButton } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
interface Props {
onClick: any,
classes: any
}
const styles = (theme: Theme) => ({
button: {
padding: '6px',
fontSize: '1.2em',
},
})
class CustomIconButton extends React.Component<Props, {}> {
constructor(props: Props) {
super(props)
}
public render() {
return (
<IconButton className={this.props.classes.button} onClick={this.onClick}>{this.props.children}</IconButton>
)
}
private onClick = (event: React.MouseEvent) => {
event.stopPropagation()
this.props.onClick(event)
}
}
export default withStyles(styles)(CustomIconButton)
+13 -86
View File
@@ -1,14 +1,6 @@
import * as React from 'react'
import BrokerStatistics from './BrokerStatistics'
import ChevronRight from '@material-ui/icons/ChevronRight'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions } from '../actions'
import { shell } from 'electron'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { TopicOrder } from '../reducers/Settings'
import { AppState } from '../reducers'
import {
Divider,
Drawer,
@@ -18,10 +10,15 @@ import {
MenuItem,
Select,
Typography,
Switch,
Tooltip,
} from '@material-ui/core'
const sha1 = require('sha1')
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import ChevronRight from '@material-ui/icons/ChevronRight'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions, treeActions } from '../actions'
import { TopicOrder } from '../reducers/Settings'
import BrokerStatistics from './BrokerStatistics'
export const autoExpandLimitSet = [{
limit: 0,
@@ -44,7 +41,6 @@ const styles: StyleRulesCallback = theme => ({
drawer: {
backgroundColor: theme.palette.background.default,
flexShrink: 0,
userSelect: 'none' as 'none',
},
paper: {
width: '300px',
@@ -58,25 +54,15 @@ const styles: StyleRulesCallback = theme => ({
minWidth: '150px',
margin: `auto ${theme.spacing(1)} auto ${theme.spacing(2)}px`,
},
author: {
margin: 'auto 8px 8px auto',
color: theme.palette.text.hint,
cursor: 'pointer' as 'pointer',
},
switchBase: {
height: theme.spacing(4),
},
})
interface Props {
actions: typeof settingsActions
autoExpandLimit: number
classes: any
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
visible: boolean
store?: any
topicOrder: TopicOrder
visible: boolean
classes: any
actions: typeof settingsActions
}
class Settings extends React.Component<Props, {}> {
@@ -110,69 +96,12 @@ class Settings extends React.Component<Props, {}> {
{this.renderAutoExpand()}
{this.renderNodeOrder()}
{this.renderHighlightTopicUpdates()}
{this.selectTopicsOnMouseOver()}
</div>
<Tooltip placement="top" title="App Author">
<Typography className={classes.author} onClick={this.openGithubPage}>
by Thomas Nordquist
</Typography>
</Tooltip>
<BrokerStatistics />
</Drawer>
)
}
private openGithubPage = () => {
shell.openExternal('https://github.com/thomasnordquist')
}
private renderHighlightTopicUpdates() {
const { highlightTopicUpdates, actions } = this.props
return this.renderSwitch('Show Activity', highlightTopicUpdates, actions.togglehighlightTopicUpdates, 'Topics blink when a new message arrives')
}
private renderSwitch(title: string, checked: boolean, action: any, tooltip: string) {
const { classes } = this.props
const clickHandler = (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
action()
}
return (
<div style={{ padding: '8px', display: 'flex' }}>
<Tooltip title={tooltip}>
<InputLabel
htmlFor={`toggle-${sha1(title)}`}
onClick={clickHandler}
style={{ flex: '1', paddingTop: '8px' }}
>
{title}
</InputLabel>
</Tooltip>
<Tooltip title={tooltip}>
<Switch
name={`toggle-${sha1(title)}`}
checked={checked}
onChange={action}
color="primary"
classes={{ switchBase: classes.switchBase }}
/>
</Tooltip>
</div>
)
}
private selectTopicsOnMouseOver() {
const { actions, selectTopicWithMouseOver } = this.props
const toggle = () => actions.selectTopicWithMouseOver(!selectTopicWithMouseOver)
return this.renderSwitch('Quick Preview', selectTopicWithMouseOver, toggle, 'Select topics on mouse over')
}
private renderAutoExpand() {
const { classes, autoExpandLimit } = this.props
@@ -203,7 +132,7 @@ class Settings extends React.Component<Props, {}> {
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Topic Order</InputLabel>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Topic order</InputLabel>
<Select
value={topicOrder}
onChange={this.onChangeSorting}
@@ -231,8 +160,6 @@ const mapStateToProps = (state: AppState) => {
autoExpandLimit: state.settings.autoExpandLimit,
topicOrder: state.settings.topicOrder,
visible: state.settings.visible,
highlightTopicUpdates: state.settings.highlightTopicUpdates,
selectTopicWithMouseOver: state.settings.selectTopicWithMouseOver,
}
}
+4 -5
View File
@@ -6,7 +6,6 @@ import { Theme, withStyles } from '@material-ui/core/styles'
interface HistoryItem {
title: JSX.Element | string
value: string | any
selected?: boolean
}
interface Props {
@@ -29,18 +28,18 @@ class MessageHistory extends React.Component<Props, State> {
}
public renderHistory() {
const style = (element: HistoryItem) => ({
backgroundColor: element.selected ? 'rgba(120, 120, 120, 0.6)' : 'rgba(80, 80, 80, 0.6)',
const style = {
backgroundColor: 'rgba(80, 80, 80, 0.6)',
margin: '8px',
padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit',
})
}
const messageStyle: React.CSSProperties = { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }
const elements = this.props.items.map((element, index) => (
<div
key={index}
style={style(element)}
style={style}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
>
<div><i>{element.title}</i></div>
@@ -1,17 +1,16 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import BarChart from '@material-ui/icons/BarChart'
import DateFormatter from '../helper/DateFormatter'
import History from './History'
import { TopicViewModel } from '../../TopicViewModel'
const PlotHistory = React.lazy(() => import('./PlotHistory'))
const throttle = require('lodash.throttle')
interface Props {
node?: q.TreeNode<TopicViewModel>
selected?: q.Message
onSelect: (message: q.Message) => void
}
@@ -53,7 +52,6 @@ class MessageHistory extends React.Component<Props, State> {
const historyElements = history.reverse().map(message => ({
title: <DateFormatter date={message.received} />,
value: message.value,
selected: message && message === this.props.selected,
}))
const numericMessages = history.filter(message => !isNaN(parseFloat(message.value)))
@@ -81,10 +79,8 @@ class MessageHistory extends React.Component<Props, State> {
}
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
if (message) {
this.props.onSelect(message)
}
const message = this.props.node && this.props.node.messageHistory.toArray()[index]
this.props.onSelect(message)
}
}
+3 -2
View File
@@ -1,7 +1,8 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { TopicViewModel } from '../../TopicViewModel'
import * as q from '../../../../backend/src/Model'
import { Typography } from '@material-ui/core'
import { TopicViewModel } from '../../TopicViewModel'
interface Props {
node: q.TreeNode<TopicViewModel>
+5 -4
View File
@@ -1,9 +1,10 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import DateFormatter from '../helper/DateFormatter'
const { XYPlot, XAxis, LineMarkSeries, Hint, YAxis, HorizontalGridLines, LineSeries } = require('react-vis')
import { default as ReactResizeDetector } from 'react-resize-detector'
import DateFormatter from '../helper/DateFormatter'
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = require('react-vis')
interface Props {
messages: q.Message[]
+2 -31
View File
@@ -19,7 +19,6 @@ import {
Checkbox,
MenuItem,
Tooltip,
Fab,
} from '@material-ui/core'
// tslint:disable-next-line
@@ -28,10 +27,10 @@ import { AppState } from '../../../reducers'
import History from '../History'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import Clear from '@material-ui/icons/Clear'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { publishActions, globalActions } from '../../../actions'
import { publishActions } from '../../../actions'
import ClearAdornment from '../../helper/ClearAdornment'
import { TopicViewModel } from '../../../TopicViewModel'
@@ -41,7 +40,6 @@ interface Props {
topic?: string
payload?: string
actions: typeof publishActions
globalActions: typeof globalActions
retain: boolean
editorMode: string
qos: 0 | 1 | 2
@@ -153,37 +151,11 @@ class Publish extends React.Component<Props, State> {
)
}
private formatJson = () => {
if (this.props.payload) {
try {
const str = JSON.stringify(JSON.parse(this.props.payload), undefined, ' ')
this.updatePayload(str)
} catch (error) {
this.props.globalActions.showError(`Format error: ${error.message}`)
}
}
}
private renderFormatJson() {
if (this.props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab style={{ width: '36px', height: '36px', marginLeft: '8px' }} onClick={this.formatJson} id="sidebar-publish-format-json">
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
}
private editorMode() {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
{this.renderEditorModeSelection()}
{this.renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()}
</div>
@@ -327,7 +299,6 @@ class Publish extends React.Component<Props, State> {
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(publishActions, dispatch),
globalActions: bindActionCreators(globalActions, dispatch),
}
}
+49 -122
View File
@@ -1,48 +1,43 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import Code from '@material-ui/icons/Code'
import Copy from '../Copy'
import CustomIconButton from '../CustomIconButton'
import DateFormatter from '../helper/DateFormatter'
import Delete from '@material-ui/icons/Delete'
import ExpandMore from '@material-ui/icons/ExpandMore'
import MessageHistory from './MessageHistory'
import NodeStats from './NodeStats'
import Reorder from '@material-ui/icons/Reorder'
import ToggleButton from '@material-ui/lab/ToggleButton'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import Topic from './Topic'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions, sidebarActons } from '../../actions'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../TopicViewModel'
import { ValueRendererDisplayMode } from '../../reducers/Settings'
import * as q from '../../../../backend/src/Model'
import {
Button,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Fade,
Paper,
Popper,
Typography,
Tooltip,
Badge,
} from '@material-ui/core'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { sidebarActons } from '../../actions'
import { AppState } from '../../reducers'
import Copy from '../Copy'
import DateFormatter from '../helper/DateFormatter'
import ExpandMore from '@material-ui/icons/ExpandMore'
import Clear from '@material-ui/icons/Clear'
import MessageHistory from './MessageHistory'
import NodeStats from './NodeStats'
import Topic from './Topic'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { TopicViewModel } from '../../TopicViewModel'
import { default as ReactResizeDetector } from 'react-resize-detector'
const throttle = require('lodash.throttle')
const Publish = React.lazy(() => import('./Publish/Publish'))
const ValueRenderer = React.lazy(() => import('./ValueRenderer'))
interface Props {
node?: q.TreeNode<TopicViewModel>
actions: typeof sidebarActons
settingsActions: typeof settingsActions
valueRendererDisplayMode: ValueRendererDisplayMode
classes: any
connectionId?: string
node?: q.TreeNode<TopicViewModel>,
actions: typeof sidebarActons,
classes: any,
connectionId?: string,
}
interface State {
@@ -89,7 +84,7 @@ class Sidebar extends React.Component<Props, State> {
public render() {
return (
<div id="Sidebar" className={this.props.classes.drawer}>
<div className={this.props.classes.drawer}>
{this.renderNode()}
</div>
)
@@ -97,105 +92,42 @@ class Sidebar extends React.Component<Props, State> {
private detailsStyle = { padding: '0px 16px 8px 8px', display: 'block' }
private renderTopicDeleteButton() {
if (!this.props.node || (!this.props.node.message || !this.props.node.message.value)) {
return null
}
return (
<CustomIconButton onClick={() => this.deleteTopic(this.props.node)}>
<Tooltip title="Clear this topic">
<Delete />
</Tooltip>
</CustomIconButton>
)
}
private renderRecursiveTopicDeleteButton() {
const deleteLimit = 50
const topicCount = this.props.node ? this.props.node.childTopicCount() : 0
if ((!this.props.node || topicCount === 0) || (this.props.node.message && topicCount === 1)) {
return null
}
return (
<CustomIconButton onClick={() => this.deleteTopic(this.props.node, true, deleteLimit)}>
<Tooltip title={`Deletes up to ${deleteLimit} sub-topics with a single click`}>
<Badge
classes={{ badge: this.props.classes.badge }}
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
color="secondary"
>
<Delete color="action" />
</Badge>
</Tooltip>
</CustomIconButton>
)
}
private deleteTopic = (topic?: q.TreeNode<TopicViewModel>, recursive: boolean = false, maxCount = 50) => {
if (!topic) {
return
}
this.props.actions.clearTopic(topic, recursive, maxCount)
}
private renderActionButtons() {
const handleValue = (_e: React.MouseEvent, value: any) => {
this.props.settingsActions.setValueDisplayMode(value)
}
return (
<ToggleButtonGroup id="valueRendererDisplayMode" value={this.props.valueRendererDisplayMode} exclusive={true} onChange={handleValue}>
<ToggleButton value="diff" id="valueRendererDisplayMode-diff">
<Tooltip title="Show difference between the current and the last message">
<Code />
</Tooltip>
</ToggleButton>
<ToggleButton value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw value">
<Reorder />
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
)
}
private renderNode() {
const { classes, node } = this.props
const copyTopic = node ? <Copy value={node.path()} /> : null
const deleteTopic = this.renderTopicDeleteButton()
const deleteRecursiveTopic = this.renderRecursiveTopicDeleteButton()
const copyValue = node && node.message ? <Copy value={node.message.value} /> : null
const summaryStyle = { minHeight: '0' }
const summeryStyle = { minHeight: '0' }
return (
<div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}</Typography>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Topic {copyTopic}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel key="value" defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Value {copyValue}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
{this.messageMetaInfo()}
<div ref={this.valueRef}>
<React.Suspense fallback={<div>Loading...</div>}>
{this.renderValue()}
</React.Suspense>
<React.Suspense fallback={<div>Loading...</div>}>
<ReactResizeDetector handleWidth={true} onResize={this.valueRenderWidthChange} />
<ValueRenderer message={this.props.node && this.props.node.message} />
</React.Suspense>
</div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} selected={this.state.compareMessage} node={this.props.node} /></div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} node={this.props.node} /></div>
<Popper open={Boolean(this.state.compareMessage)} anchorEl={this.valueRef.current} placement="left" transition={true}>
{this.showValueComparison}
</Popper>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={true}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Publish</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
@@ -205,7 +137,7 @@ class Sidebar extends React.Component<Props, State> {
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel defaultExpanded={Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summeryStyle}>
<Typography className={classes.heading}>Stats</Typography>
</ExpansionPanelSummary>
{this.renderNodeStats()}
@@ -214,20 +146,18 @@ class Sidebar extends React.Component<Props, State> {
)
}
private renderValue() {
const node = this.props.node
if (!node || !node.message) {
return null
}
return (
<ValueRenderer
message={node.message}
messageHistory={node.messageHistory}
compareWith={this.state.compareMessage} />
)
private valueRenderWidthChange = (width: number) => {
this.setState({ valueRenderWidth: width })
}
private showValueComparison = (a: any) => (
<Fade {...a.TransitionProps} timeout={350}>
<Paper style={{ maxWidth: this.state.valueRenderWidth }}>
<ValueRenderer message={this.state.compareMessage} />
</Paper>
</Fade>
)
private messageMetaInfo() {
if (!this.props.node || !this.props.node.message || !this.props.node.mqttMessage) {
return null
@@ -248,9 +178,8 @@ class Sidebar extends React.Component<Props, State> {
)
return (
<div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}>
<div style={{ width: '100%', display: 'flex' }}>
<div style={{ flex: 6 }}><Typography>QoS: {this.props.node.mqttMessage.qos}</Typography></div>
<span style={{ marginTop: '-8px' }}>{this.renderActionButtons()}</span>
<div style={{ flex: 8, textAlign: 'center' }}>
{this.props.node.mqttMessage.retain ? retainedButton : null}
</div>
@@ -283,14 +212,12 @@ class Sidebar extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => {
return {
node: state.tree.selectedTopic,
valueRendererDisplayMode: state.settings.valueRendererDisplayMode,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(sidebarActons, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
}
}
+37 -79
View File
@@ -1,108 +1,66 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import CodeDiff from '../CodeDiff'
import { AppState } from '../../reducers'
import { connect } from 'react-redux'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { ValueRendererDisplayMode } from '../../reducers/Settings'
import * as q from '../../../../backend/src/Model'
import { Theme, withTheme } from '@material-ui/core/styles'
import { default as ReactJson } from 'react-json-view'
interface Props {
message: q.Message
messageHistory: q.RingBuffer<q.Message>
compareWith?: q.Message
renderMode: ValueRendererDisplayMode
message?: q.Message
theme: Theme
}
interface State {
width: number
}
class ValueRenderer extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { width: 0 }
}
class ValueRenderer extends React.Component<Props, {}> {
public render() {
return (
<div style={{ padding: '8px 0px 8px 8px' }}>
<ReactResizeDetector handleWidth={true} onResize={this.updateWidth} />
{this.renderValue()}
</div>
)
return <div style={{ padding: '8px 0px 8px 8px' }}>{this.renderValue()}</div>
}
public renderValue() {
const { message, messageHistory, compareWith, renderMode } = this.props
const previousMessages = messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {
compareMessage = message
const { message } = this.props
if (!message) {
return null
}
let json
try {
json = JSON.parse(message.value)
} catch (error) {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
}
if (typeof json === 'string') {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'number') {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
} else if (typeof json === 'boolean') {
return this.renderRawValue(message.value, compareMessage.value)
return this.renderRawValue(message.value)
} else {
const current = this.messageToPrettyJson(message) || message.value
const compare = this.messageToPrettyJson(compareMessage) || compareMessage.value
const language = current && compare ? 'json' : undefined
return this.renderDiff(current, compare, language)
const theme = (this.props.theme.palette.type === 'dark') ? 'monokai' : 'bright:inverted'
return (
<ReactJson
style={{ width: '100%', overflowY: 'scroll', wordBreak: 'break-all' }}
src={json}
name={null}
theme={theme}
/>
)
}
}
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
return (
<CodeDiff
previous={previous}
current={current}
language={language}
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
/>
)
}
private messageToPrettyJson(message?: q.Message): string | undefined {
if (!message || !message.value) {
return undefined
private renderRawValue(value: string) {
const style: React.CSSProperties = {
padding: '8px 12px 8px 12px',
backgroundColor: 'rgba(100, 100, 100, 0.55)',
wordBreak: 'break-all',
width: '100%',
overflow: 'auto hidden',
display: 'block',
lineHeight: '1.2em',
color: this.props.theme.palette.text.primary,
}
try {
const json = JSON.parse(message.value)
return JSON.stringify(json, undefined, ' ')
} catch {
return undefined
}
}
private updateWidth = (width: number) => {
if (width !== this.state.width) {
this.setState({ width })
}
}
private renderRawValue(value: string, compare: string) {
return this.renderDiff(value, compare)
return <pre style={style}><code>{value}</code></pre>
}
}
const mapStateToProps = (state: AppState) => {
return {
renderMode: state.settings.valueRendererDisplayMode,
}
}
export default connect(mapStateToProps)(ValueRenderer)
export default withTheme()(ValueRenderer)
+7 -9
View File
@@ -1,15 +1,8 @@
import * as React from 'react'
import ClearAdornment from './helper/ClearAdornment'
import CloudOff from '@material-ui/icons/CloudOff'
import ConnectionHealthIndicator from './ConnectionHealthIndicator'
import Menu from '@material-ui/icons/Menu'
import Search from '@material-ui/icons/Search'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, settingsActions } from '../actions'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import {
AppBar,
Button,
@@ -18,6 +11,12 @@ import {
Toolbar,
Typography,
} from '@material-ui/core'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, settingsActions } from '../actions'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
const styles: StyleRulesCallback = theme => ({
title: {
@@ -95,12 +94,11 @@ class TitleBar extends React.Component<Props, {}> {
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={actions.settings.toggleSettingsVisibility}>
<Menu />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Explorer</Typography>
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
{this.renderSearch()}
<Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.connection.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
<ConnectionHealthIndicator />
</Toolbar>
</AppBar>
)
+17 -9
View File
@@ -1,12 +1,14 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import TreeNode from './TreeNode'
import * as q from '../../../../backend/src/Model'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import TreeNode from './TreeNode'
import { connect } from 'react-redux'
import { TopicOrder } from '../../reducers/Settings'
import { TopicViewModel } from '../../TopicViewModel'
import { treeActions } from '../../actions'
import { bindActionCreators } from 'redux'
const ReactKeyboardEventHandler = require('react-keyboard-event-handler')
const MovingAverage = require('moving-average')
@@ -21,10 +23,9 @@ interface Props {
tree?: q.Tree<TopicViewModel>
filter: string
host?: string
topicOrder: TopicOrder
autoExpandLimit: number
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
}
interface State {
@@ -86,6 +87,12 @@ class Tree extends React.PureComponent<Props, State> {
}, Math.max(0, timeUntilNextUpdate))
}
private handleKeyEvent = (key: string, event: any) => {
event.stopPropagation()
event.preventDefault()
this.props.actions.handleKeyEvent(key)
}
public render() {
const { tree, filter } = this.props
if (!tree) {
@@ -99,6 +106,11 @@ class Tree extends React.PureComponent<Props, State> {
return (
<div style={style}>
<ReactKeyboardEventHandler
isExclusive={true}
handleKeys={['space', 'enter', 'delete', 'backspace', 'left', 'up', 'down', 'right']}
onKeyEvent={this.handleKeyEvent}
/>
<TreeNode
key={tree.hash()}
animateChages={true}
@@ -111,8 +123,6 @@ class Tree extends React.PureComponent<Props, State> {
topicOrder={this.props.topicOrder}
lastUpdate={tree.lastUpdate}
didSelectTopic={this.props.actions.selectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
</div>
)
@@ -130,8 +140,6 @@ const mapStateToProps = (state: AppState) => {
host: state.connection.host,
autoExpandLimit: state.settings.autoExpandLimit,
topicOrder: state.settings.topicOrder,
highlightTopicUpdates: state.settings.highlightTopicUpdates,
selectTopicWithMouseOver: state.settings.selectTopicWithMouseOver,
}
}
+28 -48
View File
@@ -1,11 +1,12 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import { Theme, withStyles } from '@material-ui/core/styles'
import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle'
import { Theme, withStyles } from '@material-ui/core/styles'
import { TopicOrder } from '../../reducers/Settings'
import { TopicViewModel } from '../../TopicViewModel'
const debounce = require('lodash.debounce')
declare var performance: any
@@ -28,11 +29,8 @@ const styles = (theme: Theme) => {
cursor: 'pointer',
marginTop: '-1px',
},
subnodes: {
marginLeft: theme.spacing(1.5),
},
selected: {
backgroundColor: 'rgba(170, 170, 170, 0.55)',
backgroundColor: 'rgba(200, 200, 200, 0.55)',
},
hover: {
backgroundColor: 'rgba(100, 100, 100, 0.55)',
@@ -53,8 +51,6 @@ interface Props {
autoExpandLimit: number
lastUpdate: number
didSelectTopic: any
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
}
interface State {
@@ -68,10 +64,12 @@ class TreeNode extends React.Component<Props, State> {
private dirtyEdges: boolean = true
private dirtyMessage: boolean = true
private animationDirty: boolean = false
private lastRenderTime = 0
private cssAnimationWasSetAt?: number
private willUpdateTime: number = performance.now()
private titleRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
private nodeRef?: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>()
private subnodesDidchange = () => {
@@ -131,6 +129,7 @@ class TreeNode extends React.Component<Props, State> {
public componentWillUnmount() {
const { treeNode } = this.props
this.removeSubscriber(treeNode)
this.titleRef = undefined
this.nodeRef = undefined
}
@@ -183,35 +182,30 @@ class TreeNode extends React.Component<Props, State> {
const isDirty = this.dirtyEdges || this.dirtyMessage || this.dirtySubnodes
this.dirtyEdges = this.dirtyMessage = this.dirtySubnodes = false
const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot && this.props.highlightTopicUpdates
const shouldStartAnimation = (isDirty && !this.animationDirty) && !this.props.isRoot
const animation = shouldStartAnimation ? { willChange: 'auto', translateZ: 0, animation: 'example 0.5s' } : {}
this.animationDirty = shouldStartAnimation
const highlightClass = this.state.selected ? this.props.classes.selected : (this.state.mouseOver ? this.props.classes.hover : '')
return (
<div>
<div
key={this.props.treeNode.hash()}
className={`${classes.node} ${this.props.className}`}
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
onClick={this.didClickTitle}
ref={this.nodeRef}
>
<TreeNodeTitle
style={animation}
toggleCollapsed={this.toggleCollapsed}
didSelectNode={this.didSelectTopic}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
className={highlightClass}
/>
</div>
<div className={classes.subnodes}>
{this.renderNodes()}
</div>
<div
key={this.props.treeNode.hash()}
className={`${classes.node} ${this.props.className}`}
onClick={this.didClickNode}
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
ref={this.nodeRef}
>
<TreeNodeTitle
style={animation}
collapsed={this.collapsed()}
treeNode={this.props.treeNode}
name={this.props.name}
didSelectNode={this.didSelectTopic}
className={highlightClass}
/>
{this.renderNodes()}
</div>
)
}
@@ -220,18 +214,9 @@ class TreeNode extends React.Component<Props, State> {
this.props.didSelectTopic(this.props.treeNode)
}
private didClickTitle = (event: React.MouseEvent) => {
event.preventDefault()
this.props.didSelectTopic(this.props.treeNode)
this.toggle()
}
private mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(true)
if (this.props.selectTopicWithMouseOver && this.props.treeNode && this.props.treeNode.message && this.props.treeNode.message.value) {
this.props.didSelectTopic(this.props.treeNode)
}
}
private mouseOut = (event: React.MouseEvent) => {
@@ -243,16 +228,13 @@ class TreeNode extends React.Component<Props, State> {
this.setState({ mouseOver: hover })
}, 45)
private toggleCollapsed = (event: React.MouseEvent) => {
private didClickNode = (event: React.MouseEvent) => {
event.stopPropagation()
this.toggle()
this.didSelectTopic()
}
private renderNodes() {
if (this.collapsed()) {
return null
}
return (
<TreeNodeSubnodes
animateChanges={this.props.animateChages}
@@ -262,8 +244,6 @@ class TreeNode extends React.Component<Props, State> {
topicOrder={this.props.topicOrder}
lastUpdate={this.props.treeNode.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
)
}
+4 -6
View File
@@ -12,13 +12,13 @@ export interface Props {
filter?: string
collapsed?: boolean | undefined
classes: any
lastUpdate: number
topicOrder: TopicOrder
selectedTopic?: q.TreeNode<TopicViewModel>
autoExpandLimit: number
didSelectTopic: any
highlightTopicUpdates: boolean
selectTopicWithMouseOver: boolean
}
interface State {
@@ -52,13 +52,13 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
}
private renderMore() {
this.renderMoreAnimationFrame = (window as any).requestIdleCallback(() => {
this.renderMoreAnimationFrame = window.requestIdleCallback(() => {
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
}, { timeout: 500 })
}
public componentWillUnmount() {
(window as any).cancelIdleCallback(this.renderMoreAnimationFrame)
window.cancelIdleCallback(this.renderMoreAnimationFrame)
}
public render() {
@@ -83,8 +83,6 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
autoExpandLimit={this.props.autoExpandLimit}
lastUpdate={node.lastUpdate}
didSelectTopic={this.props.didSelectTopic}
highlightTopicUpdates={this.props.highlightTopicUpdates}
selectTopicWithMouseOver={this.props.selectTopicWithMouseOver}
/>
)
})
+14 -6
View File
@@ -11,19 +11,29 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
collapsed?: boolean | undefined
classes: any
didSelectNode: any
toggleCollapsed: any
}
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
private mouseOver = (event: React.MouseEvent) => {
event.preventDefault()
this.selectTopic()
}
private selectTopic = debounce(() => {
if (this.props.treeNode.message) {
this.props.didSelectNode(this.props.treeNode)
}
}, 5)
public render() {
const { classes, treeNode, style, className } = this.props
return (
<span
className={`${classes.title} ${className}`}
onMouseOver={treeNode.message ? this.mouseOver : undefined}
style={style}
>
<span className={classes.expander} onClick={this.props.toggleCollapsed}>{this.renderExpander()}</span>
{this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
<span className={classes.expander}>{this.renderExpander()}</span> {this.renderSourceEdge()} {this.renderCollapsedSubnodes()} {this.renderValue()}
</span>
)
}
@@ -71,8 +81,6 @@ const styles = (theme: Theme) => ({
},
expander: {
color: theme.palette.type === 'light' ? '#222' : '#eee',
cursor: 'pointer' as 'pointer',
paddingRight: theme.spacing(0.25),
},
title: {
borderRadius: '4px',
@@ -80,7 +88,7 @@ const styles = (theme: Theme) => ({
display: 'inline-block' as 'inline-block',
whiteSpace: 'nowrap' as 'nowrap',
padding: '1px 4px 0px 4px',
height: '14px',
height: '16px',
margin: '1px 0px 2px 0px',
},
collapsedSubnodes: {
-44
View File
@@ -1,44 +0,0 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
interface Props {
keyboardKey: string
classes: any
}
class Key extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = { location: 'bottom' }
}
public render() {
return (
<div className={this.props.classes.keyStyle}>
<div className={this.props.classes.keyTextStyle}>{this.props.keyboardKey}</div>
</div>
)
}
}
const style = (theme: Theme) => ({
keyStyle: {
display: 'inline-block' as 'inline-block',
width: '1em',
height: '1em',
backgroundColor: '#bbb',
borderRadius: '10%',
verticalAlign: 'middle' as 'middle',
textAlign: 'center' as 'center',
textShadow: '1px 1px rgba(255,255,255,0.45)',
boxShadow: '0.08em 0.15em 0.01em 0px rgba(100,100,100,0.75)',
},
keyTextStyle: {
marginTop: '0.65em',
fontSize: '0.4em',
fontWeight: 'bold' as 'bold',
},
})
export default withStyles(style)(Key)
-78
View File
@@ -1,78 +0,0 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
interface State {
enabled: boolean
target: {x: number, y: number}
position: {x: number, y: number}
stepSizeX: number,
stepSizeY: number,
}
class Demo extends React.Component<{}, State> {
private timer: any
private frameInterval = 20
constructor(props: any) {
super(props)
this.state = { enabled: false, target: { x: 0, y: 0 }, position: { x: 0, y: 0 }, stepSizeX: 1, stepSizeY: 1 }
}
public componentDidMount() {
(window as any).demo.enableMouse = () => {
this.setState({ enabled: true })
}
(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
this.moveCloser()
}
}
private moveCloser(steps: number = 0) {
const steSizeX = Math.min(this.state.stepSizeX, Math.abs(this.state.position.x - this.state.target.x))
const steSizeY = Math.min(this.state.stepSizeY, Math.abs(this.state.position.y - this.state.target.y))
const dirX = this.state.position.x > this.state.target.x ? -1 : 1
const dirY = this.state.position.y > this.state.target.y ? -1 : 1
if (steSizeX <= 0.1 && steSizeY <= 0.1) {
this.timer && clearTimeout(this.timer)
return
}
this.setState({
position: {
x: this.state.position.x + (dirX * steSizeX),
y: this.state.position.y + (dirY * steSizeY),
},
})
this.timer = setTimeout(() => {
this.moveCloser(steps + 1)
}, this.frameInterval)
}
public render() {
if (!this.state.enabled) {
return null
}
const style = {
width: '32px',
height: '32px',
position: 'fixed' as 'fixed',
zIndex: 1000000,
filter: 'invert(100%)',
left: this.state.position.x + 2,
top: this.state.position.y + 2,
}
return (
<img src="../cursor.png" style={style} />
)
}
}
const style = (theme: Theme) => {}
export default withStyles(style)(Demo)
-91
View File
@@ -1,91 +0,0 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import Key from './Key';
interface State {
message?: string
keys: string[]
location: string
}
class Demo extends React.Component<{classes: any}, State> {
private timer: any
constructor(props: any) {
super(props)
this.state = { location: 'bottom', keys: [] }
}
private clearTimer() {
this.timer && clearTimeout(this.timer)
}
public componentDidMount() {
(window as any).demo.showMessage = (message: string, location: string, duration: number, keys: string[] = []) => {
this.clearTimer()
this.setState({ message, location, keys })
this.timer = setTimeout(() => this.setState({ message: undefined }), duration)
}
(window as any).demo.hideMessage = () => {
this.clearTimer()
this.setState({ message: undefined })
}
}
public render() {
const positions: {[s: string]: number} = {
top: 0,
bottom: -65,
middle: -32,
}
const style = {
position: 'fixed' as 'fixed',
left: '5vw',
zIndex: 1000000,
margin: '30vw auto 50vw',
right: '5vw',
bottom: `${positions[this.state.location]}vh`,
}
const style2 = {
textAlign: 'center' as 'center',
fontSize: '4em',
color: 'white',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: '16px',
}
if (!this.state.message) {
return null
}
let keys: any[] = []
if (this.state.keys.length > 0) {
keys = this.state.keys.map(key => [<Key key={key} keyboardKey={key} />])
.reduce((prev, current) => {
return [prev, '+' as any, current]
})
}
return (
<div style={style}>
<div style={style2}>
<span>{this.state.message}</span>
{keys.length > 0 ? <div className={this.props.classes.keysStyle}>{keys}</div> : null}
</div>
</div>
)
}
}
const style = (theme: Theme) => ({
keysStyle: {
fontSize: '1em',
display: 'inline-block' as 'inline-block',
transform: 'translateY(0.3em) translateX(0.8em)',
},
})
export default withStyles(style)(Demo)
-14
View File
@@ -1,14 +0,0 @@
import * as React from 'react'
import ShowText from './ShowText'
import Mouse from './Mouse';
(window as any).demo = {}
export default function render(props: any) {
return (
<span>
<ShowText />
<Mouse />
</span>
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import { IconButton } from '@material-ui/core'
import Clear from '@material-ui/icons/Clear'
interface Props {
value?: string
+11 -12
View File
@@ -1,17 +1,16 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './App'
import Demo from './components/demo'
import reducers from './reducers'
import reduxThunk from 'redux-thunk'
import { applyMiddleware, compose, createStore } from 'redux'
import { batchDispatchMiddleware } from 'redux-batched-actions'
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'
import { Provider } from 'react-redux'
import './tracking'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import reduxThunk from 'redux-thunk'
import { batchDispatchMiddleware } from 'redux-batched-actions'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers from './reducers'
import App from './App'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
const composeEnhancers = /*(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || */ compose
const store = createStore(
@@ -28,6 +27,7 @@ const theme = createMuiTheme({
palette: {
type: 'dark',
},
typography: { useNextVariants: true },
})
setTimeout(() => {
@@ -41,8 +41,7 @@ setTimeout(() => {
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<Provider store={store}>
<App />
<Demo />
<App name="" />
</Provider>
</MuiThemeProvider>,
document.getElementById('app'),
+1 -18
View File
@@ -1,10 +1,9 @@
import * as q from '../../../backend/src/Model'
import { Action } from 'redux'
import { createReducer } from './lib'
import * as q from '../../../backend/src/Model'
import { MqttOptions } from '../../../backend/src/DataSource'
import { TopicViewModel } from '../TopicViewModel'
export type ConnectionHealth = 'offline' | 'online' | 'connecting'
export interface ConnectionState {
host?: string
tree?: q.Tree<TopicViewModel>
@@ -13,7 +12,6 @@ export interface ConnectionState {
error?: string
connected: boolean
connecting: boolean
health?: ConnectionHealth
}
export type Action = SetConnecting | SetConnected | SetDisconnected | ShowError
@@ -23,7 +21,6 @@ export enum ActionTypes {
CONNECTION_SET_CONNECTED = 'CONNECTION_SET_CONNECTED',
CONNECTION_SET_DISCONNECTED = 'CONNECTION_SET_DISCONNECTED',
CONNECTION_SET_SHOW_ERROR = 'CONNECTION_SET_SHOW_ERROR',
CONNECTION_SET_HEALTH = 'CONNECTION_SET_HEALTH',
}
export interface SetConnecting {
@@ -41,11 +38,6 @@ export interface SetDisconnected {
type: ActionTypes.CONNECTION_SET_DISCONNECTED
}
export interface SetHealth {
health: ConnectionHealth
type: ActionTypes.CONNECTION_SET_DISCONNECTED
}
export interface ShowError {
type: ActionTypes.CONNECTION_SET_SHOW_ERROR
error?: Error
@@ -54,7 +46,6 @@ export interface ShowError {
const initialState: ConnectionState = {
connected: false,
connecting: false,
health: undefined,
}
export const connectionReducer = createReducer(initialState, {
@@ -62,7 +53,6 @@ export const connectionReducer = createReducer(initialState, {
CONNECTION_SET_CONNECTED: setConnected,
CONNECTION_SET_DISCONNECTED: setDisconnected,
CONNECTION_SET_SHOW_ERROR: showError,
CONNECTION_SET_HEALTH: setHealth,
})
function setConnecting(state: ConnectionState, action: SetConnecting): ConnectionState {
@@ -74,13 +64,6 @@ function setConnecting(state: ConnectionState, action: SetConnecting): Connectio
}
}
function setHealth(state: ConnectionState, action: SetHealth): ConnectionState {
return {
...state,
health: action.health,
}
}
function setConnected(state: ConnectionState, action: SetConnected): ConnectionState {
return {
...state,
+1 -1
View File
@@ -44,7 +44,7 @@ export interface ToggleRetain {
}
const initialState: PublishState = {
editorMode: 'json',
editorMode: 'text',
retain: false,
qos: 0,
}
+8 -73
View File
@@ -8,39 +8,25 @@ export enum TopicOrder {
topics = '#topics',
}
export type ValueRendererDisplayMode = 'diff' | 'raw'
export interface SettingsState {
autoExpandLimit: number
visible: boolean
topicOrder: TopicOrder
topicFilter?: string
highlightTopicUpdates: boolean
valueRendererDisplayMode: ValueRendererDisplayMode
selectTopicWithMouseOver: boolean
}
export type Action = SetAutoExpandLimit | ToggleVisibility | SetTopicOrder | FilterTopics | TogglehighlightTopicUpdates | SetValueRendererDisplayMode
export type Action = SetAutoExpandLimit | ToggleVisibility | SetTopicOrder | FilterTopics
export enum ActionTypes {
SETTINGS_SET_AUTO_EXPAND_LIMIT = 'SETTINGS_SET_AUTO_EXPAND_LIMIT',
SETTINGS_TOGGLE_VISIBILITY = 'SETTINGS_TOGGLE_VISIBILITY',
SETTINGS_SET_TOPIC_ORDER = 'SETTINGS_SET_TOPIC_ORDER',
SETTINGS_FILTER_TOPICS = 'SETTINGS_FILTER_TOPICS',
SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY = 'SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY',
SETTINGS_DID_LOAD_SETTINGS = 'SETTINGS_DID_LOAD_SETTINGS',
SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE = 'SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE',
SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER = 'SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER',
}
const initialState: SettingsState = {
autoExpandLimit: 0,
topicOrder: TopicOrder.none,
visible: false,
highlightTopicUpdates: true,
valueRendererDisplayMode: 'diff',
selectTopicWithMouseOver: false,
}
export const settingsReducer = createReducer(initialState, {
@@ -48,53 +34,8 @@ export const settingsReducer = createReducer(initialState, {
SETTINGS_TOGGLE_VISIBILITY: toggleVisibility,
SETTINGS_SET_TOPIC_ORDER: setTopicOrder,
SETTINGS_FILTER_TOPICS: filterTopics,
SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY: togglehighlightTopicUpdates,
SETTINGS_DID_LOAD_SETTINGS: didLoadSettings,
SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE: setValueRendererDisplayMode,
SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER: setSelectTopicWithMouseOver,
})
export interface DidLoadSettings {
type: ActionTypes.SETTINGS_DID_LOAD_SETTINGS
settings: Partial<SettingsState>
}
function didLoadSettings(state: SettingsState, action: DidLoadSettings) {
return {
...state,
...action.settings,
}
}
export interface SetSelectTopicWithMouseOver {
type: ActionTypes.SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER
selectTopicWithMouseOver: boolean
}
export function setSelectTopicWithMouseOver(state: SettingsState, action: SetSelectTopicWithMouseOver) {
return {
...state,
selectTopicWithMouseOver: action.selectTopicWithMouseOver,
}
}
export interface SetValueRendererDisplayMode {
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE
valueRendererDisplayMode: ValueRendererDisplayMode
}
export function setValueRendererDisplayMode(state: SettingsState, action: SetValueRendererDisplayMode) {
return {
...state,
valueRendererDisplayMode: action.valueRendererDisplayMode,
}
}
export interface SetAutoExpandLimit {
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT
autoExpandLimit: number
}
function setAutoExpandLimit(state: SettingsState, action: SetAutoExpandLimit) {
return {
...state,
@@ -102,19 +43,9 @@ function setAutoExpandLimit(state: SettingsState, action: SetAutoExpandLimit) {
}
}
export interface TogglehighlightTopicUpdates {
type: ActionTypes.SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY
}
function togglehighlightTopicUpdates(state: SettingsState, _action: TogglehighlightTopicUpdates) {
return {
...state,
highlightTopicUpdates: !state.highlightTopicUpdates,
}
}
export interface ToggleVisibility {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY
export interface SetAutoExpandLimit {
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT
autoExpandLimit: number
}
function toggleVisibility(state: SettingsState, action: ToggleVisibility) {
@@ -124,6 +55,10 @@ function toggleVisibility(state: SettingsState, action: ToggleVisibility) {
}
}
export interface ToggleVisibility {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY
}
function setTopicOrder(state: SettingsState, action: SetTopicOrder) {
return {
...state,
+7 -7
View File
@@ -1,11 +1,11 @@
import { Action, combineReducers, Reducer } from 'redux'
import { connectionManagerReducer, ConnectionManagerState } from './ConnectionManager'
import { connectionReducer, ConnectionState } from './Connection'
import { publishReducer, PublishState } from './Publish'
import { settingsReducer, SettingsState } from './Settings'
import { trackEvent } from '../tracking'
import { treeReducer, TreeState } from './Tree'
import { Action, Reducer, combineReducers } from 'redux'
import { trackEvent } from '../tracking'
import { PublishState, publishReducer } from './Publish'
import { ConnectionState, connectionReducer } from './Connection'
import { SettingsState, settingsReducer } from './Settings'
import { TreeState, treeReducer } from './Tree'
import { ConnectionManagerState, connectionManagerReducer } from './ConnectionManager'
export enum ActionTypes {
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
+615 -556
View File
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
image: Visual Studio 2017
platform: x64
cache:
- node_modules
- '%LOCALAPPDATA%\electron\Cache'
- '%LOCALAPPDATA%\electron-builder\cache'
install:
- ps: Install-Product node 10
build_script:
- yarn
- yarn build
- yarn prepare-release
- yarn package appx
test_script:
- yarn test
artifacts:
- path: 'build\clean\build\MQTT-Explorer*.appx'
+3 -3
View File
@@ -33,8 +33,8 @@ export default class ConfigStorage {
await db.set(event.store, event.data).write()
backendEvents.emit(ack, undefined)
} catch (error) {
console.error(error)
backendEvents.emit(ack, { error, transactionId: event.transactionId, store: event.store })
throw error
}
})
@@ -45,8 +45,8 @@ export default class ConfigStorage {
const data = await db.get(event.store).value()
backendEvents.emit(responseEvent, { data, transactionId: event.transactionId, store: event.store })
} catch (error) {
console.error(error)
backendEvents.emit(responseEvent, { error, transactionId: event.transactionId, store: event.store })
throw error
}
})
@@ -60,8 +60,8 @@ export default class ConfigStorage {
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), undefined)
} catch (error) {
backendEvents.emit(makeStorageAcknoledgementEvent(event.transactionId), { error, transactionId: event.transactionId })
throw error
}
})
}
}
+3 -4
View File
@@ -25,17 +25,16 @@ export class DataSourceStateMachine {
public setError(error: Error) {
this.state = {
...this.state,
error: error.message,
// connected: false,
// connecting: false,
connected: false,
connecting: false,
}
this.onUpdate.dispatch(this.state)
}
public setConnecting() {
this.state = {
...this.state,
error: undefined,
connected: false,
connecting: true,
}
-4
View File
@@ -55,10 +55,6 @@ export class MqttSource implements DataSource<MqttOptions> {
this.stateMachine.setConnected(false)
})
client.on('end', () => {
this.stateMachine.setConnected(false)
})
client.on('reconnect', () => {
this.stateMachine.setConnecting()
})
+1 -1
View File
@@ -6,7 +6,7 @@ export class RingBuffer<T extends Lengthwise> {
private capacity: number
private maxItems: number
private usage: number = 0
private items: T[] = []
private items: (any)[] = []
private start: number = 0
private end: number = 0
-35
View File
@@ -47,9 +47,6 @@ export class TreeNode<ViewModel> {
this.lastUpdate = Date.now()
})
this.onEdgesChange.subscribe(() => {
this.cachedChildTopics = undefined
this.cachedChildTopicCount = undefined
this.cachedLeafMessageCount = undefined
this.lastUpdate = Date.now()
})
this.onMessage.subscribe(() => {
@@ -105,34 +102,6 @@ export class TreeNode<ViewModel> {
return previous.branch().concat([this])
}
private isTopicEmptyLeaf() {
const hasNoMessage = (!this.message || !this.message.value)
return hasNoMessage && this.isLeaf()
}
private isLeaf() {
return this.edgeArray.length === 0
}
private removeFromParent() {
const previous = this.previous()
if (!previous || !this.sourceEdge) {
return
}
previous.removeEdge(this.sourceEdge)
}
public removeEdge(edge: Edge<any>) {
delete this.edges[edge.name]
this.edgeArray = Object.values(this.edges)
this.onMerge.dispatch()
if (this.isTopicEmptyLeaf()) {
debugger
this.removeFromParent()
}
}
public updateWithNode(node: TreeNode<ViewModel>) {
if (node.message) {
this.setMessage(node.message)
@@ -140,10 +109,6 @@ export class TreeNode<ViewModel> {
this.mqttMessage = node.mqttMessage
}
if (this.isTopicEmptyLeaf()) {
this.removeFromParent()
}
this.mergeEdges(node)
this.onMerge.dispatch()
}
+2 -2
View File
@@ -43,10 +43,10 @@ describe('TreeNode', () => {
it('updateWithNode should add nodes to the tree', () => {
const topics1 = 'foo/bar'.split('/')
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, 'foo')
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined)
const topics2 = 'foo/bar/baz'.split('/')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, 'bar')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined)
leaf1.firstNode().updateWithNode(leaf2.firstNode())
+6 -16
View File
@@ -11,8 +11,8 @@ import {
removeConnection,
updateAvailable,
} from '../../events'
import { DataSource, MqttSource } from './DataSource'
import ConfigStorage from './ConfigStorage'
import { UpdateInfo } from 'builder-util-runtime'
export class ConnectionManager {
@@ -20,19 +20,11 @@ export class ConnectionManager {
public manageConnections() {
backendEvents.subscribe(addMqttConnectionEvent, this.handleConnectionRequest)
backendEvents.subscribe(removeConnection, (connectionId: string) => {
this.removeConnection(connectionId)
})
backendEvents.subscribe(removeConnection, (connectionId: string) => this.removeConnection(connectionId))
}
private handleConnectionRequest = (event: AddMqttConnection) => {
const connectionId = event.id
// Prevent double connections when reloading
if (this.connections[connectionId]) {
this.removeConnection(connectionId)
}
const options = event.options
const connection = new MqttSource()
this.connections[connectionId] = connection
@@ -61,19 +53,17 @@ export class ConnectionManager {
})
}
public removeConnection(conenctionId: string) {
const connection = this.connections[conenctionId]
public removeConnection(hash: string) {
const connection = this.connections[hash]
if (connection) {
backendEvents.unsubscribeAll(makePublishEvent(conenctionId))
connection.disconnect()
delete this.connections[conenctionId]
connection.stateMachine.onUpdate.removeAllListeners()
delete this.connections[hash]
}
}
public closeAllConnections() {
Object.keys(this.connections)
.forEach(conenctionId => this.removeConnection(conenctionId))
.forEach(hash => this.removeConnection(hash))
}
}
-1
View File
@@ -30,7 +30,6 @@ class IpcMainEventBus implements EventBusInterface {
}
public unsubscribeAll<MessageType>(event: Event<MessageType>) {
console.log('unsubscribeAll', event.topic)
this.ipc.removeAllListeners(event.topic)
}
+7 -17
View File
@@ -1,15 +1,15 @@
{
"name": "MQTT-Explorer",
"version": "0.2.1-alpha6",
"version": "0.1.3",
"description": "Explore your message queues",
"main": "dist/src/electron.js",
"scripts": {
"start": "electron .",
"test": "yarn run test-backend",
"install": "cd app && yarn && cd ..",
"install": "cd app; yarn; cd ..",
"build": "tsc && cd app && yarn run build && cd ..",
"test-backend": "cd backend && yarn run test && cd ..",
"prepare-release": "ts-node scripts/prepare-release.ts",
"prepare-release": "./scripts/prepare-release.sh",
"package": "ts-node package.ts",
"ui-test": "./scripts/uiTests.sh",
"upload-video-artifacts": "./scripts/uploadVideoAsset.ts ui-test.mp4 ui-test.gif",
@@ -32,19 +32,11 @@
"category": "Development",
"maintainer": "Thomas Nordquist"
},
"appx": {
"applicationId": "mqttexplorer",
"identityName": "51031thomas.nordquist.MQTT-Explorer",
"publisherDisplayName": "Thomas Nordquist",
"publisher": "CN=0A6DE643-4AA2-4FF2-9729-6935C9ED8C13",
"backgroundColor": "#FFFFFF"
},
"directories": {
"app": "./",
"buildResources": "res",
"output": "build"
},
"afterAllArtifactBuild": "./dist/scripts/afterPack.js"
}
},
"author": "Thomas Nordquist",
"email": "xxnerowingerxx@gmail.com",
@@ -52,7 +44,6 @@
"license": "ISC",
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/fs-extra": "^5.0.5",
"@types/lowdb": "^1.0.6",
"@types/mime": "^2.0.0",
"@types/mocha": "^5.2.5",
@@ -62,9 +53,8 @@
"@types/sha1": "^1.1.1",
"axios": "^0.18.0",
"chai": "^4.2.0",
"electron": "^4.0.6",
"electron-builder": "^20.38.5",
"fs-extra": "^7.0.1",
"electron": "^4.0.2",
"electron-builder": "^20.38.4",
"mime": "^2.4.0",
"mocha": "^5.2.0",
"mustache": "^3.0.1",
@@ -81,7 +71,7 @@
"webdriverio": "5.4"
},
"dependencies": {
"about-window": "^1.12.1",
"electron-debug": "^2.0.0",
"electron-is-dev": "^1.0.1",
"electron-log": "^2.2.17",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
+18 -47
View File
@@ -1,12 +1,11 @@
import * as builder from 'electron-builder'
import * as fs from 'fs'
import * as path from 'path'
const linuxAppImage: builder.CliOptions = {
const linux: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: true,
arm64: false,
linux: ['AppImage'],
projectDir: './build/clean',
publish: 'always',
}
@@ -16,83 +15,55 @@ const linuxSnap: builder.CliOptions = {
ia32: false,
armv7l: false,
arm64: false,
linux: ['snap'],
projectDir: './build/clean',
publish: 'always',
}
const winPortable: builder.CliOptions = {
const win: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
win: ['portable', 'nsis'],
projectDir: './build/clean',
publish: 'always',
}
const winNsis: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
projectDir: './build/clean',
publish: 'always',
}
const winAppx: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
projectDir: './build/clean',
publish: 'onTag',
}
const mac: builder.CliOptions = {
x64: true,
ia32: true,
armv7l: false,
arm64: false,
mac: ['dmg'],
projectDir: './build/clean',
publish: 'always',
}
async function executeBuild() {
console.log(process.argv[2])
switch (process.argv[2]) {
case 'win':
await buildWithOptions(winPortable, { platform: 'win', package: 'portable' })
await buildWithOptions(winNsis, { platform: 'win', package: 'nsis' })
break
case 'appx':
await buildWithOptions(winAppx, { platform: 'win', package: 'appx' })
await builder.build(win)
break
case 'linux':
await buildWithOptions(linuxAppImage, { platform: 'linux', package: 'AppImage' })
await buildWithOptions(linuxSnap, { platform: 'linux', package: 'snap' })
await builder.build(linux)
break
case 'snap':
try {
await builder.build(linuxSnap)
} catch {
// ignore
}
break
case 'mac':
await buildWithOptions(mac, { platform: 'mac', package: 'mas' })
await buildWithOptions(mac, { platform: 'mac', package: 'dmg' })
await buildWithOptions(mac, { platform: 'mac', package: 'zip' })
await builder.build(mac)
break
default:
throw new Error('No target selected')
await builder.build(mac)
}
}
export interface BuildInfo {
platform: 'win' | 'linux' | 'mac'
package: 'portable' | 'nsis' | 'appx' | 'AppImage' | 'snap' | 'dmg' | 'zip' | 'mas'
}
async function buildWithOptions(options: builder.CliOptions, buildInfo: BuildInfo) {
fs.writeFileSync(path.join(options.projectDir!, 'buildOptions.json'), JSON.stringify(buildInfo))
await builder.build({
...options,
[buildInfo.platform]: [buildInfo.package],
})
}
function build() {
try {
executeBuild()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.
-22
View File
@@ -1,22 +0,0 @@
import * as fs from 'fs-extra'
import * as path from 'path'
import { chdir } from 'process'
import { exec } from './util'
export default async function (info: any) {
for (const snapFile of info.artifactPaths) {
if (/\.snap$/.test(snapFile)) {
const originalDir = __dirname
const dirname = path.dirname(snapFile)
chdir(dirname)
await exec('sudo', ['unsquashfs', snapFile])
await fs.remove(snapFile)
await exec('sudo', ['chmod', '-R', 'g-s', 'squashfs-root'])
await exec('sudo', ['snap', 'run', 'snapcraft', 'pack', 'squashfs-root', '--output', snapFile])
await fs.remove('squashfs-root')
chdir(originalDir)
}
}
}
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
set -e
ORIGINAL_DIR=`pwd`
DIR=build/clean
rm -rf "$DIR" || echo "Directory did not exist"
mkdir -p "$DIR"
git clone .git "$DIR"
cd $DIR
# App
cd app
yarn
cd ..
# Build
yarn
yarn build
rm -rf node_modules
yarn install --production
rm -rf app/node_modules
cd "$ORIGINAL_DIR"
-37
View File
@@ -1,37 +0,0 @@
import * as fs from 'fs-extra'
import * as path from 'path'
import { chdir } from 'process'
import { exec } from './util'
const targetDir = path.join('build', 'clean')
async function prepareRelease() {
const originalDir = __dirname
await fs.remove(targetDir)
await fs.mkdirp(targetDir)
// Create fresh clone of the local git repo
await exec('git', ['clone', '.git', targetDir])
// Enter git repo
chdir(targetDir)
// Install app dependencies
chdir('app')
await exec('yarn')
chdir('..')
// Install electron dependencies
await exec('yarn')
// Build App and Electron backend
await exec('yarn', ['build'])
// Clean up
await fs.remove('node_modules')
await exec('yarn', ['install', '--production'])
await fs.remove(path.join('app', 'node_modules'))
chdir(originalDir)
}
prepareRelease()
+8 -14
View File
@@ -1,27 +1,21 @@
#!/bin/bash
DIMENSIONS="1024x720"
GIF_SCALE="1024"
ffmpeg -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv app2.mp4
cp app.mp4 original.mp4
# The video starts with a few blank frames, we want to know when the stop
ffprobe -f lavfi -i "movie=app2.mp4,blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet > ffmpeg_info
ffprobe -f lavfi -i "movie=app.mp4,blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet > ffmpeg_info
END_OF_BLACK=`cat ffmpeg_info | grep end | head -n1 | cut -d'=' -f2`
# Remove grey frames at the beginning (app start and splash screen)
END_OF_BLACK=`awk "BEGIN {print $END_OF_BLACK+0.8; exit}"`
# Trim black frames at start
ffmpeg -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -ss $END_OF_BLACK app.mp4
ffmpeg -ss $END_OF_BLACK -i app.mp4 app2.mp4
mv app2.mp4 app.mp4
# Generate gif palette
ffmpeg -y -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -vf "fps=10,scale=$GIF_SCALE:-1:flags=lanczos,palettegen" palette1024.png
ffmpeg -y -i app.mp4 -vf fps=10,scale=720:-1:flags=lanczos,palettegen palette.png
# Create gif
ffmpeg -s:v $DIMENSIONS -r 20 -f rawvideo -pix_fmt yuv420p -i qrawvideorgb24.yuv -i palette1024.png -ss $END_OF_BLACK -filter_complex "fps=10,scale=$GIF_SCALE:-1:flags=lanczos[x];[x][1:v]paletteuse" app720.gif
ffmpeg -i app.mp4 -i palette.png -filter_complex "fps=10,scale=720:-1:flags=lanczos[x];[x][1:v]paletteuse" app.gif
# Clean up
rm ffmpeg_info palette*.png qrawvideorgb24.yuv
rm ffmpeg_info palette.png
mv app.mp4 ui-test.mp4
mv app720.gif ui-test.gif
mv app.gif ui-test.gif
+3 -4
View File
@@ -9,7 +9,7 @@ function finish {
trap finish EXIT
DIMENSIONS="1024x720"
DIMENSIONS="1024x700"
SCR=99
# Start new window manager
Xvfb :$SCR -screen 0 "$DIMENSIONS"x24 -ac &
@@ -17,7 +17,7 @@ export PID_XVFB=$!
sleep 2
# Debug with VNC
while [ "$TEST_EXIT_CODE" = "" ]; do x11vnc -localhost -passwd "bierbier" -display :$SCR; done &
while [ "$TEST_EXIT_CODE" = "" ]; do x11vnc -passwd "bierbier" -display :$SCR; done &
export PID_VNC=$!
# Start mqtt broker
@@ -32,8 +32,7 @@ sleep 2
rm ./app.mp4 || echo no need to delete ./app.mp4
# Start recoring in tmux
#tmux new-session -d -s record ffmpeg -f x11grab -draw_mouse 0 -video_size $DIMENSIONS -i :$SCR -codec:v libx264 -r 20 ./app.mp4
tmux new-session -d -s record ffmpeg -f x11grab -draw_mouse 0 -video_size $DIMENSIONS -i :$SCR -r 20 -vcodec rawvideo -pix_fmt yuv420p qrawvideorgb24.yuv
tmux new-session -d -s record ffmpeg -f x11grab -draw_mouse 0 -video_size $DIMENSIONS -i :$SCR -codec:v libx264 -r 20 ./app.mp4
# Start tests
node dist/src/spec/webdriverio.js
-29
View File
@@ -1,29 +0,0 @@
import { spawn, ChildProcess } from 'child_process'
export async function exec(cmd: string, args: string[] = []) {
const child = spawn(cmd, args, { shell: true })
redirectOutputFor(child)
await waitFor(child)
}
function redirectOutputFor(child: ChildProcess) {
const printStdout = (data: Buffer) => {
process.stdout.write(data.toString())
}
const printStderr = (data: Buffer) => {
process.stderr.write(data.toString())
}
child.stdout.on('data', printStdout)
child.stderr.on('data', printStderr)
child.once('close', () => {
child.stdout.off('data', printStdout)
child.stderr.off('data', printStderr)
})
}
async function waitFor(child: ChildProcess) {
return new Promise((resolve) => {
child.once('close', () => resolve())
})
}
+13 -64
View File
@@ -1,21 +1,11 @@
import { Menu, app, BrowserWindow, webContents, MenuItemConstructorOptions, MenuItem } from 'electron'
import openAboutWindow from 'about-window'
import * as path from 'path'
import { Menu, app, BrowserWindow, webContents } from 'electron'
const applicationMenu: MenuItemConstructorOptions = {
const applicationMenu = {
label: 'Application',
submenu: [
{
label: 'About Application',
click: () => {
openAboutWindow({
icon_path: path.join(__dirname, '..', '..', 'icon.png'),
license: 'AGPL-3.0',
homepage: 'https://thomasnordquist.github.io/MQTT-Explorer/',
bug_report_url: 'https://github.com/thomasnordquist/MQTT-Explorer/issues',
description: 'Author: Thomas Nordquist',
})
},
selector: 'orderFrontStandardAboutPanel:',
},
{
type: 'separator' as 'separator',
@@ -23,7 +13,7 @@ const applicationMenu: MenuItemConstructorOptions = {
{
label: 'Dev Tools',
accelerator: 'CmdOrCtrl+Alt+I',
role: 'toggledevtools' as 'toggledevtools',
role: 'toggleDevTools',
},
{
label: 'Quit',
@@ -35,89 +25,48 @@ const applicationMenu: MenuItemConstructorOptions = {
],
}
const editMenu: MenuItemConstructorOptions = {
const editMenu = {
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
role: 'undo',
selector: 'undo:',
},
{
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
role: 'redo',
selector: 'redo:',
},
{
type: 'separator',
type: 'separator' as 'separator',
},
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut',
selector: 'cut:',
},
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy',
selector: 'copy:',
},
{
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
role: 'paste',
selector: 'paste:',
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall',
selector: 'selectAll:',
},
],
}
const viewMenu: MenuItemConstructorOptions = {
label: 'View',
submenu: [
{
label: 'Default size',
accelerator: 'CmdOrCtrl+0',
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.setZoomFactor(1)
}
},
},
{
label: 'Increase size',
accelerator: 'CmdOrCtrl+Plus',
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.getZoomFactor((zoom) => {
window.webContents.setZoomFactor(Math.min(zoom + 0.1, 2.0))
})
}
},
},
{
label: 'Reduce size',
accelerator: 'CmdOrCtrl+-',
click: () => {
const window = BrowserWindow.getFocusedWindow()
if (window) {
window.webContents.getZoomFactor((zoom) => {
window.webContents.setZoomFactor(Math.max(zoom - 0.1, 0.5))
})
}
},
},
],
}
const template: any = [
const template = [
applicationMenu,
editMenu,
viewMenu,
]
export const menuTemplate = Menu.buildFromTemplate(template)
+8 -19
View File
@@ -1,4 +1,3 @@
import * as fs from 'fs'
import * as log from 'electron-log'
import * as path from 'path'
import ConfigStorage from '../backend/src/ConfigStorage'
@@ -8,27 +7,17 @@ import { ConnectionManager, updateNotifier } from '../backend/src/index'
import { electronTelemetryFactory } from 'electron-telemetry'
import { menuTemplate } from './MenuTemplate'
import { UpdateInfo } from '../events'
import { BuildInfo } from 'electron-telemetry/build/Model';
const isDev = require('electron-is-dev')
let electronTelemetry: any
if (!isDev) {
let buildOptions: BuildInfo = ({ platform: 'unknown', package: 'unknown' } as any)
try {
const options = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'buildOptions.json')).toString())
if (typeof options.platform === 'string' && typeof options.package === 'string') {
buildOptions = options
}
} catch (error) {
console.log(error)
}
console.log(buildOptions)
const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions)
electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5')
}
// const isDebugEnabled = Boolean(process.argv.find(arg => arg === 'debug'))
const runningUiTestOnCi = Boolean(process.argv.find(arg => arg === '--runningUiTestOnCi'))
const isDebugEnabled = Boolean(process.argv.find(arg => arg === 'debug'))
const isFullscreen = Boolean(process.argv.find(arg => arg === '--fullscreen'))
require('electron-debug')({ enabled: isDebugEnabled })
autoUpdater.logger = log
log.info('App starting...')
@@ -48,7 +37,7 @@ function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1024,
height: 720,
height: 700,
show: false,
webPreferences: {
nodeIntegration: true,
@@ -59,7 +48,7 @@ function createWindow() {
mainWindow.once('ready-to-show', () => {
if (mainWindow) {
runningUiTestOnCi && mainWindow.setFullScreen(true)
isFullscreen && mainWindow.setFullScreen(true)
mainWindow.show()
}
})
-3
View File
@@ -1,3 +0,0 @@
export const Control = '\uE009'
export const Unknown = '\uE000'
export const Plus = '\uE025'
+10 -12
View File
@@ -87,19 +87,17 @@ function generateData(client: mqtt.MqttClient) {
let state = true
intervals.push(setInterval(() => {
state = !state
const js = {
tags:{
entityId: 33512,
entityType: 'person',
host: 'd44ad81e10f9',
server: 'http://localhost/dataActuality',
status: state ? 'live' : 'inactive',
},
timestamp: Date.now(),
}
const enitityId = Math.round(Math.random() * 3000)
client.publish(
'actuality/showcase',
JSON.stringify(js),
'actuality/showcase', `{
"tags":{
"entityId":${enitityId},
"entityType":"person",
"host":"d44ad81e10f9",
"server":"${state ? 'http://localhost/dataActuality' : 'http://localhost/dataStorage' }",
"status":"${state ? 'live' : 'inactive'}"},
"timestamp":${Date.now()}
}`.replace(/\s/g, ''),
{ retain: true, qos: 0 },
)
}, 2102))
@@ -0,0 +1,14 @@
import { clickOn, sleep, writeText, expandTopic, hideText } from '../util'
import { Browser } from 'webdriverio'
// Expects a topic with at least two messages to be selected
export async function compareJsonSideBySide(browser: Browser<void>) {
const firstEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[1]/div')
const secondEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[2]/div')
await clickOn(secondEntry, browser)
await sleep(2000)
await clickOn(firstEntry, browser)
await sleep(2000)
await clickOn(firstEntry, browser)
await sleep(1000)
}
+10 -1
View File
@@ -1,4 +1,4 @@
import { clickOn, writeTextToInput } from '../util'
import { clickOn, sleep, writeText } from '../util'
import { Browser } from 'webdriverio'
export async function connectTo(host: string, browser: Browser<void>) {
@@ -9,3 +9,12 @@ export async function connectTo(host: string, browser: Browser<void>) {
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
clickOn(connectButton, browser)
}
async function writeTextToInput(name: string, text: string, browser: Browser<void>, wait: boolean = true) {
const input = await browser.$(`//label[contains(text(), "${name}")]/..//input`)
await clickOn(input, browser, 1)
wait && await sleep(500)
input.clearValue()
wait && await sleep(300)
await writeText(text, browser)
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { clickOn } from '../util'
import { clickOn, sleep, writeText, expandTopic } from '../util'
import { Browser } from 'webdriverio'
export async function copyTopicToClipboard(browser: Browser<void>) {
@@ -1,29 +0,0 @@
import { clickOn, writeTextToInput, sleep } from '../util'
import { Browser } from 'webdriverio'
export async function showAdvancedConnectionSettings(browser: Browser<void>) {
const advancedSettingsButton = await browser.$('//button/span[contains(text(),"Advanced")]')
const addButton = await browser.$('//button/span[contains(text(),"Add")]')
await clickOn(advancedSettingsButton, browser)
await writeTextToInput('Subscription', 'garden/#', browser, false)
await clickOn(addButton, browser)
await writeTextToInput('Subscription', 'livingroom/#', browser, false)
await clickOn(addButton, browser)
await deleteFirstSubscribedTopic(browser)
await deleteFirstSubscribedTopic(browser)
await sleep(1000)
const backButton = await browser.$('//button/span[contains(text(),"Back")]')
await clickOn(backButton, browser)
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
await clickOn(connectButton, browser)
}
async function deleteFirstSubscribedTopic(browser: Browser<void>) {
const deleteButton = await browser.$('.advanced-connection-settings-topic-list button')
await clickOn(deleteButton, browser)
}
-31
View File
@@ -1,31 +0,0 @@
import { Browser } from 'webdriverio'
import {
clickOn,
sleep,
} from '../util'
export async function showJsonFormatting(browser: Browser<void>) {
const editor = await browser.$('//*[contains(@class, "ace_editor")]')
const formatJsonButton = await browser.$('#sidebar-publish-format-json')
const payloadInput = await browser.$('//*[contains(@class, "ace_text-input")]')
await clickOn(editor, browser)
await browser.keys(['\uE009', 'A']) // Ctrl + A
await browser.keys(['\uE000']) // End keyboard modifier
await browser.keys(['\uE003']) // Backspace
await sleep(500)
await writeTextPayload(payloadInput, '{"action": "setState", "state": "on" }')
await sleep(300)
await clickOn(formatJsonButton, browser)
await sleep(1200)
const sidebarDrawer = await browser.$('#Sidebar')
await sidebarDrawer.scrollIntoView()
}
async function writeTextPayload(payloadInput: any, text: string) {
const chars = text.split('')
for (const char of chars) {
await payloadInput.setValue(char)
await sleep(10)
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { clickOn, sleep, writeText, expandTopic } from '../util'
import { Browser } from 'webdriverio'
import { expandTopic, sleep } from '../util'
export async function showJsonPreview(browser: Browser<void>) {
await expandTopic('actuality/showcase', browser)
@@ -1,21 +0,0 @@
import { clickOn, sleep, showText } from '../util'
import { Browser } from 'webdriverio'
// Expects a topic with at least two messages to be selected
export async function showOffDiffCapability(browser: Browser<void>) {
await showText('Compare messages', 2000, browser, 'top')
await showText('Show raw message', 2000, browser, 'bottom')
const rawMessage = await browser.$('#valueRendererDisplayMode-raw')
await clickOn(rawMessage, browser)
await sleep(1000)
await showText('Compare with others', 2000, browser, 'bottom')
const diffMessages = await browser.$('#valueRendererDisplayMode-diff')
await clickOn(diffMessages, browser)
// const firstEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[1]/div')
const secondEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[2]/div')
await clickOn(secondEntry, browser)
await sleep(2000)
}
-11
View File
@@ -1,11 +0,0 @@
import { sleep, showKeys, showText } from '../util'
import { Browser } from 'webdriverio'
export async function showZoomLevel(browser: Browser<void>) {
await showKeys('Zoom in', 2000, browser, 'top', ['Ctrl', '+'])
await sleep(2000)
await showKeys('Zoom out', 2000, browser, 'middle', ['Ctrl', '-'])
await sleep(2000)
await showKeys('Reset zoom level', 2000, browser, 'bottom', ['Ctrl', 'Zero'])
await sleep(2000)
}
+69 -28
View File
@@ -1,4 +1,4 @@
import { Browser, Element } from 'webdriverio'
import { Element, Browser } from 'webdriverio'
export { expandTopic } from './expandTopic'
export function sleep(ms: number, required = false) {
@@ -26,28 +26,47 @@ export async function delteTextWithBackspaces(element: Element<void>, browser: B
}
}
export async function writeTextToInput(name: string, text: string, browser: Browser<void>, wait: boolean = true) {
const input = await browser.$(`//label[contains(text(), "${name}")]/..//input`)
await clickOn(input, browser, 1)
wait && await sleep(500)
input.clearValue()
wait && await sleep(300)
await writeText(text, browser)
}
export async function moveToCenterOfElement(element: Element<void>, browser: Browser<void>) {
const { x, y } = await element.getLocation()
const { width, height } = await element.getSize()
const targetX = x + width / 2
const targetY = y + height / 2
const js = `{
const targetX = ${x + width / 2}
const targetY = ${y + height / 2}
const duration = 500
const duration = 500
const maxStepSize = 10
const e = document.getElementById("bier")
const top = parseFloat(e.style.top)
const left = parseFloat(e.style.left)
const deltaY = targetY - top
const deltaX = targetX - left
const js = `window.demo.moveMouse(${targetX}, ${targetY}, ${duration});`
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
const steps = Math.ceil(distance / maxStepSize)
const stepX = deltaX / steps
const stepY = deltaY / steps
let currentStep = 0
function getCloser() {
e.style.left = String(left + (stepX * currentStep)) + 'px'
e.style.top = String(top + (stepY * currentStep)) + 'px'
if (currentStep < steps) {
setTimeout(() => {
currentStep += 1
if (currentStep === steps) {
e.style.left = String(targetX + 5) + 'px'
e.style.top = String(targetY + 1) + 'px'
} else {
getCloser()
}
}, duration/steps)
}
}
getCloser()
}`
await browser.execute(js)
await sleep(duration + 500, true)
await sleep(550, true)
await element.moveTo()
}
@@ -65,25 +84,47 @@ export async function clickOn(element: Element<void>, browser: Browser<void>, cl
}
export async function createFakeMousePointer(browser: Browser<void>) {
const js = 'window.demo.enableMouse();'
const addCursorImage = 'const i=document.createElement("img");'
+ 'i.src="../cursor.png";'
+ 'i.width="32";'
+ 'i.height="32";'
+ 'i.id="bier";'
+ 'i.style="position: fixed; z-index:10000000; filter: invert(100%);left: 0px; top: 0px;";'
+ 'document.body.appendChild(i)'
await browser.execute(js)
await browser.execute(addCursorImage)
}
export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys = []) {
const js = `window.demo.showMessage('${text}', '${location}', ${duration});`
browser.execute(js)
}
export async function showKeys(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom', keys: string[] = []) {
const js = `window.demo.showMessage('${text}', '${location}', ${duration}, ${JSON.stringify(keys)});`
export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom') {
const positions = {
top: 0,
bottom: -65,
middle: -32,
}
const js = `
const postition = "${positions[location]}vh"
let previousDiv = document.getElementById('tests-text-overlay')
previousDiv && previousDiv.remove()
let div = document.createElement('div')
div.id = "tests-text-overlay"
div.style = "background-color: rgba(0, 0, 0, 0.8);position: fixed;left: 5vw;z-index: 1000000;margin: 30vw auto 50vw;border-radius: 16px;right: 5vw;bottom: "+ postition +";"
let div2 = document.createElement('div')
div2.style = "text-align: center;font-size: 4em;color: white;"
div2.innerHTML = \`${text}\`
div.appendChild(div2)
document.body.appendChild(div)
if (${duration} > 0) {
setTimeout(() => div.remove(), ${duration})
}
`
browser.execute(js)
}
export async function hideText(browser: Browser<void>) {
const js = 'window.demo.hideMessage();'
const js = `
let previousDiv = document.getElementById('tests-text-overlay')
previousDiv && previousDiv.remove()
`
browser.execute(js)
await sleep(600)
}
+30 -53
View File
@@ -1,76 +1,65 @@
import * as os from 'os'
import * as webdriverio from 'webdriverio'
import mockMqtt, { stop } from './mock-mqtt'
import { clearOldTopics } from './scenarios/clearOldTopics'
import { clearSearch, searchTree } from './scenarios/searchTree'
import { connectTo } from './scenarios/connect'
import { copyTopicToClipboard } from './scenarios/copyTopicToClipboard'
import { copyValueToClipboard } from './scenarios/copyValueToClipboard'
import { disconnect } from './scenarios/disconnect'
import { publishTopic } from './scenarios/publishTopic'
import { showJsonFormatting } from './scenarios/showJsonFormatting'
import { showJsonPreview } from './scenarios/showJsonPreview'
import { showMenu } from './scenarios/showMenu'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showOffDiffCapability } from './scenarios/showOffDiffCapability'
import { showZoomLevel } from './scenarios/showZoomLevel'
import { showAdvancedConnectionSettings } from './scenarios/showAdvancedConnectionSettings'
import {
clickOnHistory,
createFakeMousePointer,
hideText,
showText,
sleep,
} from './util'
process.on('unhandledRejection', (error: Error) => {
console.error('unhandledRejection', error.message, error.stack)
process.exit(1)
})
const runningUiTestOnCi = os.platform() === 'darwin' ? [] : ['--runningUiTestOnCi']
import * as webdriverio from 'webdriverio'
import * as os from 'os'
import mockMqtt, { stop } from './mock-mqtt'
import { connectTo } from './scenarios/connect'
import { disconnect } from './scenarios/disconnect'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showJsonPreview } from './scenarios/showJsonPreview'
import { compareJsonSideBySide } from './scenarios/compareJsonSideBySide'
import { searchTree, clearSearch } from './scenarios/searchTree'
import { copyTopicToClipboard } from './scenarios/copyTopicToClipboard'
import { copyValueToClipboard } from './scenarios/copyValueToClipboard'
import { publishTopic } from './scenarios/publishTopic'
import { clearOldTopics } from './scenarios/clearOldTopics'
import { showMenu } from './scenarios/showMenu'
import { createFakeMousePointer, sleep, showText, hideText, clickOnHistory } from './util'
const binary = os.platform() === 'darwin' ? 'Electron.app/Contents/MacOS/Electron' : 'electron'
const fullscreen = os.platform() === 'darwin' ? [] : ['--fullscreen']
console.log(`${__dirname}/../../../node_modules/.bin/electron`)
const options = {
host: '127.0.0.1', // Use localhost as chrome driver server
host: 'localhost', // Use localhost as chrome driver server
port: 9515, // "9515" is the port opened by chrome driver.
capabilities: {
browserName: 'electron',
chromeOptions: {
binary: `${__dirname}/../../../node_modules/.bin/electron`,
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(runningUiTestOnCi),
binary: `${__dirname}/../../../node_modules/electron/dist/${binary}`,
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(fullscreen),
},
windowTypes: ['app', 'webview'],
},
}
async function doStuff() {
console.log('mock mqtt')
await mockMqtt()
console.log('start webdriver')
const browser = await webdriverio.remote(options)
await createFakeMousePointer(browser)
await connectTo('127.0.0.1', browser)
await sleep(1000)
await sleep(1000)
await showText('Topic overview', 2000, browser, 'top')
await showText('Overview of topics', 2000, browser, 'top')
await sleep(2000)
await showText('Indicate topic updates', 2000, browser, 'bottom')
await showText('Indicates which topics change', 2000, browser, 'bottom')
await sleep(3000)
await showText('Plot topic history', 2000, browser)
await showText('Plot topics', 2000, browser)
await showNumericPlot(browser)
await sleep(2000)
await showText('Formatted messages', 2000, browser, 'top')
await showJsonPreview(browser)
await showText('Formatted messages', 1500, browser, 'top')
await sleep(1500)
await sleep(2000)
await showOffDiffCapability(browser)
await showText('Compare messages', 2000, browser, 'top')
await compareJsonSideBySide(browser)
await hideText(browser)
await showText('Publish topics', 2000, browser, 'top')
@@ -78,10 +67,6 @@ async function doStuff() {
await publishTopic(browser)
await sleep(1000)
await showText('Write JSON with ease', 2000, browser, 'top')
await showJsonFormatting(browser)
await sleep(1000)
await showText('Copy to Clipboard', 2000, browser)
await copyTopicToClipboard(browser)
await hideText(browser)
@@ -106,18 +91,10 @@ async function doStuff() {
await sleep(2000)
await disconnect(browser)
await showText('Customize Subscriptions', 3000, browser, 'top')
await showAdvancedConnectionSettings(browser)
await showText('Keyboard shortcuts', 1750, browser, 'middle')
await sleep(1750)
await showZoomLevel(browser)
await showText('The End', 3000, browser, 'middle')
await sleep(3000)
browser.closeWindow()
stop()
}
+1 -1
View File
@@ -11,5 +11,5 @@
"lib": ["es2017", "dom"],
"sourceMap": true
},
"include": ["src/electron.ts", "src/spec/electron.ts", "src/spec/webdriverio.ts", "scripts/*.ts"]
"include": ["src/electron.ts", "src/spec/electron.ts", "src/spec/webdriverio.ts"]
}
+45 -19
View File
@@ -119,13 +119,6 @@
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a"
integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==
"@types/fs-extra@^5.0.5":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.5.tgz#080d90a792f3fa2c5559eb44bd8ef840aae9104b"
integrity sha512-w7iqhDH9mN8eLClQOYTkhdYUOSpp25eXxfc6VbFOGtzxW34JcvctH2bKjj4jD4++z4R5iO5D+pg48W2e03I65A==
dependencies:
"@types/node" "*"
"@types/lodash@*":
version "4.14.121"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.121.tgz#9327e20d49b95fc2bf983fc2f045b2c6effc80b9"
@@ -196,11 +189,6 @@
dependencies:
"@wdio/config" "^5.4.6"
about-window@^1.12.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/about-window/-/about-window-1.12.1.tgz#b81c10f6de58d38bce2aa5f31f61a82e6c3d6c2b"
integrity sha512-P/fJACq7RBFpv5Ql0kIqpOYgFqQ8WdoZoB7wrKmDvwTSKVOMbUZMYnQYtnOW6dHmMCYuNrmQFf+MxvuhE+KrzQ==
ajv-keywords@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
@@ -1068,7 +1056,7 @@ ejs@~2.5.6:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5"
integrity sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==
electron-builder@^20.38.5:
electron-builder@^20.38.4:
version "20.38.5"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.38.5.tgz#31b3913a68b4911afd4cfc7bcd2522c5808040cd"
integrity sha512-p88IDHhH2J4hA6KwRBJY+OfVZuFtFIShY3Uh/TwYAfbX0v1RhKZytuGdO8sty2zcWxDYX74xDBv+X9oN6qEIRQ==
@@ -1095,6 +1083,14 @@ electron-chromedriver@~3.0.0:
electron-download "^4.1.0"
extract-zip "^1.6.5"
electron-debug@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/electron-debug/-/electron-debug-2.0.0.tgz#3059a6557acbfb091f138d83875f57bac80cea6d"
integrity sha512-orGlw9uErUztdD7cgdKz78txq3czpOnKG/zvvsINkUsugqL+dn77UFrbwRGVgPwuLJ7Ejbjjk9EcxIcgTivMbA==
dependencies:
electron-is-dev "^0.3.0"
electron-localshortcut "^3.0.0"
electron-download@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
@@ -1110,11 +1106,31 @@ electron-download@^4.1.0:
semver "^5.4.1"
sumchecker "^2.0.2"
electron-is-accelerator@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz#509e510c26a56b55e17f863a4b04e111846ab27b"
integrity sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=
electron-is-dev@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.3.0.tgz#14e6fda5c68e9e4ecbeff9ccf037cbd7c05c5afe"
integrity sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=
electron-is-dev@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.0.1.tgz#6e0a184736fe7aea77d18210b0b0f6a02402c4bc"
integrity sha512-iwM3EotA9HTXqMGpQRkR/kT8OZqBbdfHTnlwcxsjSLYqY8svvsq0MuujsWCn3/vtgRmDv/PC/gKUUpoZvi5C1w==
electron-localshortcut@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/electron-localshortcut/-/electron-localshortcut-3.1.0.tgz#10c1ffd537b8d39170aaf6e1551341f7780dd2ce"
integrity sha512-MgL/j5jdjW7iA0R6cI7S045B0GlKXWM1FjjujVPjlrmyXRa6yH0bGSaIAfxXAF9tpJm3pLEiQzerYHkRh9JG/A==
dependencies:
debug "^2.6.8"
electron-is-accelerator "^0.1.0"
keyboardevent-from-electron-accelerator "^1.1.0"
keyboardevents-areequal "^0.2.1"
electron-log@^2.2.17:
version "2.2.17"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-2.2.17.tgz#e71e2ebb949fc96ded7cdb99eeee7202e48981d2"
@@ -1147,7 +1163,7 @@ electron-publish@20.38.5:
"electron-telemetry@git+https://github.com/thomasnordquist/electron-telemetry.git#dist":
version "1.0.0"
resolved "git+https://github.com/thomasnordquist/electron-telemetry.git#3d6c0c5e3fd9d8101dcb7a0435256572ed58b2b6"
resolved "git+https://github.com/thomasnordquist/electron-telemetry.git#24792a0d8d6c2b047855acfda7d740ee4e51b97f"
dependencies:
axios "^0.18.0"
pako "^1.0.8"
@@ -1168,10 +1184,10 @@ electron-updater@^4.0.6:
semver "^5.6.0"
source-map-support "^0.5.9"
electron@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/electron/-/electron-4.0.6.tgz#f936772ddb5eaa026591ea14ef806e1385fbbda9"
integrity sha512-r2ow/EmDibjoCNJp35mB1CcPs2xBF9fp2eoWuUOJmpVQyzdba6EnqsSD1BxwXzF9vQ5WkQ2UbfXBIvpomrdioQ==
electron@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/electron/-/electron-4.0.2.tgz#b5e074c149841490825d0785aa2bc4f5e29bbea4"
integrity sha512-H0pmSvOVuC+Mq/+cYNXbCDBmqq5d1xYUVdBOjqGJuwuwJeP7qDHF35JA3cq+ARzq/CbwlXq98zdW6i6+x3U24g==
dependencies:
"@types/node" "^10.12.18"
electron-download "^4.1.0"
@@ -1439,7 +1455,7 @@ fs-extra@^4.0.1:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^7.0.0, fs-extra@^7.0.1:
fs-extra@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
@@ -2062,6 +2078,16 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
keyboardevent-from-electron-accelerator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.0.tgz#324614f6e33490c37ffc5be5876b3e85fe223c84"
integrity sha512-VDC4vKWGrR3VgIKCE4CsXnvObGgP8C2idnTKEMUkuEuvDGE1GEBX9FtNdJzrD00iQlhI3xFxRaeItsUmlERVng==
keyboardevents-areequal@^0.2.1:
version "0.2.2"
resolved "https://registry.yarnpkg.com/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz#88191ec738ce9f7591c25e9056de928b40277194"
integrity sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==
latest-version@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"