Compare commits

..
Author SHA1 Message Date
Thomas Nordquist 679e319912 Add bad examples 2019-10-10 10:42:09 +02:00
115 changed files with 3681 additions and 5788 deletions
+1 -5
View File
@@ -1,9 +1,6 @@
{
"language": "en",
"words": [
"Bbreak",
"nodered",
"goog",
"thomasnordquist",
"nowrap",
"subheader",
@@ -46,7 +43,6 @@
"DEVTOOLS",
"mixins",
"Explorerdmg",
"heapsnapshot",
"noconflict"
"heapsnapshot"
]
}
+2 -8
View File
@@ -19,7 +19,7 @@ os:
osx_image: xcode10.2
dist: bionic
dist: xenial
services:
- docker
@@ -35,12 +35,6 @@ script:
- export TRAVIS_BUILD_NUMBER="" # Override travis build number since it is uses for tagging the binary version https://github.com/electron-userland/electron-builder/issues/3730
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app thomasnordquist/ui-test-recording-env 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
openssl aes-256-cbc -d -in res/snapstore-credentials.enc -out credentials -k $SNAPSTORE_CREDENTIALS_DECRYPTION_KEY;
snapcraft login --with credentials;
rm credentials;
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 unset CSC_LINK; yarn run package -- win; fi
+3 -32
View File
@@ -20,7 +20,6 @@ Pull-Requests and error reports are welcome.
## Run from sources
```bash
npm install -g yarn
yarn
yarn build
yarn start
@@ -30,7 +29,6 @@ yarn start
Launch Application
```bash
npm install -g yarn
yarn
yarn dev
```
@@ -41,52 +39,25 @@ The `app` directory contains all the rendering logic, the `backend` directory cu
To achieve a reliable product automated tests run regularly on travis.
- Data model
- MQTT integration
- UI-Tests (The demo is a recorded ui test)
- Data model
- MQTT integration
- UI-Tests (The demo is a recorded ui test)
## Run UI-tests
A [mosquitto](https://mosquitto.org/) MQTT broker is required to run the ui-tests.
Run tests with
```bash
# Run chromedriver in a separate terminal session
./node_modules/.bin/chromedriver --url-base=wd/hub --port=9515 --verbose
```
Compile and execute tests
```bash
npm run build
node dist/src/spec/webdriverio.js
```
## Write docs
```
git clone https://github.com/thomasnordquist/MQTT-Explorer.git mqtt-explorer-pages
cd mqtt-explorer-pages
git checkout gh-pages
bundle install
bundle exec jekyll serve --incremental
```
Readme file: `Readme.tpl.md`
Preview is available at
http://localhost:4000/Readme.tpl
## Update docs
```
npm install
./updateReadme.ts
```
The readme will be generated from the docs.
## License
![CC-BY-ND 4.0](https://img.shields.io/badge/License-CC%20BY--ND%204.0-blue.svg)
+3 -4
View File
@@ -160,7 +160,7 @@
</style>
</head>
<body>
<div id="app" style="font: -webkit-control;"></div>
<div id="app" style="font:-webkit-control"></div>
<script>
function loadScript(path) {
var script = document.createElement('script')
@@ -174,8 +174,7 @@
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>
<script src="<%- file %>"></script>
<% }); %>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script
><% }); %>
</body>
</html>
+17 -22
View File
@@ -6,9 +6,7 @@
"scripts": {
"build": "yarn rebuild && webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=7.1.1 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -",
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
"mochatest": "mocha --require ts-node/register src/**/*.spec.ts"
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=5.0.7 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
},
"author": "",
"license": "CC-BY-ND-4.0",
@@ -17,17 +15,17 @@
"@material-ui/icons": "^4",
"@material-ui/lab": "^4.0.0-alpha",
"@material-ui/styles": "^4",
"@types/react-transition-group": "^4",
"ace-builds": "^1.4.11",
"@types/react-transition-group": "^2.9.2",
"axios": "^0.19.0",
"compare-versions": "^3.5.0",
"brace": "^0.11.1",
"compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^2.1.0",
"d3": "^5.9.7",
"d3": "^5.9.2",
"d3-shape": "^1.3.5",
"diff": "^4.0.1",
"dot-prop": "^5.0.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"file-loader": "6",
"file-loader": "^4.0.0",
"get-value": "^3.0.1",
"immutable": "^4.0.0-rc.12",
"in-viewport": "^3.6.0",
@@ -40,50 +38,47 @@
"number-abbreviate": "^2.0.0",
"parse-duration": "^0.1.1",
"prismjs": "^1.15.0",
"react": "^16.11",
"react-ace": "^8",
"react": "16.8",
"react-ace": "^7.0.1",
"react-dom": "^16.7.0",
"react-redux": "^7.0.3",
"react-resize-detector": "^4.1.4",
"react-split-pane": "^0.1.85",
"react-transition-group": "^4",
"react-transition-group": "^4.1.1",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "0.5",
"redux-batched-actions": "^0.4.1",
"redux-thunk": "^2.3.0",
"sha1": "^1.1.1",
"socket.io-client": "^2.2.0",
"uuid": "7"
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/d3": "^5.7.2",
"@types/diff": "^4.0.1",
"@types/get-value": "^3.0.1",
"@types/node": "^12.7.8",
"@types/node": "^12.0.4",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.9.4",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^7.0.9",
"@types/react-resize-detector": "^4.0.1",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^1.4.32",
"@types/uuid": "^7.0.2",
"@types/uuid": "^3.4.4",
"@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1",
"chai": "^4.2.0",
"cross-env": "^7.0.2",
"css-loader": "^3.0.0",
"hard-source-webpack-plugin": "^0.13.1",
"heapdump": "^0.3.12",
"html-webpack-plugin": "^4.0.0-beta.5",
"mocha": "^7.1.1",
"node-loader": "^0.6.0",
"source-map-loader": "^0.2.4",
"style-loader": "^1",
"typescript": "^3.6.3",
"style-loader": "^0.23.1",
"typescript": "^3.2.2",
"webpack": "^4.28.2",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.3.6",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
},
"peerDependencies": {
+3 -1
View File
@@ -45,7 +45,9 @@ export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () =>
return
}
const charts = getState().charts.get('charts').toArray()
const charts = getState()
.charts.get('charts')
.toArray()
let viewStates: ConnectionViewStateDictionary | undefined
try {
+61 -31
View File
@@ -10,13 +10,12 @@ import { default as persistentStorage, StorageIdentifier } from '../utils/Persis
import { Dispatch } from 'redux'
import { showError } from './Global'
import { remote } from 'electron'
import { promises as fsPromise } from 'fs'
import * as fs from 'fs'
import * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
import { connectionsMigrator } from './migrations/Connection'
export interface ConnectionDictionary {
import { ActionTypes, Action } from '../reducers/ConnectionManager'
interface ConnectionDictionary {
[s: string]: ConnectionOptions
}
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
@@ -28,12 +27,6 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
try {
await ensureConnectionsHaveBeenInitialized()
connections = await persistentStorage.load(storedConnectionsIdentifier)
// Apply migrations
if (connections && connectionsMigrator.isMigrationNecessary(connections)) {
connections = connectionsMigrator.applyMigrations(connections)
await persistentStorage.store(storedConnectionsIdentifier, connections)
}
} catch (error) {
dispatch(showError(error))
}
@@ -72,25 +65,35 @@ async function openCertificate(): Promise<CertificateParameters> {
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
}
const openDialogReturnValue = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
properties: ['openFile'],
securityScopedBookmarks: true,
return new Promise((resolve, reject) => {
remote.dialog.showOpenDialog(
{ properties: ['openFile'], securityScopedBookmarks: true },
(filePaths?: Array<string>) => {
const selectedFile = filePaths && filePaths[0]
if (!selectedFile) {
reject(rejectReasons.noCertificateSelected)
return
}
fs.readFile(selectedFile, (error, data) => {
if (error) {
reject(error)
return
}
if (data.length > 16_384 || data.length < 128) {
reject(rejectReasons.certificateSizeDoesNotMatch)
return
}
resolve({
data: data.toString('base64'),
name: path.basename(selectedFile),
})
})
}
)
})
const selectedFile = openDialogReturnValue.filePaths && openDialogReturnValue.filePaths[0]
if (!selectedFile) {
throw rejectReasons.noCertificateSelected
}
const data = await fsPromise.readFile(selectedFile)
if (data.length > 16_384 || data.length < 128) {
throw rejectReasons.certificateSizeDoesNotMatch
}
return {
data: data.toString('base64'),
name: path.basename(selectedFile),
}
}
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
@@ -108,13 +111,13 @@ export const updateConnection = (connectionId: string, changeSet: Partial<Connec
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,
})
export const addSubscription = (subscription: Subscription, connectionId: string): Action => ({
export const addSubscription = (subscription: string, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION,
})
export const deleteSubscription = (subscription: Subscription, connectionId: string): Action => ({
export const deleteSubscription = (subscription: string, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION,
@@ -181,4 +184,31 @@ async function ensureConnectionsHaveBeenInitialized() {
clearLegacyConnectionOptions()
}
// Migrate connections, rewrite dictionary to "keep" it "ordered" (dictionaries do not have a guaranteed order)
const mayNeedMigrations = connections && connections['iot.eclipse.org']
if (connections && mayNeedMigrations) {
let newConnections = {}
for (const connection of Object.values(connections)) {
addMigratedConnection(newConnections, connection)
}
await persistentStorage.store(storedConnectionsIdentifier, newConnections)
}
}
function addMigratedConnection(newConnections: { [key: string]: ConnectionOptions }, connection: ConnectionOptions) {
// The host has been renamed, only change the host if it has not been changed
// Also check for ssl since SSL is not yet working
if (
connection.id === 'iot.eclipse.org' &&
connection.host === 'iot.eclipse.org' &&
connection.port === 1883 &&
!connection.encryption
) {
connection.id = 'mqtt.eclipse.org'
connection.host = 'mqtt.eclipse.org'
connection.name = 'mqtt.eclipse.org'
}
newConnections[connection.id] = connection
}
+1 -1
View File
@@ -34,7 +34,7 @@ export const setEditorMode = (editorMode: string): Action => {
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const state = getState()
const topic = state.publish.manualTopic ?? state.tree.get('selectedTopic')?.path()
const topic = state.publish.topic
if (!topic) {
return
+9 -3
View File
@@ -105,15 +105,21 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
const topicFilter = filterStr.toLowerCase()
const nodeFilter = (node: q.TreeNode<TopicViewModel>): boolean => {
const topicMatches = node.path().toLowerCase().indexOf(topicFilter) !== -1
const topicMatches =
node
.path()
.toLowerCase()
.indexOf(topicFilter) !== -1
if (topicMatches) {
return true
}
const messageMatches =
node.message &&
node.message.payload &&
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1
node.message.value &&
Base64Message.toUnicodeString(node.message.value)
.toLowerCase()
.indexOf(filterStr) !== -1
return Boolean(messageMatches)
}
+2 -2
View File
@@ -29,9 +29,9 @@ const debouncedSelectTopic = debounce(
// Update publish topic
let setTopicDispatch: any | undefined
if (!getState().publish.manualTopic) {
if (!getState().publish.topic) {
setTopicDispatch = setTopic(topic.path())
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.manualTopic) {
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.topic) {
setTopicDispatch = setTopic(topic.path())
}
-1
View File
@@ -48,7 +48,6 @@ export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean) => async
payload: null,
retain: true,
qos: 0 as 0,
messageId: undefined,
}
// Rate limit deletion
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
-94
View File
@@ -1,94 +0,0 @@
import { ConfigMigrator, Migration } from '../../utils/ConfigMigrator'
import { ConnectionDictionary } from '../ConnectionManager'
import { ConnectionOptions } from '../../model/ConnectionOptions'
export interface ConnectionOptionsV0 {
type: 'mqtt'
id: string
host: string
protocol: 'mqtt' | 'ws'
basePath?: string
port: number
name: string
username?: string
password?: string
encryption: boolean
certValidation: boolean
// selfSignedCertificate?: CertificateParameters
// clientCertificate?: CertificateParameters
// clientKey?: CertificateParameters
clientId?: string
subscriptions: Array<string>
}
let migrations: Migration[] = [
// iot.eclipse.org ha moved to mqtt.eclipse.org
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
if (connection.id == 'iot.eclipse.org' && connection.host == 'iot.eclipse.org' && connection.port == 1883) {
return {
...connection,
id: 'mqtt.eclipse.org',
host: 'mqtt.eclipse.org',
name: 'mqtt.eclipse.org',
}
}
return {
...connection,
}
},
},
// Remove stored clientId if it is the default generated client id. This allows to connect multiple instances of mqtt explorer to the same broker.
// A randomly generated clientId will be used if no clientId is set.
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
if (connection.clientId && /mqtt-explorer-[0-9a-f]{8}/.test(connection.clientId)) {
return {
...connection,
clientId: undefined,
}
}
return {
...connection,
}
},
},
// Added QoS level to subscription options
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptions => {
return {
...connection,
configVersion: 1,
subscriptions: connection.subscriptions.map(topic => ({ topic, qos: 0 })),
}
},
},
]
const connectionMigrator = new ConfigMigrator(migrations)
function isMigrationNecessary(connections: ConnectionDictionary): boolean {
return Object.values(connections)
.map(connection => connectionMigrator.isMigrationNecessary(connection))
.reduce((a, b) => a || b, false)
}
function applyMigrations(connections: ConnectionDictionary): ConnectionDictionary {
let newConnectionDictionary: ConnectionDictionary = {}
Object.keys(connections).forEach(key => {
let newConnection = connectionMigrator.applyMigrations(connections[key]) as any
newConnectionDictionary[newConnection.id] = newConnection
})
return newConnectionDictionary
}
export const connectionsMigrator = {
isMigrationNecessary,
applyMigrations,
}
+6 -1
View File
@@ -157,4 +157,9 @@ const mapStateToProps = (state: AppState) => {
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(App)
)
@@ -2,20 +2,16 @@ import { Props } from '../Chart'
import { useMemo } from 'react'
import { Point } from '../Model'
function defaultFor(a: number | undefined, b: number) {
return a === undefined ? b : a
}
export function useCustomYDomain(props: Props) {
return useMemo(() => {
const data = props.data
const calculatedDomain = domainForData(data)
const yDomain: [number, number] = props.range
? [defaultFor(props.range[0], calculatedDomain[0]), defaultFor(props.range[1], calculatedDomain[1])]
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
: calculatedDomain
return yDomain
}, [props.data, props.range])
}, [props.data])
}
function domainForData(data: Array<Point>): [number, number] {
@@ -23,10 +19,8 @@ function domainForData(data: Array<Point>): [number, number] {
const defaultDomain: [number, number] = [-1, 1]
return defaultDomain
}
let max = data[0].y
let min = data[0].y
data.forEach(d => {
if (max < d.y) {
max = d.y
@@ -65,4 +65,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(memo(ColorSettings))
export default connect(
undefined,
mapDispatchToProps
)(memo(ColorSettings))
@@ -60,4 +60,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(InterpolationSettings)
export default connect(
undefined,
mapDispatchToProps
)(InterpolationSettings)
@@ -33,4 +33,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(MoveUp)
export default connect(
undefined,
mapDispatchToProps
)(MoveUp)
@@ -95,7 +95,10 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(RangeSettings)
export default connect(
undefined,
mapDispatchToProps
)(RangeSettings)
function useRangeStateToFireUpdateAction(
rangeFrom: string | number | undefined,
@@ -47,4 +47,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(memo(Size))
export default connect(
undefined,
mapDispatchToProps
)(memo(Size))
@@ -96,4 +96,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
export default connect(
undefined,
mapDispatchToProps
)(TimeRangeSettings)
@@ -25,7 +25,6 @@ function ChartSettings(props: {
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
const [sizeVisible, setSizeVisible] = React.useState(false)
const [colorVisible, setColorVisible] = React.useState(false)
const open = props.open
const toggleRange = React.useCallback(() => {
if (open) {
+4 -1
View File
@@ -133,4 +133,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
export default connect(
undefined,
mapDispatchToProps
)(memo(TopicChart))
+4 -1
View File
@@ -126,4 +126,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ChartPanel))
@@ -1,6 +1,7 @@
import * as React from 'react'
import { useState, useCallback, memo } from 'react'
import Add from '@material-ui/icons/Add'
import ClearAdornment from '../helper/ClearAdornment'
import Delete from '@material-ui/icons/Delete'
import Lock from '@material-ui/icons/Lock'
import Undo from '@material-ui/icons/Undo'
import { bindActionCreators } from 'redux'
@@ -8,10 +9,8 @@ import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Button, Grid, TextField, Tooltip } from '@material-ui/core'
import { QosSelect } from '../QosSelect'
import { QoS } from '../../../../backend/src/DataSource/MqttSource'
import Subscriptions from './Subscriptions'
import { Button, Grid, IconButton, TextField, List, ListItem, ListItemText, Tooltip } from '@material-ui/core'
interface Props {
connection: ConnectionOptions
@@ -19,93 +18,116 @@ interface Props {
managerActions: typeof connectionManagerActions
}
const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
const [qos, setQos] = useState<QoS>(0)
const [topic, setTopic] = useState('')
const { classes } = props
interface State {
subscription: string
}
const updateSubscription = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => setTopic(event.target.value),
[]
)
class ConnectionSettings extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { subscription: '' }
}
const handleChange = useCallback(
(name: string) => (event: any) => {
props.managerActions.updateConnection(props.connection.id, {
[name]: event.target.value,
})
},
[]
)
private handleChange = (name: string) => (event: any) => {
this.props.managerActions.updateConnection(this.props.connection.id, {
[name]: event.target.value,
})
}
private renderSubscriptions() {
const connection = this.props.connection
return connection.subscriptions.map(subscription => (
<Subscription
deleteAction={() => this.props.managerActions.deleteSubscription(subscription, connection.id)}
subscription={subscription}
key={subscription}
/>
))
}
public render() {
const { classes } = this.props
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={10} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="Subscription"
margin="normal"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ subscription: event.target.value })
}
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
className={classes.button}
color="secondary"
onClick={() =>
this.props.managerActions.addSubscription(this.state.subscription, this.props.connection.id)
}
variant="contained"
>
<Add /> Add
</Button>
</Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}>
<List className={`${classes.topicList} advanced-connection-settings-topic-list`} component="nav">
<div className={classes.list}>{this.renderSubscriptions()}</div>
</List>
</Grid>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="MQTT Client ID"
margin="normal"
value={this.props.connection.clientId}
onChange={this.handleChange('clientId')}
/>
</Grid>
<Grid item={true} xs={3} className={classes.gridPadding}>
<div>
<Tooltip title="Manage tls connection certificates" placement="top">
<Button
variant="contained"
className={classes.button}
onClick={() => this.props.managerActions.toggleCertificateSettings()}
>
<Lock /> Certificates
</Button>
</Tooltip>
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
onClick={this.props.managerActions.toggleAdvancedSettings}
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
)
}
}
const Subscription = (props: { subscription: string; deleteAction: any }) => {
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={8} className={classes.gridPadding}>
<TextField
className={`${classes.fullWidth} advanced-connection-settings-topic-input`}
label="Topic"
placeholder="example/topic"
margin="normal"
value={topic}
onChange={updateSubscription}
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<div className={classes.qos}>
<QosSelect label="QoS" selected={qos} onChange={setQos} />
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
className={classes.button}
color="secondary"
onClick={() => props.managerActions.addSubscription({ topic, qos }, props.connection.id)}
variant="contained"
>
<Add /> Add
</Button>
</Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}>
<Subscriptions connection={props.connection} />
</Grid>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="MQTT Client ID"
margin="normal"
value={props.connection.clientId}
onChange={handleChange('clientId')}
/>
</Grid>
<Grid item={true} xs={3} className={classes.gridPadding}>
<div>
<Tooltip title="Manage tls connection certificates" placement="top">
<Button
variant="contained"
className={classes.button}
onClick={() => props.managerActions.toggleCertificateSettings()}
>
<Lock /> Certificates
</Button>
</Tooltip>
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
onClick={props.managerActions.toggleAdvancedSettings}
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
<ListItem style={{ padding: '0 0 0 8px' }}>
<ListItemText>
<IconButton onClick={props.deleteAction} style={{ padding: '6px' }}>
<Delete />
</IconButton>
{props.subscription}
</ListItemText>
</ListItem>
)
})
}
const mapDispatchToProps = (dispatch: any) => {
return {
@@ -120,13 +142,19 @@ const styles = (theme: Theme) => ({
gridPadding: {
padding: '0 12px !important',
},
topicList: {
height: '180px',
overflowY: 'scroll' as 'scroll',
margin: '8px 16px',
backgroundColor: theme.palette.background.default,
},
button: {
marginTop: theme.spacing(3),
float: 'right' as 'right',
},
qos: {
marginTop: theme.spacing(1),
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(ConnectionSettings))
@@ -81,4 +81,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection))
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(CertificateFileSelection))
@@ -104,4 +104,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Certificates))
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(Certificates))
@@ -1,7 +1,7 @@
import ConnectButton from './ConnectButton'
import Delete from '@material-ui/icons/Delete'
import React, { useCallback, useState } from 'react'
import Save from '@material-ui/icons/Save'
import Delete from '@material-ui/icons/Delete'
import Settings from '@material-ui/icons/Settings'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
@@ -285,4 +285,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSettings))
@@ -134,4 +134,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSetup))
@@ -51,4 +51,7 @@ export const connectionItemStyle = (theme: Theme) => ({
},
})
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
export default connect(
null,
mapDispatchToProps
)(withStyles(connectionItemStyle)(ConnectionItem))
@@ -77,4 +77,7 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ProfileList))
@@ -1,90 +0,0 @@
import React, { useCallback, useState } from 'react'
import Delete from '@material-ui/icons/Delete'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import {
IconButton,
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Theme,
} from '@material-ui/core'
import { bindActionCreators } from 'redux'
import { withStyles } from '@material-ui/styles'
import { connect } from 'react-redux'
function Subscriptions(props: {
classes: any
connection: ConnectionOptions
managerActions: typeof connectionManagerActions
}) {
const { classes, connection, managerActions } = props
return (
<TableContainer component={Paper} className={`${classes.topicList} advanced-connection-settings-topic-list`}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="left" padding="checkbox" className={classes.tableTitleCell}></TableCell>
<TableCell className={classes.tableTitleCell}>Topic</TableCell>
<TableCell align="right" className={classes.tableTitleCell}>
QoS
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{connection.subscriptions.map(subscription => (
<TableRow key={subscription.topic + '_qos_' + subscription.qos}>
<TableCell align="right" className={classes.tableCell}>
<IconButton
onClick={() => managerActions.deleteSubscription(subscription, connection.id)}
style={{ padding: '6px' }}
>
<Delete />
</IconButton>
</TableCell>
<TableCell component="th" scope="row" className={classes.tableCell}>
{subscription.topic}
</TableCell>
<TableCell align="right" className={classes.tableCell}>
{subscription.qos}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
tableCell: {
paddingTop: 0,
paddingBottom: 0,
wordBbreak: 'break-word',
},
tableTitleCell: {
paddingTop: `${theme.spacing(0.5)}px`,
paddingBottom: `${theme.spacing(0.5)}px`,
},
topicList: {
height: '196px',
overflowY: 'scroll' as 'scroll',
margin: `${theme.spacing(1)}px ${theme.spacing(1)}px 0 ${theme.spacing(1)}px`,
backgroundColor: theme.palette.background.default,
width: 'auto',
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions))
+1 -1
View File
@@ -1,6 +1,6 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import cursor from './cursor.png'
const cursor = require('./cursor.png')
interface State {
enabled: boolean
+4 -1
View File
@@ -102,4 +102,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(PauseButton))
+4 -3
View File
@@ -35,7 +35,6 @@ function SearchBar(props: {
useGlobalKeyEventHandler(undefined, event => {
const isCharacter = event.key.length === 1
const isModifierKey = event.metaKey || event.ctrlKey
const isAllowedControlCharacter = event.keyCode === KeyCodes.backspace || event.keyCode === KeyCodes.delete
const tagNameBlacklist = ['INPUT', 'TEXTAREA', 'RADIO', 'CHECKBOX', 'OPTION', 'FORM']
@@ -44,7 +43,6 @@ function SearchBar(props: {
if (
(isCharacter || isAllowedControlCharacter) &&
!isModifierKey &&
!event.defaultPrevented &&
!hasFocus &&
tagElementIsNotBlacklisted &&
@@ -142,4 +140,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SearchBar))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(SearchBar))
+4 -1
View File
@@ -103,4 +103,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(TitleBar))
@@ -123,8 +123,8 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
return null
}
const str = node.message.payload ? Base64Message.toUnicodeString(node.message.payload) : ''
let value = node.message && node.message.payload ? parseFloat(str) : NaN
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
let value = node.message && node.message.value ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str
return (
@@ -255,4 +255,9 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Settings))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Settings)
)
@@ -82,4 +82,9 @@ const styles = (theme: Theme) => ({
},
})
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TimeLocaleSettings))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(TimeLocaleSettings)
)
@@ -85,4 +85,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(ChartPreview)
export default connect(
undefined,
mapDispatchToProps
)(ChartPreview)
-20
View File
@@ -1,20 +0,0 @@
import React, { memo } from 'react'
import { Message } from '../../../../backend/src/Model'
import { Tooltip } from '@material-ui/core'
export const MessageId = memo(function MessageId(props: { message: Message; addComma?: boolean }) {
const { message, addComma } = props
if (!message.messageId) {
return null
}
return (
<Tooltip title="MessageIds are used to signal a successful transmission of a message.">
<span>
#msg: {message.messageId}
{addComma ? ', ' : ''}
</span>
</Tooltip>
)
})
+5 -14
View File
@@ -1,14 +1,11 @@
import * as React from 'react'
import { default as AceEditor } from 'react-ace'
import { Theme, withTheme } from '@material-ui/core'
import 'ace-builds'
import 'ace-builds/webpack-resolver'
import 'ace-builds/src-noconflict/mode-json'
import 'ace-builds/src-noconflict/mode-xml'
import 'ace-builds/src-noconflict/snippets/json'
import 'ace-builds/src-noconflict/snippets/xml'
import 'ace-builds/src-noconflict/mode-text'
import 'ace-builds/src-noconflict/theme-monokai'
import 'brace/mode/json'
import 'brace/theme/dawn'
import 'brace/theme/monokai'
import 'brace/mode/xml'
import 'brace/mode/text'
import 'react-ace'
function Editor(props: {
@@ -16,7 +13,6 @@ function Editor(props: {
theme: Theme
value: string | undefined
onChange: (value: string) => void
editorRef: React.Ref<AceEditor>
}) {
const editorOptions = {
showLineNumbers: false,
@@ -25,17 +21,12 @@ function Editor(props: {
return (
<AceEditor
ref={props.editorRef}
style={{}}
mode={props.editorMode}
theme={props.theme.palette.type === 'dark' ? 'monokai' : 'dawn'}
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
enableSnippets={true}
enableBasicAutocompletion={true}
enableLiveAutocompletion={true}
showPrintMargin={false}
showGutter={true}
value={props.value}
onChange={props.onChange}
@@ -4,7 +4,6 @@ import { FormControlLabel, Radio, RadioGroup } from '@material-ui/core'
interface Props {
value: string
onChange: (event: React.ChangeEvent<{}>, value: string) => void
focusEditor: () => void
}
export function EditorModeSelect(props: Props) {
const labelStyle = { margin: '0 8px 0 8px' }
@@ -12,7 +11,6 @@ export function EditorModeSelect(props: Props) {
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={props.value}
onFocus={props.focusEditor}
onChange={props.onChange}
row={true}
>
+48 -64
View File
@@ -3,7 +3,7 @@ import FormatAlignLeft from '@material-ui/icons/FormatAlignLeft'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import PublishHistory from './PublishHistory'
import React, { useCallback, useMemo, useState, useRef, memo } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import RetainSwitch from './RetainSwitch'
import TopicInput from './TopicInput'
import { AppState } from '../../../reducers'
@@ -13,7 +13,6 @@ import { connect } from 'react-redux'
import { EditorModeSelect } from './EditorModeSelect'
import { globalActions, publishActions } from '../../../actions'
import { KeyCodes } from '../../../utils/KeyCodes'
import { default as AceEditor } from 'react-ace'
interface Props {
connectionId?: string
@@ -42,13 +41,8 @@ function useHistory(): [Array<Message>, (topic: string, payload?: string) => voi
}
function Publish(props: Props) {
const editorRef = useRef<AceEditor>()
const [history, amendToHistory] = useHistory()
const focusEditor = useCallback(() => {
editorRef.current?.editor.focus()
}, [editorRef])
const publish = useCallback(() => {
if (!props.connectionId) {
return
@@ -76,23 +70,17 @@ function Publish(props: Props) {
return useMemo(
() => (
<div style={{ flexGrow: 1, width: '100%' }} onKeyDown={handleSubmit}>
<div style={{ flexGrow: 1 }} onKeyDown={handleSubmit}>
<TopicInput />
<div style={{ width: '100%', display: 'block' }}>
<EditorMode
focusEditor={focusEditor}
actions={props.actions}
globalActions={props.globalActions}
payload={props.payload}
editorMode={props.editorMode}
publish={publish}
/>
<Editor
value={props.payload}
editorMode={props.editorMode}
onChange={props.actions.setPayload}
editorRef={editorRef as any}
/>
<Editor value={props.payload} editorMode={props.editorMode} onChange={props.actions.setPayload} />
<RetainSwitch />
</div>
<PublishHistory history={history} />
@@ -102,10 +90,9 @@ function Publish(props: Props) {
)
}
const EditorMode = memo(function EditorMode(props: {
function EditorMode(props: {
payload?: string
editorMode: string
focusEditor: () => void
actions: typeof publishActions
globalActions: typeof globalActions
publish: () => void
@@ -127,43 +114,41 @@ const EditorMode = memo(function EditorMode(props: {
}
}, [props.payload])
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} focusEditor={props.focusEditor} />
<FormatJsonButton editorMode={props.editorMode} focusEditor={props.focusEditor} formatJson={formatJson} />
<div style={{ float: 'right' }}>
<PublishButton publish={props.publish} focusEditor={props.focusEditor} />
const renderFormatJson = useCallback(() => {
if (props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', margin: '0 8px' }}
onClick={formatJson}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
}, [formatJson, props.editorMode, props.publish])
return useMemo(
() => (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px', textAlign: 'center' }}>
<EditorModeSelect value={props.editorMode} onChange={updateMode} />
{renderFormatJson()}
<div style={{ float: 'right' }}>
<PublishButton publish={props.publish} />
</div>
</div>
</div>
</div>
),
[props.editorMode, renderFormatJson]
)
})
}
const FormatJsonButton = React.memo(function FormatJsonButton(props: {
editorMode: string
focusEditor: () => void
formatJson: () => void
}) {
if (props.editorMode !== 'json') {
return null
}
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', margin: '0 8px' }}
onClick={props.formatJson}
onFocus={props.focusEditor}
id="sidebar-publish-format-json"
>
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
)
})
const PublishButton = memo(function PublishButton(props: { publish: () => void; focusEditor: () => void }) {
const PublishButton = (props: { publish: () => void }) => {
const handleClickPublish = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
@@ -172,19 +157,15 @@ const PublishButton = memo(function PublishButton(props: { publish: () => void;
[props.publish]
)
return (
<Button
variant="contained"
size="small"
color="primary"
onClick={handleClickPublish}
onFocus={props.focusEditor}
id="publish-button"
>
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
return useMemo(
() => (
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
),
[handleClickPublish]
)
})
}
const mapDispatchToProps = (dispatch: any) => {
return {
@@ -195,11 +176,14 @@ const mapDispatchToProps = (dispatch: any) => {
const mapStateToProps = (state: AppState) => {
return {
topic: state.publish.manualTopic,
topic: state.publish.topic,
payload: state.publish.payload,
editorMode: state.publish.editorMode,
retain: state.publish.retain,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(Publish))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTheme(Publish))
@@ -34,4 +34,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(PublishHistory)
export default connect(
undefined,
mapDispatchToProps
)(PublishHistory)
@@ -1,34 +0,0 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { publishActions } from '../../../actions'
import { QosSelect } from '../../QosSelect'
import { QoS } from '../../../../../backend/src/DataSource/MqttSource'
interface Props {
qos: QoS
actions: {
publish: typeof publishActions
}
}
function QosPublishOption(props: Props) {
return <QosSelect onChange={props.actions.publish.setQoS} selected={props.qos} />
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
publish: bindActionCreators(publishActions, dispatch),
},
}
}
const mapStateToProps = (state: AppState) => {
return {
qos: state.publish.qos,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(QosPublishOption)
@@ -1,8 +1,18 @@
import * as React from 'react'
import { TextField, MenuItem, Tooltip } from '@material-ui/core'
import { QoS } from '../../../backend/src/DataSource/MqttSource'
import { connect } from 'react-redux'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { publishActions } from '../../../actions'
export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void; label?: string }) {
interface Props {
qos: 0 | 1 | 2
actions: {
publish: typeof publishActions
}
}
function QosSelect(props: Props) {
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
const itemStyle = { padding: '0' }
@@ -12,16 +22,16 @@ export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void
if (value !== 0 && value !== 1 && value !== 2) {
return
}
props.onChange(value)
props.actions.publish.setQoS(value)
},
[props.onChange]
[props.actions.publish]
)
return (
<TextField
select={true}
label={props.label}
value={props.selected}
value={props.qos}
margin="normal"
style={{ margin: '8px 0 8px 8px' }}
onChange={onChangeQos}
@@ -45,4 +55,21 @@ export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void
)
}
export default React.memo(QosSelect)
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
publish: bindActionCreators(publishActions, dispatch),
},
}
}
const mapStateToProps = (state: AppState) => {
return {
qos: state.publish.qos,
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(QosSelect)
@@ -1,4 +1,4 @@
import QosSelect from './QosPublishOption'
import QosSelect from './QosSelect'
import React from 'react'
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
import { publishActions } from '../../../actions'
@@ -41,4 +41,7 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RetainSwitch)
export default connect(
mapStateToProps,
mapDispatchToProps
)(RetainSwitch)
@@ -1,21 +1,18 @@
import ClearAdornment from '../../helper/ClearAdornment'
import React, { useCallback, useMemo, useRef } from 'react'
import React, { useCallback, useMemo } from 'react'
import { FormControl, Input, InputLabel } from '@material-ui/core'
import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
function TopicInput(props: { actions: typeof publishActions; manualTopic?: string; selectedTopic?: string }) {
const inputElement = useRef<HTMLInputElement>(null)
function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
const updateTopic = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
props.actions.setTopic(e.target.value)
}, [])
const clearTopic = useCallback(() => {
props.actions.setTopic('')
inputElement.current?.focus()
}, [])
const onTopicBlur = useCallback((e: React.FocusEvent<HTMLInputElement>) => {
@@ -24,7 +21,7 @@ function TopicInput(props: { actions: typeof publishActions; manualTopic?: strin
}
}, [])
const topicStr = props.manualTopic || ''
const topicStr = props.topic || ''
return useMemo(
() => (
@@ -32,7 +29,6 @@ function TopicInput(props: { actions: typeof publishActions; manualTopic?: strin
<FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
<Input
inputRef={inputElement}
id="publish-topic"
value={topicStr}
startAdornment={<span />}
@@ -40,7 +36,7 @@ function TopicInput(props: { actions: typeof publishActions; manualTopic?: strin
onBlur={onTopicBlur}
onChange={updateTopic}
multiline={false}
placeholder={props.selectedTopic ?? 'example/topic'}
placeholder="example/topic"
/>
</FormControl>
</div>
@@ -57,9 +53,11 @@ const mapDispatchToProps = (dispatch: any) => {
const mapStateToProps = (state: AppState) => {
return {
manualTopic: state.publish.manualTopic,
selectedTopic: state.tree.get('selectedTopic')?.path(),
topic: state.publish.topic,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TopicInput)
export default connect(
mapStateToProps,
mapDispatchToProps
)(TopicInput)
+6 -1
View File
@@ -99,4 +99,9 @@ const styles = (theme: Theme) => ({
},
})
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar)
)
@@ -66,4 +66,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic))
export default connect(
null,
mapDispatchToProps
)(withStyles(styles, { withTheme: true })(Topic))
@@ -8,7 +8,7 @@ export const TopicDeleteButton = (props: {
deleteTopicAction: (node: q.TreeNode<any>) => void
}) => {
const { node } = props
if (!node || !node.message || !node.message.payload) {
if (!node || !node.message || !node.message.value) {
return null
}
return (
@@ -43,4 +43,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(TopicPanel)
export default connect(
undefined,
mapDispatchToProps
)(TopicPanel)
@@ -73,4 +73,7 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ActionButtons))
@@ -33,4 +33,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(DeleteSelectedTopicButton)
export default connect(
undefined,
mapDispatchToProps
)(DeleteSelectedTopicButton)
@@ -12,7 +12,6 @@ import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { connect } from 'react-redux'
import CustomIconButton from '../../helper/CustomIconButton'
import { MessageId } from '../MessageId'
const throttle = require('lodash.throttle')
@@ -82,25 +81,19 @@ class MessageHistory extends React.PureComponent<Props, State> {
const history = node.messageHistory.toArray()
let previousMessage: q.Message | undefined = node.message
const historyElements = [...history].reverse().map((message, idx) => {
const value = message.payload ? Base64Message.toUnicodeString(message.payload) : ''
const value = message.value ? Base64Message.toUnicodeString(message.value) : ''
const element = {
value,
key: `${message.messageNumber}-${message.received}`,
title: (
<span>
<div style={{ float: 'left' }}>
<DateFormatter date={message.received} />
{previousMessage && previousMessage !== message ? (
<i>
(-
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
</i>
) : null}
</div>
<span>
&nbsp;
<MessageId message={message} />
</span>
<DateFormatter date={message.received} />
{previousMessage && previousMessage !== message ? (
<i>
(-
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
</i>
) : null}
<div style={{ float: 'right' }}>
<Copy value={value} />
</div>
@@ -113,7 +106,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
})
const isMessagePlottable =
node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload))
node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
return (
<div>
<History
@@ -127,7 +120,9 @@ class MessageHistory extends React.PureComponent<Props, State> {
>
<ShowChart style={{ marginTop: '-5px' }} />
</CustomIconButton>
) : undefined
) : (
undefined
)
}
onClick={this.displayMessage}
>
@@ -144,4 +139,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(null, mapDispatchToProps)(MessageHistory)
export default connect(
null,
mapDispatchToProps
)(MessageHistory)
@@ -13,7 +13,6 @@ import { Theme, Typography, withStyles } from '@material-ui/core'
import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions'
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
import { MessageId } from '../MessageId'
interface Props {
node?: q.TreeNode<any>
@@ -37,7 +36,7 @@ function ValuePanel(props: Props) {
const { node, compareMessage } = props
function renderViewOptions() {
if (!props.node || !props.node.message) {
if (!props.node || !props.node.message || !props.node.mqttMessage) {
return null
}
@@ -47,7 +46,7 @@ function ValuePanel(props: Props) {
<ActionButtons />
</span>
<span style={{ flex: 6, textAlign: 'right' }}>
{props.node.message.retain ? <DeleteSelectedTopicButton /> : null}
{props.node.mqttMessage.retain ? <DeleteSelectedTopicButton /> : null}
</span>
{messageMetaInfo()}
</div>
@@ -55,16 +54,13 @@ function ValuePanel(props: Props) {
}
function messageMetaInfo() {
if (!props.node || !props.node.message) {
if (!props.node || !props.node.message || !props.node.mqttMessage) {
return null
}
return (
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
<Typography style={{ textAlign: 'right' }}>
<MessageId message={props.node.message} addComma={true} />
{`QoS: ${props.node.message.qos}`}
</Typography>
<Typography style={{ textAlign: 'right' }}>QoS: {props.node.mqttMessage.qos}</Typography>
<Typography style={{ textAlign: 'right' }}>
<i>
<DateFormatter date={props.node.message.received} />
@@ -86,8 +82,8 @@ function ValuePanel(props: Props) {
)
const copyValue =
node && node.message && node.message.payload ? (
<Copy value={Base64Message.toUnicodeString(node.message.payload)} />
node && node.message && node.message.value ? (
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
) : null
return (
@@ -128,4 +124,7 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ValuePanel))
@@ -63,12 +63,12 @@ class ValueRenderer extends React.Component<Props, State> {
}
private renderRawMode(message: q.Message, compare?: q.Message) {
if (!message.payload) {
if (!message.value) {
return
}
const [value, valueLanguage] = this.convertMessage(message.payload)
const [value, valueLanguage] = this.convertMessage(message.value)
const [compareStr, compareStrLanguage] =
compare && compare.payload ? this.convertMessage(compare.payload) : [undefined, undefined]
compare && compare.value ? this.convertMessage(compare.value) : [undefined, undefined]
return (
<div>
@@ -95,12 +95,12 @@ class ValueRenderer extends React.Component<Props, State> {
if (renderMode === 'raw') {
return this.renderRawMode(message, compareWith)
}
if (!message.payload) {
if (!message.value) {
return null
}
const compareValue = compareMessage.payload || message.payload
const [current, currentLanguage] = this.convertMessage(message.payload)
const compareValue = compareMessage.value || message.value
const [current, currentLanguage] = this.convertMessage(message.value)
const [compare, compareLanguage] = this.convertMessage(compareValue)
const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined
+2 -2
View File
@@ -28,7 +28,7 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
return { x: message.received.getTime(), y: toPlottableValue(value) }
})
.filter(data => !isNaN(data.y as any)) as any
@@ -39,7 +39,7 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
.map((message: q.Message) => {
let json = {}
try {
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {}
json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
} catch (ignore) {}
const value = dotProp.get(json, dotPath)
@@ -27,17 +27,17 @@ class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
private truncatedMessage() {
const limit = 400
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) {
if (!this.props.treeNode.message || !this.props.treeNode.message.value) {
return ''
}
const str = Base64Message.toUnicodeString(this.props.treeNode.message.payload)
const str = Base64Message.toUnicodeString(this.props.treeNode.message.value)
return str.length > limit ? `${str.slice(0, limit)}` : str
}
private renderValue() {
return this.props.treeNode.message &&
this.props.treeNode.message.payload &&
this.props.treeNode.message.value &&
this.props.treeNode.message.length > 0 ? (
<span key="value" className={this.props.classes.value}>
{' '}
+1 -1
View File
@@ -81,7 +81,7 @@ function TreeNodeComponent(props: Props) {
const mouseOver = useCallback(
(event: React.MouseEvent) => {
event.stopPropagation()
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.payload) {
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.value) {
didSelectTopic()
}
},
+4 -1
View File
@@ -169,4 +169,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
export default connect(
mapStateToProps,
mapDispatchToProps
)(TreeComponent)
+6 -1
View File
@@ -266,4 +266,9 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(UpdateNotifier)
)
+4 -1
View File
@@ -59,4 +59,7 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(undefined, mapDispatchToProps)(Copy)
export default connect(
undefined,
mapDispatchToProps
)(Copy)
+2 -9
View File
@@ -1,6 +1,5 @@
import { MqttOptions } from '../../../backend/src/DataSource'
import { v4 } from 'uuid'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
const sha1 = require('sha1')
export interface CertificateParameters {
@@ -8,9 +7,7 @@ export interface CertificateParameters {
/** @property data base64 encoded data */
data: string
}
export interface ConnectionOptions {
configVersion: 1
type: 'mqtt'
id: string
host: string
@@ -26,7 +23,7 @@ export interface ConnectionOptions {
clientCertificate?: CertificateParameters
clientKey?: CertificateParameters
clientId?: string
subscriptions: Array<Subscription>
subscriptions: Array<string>
}
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
@@ -55,7 +52,6 @@ function generateClientId() {
export function createEmptyConnection(): ConnectionOptions {
return {
configVersion: 1,
certValidation: true,
clientId: generateClientId(),
id: v4() as string,
@@ -63,10 +59,7 @@ export function createEmptyConnection(): ConnectionOptions {
encryption: false,
password: undefined,
username: undefined,
subscriptions: [
{ topic: '#', qos: 0 },
{ topic: '$SYS/#', qos: 0 },
],
subscriptions: ['#', '$SYS/#'],
type: 'mqtt',
host: '',
port: 1883,
+1
View File
@@ -1,3 +1,4 @@
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record, List } from 'immutable'
import MoveUp from '../components/ChartPanel/ChartSettings/MoveUp'
+1
View File
@@ -1,4 +1,5 @@
import * as q from '../../../backend/src/Model'
import { Action } from 'redux'
import { createReducer } from './lib'
import { MqttOptions } from '../../../backend/src/DataSource'
import { TopicViewModel } from '../model/TopicViewModel'
+5 -28
View File
@@ -1,6 +1,6 @@
import { Action } from 'redux'
import { ConnectionOptions } from '../model/ConnectionOptions'
import { createReducer } from './lib'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
export interface ConnectionManagerState {
connections: { [s: string]: ConnectionOptions }
@@ -51,13 +51,13 @@ export interface SelectConnection {
export interface AddSubscription {
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION
subscription: Subscription
subscription: string
connectionId: string
}
export interface DeleteSubscription {
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION
subscription: Subscription
subscription: string
connectionId: string
}
@@ -160,12 +160,8 @@ function addSubscription(state: ConnectionManagerState, action: AddSubscription)
}
function deleteSubscription(state: ConnectionManagerState, action: AddSubscription): ConnectionManagerState {
function subscriptionsEqual(v1: Subscription, v2: Subscription): boolean {
return v1.topic == v2.topic && v1.qos == v2.qos
}
const connection = state.connections[action.connectionId]
const newSubscriptions = connection.subscriptions.filter(s => !subscriptionsEqual(s, action.subscription))
const newSubscriptions = connection.subscriptions.filter(s => s !== action.subscription)
return {
...state,
@@ -191,29 +187,10 @@ function deleteConnection(state: ConnectionManagerState, action: DeleteConnectio
function updateConnection(state: ConnectionManagerState, action: UpdateConnection): ConnectionManagerState {
let connection = state.connections[action.connectionId]
let changeSet = action.changeSet
// Reset empty username to undefined
if (changeSet.username !== undefined) {
changeSet = {
changeSet,
username: changeSet.username === '' ? undefined : changeSet.username,
}
}
// Reset empty password to undefined
if (changeSet.password !== undefined) {
changeSet = {
changeSet,
password: changeSet.password === '' ? undefined : changeSet.password,
}
}
connection = {
...connection,
...changeSet,
...action.changeSet,
}
return {
...state,
connections: {
+3 -2
View File
@@ -1,7 +1,8 @@
import { Action } from 'redux'
import { createReducer } from './lib'
export interface PublishState {
manualTopic?: string
topic?: string
payload?: string
retain: boolean
editorMode: string
@@ -59,7 +60,7 @@ export const publishReducer = createReducer(initialState, {
function setTopic(state: PublishState, action: SetTopic) {
return {
...state,
manualTopic: action.topic,
topic: action.topic,
}
}
+3 -4
View File
@@ -1,5 +1,5 @@
import * as q from '../../../backend/src/Model'
import { Action as ReduxAction } from 'redux'
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record } from 'immutable'
@@ -11,8 +11,7 @@ const initialStateFactory = Record<SidebarModel>({
compareMessage: undefined,
})
export type Action = SetCompareMessage | ResetStore
export type Action = SetCompareMessage
export enum ActionTypes {
SIDEBAR_SET_COMPARE_MESSAGE = 'SIDEBAR_SET_COMPARE_MESSAGE',
SIDEBAR_RESET_STORE = 'SIDEBAR_RESET_STORE',
@@ -21,7 +20,7 @@ export enum ActionTypes {
export type SidebarState = Record<SidebarModel>
const actions: {
[s: string]: (state: SidebarState, action: ReduxAction) => SidebarState
[s: string]: (state: SidebarState, action: Action) => SidebarState
} = {
SIDEBAR_SET_COMPARE_MESSAGE: setCompareMessage,
SIDEBAR_RESET_STORE: resetStore,
+2 -2
View File
@@ -1,5 +1,5 @@
import * as q from '../../../backend/src/Model'
import { Action as ReduxAction } from 'redux'
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record } from 'immutable'
import { TopicViewModel } from '../model/TopicViewModel'
@@ -50,7 +50,7 @@ const setPaused = (pause: boolean) => (state: TreeState, action: ShowTree): Tree
}
const actions: {
[s: string]: (state: TreeState, action: ReduxAction) => TreeState
[s: string]: (state: TreeState, action: Action) => TreeState
} = {
TREE_SHOW_TREE: showTree,
TREE_SELECT_TOPIC: selectTopic,
-34
View File
@@ -1,34 +0,0 @@
export interface Migration {
from: number | undefined
apply: (config: any) => any
}
interface MigrationSubject {
configVersion: number | undefined
}
export class ConfigMigrator {
private migrations: Migration[]
constructor(migrations: Migration[]) {
this.migrations = migrations
}
public applyMigrations(subject: MigrationSubject): Partial<MigrationSubject> {
while (this.isMigrationNecessary(subject)) {
subject = this.applicableMigrations(subject)
.filter(migration => migration.from === subject.configVersion)
.reduce((currentSubject, migration) => migration.apply(currentSubject as any), subject)
}
return subject
}
public isMigrationNecessary(subject: MigrationSubject): boolean {
return this.applicableMigrations(subject).length > 0
}
private applicableMigrations(subject: MigrationSubject): Migration[] {
return this.migrations.filter(migration => migration.from === subject.configVersion)
}
}
-115
View File
@@ -1,115 +0,0 @@
import { expect } from 'chai'
import { ConfigMigrator } from '../ConfigMigrator'
import 'mocha'
describe('ConfigMigrator', () => {
it('applies migrations to a subject with undefined configVersion', () => {
let migrator = new ConfigMigrator([
{
from: undefined,
apply: subject => {
return {
...subject,
configVersion: 1,
drink: 'Beer',
}
},
},
])
let config = {
drink: 'water',
}
let migratedConfig = migrator.applyMigrations(config as any) as any
expect(migratedConfig.drink).to.eq('Beer')
expect(migratedConfig.configVersion).to.eq(1)
})
it('applies multiple migrations from the same version', () => {
let migrator = new ConfigMigrator([
{
from: undefined,
apply: subject => {
return {
...subject,
drink: 'Beer',
}
},
},
{
from: undefined,
apply: subject => {
return {
...subject,
configVersion: 1,
}
},
},
])
let config = {
drink: 'water',
}
let migratedConfig = migrator.applyMigrations(config as any) as any
expect(migratedConfig.drink).to.eq('Beer')
expect(migratedConfig.configVersion).to.eq(1)
})
it('applies multiple migrations with ascending versions', () => {
let migrator = new ConfigMigrator([
{
from: undefined,
apply: subject => {
return {
...subject,
configVersion: 1,
}
},
},
{
from: 1,
apply: subject => {
return {
...subject,
configVersion: 2,
}
},
},
])
let config = {
drink: 'water',
}
let migratedConfig = migrator.applyMigrations(config as any)
expect(migratedConfig.configVersion).to.eq(2)
})
it('does not apply non-matching migrations', () => {
let migrator = new ConfigMigrator([
{
from: 2,
apply: subject => {
return {
...subject,
configVersion: 3,
}
},
},
])
let config = {
configVersion: 10,
drink: 'water',
}
let migratedConfig = migrator.applyMigrations(config as any)
expect(migratedConfig.configVersion).to.eq(10)
})
})
@@ -1,71 +0,0 @@
import 'mocha'
import { expect } from 'chai'
import { connectionsMigrator } from '../../actions/migrations/Connection'
describe('ConnectionsMigrator', () => {
it('applies migrations', () => {
let connections: any = {
'763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7': {
certValidation: true,
clientId: 'mqtt-explorer-2783f48c',
encryption: false,
host: 'nodered',
id: '763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7',
name: 'nodered',
port: 1883,
protocol: 'mqtt',
subscriptions: ['#', '$SYS/#'],
type: 'mqtt',
},
'iot.eclipse.org': {
certValidation: true,
clientId: 'mqtt-explorer-d913aad3',
encryption: false,
host: 'iot.eclipse.org',
id: 'iot.eclipse.org',
name: 'iot.eclipse.org',
port: 1883,
protocol: 'mqtt',
subscriptions: ['#', '$SYS/#'],
type: 'mqtt',
},
}
let migratedConnections: any = {
'763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7': {
configVersion: 1,
certValidation: true,
clientId: undefined,
encryption: false,
host: 'nodered',
id: '763b2e5c-c1ed-4c9b-ac9a-0970b3be29a7',
name: 'nodered',
port: 1883,
protocol: 'mqtt',
subscriptions: [
{ topic: '#', qos: 0 },
{ topic: '$SYS/#', qos: 0 },
],
type: 'mqtt',
},
'mqtt.eclipse.org': {
configVersion: 1,
certValidation: true,
clientId: undefined,
encryption: false,
host: 'mqtt.eclipse.org',
id: 'mqtt.eclipse.org',
name: 'mqtt.eclipse.org',
port: 1883,
protocol: 'mqtt',
subscriptions: [
{ topic: '#', qos: 0 },
{ topic: '$SYS/#', qos: 0 },
],
type: 'mqtt',
},
}
expect(connectionsMigrator.applyMigrations(connections)).to.deep.eq(migratedConnections)
})
})
-3
View File
@@ -1,3 +0,0 @@
--require ts-node/register
--require source-map-support/register
--recursive ./src/**/*.spec.ts
-6
View File
@@ -1,6 +0,0 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"module": "commonjs"
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
"allowSyntheticDefaultImports": true
},
"include": ["./src/**/*"],
"exclude": ["**/*.d.ts", ".src/**/*.png"],
"exclude": ["**/*.d.ts"],
"awesomeTypescriptLoaderOptions": {
"useCache": true,
"transpileModule": true,
+1411 -2343
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,3 +21,4 @@ test_script:
artifacts:
- path: 'build\clean\build\*.appx'
+1 -1
View File
@@ -1,7 +1,7 @@
import { DataSourceStateMachine } from './'
import { MqttMessage } from '../../../events'
type MessageCallback = (topic: string, payload: Buffer, packet: any) => void
type MessageCallback = (topic: string, payload: Buffer) => void
// A DataSource should automatically reconnect if connection was broken
interface DataSource<DataSourceOptions> {
+1 -1
View File
@@ -11,7 +11,7 @@ export class DataSourceStateMachine {
private state: DataSourceState = {
error: undefined,
connected: false,
connecting: false,
connecting: false
}
public setConnected(connected: boolean) {
+2 -9
View File
@@ -12,19 +12,12 @@ export interface MqttOptions {
tls: boolean
certValidation: boolean
clientId?: string
subscriptions: Array<Subscription>
subscriptions: Array<string>
certificateAuthority?: string
clientCertificate?: string
clientKey?: string
}
export interface Subscription {
topic: string
qos: QoS
}
export type QoS = 0 | 1 | 2
export class MqttSource implements DataSource<MqttOptions> {
public stateMachine: DataSourceStateMachine = new DataSourceStateMachine()
private client: Client | undefined
@@ -81,7 +74,7 @@ export class MqttSource implements DataSource<MqttOptions> {
client.on('connect', () => {
this.stateMachine.setConnected(true)
options.subscriptions.forEach(subscription => {
client.subscribe(subscription.topic, { qos: subscription.qos }, (err: Error) => {
client.subscribe(subscription, (err: Error) => {
if (err) {
this.stateMachine.setError(err)
}
+3 -5
View File
@@ -70,11 +70,9 @@ function jsonToPropertyPaths(ast: JsonAst, previousPath: Array<string> = []): Ar
} else if (ast.type === 'Array') {
children = ast.children.map((value, idx) => jsonToPropertyPaths(value, previousPath.slice().concat([String(idx)])))
} else if (ast.type === 'Object') {
children = ast.children.map(property => {
const path = property.key.value.replace('.', '\\.')
return jsonToPropertyPaths(property.value, previousPath.slice().concat([path]))
})
children = ast.children.map(property =>
jsonToPropertyPaths(property.value, previousPath.slice().concat([property.key.value]))
)
}
return children.reduce((a, b) => a.concat(b), [])
+6 -2
View File
@@ -1,8 +1,8 @@
const { Base64 } = require('js-base64')
export class Base64Message {
private base64Message: string
private unicodeValue: string
private base64Message: string;
private unicodeValue: string;
public length: number
@@ -27,4 +27,8 @@ export class Base64Message {
public static toDataUri(message: Base64Message, mimeType: string) {
return `data:${mimeType};base64,${message.base64Message}`
}
public static doStuff() {
}
}
+11
View File
@@ -32,6 +32,17 @@ export class ChangeBuffer {
return this.size / this.maxSize
}
public updateSize() {
let size
if (this.isFull()) {
size = this.maxSize
} else {
size = 0
}
this.size = size
}
public popAll(): Array<BufferedMessage> {
const tmpBuffer = this.buffer
this.buffer = []
+19 -1
View File
@@ -4,13 +4,15 @@ const sha1 = require('sha1')
export class Edge<ViewModel extends Destroyable> implements Hashable {
public name: string
public color?: Color
public target!: TreeNode<ViewModel>
public source?: TreeNode<ViewModel> | undefined
private cachedHash?: string
constructor(name: string) {
this.name = name
const hash = this.updateCache()
console.log(hash)
}
public edges(): Array<Edge<ViewModel>> {
@@ -40,4 +42,20 @@ export class Edge<ViewModel extends Destroyable> implements Hashable {
return this
}
public updateCache(): void {
if (this.cachedHash = "") {
this.cachedHash = sha1(this.name)
}
}
}
class Color implements Hashable {
public red: number = 0
public green: number = 0
public blue: number = 0
hash(): string {
return `${this.red}${this.green}${this.blue}`
}
}
+1 -10
View File
@@ -1,17 +1,8 @@
import { Base64Message } from './Base64Message'
import { QoS } from '../DataSource/MqttSource'
export interface Message {
// mqtt based info
payload: Base64Message | null
messageId?: number
retain: boolean
qos: QoS
// meta info
value?: Base64Message
length: number
received: Date
// Global message counter, not mqtt related
messageNumber: number
}
+8 -2
View File
@@ -31,7 +31,7 @@ export class Tree<ViewModel extends Destroyable> extends TreeNode<ViewModel> {
if (!this.paused && this.applyChangesHasCompleted) {
this.applyChangesHasCompleted = false
if ((window as any).requestIdleCallback) {
;(window as any).requestIdleCallback(() => this.applyUnmergedChanges(), { timeout: 500 })
; (window as any).requestIdleCallback(() => this.applyUnmergedChanges(), { timeout: 500 })
} else {
this.applyUnmergedChanges()
}
@@ -75,7 +75,13 @@ export class Tree<ViewModel extends Destroyable> extends TreeNode<ViewModel> {
public applyUnmergedChanges() {
this.unmergedMessages.popAll().forEach(bufferedItem => {
const node = TreeNodeFactory.fromMessage<ViewModel>(bufferedItem.message, bufferedItem.received)
const edges = bufferedItem.message.topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue<ViewModel>(
edges,
bufferedItem.message.payload,
bufferedItem.received
)
node.mqttMessage = bufferedItem.message
if (!this.nodeFilter || this.nodeFilter(node)) {
this.updateWithNode(node.firstNode())
+6 -3
View File
@@ -1,10 +1,11 @@
import { Destroyable } from './Destroyable'
import { Edge, Message, RingBuffer, MessageHistory } from './'
import { EventDispatcher } from '../../../events'
import { EventDispatcher, MqttMessage } from '../../../events'
export class TreeNode<ViewModel extends Destroyable> {
public sourceEdge?: Edge<ViewModel>
public message?: Message
public mqttMessage?: MqttMessage
public messageHistory: MessageHistory = new RingBuffer<Message>(20000, 100)
public viewModel?: ViewModel
public edges: { [s: string]: Edge<ViewModel> } = {}
@@ -104,7 +105,7 @@ export class TreeNode<ViewModel extends Destroyable> {
}
public hasMessage() {
return this.message && this.message.payload && this.message.length !== 0
return this.message && this.message.value && this.message.value.length !== 0
}
public destroy() {
@@ -130,6 +131,7 @@ export class TreeNode<ViewModel extends Destroyable> {
public unconnectedClone() {
const node = new TreeNode<ViewModel>()
node.message = this.message
node.mqttMessage = this.mqttMessage
node.messageHistory = this.messageHistory.clone()
node.messages = this.messages
node.lastUpdate = this.lastUpdate
@@ -201,6 +203,7 @@ export class TreeNode<ViewModel extends Destroyable> {
if (node.message) {
this.setMessage(node.message)
this.onMessage.dispatch(node.message)
this.mqttMessage = node.mqttMessage
}
this.removeFromTreeIfEmpty()
@@ -233,7 +236,7 @@ export class TreeNode<ViewModel extends Destroyable> {
public childTopics(): Array<TreeNode<ViewModel>> {
if (this.cachedChildTopics === undefined) {
const initialValue = this.message && this.message.payload ? [this] : []
const initialValue = this.message && this.message.value ? [this] : []
this.cachedChildTopics = this.edgeArray
.map(e => e.target.childTopics())
+7 -9
View File
@@ -1,6 +1,6 @@
import { Base64Message } from './Base64Message'
import { Destroyable } from './Destroyable'
import { Edge, Tree, TreeNode } from './'
import { MqttMessage } from '../../../events'
export abstract class TreeNodeFactory {
private static messageCounter = 0
@@ -20,23 +20,21 @@ export abstract class TreeNodeFactory {
node.sourceEdge!.target = node
}
public static fromMessage<ViewModel extends Destroyable>(
mqttMessage: MqttMessage,
public static fromEdgesAndValue<ViewModel extends Destroyable>(
edgeNames: Array<string>,
value?: Base64Message | null,
receiveDate: Date = new Date()
): TreeNode<ViewModel> {
const node = new TreeNode<ViewModel>()
const edges = mqttMessage.topic.split('/')
mqttMessage.retain
node.setMessage({
...mqttMessage,
length: mqttMessage.payload?.length ?? 0,
value: value || undefined,
length: value ? value.length : 0,
received: receiveDate,
messageNumber: this.messageCounter,
})
this.messageCounter += 1
this.insertNodeAtPosition<ViewModel>(edges, node)
this.insertNodeAtPosition<ViewModel>(edgeNames, node)
return node
}
+9 -6
View File
@@ -1,7 +1,6 @@
import 'mocha'
import { Edge, TreeNodeFactory } from '../'
import { expect } from 'chai'
import { Edge } from '../'
import { makeTreeNode } from './makeTreeNode'
import 'mocha'
describe('Edge', () => {
it('should contain a name', () => {
@@ -10,7 +9,8 @@ describe('Edge', () => {
})
it('firstEdge should retrieve the first edge', () => {
const leaf = makeTreeNode('foo/bar/baz')
const topics = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
const bazEdge = leaf.sourceEdge
if (!bazEdge) {
@@ -34,8 +34,11 @@ describe('Edge', () => {
})
it('hash should include change if parents are different', () => {
const bazEdge1 = makeTreeNode('foo/bar/baz').sourceEdge
const bazEdge2 = makeTreeNode('foo/foo/baz').sourceEdge
const topics1 = 'foo/bar/baz'.split('/')
const bazEdge1 = TreeNodeFactory.fromEdgesAndValue(topics1, undefined).sourceEdge
const topics2 = 'foo/foo/baz'.split('/')
const bazEdge2 = TreeNodeFactory.fromEdgesAndValue(topics2, undefined).sourceEdge
if (!bazEdge1 || !bazEdge2) {
throw Error('should not happen')
@@ -3,7 +3,7 @@ import { expect } from 'chai'
import 'mocha'
describe('EventDispatcher', async () => {
it('should dispatch', async function () {
it('should dispatch', async function() {
const dispatcher = new EventDispatcher<string>()
this.timeout(300)
@@ -17,7 +17,7 @@ describe('EventDispatcher', async () => {
expect(response).to.eq('hello')
})
it('should unsubscribe', async function () {
it('should unsubscribe', async function() {
const dispatcher = new EventDispatcher<string>()
this.timeout(300)
let callbackCounter = 0
+6 -4
View File
@@ -1,16 +1,18 @@
import 'mocha'
import { Tree, TreeNodeFactory } from '../'
import { expect } from 'chai'
import { Tree } from '../'
import { makeTreeNode } from './makeTreeNode'
describe('Tree', () => {
it('node can be merged into a tree', () => {
const tree = new Tree()
const leaf = makeTreeNode('foo/bar')
const topics = 'foo/bar'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
tree.updateWithNode(leaf.firstNode())
const expectedNode = tree.findNode('foo/bar')
expect(expectedNode).to.eq(leaf)
})
})
@@ -2,11 +2,11 @@ import 'mocha'
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
import { makeTreeNode } from './makeTreeNode'
describe('TreeNode.findNode', () => {
it('findNode should retrieve node', () => {
const leaf = makeTreeNode('foo/bar/baz')
const topics = 'foo/bar/baz'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
const root = leaf.firstNode()
expect(root.sourceEdge).to.eq(undefined)
+19 -15
View File
@@ -1,51 +1,55 @@
import 'mocha'
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
import { Base64Message } from '../Base64Message'
import { makeTreeNode } from './makeTreeNode'
describe('TreeNode', () => {
const number3 = Base64Message.fromString('3')
const number5 = Base64Message.fromString('5')
it('firstNode should retrieve first node', () => {
const leaf = makeTreeNode('foo/bar')
const topics = 'foo/bar'.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
expect(leaf.firstNode().edges['foo']).to.not.eq(undefined)
})
it('updateWithNode should update value', () => {
const topics = 'foo/bar'.split('/')
const leaf = makeTreeNode('foo/bar', '3')
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
const updateLeave = makeTreeNode('foo/bar', '5')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, Base64Message.fromString('3'))
expect(Base64Message.toUnicodeString(leaf.message!.value!)).to.eq('3')
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics, Base64Message.fromString('5'))
const root = leaf.firstNode()
root.updateWithNode(updateLeave.firstNode())
expect(root.sourceEdge).to.eq(undefined)
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('5')
expect(Base64Message.toUnicodeString(leaf.message!.value!)).to.eq('5')
})
it('updateWithNode should update intermediate nodes', () => {
const topics1 = 'foo/bar/baz'.split('/')
const leaf = makeTreeNode('foo/bar/baz', '3')
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
const leaf = TreeNodeFactory.fromEdgesAndValue(topics1, Base64Message.fromString('3'))
expect(Base64Message.toUnicodeString(leaf.message!.value!)).to.eq('3')
const topics2 = 'foo/bar'.split('/')
const updateLeave = makeTreeNode('foo/bar', '5')
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics2, Base64Message.fromString('5'))
leaf.firstNode().updateWithNode(updateLeave.firstNode())
const barNode = leaf.firstNode().findNode('foo/bar')
expect(barNode && barNode.sourceEdge && barNode.sourceEdge.name).to.eq('bar')
expect(Base64Message.toUnicodeString(barNode!.message!.payload!)).to.eq('5')
expect(Base64Message.toUnicodeString(barNode!.message!.value!)).to.eq('5')
expect(leaf.sourceEdge && leaf.sourceEdge.name).to.eq('baz')
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
expect(Base64Message.toUnicodeString(leaf.message!.value!)).to.eq('3')
})
it('updateWithNode should add nodes to the tree', () => {
const leaf1 = makeTreeNode('foo/bar', 'foo')
const leaf2 = makeTreeNode('foo/bar/baz', 'bar')
const topics1 = 'foo/bar'.split('/')
const leaf1 = TreeNodeFactory.fromEdgesAndValue(topics1, Base64Message.fromString('foo'))
const topics2 = 'foo/bar/baz'.split('/')
const leaf2 = TreeNodeFactory.fromEdgesAndValue(topics2, Base64Message.fromString('bar'))
leaf1.firstNode().updateWithNode(leaf2.firstNode())
+13 -6
View File
@@ -1,17 +1,22 @@
import 'mocha'
import { TreeNodeFactory } from '../'
import { expect } from 'chai'
import { Base64Message } from '../Base64Message'
import { makeTreeNode } from './makeTreeNode'
describe('TreeNodeFactory', () => {
it('root node must not have a sourceEdge', () => {
const leaf = makeTreeNode('foo/bar')
const topic = 'foo/bar'
const edges = topic.split('/')
const leaf = TreeNodeFactory.fromEdgesAndValue(edges, undefined)
expect(leaf.firstNode().sourceEdge).to.eq(undefined)
})
it('should create node', () => {
const node = makeTreeNode('foo/bar', '5')
const topic = 'foo/bar'
const edges = topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue(edges, Base64Message.fromString('5'))
if (!node.sourceEdge || !node.sourceEdge.source || !node.message) {
expect.fail('should not happen')
@@ -20,21 +25,23 @@ describe('TreeNodeFactory', () => {
expect(node).to.not.eq(undefined)
expect(node.sourceEdge.name).to.eq('bar')
expect(Base64Message.toUnicodeString(node.message.payload!)).to.eq('5')
expect(Base64Message.toUnicodeString(node.message.value!)).to.eq('5')
const foo = node.firstNode().findNode('foo')
expect(foo && foo.sourceEdge && foo.sourceEdge.name).to.eq('foo')
})
it('node should contain edges in order', () => {
const node = makeTreeNode('foo/bar/baz', '5')
const topic = 'foo/bar/baz'
const edges = topic.split('/')
const node = TreeNodeFactory.fromEdgesAndValue(edges, Base64Message.fromString('5'))
if (!node.sourceEdge || !node.sourceEdge.source || !node.message) {
expect.fail('should not happen')
return
}
expect(Base64Message.toUnicodeString(node.message.payload!)).to.eq('5')
expect(Base64Message.toUnicodeString(node.message.value!)).to.eq('5')
expect(node.sourceEdge.name).to.eq('baz')
const barNode = node.sourceEdge.source
-16
View File
@@ -1,16 +0,0 @@
import { TreeNodeFactory } from '../'
import { Base64Message } from '../Base64Message'
import { TreeNode } from '../TreeNode'
import { MqttMessage } from '../../../../events'
export function makeTreeNode(topic: string, message?: string): TreeNode<any> {
const mqttMessage: MqttMessage = {
topic,
payload: message ? Base64Message.fromString(message) : null,
qos: 0,
retain: false,
messageId: undefined,
}
return TreeNodeFactory.fromMessage(mqttMessage)
}
-2
View File
@@ -45,13 +45,11 @@ export class ConnectionManager {
if (buffer.length > 20000) {
buffer = buffer.slice(0, 20000)
}
backendEvents.emit(messageEvent, {
topic,
payload: Base64Message.fromBuffer(buffer),
qos: packet.qos,
retain: packet.retain,
messageId: packet.messageId,
})
})
}
-2
View File
@@ -34,8 +34,6 @@ export interface MqttMessage {
payload: Base64Message | null
qos: 0 | 1 | 2
retain: boolean
// Set if QoS is > 0 on received messages
messageId: number | undefined
}
export function makePublishEvent(connectionId: string): Event<MqttMessage> {
+23 -27
View File
@@ -1,13 +1,11 @@
{
"name": "MQTT-Explorer",
"version": "0.4.0-beta1",
"version": "0.3.6-no-delete-limit",
"description": "Explore your message queues",
"main": "dist/src/electron.js",
"scripts": {
"start": "electron .",
"test": "yarn test:app && yarn test:backend",
"test:app": "cd app && yarn test",
"test:backend": "cd backend && yarn test",
"test": "yarn run test-backend",
"install": "cd app && yarn && cd ..",
"dev": "npm-run-all --parallel dev:*",
"dev:app": "cd app && npm run dev",
@@ -17,6 +15,7 @@
"lint:tslint": "tslint -p ./",
"lint:spellcheck": "cspell -e ./build -e \"node_modules\" \"**/*.ts{x,}\"",
"build": "tsc && cd app && yarn run build && cd ..",
"test-backend": "cd backend && yarn run test && cd ..",
"prepare-release": "ts-node scripts/prepare-release.ts",
"package": "ts-node package.ts",
"ui-test": "./scripts/uiTests.sh",
@@ -34,7 +33,6 @@
"mac": {
"appId": "de.t7n.apps.mqtt-explorer",
"category": "public.app-category.developer-tools",
"hardenedRuntime": true,
"publish": [
"github"
],
@@ -62,8 +60,7 @@
"buildResources": "res",
"output": "build"
},
"afterPack": "./dist/scripts/afterPack.js",
"afterSign": "./scripts/afterSign.js"
"afterPack": "./dist/scripts/afterPack.js"
},
"author": "Thomas Nordquist",
"email": "xxnerowingerxx@gmail.com",
@@ -71,49 +68,48 @@
"license": "CC-BY-ND-4.0",
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/fs-extra": "8",
"@types/fs-extra": "^7.0.0",
"@types/lowdb": "^1.0.6",
"@types/mime": "^2.0.0",
"@types/mocha": "^7.0.2",
"@types/mustache": "4",
"@types/node": "^12.6.8",
"@types/semver": "7",
"@types/mocha": "^5.2.5",
"@types/mustache": "^0.8.32",
"@types/node": "^12.0.4",
"@types/semver": "^6.0.0",
"@types/sha1": "^1.1.1",
"builder-util-runtime": "^8.2.5",
"chai": "^4.2.0",
"cspell": "^4.0.28",
"electron": "8.2.3",
"electron-builder": "22.5.1",
"electron-notarize": "^0.3.0",
"mocha": "7.1",
"mustache": "4",
"cspell": "^4.0.23",
"electron": "5.0.7",
"electron-builder": "21.1.2",
"mime": "^2.4.0",
"mocha": "^6.1.4",
"mustache": "^3.0.1",
"npm-run-all": "^4.1.5",
"nyc": "15",
"prettier": "2",
"nyc": "^14.1.1",
"prettier": "1.18.2",
"redux-thunk": "^2.3.0",
"source-map-support": "^0.5.9",
"spectron": "10",
"spectron": "^6.0.0",
"ts-node": "^8.2.0",
"tslint": "6",
"tslint": "^5.15.0",
"tslint-config-airbnb": "^5.11.1",
"tslint-react": "^4.0.0",
"tslint-react-recommended": "^1.0.15",
"tslint-strict-null-checks": "^1.0.1",
"typescript": "^3.2.2",
"webdriverio": "6"
"webdriverio": "5.5"
},
"dependencies": {
"about-window": "^1.12.1",
"axios": "^0.21.1",
"axios": "^0.19.0",
"dot-prop": "^5.0.0",
"electron-log": "4.1.1",
"electron-log": "^3.0.6",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"electron-updater": "^4.0.6",
"fs-extra": "9",
"fs-extra": "^8.0.1",
"js-base64": "^2.5.1",
"json-to-ast": "^2.1.0",
"lowdb": "^1.0.0",
"mime": "^2.4.4",
"mqtt": "^3.0.0",
"sha1": "^1.1.1",
"yarn-run-all": "^3.1.1"
-1
View File
@@ -4,5 +4,4 @@ module.exports = {
semi: false,
singleQuote: true,
printWidth: 120,
arrowParens: 'avoid',
};

Some files were not shown because too many files have changed in this diff Show More