mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-11 17:13:53 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
679e319912 |
+1
-5
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
+3
-4
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -133,4 +133,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(TopicChart))
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -102,4 +102,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(PauseButton))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
+34
-7
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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
|
||||
|
||||
@@ -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}>
|
||||
{' '}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -169,4 +169,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TreeComponent)
|
||||
|
||||
@@ -266,4 +266,9 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UpdateNotifier)
|
||||
)
|
||||
|
||||
@@ -59,4 +59,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(Copy)
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(Copy)
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
import { Action } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
import { Record, List } from 'immutable'
|
||||
import MoveUp from '../components/ChartPanel/ChartSettings/MoveUp'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
--require ts-node/register
|
||||
--require source-map-support/register
|
||||
--recursive ./src/**/*.spec.ts
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["**/*.d.ts", ".src/**/*.png"],
|
||||
"exclude": ["**/*.d.ts"],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
"useCache": true,
|
||||
"transpileModule": true,
|
||||
|
||||
+1411
-2343
File diff suppressed because it is too large
Load Diff
@@ -21,3 +21,4 @@ test_script:
|
||||
|
||||
artifacts:
|
||||
- path: 'build\clean\build\*.appx'
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -11,7 +11,7 @@ export class DataSourceStateMachine {
|
||||
private state: DataSourceState = {
|
||||
error: undefined,
|
||||
connected: false,
|
||||
connecting: false,
|
||||
connecting: false
|
||||
}
|
||||
|
||||
public setConnected(connected: boolean) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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), [])
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user