mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-13 10:02:46 +00:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbb6ead7ba | ||
|
|
ad888f6f9a | ||
|
|
07458cd712 | ||
|
|
6fc0d3f28d | ||
|
|
7a91e4dee6 | ||
|
|
3c40be97b7 | ||
|
|
ff00f7a99e | ||
|
|
b72fc48bdb | ||
|
|
e87a7115c5 | ||
|
|
14dac22732 | ||
|
|
ccdaaa6ce2 | ||
|
|
487eee93cd | ||
|
|
59737edfb4 | ||
|
|
30af13f793 | ||
|
|
19e8bfdb37 | ||
|
|
a89d7cf62e | ||
|
|
65e9f2e074 | ||
|
|
ff66d10aaa | ||
|
|
5203d40a24 | ||
|
|
237c718a0a | ||
|
|
fbfbe94d19 | ||
|
|
e24e505cc0 | ||
|
|
28743ba646 | ||
|
|
f63e73ee0a | ||
|
|
fbad7afa79 | ||
|
|
34cc38ab3c | ||
|
|
8645789550 | ||
|
|
4db6e7b1d7 | ||
|
|
1fae61b1fa | ||
|
|
b26eac3edb | ||
|
|
6d1354bf07 | ||
|
|
7148e302f6 | ||
|
|
763a1aea69 | ||
|
|
94a92e9b29 | ||
|
|
922f90c4eb | ||
|
|
ff11973a9a | ||
|
|
29fc2eea4d | ||
|
|
577e1e6b10 | ||
|
|
d113fab279 | ||
|
|
a6c401ab6a | ||
|
|
6cc8c8810b | ||
|
|
040a29da30 | ||
|
|
45f31ad931 | ||
|
|
39ddb984ab | ||
|
|
197788eb79 | ||
|
|
c85bc0323c | ||
|
|
65c2a6eff1 | ||
|
|
5a4f3a0388 | ||
|
|
03f9c6a26e | ||
|
|
ae7ecd4bc9 | ||
|
|
25469936fa | ||
|
|
bc2d066bb9 | ||
|
|
71e376e0e4 | ||
|
|
354b014245 | ||
|
|
d69779c497 | ||
|
|
af1219cda8 |
+5
-1
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"language": "en",
|
||||
"words": [
|
||||
"Bbreak",
|
||||
"nodered",
|
||||
"goog",
|
||||
"thomasnordquist",
|
||||
"nowrap",
|
||||
"subheader",
|
||||
@@ -43,6 +46,7 @@
|
||||
"DEVTOOLS",
|
||||
"mixins",
|
||||
"Explorerdmg",
|
||||
"heapsnapshot"
|
||||
"heapsnapshot",
|
||||
"noconflict"
|
||||
]
|
||||
}
|
||||
|
||||
+8
-2
@@ -19,7 +19,7 @@ os:
|
||||
|
||||
osx_image: xcode10.2
|
||||
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
|
||||
services:
|
||||
- docker
|
||||
@@ -35,6 +35,12 @@ 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 yarn run package linux; 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" == "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,6 +20,7 @@ Pull-Requests and error reports are welcome.
|
||||
## Run from sources
|
||||
|
||||
```bash
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn build
|
||||
yarn start
|
||||
@@ -29,6 +30,7 @@ yarn start
|
||||
|
||||
Launch Application
|
||||
```bash
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn dev
|
||||
```
|
||||
@@ -39,25 +41,52 @@ 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
|
||||
|
||||

|
||||
|
||||
+4
-3
@@ -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,7 +174,8 @@
|
||||
// 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>
|
||||
|
||||
+22
-17
@@ -6,7 +6,9 @@
|
||||
"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=5.0.7 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
|
||||
"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"
|
||||
},
|
||||
"author": "",
|
||||
"license": "CC-BY-ND-4.0",
|
||||
@@ -15,17 +17,17 @@
|
||||
"@material-ui/icons": "^4",
|
||||
"@material-ui/lab": "^4.0.0-alpha",
|
||||
"@material-ui/styles": "^4",
|
||||
"@types/react-transition-group": "^2.9.2",
|
||||
"@types/react-transition-group": "^4",
|
||||
"ace-builds": "^1.4.11",
|
||||
"axios": "^0.19.0",
|
||||
"brace": "^0.11.1",
|
||||
"compare-versions": "^3.4.0",
|
||||
"compare-versions": "^3.5.0",
|
||||
"copy-text-to-clipboard": "^2.1.0",
|
||||
"d3": "^5.9.2",
|
||||
"d3": "^5.9.7",
|
||||
"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": "^4.0.0",
|
||||
"file-loader": "6",
|
||||
"get-value": "^3.0.1",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"in-viewport": "^3.6.0",
|
||||
@@ -38,47 +40,50 @@
|
||||
"number-abbreviate": "^2.0.0",
|
||||
"parse-duration": "^0.1.1",
|
||||
"prismjs": "^1.15.0",
|
||||
"react": "16.8",
|
||||
"react-ace": "^7.0.1",
|
||||
"react": "^16.11",
|
||||
"react-ace": "^8",
|
||||
"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.1.1",
|
||||
"react-transition-group": "^4",
|
||||
"react-vis": "^1.11.6",
|
||||
"redux": "^4.0.1",
|
||||
"redux-batched-actions": "^0.4.1",
|
||||
"redux-batched-actions": "0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sha1": "^1.1.1",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"uuid": "^3.3.2"
|
||||
"uuid": "7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^5.7.2",
|
||||
"@types/diff": "^4.0.1",
|
||||
"@types/get-value": "^3.0.1",
|
||||
"@types/node": "^12.0.4",
|
||||
"@types/node": "^12.7.8",
|
||||
"@types/prismjs": "^1.9.1",
|
||||
"@types/react": "^16.7.18",
|
||||
"@types/react": "^16.9.4",
|
||||
"@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": "^3.4.4",
|
||||
"@types/uuid": "^7.0.2",
|
||||
"@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": "^0.23.1",
|
||||
"typescript": "^3.2.2",
|
||||
"style-loader": "^1",
|
||||
"typescript": "^3.6.3",
|
||||
"webpack": "^4.28.2",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-cli": "^3.3.6",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -45,9 +45,7 @@ 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,12 +10,13 @@ import { default as persistentStorage, StorageIdentifier } from '../utils/Persis
|
||||
import { Dispatch } from 'redux'
|
||||
import { showError } from './Global'
|
||||
import { remote } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import { promises as fsPromise } 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'
|
||||
|
||||
interface ConnectionDictionary {
|
||||
export interface ConnectionDictionary {
|
||||
[s: string]: ConnectionOptions
|
||||
}
|
||||
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
|
||||
@@ -27,6 +28,12 @@ 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))
|
||||
}
|
||||
@@ -65,35 +72,25 @@ async function openCertificate(): Promise<CertificateParameters> {
|
||||
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
|
||||
}
|
||||
|
||||
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 openDialogReturnValue = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), {
|
||||
properties: ['openFile'],
|
||||
securityScopedBookmarks: true,
|
||||
})
|
||||
|
||||
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) => {
|
||||
@@ -111,13 +108,13 @@ export const updateConnection = (connectionId: string, changeSet: Partial<Connec
|
||||
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,
|
||||
})
|
||||
|
||||
export const addSubscription = (subscription: string, connectionId: string): Action => ({
|
||||
export const addSubscription = (subscription: Subscription, connectionId: string): Action => ({
|
||||
connectionId,
|
||||
subscription,
|
||||
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION,
|
||||
})
|
||||
|
||||
export const deleteSubscription = (subscription: string, connectionId: string): Action => ({
|
||||
export const deleteSubscription = (subscription: Subscription, connectionId: string): Action => ({
|
||||
connectionId,
|
||||
subscription,
|
||||
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION,
|
||||
@@ -184,31 +181,4 @@ 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.topic
|
||||
const topic = state.publish.manualTopic ?? state.tree.get('selectedTopic')?.path()
|
||||
|
||||
if (!topic) {
|
||||
return
|
||||
|
||||
@@ -105,21 +105,15 @@ 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.value &&
|
||||
Base64Message.toUnicodeString(node.message.value)
|
||||
.toLowerCase()
|
||||
.indexOf(filterStr) !== -1
|
||||
node.message.payload &&
|
||||
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1
|
||||
|
||||
return Boolean(messageMatches)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ const debouncedSelectTopic = debounce(
|
||||
|
||||
// Update publish topic
|
||||
let setTopicDispatch: any | undefined
|
||||
if (!getState().publish.topic) {
|
||||
if (!getState().publish.manualTopic) {
|
||||
setTopicDispatch = setTopic(topic.path())
|
||||
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.topic) {
|
||||
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.manualTopic) {
|
||||
setTopicDispatch = setTopic(topic.path())
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ 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)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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,9 +157,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(App)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
|
||||
|
||||
@@ -2,16 +2,20 @@ 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
|
||||
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
|
||||
? [defaultFor(props.range[0], calculatedDomain[0]), defaultFor(props.range[1], calculatedDomain[1])]
|
||||
: calculatedDomain
|
||||
|
||||
return yDomain
|
||||
}, [props.data])
|
||||
}, [props.data, props.range])
|
||||
}
|
||||
|
||||
function domainForData(data: Array<Point>): [number, number] {
|
||||
@@ -19,8 +23,10 @@ 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,7 +65,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(ColorSettings))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(ColorSettings))
|
||||
|
||||
@@ -60,7 +60,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(InterpolationSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(InterpolationSettings)
|
||||
|
||||
@@ -33,7 +33,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(MoveUp)
|
||||
export default connect(undefined, mapDispatchToProps)(MoveUp)
|
||||
|
||||
@@ -95,10 +95,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(RangeSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(RangeSettings)
|
||||
|
||||
function useRangeStateToFireUpdateAction(
|
||||
rangeFrom: string | number | undefined,
|
||||
|
||||
@@ -47,7 +47,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(Size))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(Size))
|
||||
|
||||
@@ -96,7 +96,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(TimeRangeSettings)
|
||||
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
|
||||
|
||||
@@ -25,6 +25,7 @@ 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,7 +133,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(memo(TopicChart))
|
||||
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
|
||||
|
||||
@@ -126,7 +126,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ChartPanel))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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'
|
||||
@@ -9,8 +8,10 @@ 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, IconButton, TextField, List, ListItem, ListItemText, Tooltip } from '@material-ui/core'
|
||||
import { Button, Grid, TextField, Tooltip } from '@material-ui/core'
|
||||
import { QosSelect } from '../QosSelect'
|
||||
import { QoS } from '../../../../backend/src/DataSource/MqttSource'
|
||||
import Subscriptions from './Subscriptions'
|
||||
|
||||
interface Props {
|
||||
connection: ConnectionOptions
|
||||
@@ -18,116 +19,93 @@ interface Props {
|
||||
managerActions: typeof connectionManagerActions
|
||||
}
|
||||
|
||||
interface State {
|
||||
subscription: string
|
||||
}
|
||||
const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
|
||||
const [qos, setQos] = useState<QoS>(0)
|
||||
const [topic, setTopic] = useState('')
|
||||
const { classes } = props
|
||||
|
||||
class ConnectionSettings extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { subscription: '' }
|
||||
}
|
||||
|
||||
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 (
|
||||
<ListItem style={{ padding: '0 0 0 8px' }}>
|
||||
<ListItemText>
|
||||
<IconButton onClick={props.deleteAction} style={{ padding: '6px' }}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
{props.subscription}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
const updateSubscription = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => setTopic(event.target.value),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
(name: string) => (event: any) => {
|
||||
props.managerActions.updateConnection(props.connection.id, {
|
||||
[name]: event.target.value,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
@@ -142,19 +120,13 @@ 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,7 +81,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(CertificateFileSelection))
|
||||
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection))
|
||||
|
||||
@@ -104,7 +104,4 @@ 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,7 +285,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ConnectionSettings))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
|
||||
|
||||
@@ -134,7 +134,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ConnectionSetup))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
|
||||
|
||||
@@ -51,7 +51,4 @@ export const connectionItemStyle = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
|
||||
|
||||
@@ -77,7 +77,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ProfileList))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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'
|
||||
const cursor = require('./cursor.png')
|
||||
import cursor from './cursor.png'
|
||||
|
||||
interface State {
|
||||
enabled: boolean
|
||||
|
||||
@@ -102,7 +102,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(PauseButton))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
|
||||
|
||||
@@ -35,6 +35,7 @@ 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']
|
||||
|
||||
@@ -43,6 +44,7 @@ function SearchBar(props: {
|
||||
|
||||
if (
|
||||
(isCharacter || isAllowedControlCharacter) &&
|
||||
!isModifierKey &&
|
||||
!event.defaultPrevented &&
|
||||
!hasFocus &&
|
||||
tagElementIsNotBlacklisted &&
|
||||
@@ -140,7 +142,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(SearchBar))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SearchBar))
|
||||
|
||||
@@ -103,7 +103,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(TitleBar))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
|
||||
|
||||
+7
-34
@@ -1,18 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { TextField, MenuItem, Tooltip } from '@material-ui/core'
|
||||
import { connect } from 'react-redux'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { publishActions } from '../../../actions'
|
||||
import { QoS } from '../../../backend/src/DataSource/MqttSource'
|
||||
|
||||
interface Props {
|
||||
qos: 0 | 1 | 2
|
||||
actions: {
|
||||
publish: typeof publishActions
|
||||
}
|
||||
}
|
||||
|
||||
function QosSelect(props: Props) {
|
||||
export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void; label?: string }) {
|
||||
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
|
||||
const itemStyle = { padding: '0' }
|
||||
|
||||
@@ -22,16 +12,16 @@ function QosSelect(props: Props) {
|
||||
if (value !== 0 && value !== 1 && value !== 2) {
|
||||
return
|
||||
}
|
||||
|
||||
props.actions.publish.setQoS(value)
|
||||
props.onChange(value)
|
||||
},
|
||||
[props.actions.publish]
|
||||
[props.onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<TextField
|
||||
select={true}
|
||||
value={props.qos}
|
||||
label={props.label}
|
||||
value={props.selected}
|
||||
margin="normal"
|
||||
style={{ margin: '8px 0 8px 8px' }}
|
||||
onChange={onChangeQos}
|
||||
@@ -55,21 +45,4 @@ function QosSelect(props: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
export default React.memo(QosSelect)
|
||||
@@ -123,8 +123,8 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
|
||||
return null
|
||||
}
|
||||
|
||||
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
|
||||
let value = node.message && node.message.value ? parseFloat(str) : NaN
|
||||
const str = node.message.payload ? Base64Message.toUnicodeString(node.message.payload) : ''
|
||||
let value = node.message && node.message.payload ? parseFloat(str) : NaN
|
||||
value = !isNaN(value) ? abbreviate(value) : str
|
||||
|
||||
return (
|
||||
|
||||
@@ -255,9 +255,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Settings)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Settings))
|
||||
|
||||
@@ -82,9 +82,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TimeLocaleSettings)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TimeLocaleSettings))
|
||||
|
||||
@@ -85,7 +85,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(ChartPreview)
|
||||
export default connect(undefined, mapDispatchToProps)(ChartPreview)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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,11 +1,14 @@
|
||||
import * as React from 'react'
|
||||
import { default as AceEditor } from 'react-ace'
|
||||
import { Theme, withTheme } from '@material-ui/core'
|
||||
import 'brace/mode/json'
|
||||
import 'brace/theme/dawn'
|
||||
import 'brace/theme/monokai'
|
||||
import 'brace/mode/xml'
|
||||
import 'brace/mode/text'
|
||||
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 'react-ace'
|
||||
|
||||
function Editor(props: {
|
||||
@@ -13,6 +16,7 @@ function Editor(props: {
|
||||
theme: Theme
|
||||
value: string | undefined
|
||||
onChange: (value: string) => void
|
||||
editorRef: React.Ref<AceEditor>
|
||||
}) {
|
||||
const editorOptions = {
|
||||
showLineNumbers: false,
|
||||
@@ -21,12 +25,17 @@ 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,6 +4,7 @@ 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' }
|
||||
@@ -11,6 +12,7 @@ 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 } from 'react'
|
||||
import React, { useCallback, useMemo, useState, useRef, memo } from 'react'
|
||||
import RetainSwitch from './RetainSwitch'
|
||||
import TopicInput from './TopicInput'
|
||||
import { AppState } from '../../../reducers'
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -41,8 +42,13 @@ 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
|
||||
@@ -70,17 +76,23 @@ function Publish(props: Props) {
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<div style={{ flexGrow: 1 }} onKeyDown={handleSubmit}>
|
||||
<div style={{ flexGrow: 1, width: '100%' }} 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} />
|
||||
<Editor
|
||||
value={props.payload}
|
||||
editorMode={props.editorMode}
|
||||
onChange={props.actions.setPayload}
|
||||
editorRef={editorRef as any}
|
||||
/>
|
||||
<RetainSwitch />
|
||||
</div>
|
||||
<PublishHistory history={history} />
|
||||
@@ -90,9 +102,10 @@ function Publish(props: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
function EditorMode(props: {
|
||||
const EditorMode = memo(function EditorMode(props: {
|
||||
payload?: string
|
||||
editorMode: string
|
||||
focusEditor: () => void
|
||||
actions: typeof publishActions
|
||||
globalActions: typeof globalActions
|
||||
publish: () => void
|
||||
@@ -114,41 +127,43 @@ function EditorMode(props: {
|
||||
}
|
||||
}, [props.payload])
|
||||
|
||||
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>
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[props.editorMode, renderFormatJson]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const PublishButton = (props: { publish: () => void }) => {
|
||||
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 handleClickPublish = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -157,15 +172,19 @@ const PublishButton = (props: { publish: () => void }) => {
|
||||
[props.publish]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Button variant="contained" size="small" color="primary" onClick={handleClickPublish} id="publish-button">
|
||||
<Navigation style={{ marginRight: '8px' }} /> Publish
|
||||
</Button>
|
||||
),
|
||||
[handleClickPublish]
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={handleClickPublish}
|
||||
onFocus={props.focusEditor}
|
||||
id="publish-button"
|
||||
>
|
||||
<Navigation style={{ marginRight: '8px' }} /> Publish
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
@@ -176,14 +195,11 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topic: state.publish.topic,
|
||||
topic: state.publish.manualTopic,
|
||||
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,7 +34,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(PublishHistory)
|
||||
export default connect(undefined, mapDispatchToProps)(PublishHistory)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { publishActions } from '../../../actions'
|
||||
import { QosSelect } from '../../QosSelect'
|
||||
import { QoS } from '../../../../../backend/src/DataSource/MqttSource'
|
||||
|
||||
interface Props {
|
||||
qos: QoS
|
||||
actions: {
|
||||
publish: typeof publishActions
|
||||
}
|
||||
}
|
||||
|
||||
function QosPublishOption(props: Props) {
|
||||
return <QosSelect onChange={props.actions.publish.setQoS} selected={props.qos} />
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
actions: {
|
||||
publish: bindActionCreators(publishActions, dispatch),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
qos: state.publish.qos,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QosPublishOption)
|
||||
@@ -1,4 +1,4 @@
|
||||
import QosSelect from './QosSelect'
|
||||
import QosSelect from './QosPublishOption'
|
||||
import React from 'react'
|
||||
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
|
||||
import { publishActions } from '../../../actions'
|
||||
@@ -41,7 +41,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(RetainSwitch)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RetainSwitch)
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import ClearAdornment from '../../helper/ClearAdornment'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import React, { useCallback, useMemo, useRef } 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; topic?: string }) {
|
||||
function TopicInput(props: { actions: typeof publishActions; manualTopic?: string; selectedTopic?: string }) {
|
||||
const inputElement = useRef<HTMLInputElement>(null)
|
||||
|
||||
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>) => {
|
||||
@@ -21,7 +24,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const topicStr = props.topic || ''
|
||||
const topicStr = props.manualTopic || ''
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
@@ -29,6 +32,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
<FormControl style={{ width: '100%' }}>
|
||||
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
|
||||
<Input
|
||||
inputRef={inputElement}
|
||||
id="publish-topic"
|
||||
value={topicStr}
|
||||
startAdornment={<span />}
|
||||
@@ -36,7 +40,7 @@ function TopicInput(props: { actions: typeof publishActions; topic?: string }) {
|
||||
onBlur={onTopicBlur}
|
||||
onChange={updateTopic}
|
||||
multiline={false}
|
||||
placeholder="example/topic"
|
||||
placeholder={props.selectedTopic ?? 'example/topic'}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -53,11 +57,9 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topic: state.publish.topic,
|
||||
manualTopic: state.publish.manualTopic,
|
||||
selectedTopic: state.tree.get('selectedTopic')?.path(),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TopicInput)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TopicInput)
|
||||
|
||||
@@ -99,9 +99,4 @@ const styles = (theme: Theme) => ({
|
||||
},
|
||||
})
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Sidebar)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
|
||||
|
||||
@@ -66,7 +66,4 @@ 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.value) {
|
||||
if (!node || !node.message || !node.message.payload) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -43,7 +43,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(TopicPanel)
|
||||
export default connect(undefined, mapDispatchToProps)(TopicPanel)
|
||||
|
||||
@@ -73,7 +73,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(ActionButtons))
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons))
|
||||
|
||||
@@ -33,7 +33,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(DeleteSelectedTopicButton)
|
||||
export default connect(undefined, mapDispatchToProps)(DeleteSelectedTopicButton)
|
||||
|
||||
@@ -12,6 +12,7 @@ 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')
|
||||
|
||||
@@ -81,19 +82,25 @@ 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.value ? Base64Message.toUnicodeString(message.value) : ''
|
||||
const value = message.payload ? Base64Message.toUnicodeString(message.payload) : ''
|
||||
const element = {
|
||||
value,
|
||||
key: `${message.messageNumber}-${message.received}`,
|
||||
title: (
|
||||
<span>
|
||||
<DateFormatter date={message.received} />
|
||||
{previousMessage && previousMessage !== message ? (
|
||||
<i>
|
||||
(-
|
||||
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
|
||||
</i>
|
||||
) : null}
|
||||
<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>
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value} />
|
||||
</div>
|
||||
@@ -106,7 +113,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
})
|
||||
|
||||
const isMessagePlottable =
|
||||
node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
|
||||
node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload))
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
@@ -120,9 +127,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
undefined
|
||||
)
|
||||
) : undefined
|
||||
}
|
||||
onClick={this.displayMessage}
|
||||
>
|
||||
@@ -139,7 +144,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(MessageHistory)
|
||||
export default connect(null, mapDispatchToProps)(MessageHistory)
|
||||
|
||||
@@ -13,6 +13,7 @@ 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>
|
||||
@@ -36,7 +37,7 @@ function ValuePanel(props: Props) {
|
||||
const { node, compareMessage } = props
|
||||
|
||||
function renderViewOptions() {
|
||||
if (!props.node || !props.node.message || !props.node.mqttMessage) {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ function ValuePanel(props: Props) {
|
||||
<ActionButtons />
|
||||
</span>
|
||||
<span style={{ flex: 6, textAlign: 'right' }}>
|
||||
{props.node.mqttMessage.retain ? <DeleteSelectedTopicButton /> : null}
|
||||
{props.node.message.retain ? <DeleteSelectedTopicButton /> : null}
|
||||
</span>
|
||||
{messageMetaInfo()}
|
||||
</div>
|
||||
@@ -54,13 +55,16 @@ function ValuePanel(props: Props) {
|
||||
}
|
||||
|
||||
function messageMetaInfo() {
|
||||
if (!props.node || !props.node.message || !props.node.mqttMessage) {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
|
||||
<Typography style={{ textAlign: 'right' }}>QoS: {props.node.mqttMessage.qos}</Typography>
|
||||
<Typography style={{ textAlign: 'right' }}>
|
||||
<MessageId message={props.node.message} addComma={true} />
|
||||
{`QoS: ${props.node.message.qos}`}
|
||||
</Typography>
|
||||
<Typography style={{ textAlign: 'right' }}>
|
||||
<i>
|
||||
<DateFormatter date={props.node.message.received} />
|
||||
@@ -82,8 +86,8 @@ function ValuePanel(props: Props) {
|
||||
)
|
||||
|
||||
const copyValue =
|
||||
node && node.message && node.message.value ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
|
||||
node && node.message && node.message.payload ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.payload)} />
|
||||
) : null
|
||||
|
||||
return (
|
||||
@@ -124,7 +128,4 @@ 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.value) {
|
||||
if (!message.payload) {
|
||||
return
|
||||
}
|
||||
const [value, valueLanguage] = this.convertMessage(message.value)
|
||||
const [value, valueLanguage] = this.convertMessage(message.payload)
|
||||
const [compareStr, compareStrLanguage] =
|
||||
compare && compare.value ? this.convertMessage(compare.value) : [undefined, undefined]
|
||||
compare && compare.payload ? this.convertMessage(compare.payload) : [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.value) {
|
||||
if (!message.payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
const compareValue = compareMessage.value || message.value
|
||||
const [current, currentLanguage] = this.convertMessage(message.value)
|
||||
const compareValue = compareMessage.payload || message.payload
|
||||
const [current, currentLanguage] = this.convertMessage(message.payload)
|
||||
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.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
|
||||
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : 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.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
|
||||
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {}
|
||||
} 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.value) {
|
||||
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const str = Base64Message.toUnicodeString(this.props.treeNode.message.value)
|
||||
const str = Base64Message.toUnicodeString(this.props.treeNode.message.payload)
|
||||
return str.length > limit ? `${str.slice(0, limit)}…` : str
|
||||
}
|
||||
|
||||
private renderValue() {
|
||||
return this.props.treeNode.message &&
|
||||
this.props.treeNode.message.value &&
|
||||
this.props.treeNode.message.payload &&
|
||||
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.value) {
|
||||
if (settings.get('selectTopicWithMouseOver') && treeNode && treeNode.message && treeNode.message.payload) {
|
||||
didSelectTopic()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -169,7 +169,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TreeComponent)
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
|
||||
|
||||
@@ -266,9 +266,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UpdateNotifier)
|
||||
)
|
||||
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
|
||||
|
||||
@@ -59,7 +59,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
undefined,
|
||||
mapDispatchToProps
|
||||
)(Copy)
|
||||
export default connect(undefined, mapDispatchToProps)(Copy)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MqttOptions } from '../../../backend/src/DataSource'
|
||||
import { v4 } from 'uuid'
|
||||
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
|
||||
const sha1 = require('sha1')
|
||||
|
||||
export interface CertificateParameters {
|
||||
@@ -7,7 +8,9 @@ export interface CertificateParameters {
|
||||
/** @property data base64 encoded data */
|
||||
data: string
|
||||
}
|
||||
|
||||
export interface ConnectionOptions {
|
||||
configVersion: 1
|
||||
type: 'mqtt'
|
||||
id: string
|
||||
host: string
|
||||
@@ -23,7 +26,7 @@ export interface ConnectionOptions {
|
||||
clientCertificate?: CertificateParameters
|
||||
clientKey?: CertificateParameters
|
||||
clientId?: string
|
||||
subscriptions: Array<string>
|
||||
subscriptions: Array<Subscription>
|
||||
}
|
||||
|
||||
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
|
||||
@@ -52,6 +55,7 @@ function generateClientId() {
|
||||
|
||||
export function createEmptyConnection(): ConnectionOptions {
|
||||
return {
|
||||
configVersion: 1,
|
||||
certValidation: true,
|
||||
clientId: generateClientId(),
|
||||
id: v4() as string,
|
||||
@@ -59,7 +63,10 @@ export function createEmptyConnection(): ConnectionOptions {
|
||||
encryption: false,
|
||||
password: undefined,
|
||||
username: undefined,
|
||||
subscriptions: ['#', '$SYS/#'],
|
||||
subscriptions: [
|
||||
{ topic: '#', qos: 0 },
|
||||
{ topic: '$SYS/#', qos: 0 },
|
||||
],
|
||||
type: 'mqtt',
|
||||
host: '',
|
||||
port: 1883,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Action } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
import { Record, List } from 'immutable'
|
||||
import MoveUp from '../components/ChartPanel/ChartSettings/MoveUp'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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: string
|
||||
subscription: Subscription
|
||||
connectionId: string
|
||||
}
|
||||
|
||||
export interface DeleteSubscription {
|
||||
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION
|
||||
subscription: string
|
||||
subscription: Subscription
|
||||
connectionId: string
|
||||
}
|
||||
|
||||
@@ -160,8 +160,12 @@ 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 => s !== action.subscription)
|
||||
const newSubscriptions = connection.subscriptions.filter(s => !subscriptionsEqual(s, action.subscription))
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -187,10 +191,29 @@ 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,
|
||||
...action.changeSet,
|
||||
...changeSet,
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
connections: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Action } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
|
||||
export interface PublishState {
|
||||
topic?: string
|
||||
manualTopic?: string
|
||||
payload?: string
|
||||
retain: boolean
|
||||
editorMode: string
|
||||
@@ -60,7 +59,7 @@ export const publishReducer = createReducer(initialState, {
|
||||
function setTopic(state: PublishState, action: SetTopic) {
|
||||
return {
|
||||
...state,
|
||||
topic: action.topic,
|
||||
manualTopic: action.topic,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { Action } from 'redux'
|
||||
import { Action as ReduxAction } from 'redux'
|
||||
import { createReducer } from './lib'
|
||||
import { Record } from 'immutable'
|
||||
|
||||
@@ -11,7 +11,8 @@ const initialStateFactory = Record<SidebarModel>({
|
||||
compareMessage: undefined,
|
||||
})
|
||||
|
||||
export type Action = SetCompareMessage
|
||||
export type Action = SetCompareMessage | ResetStore
|
||||
|
||||
export enum ActionTypes {
|
||||
SIDEBAR_SET_COMPARE_MESSAGE = 'SIDEBAR_SET_COMPARE_MESSAGE',
|
||||
SIDEBAR_RESET_STORE = 'SIDEBAR_RESET_STORE',
|
||||
@@ -20,7 +21,7 @@ export enum ActionTypes {
|
||||
export type SidebarState = Record<SidebarModel>
|
||||
|
||||
const actions: {
|
||||
[s: string]: (state: SidebarState, action: Action) => SidebarState
|
||||
[s: string]: (state: SidebarState, action: ReduxAction) => SidebarState
|
||||
} = {
|
||||
SIDEBAR_SET_COMPARE_MESSAGE: setCompareMessage,
|
||||
SIDEBAR_RESET_STORE: resetStore,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { Action } from 'redux'
|
||||
import { Action as ReduxAction } 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: Action) => TreeState
|
||||
[s: string]: (state: TreeState, action: ReduxAction) => TreeState
|
||||
} = {
|
||||
TREE_SHOW_TREE: showTree,
|
||||
TREE_SELECT_TOPIC: selectTopic,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
--require ts-node/register
|
||||
--require source-map-support/register
|
||||
--recursive ./src/**/*.spec.ts
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["**/*.d.ts"],
|
||||
"exclude": ["**/*.d.ts", ".src/**/*.png"],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
"useCache": true,
|
||||
"transpileModule": true,
|
||||
|
||||
+2360
-1428
File diff suppressed because it is too large
Load Diff
@@ -21,4 +21,3 @@ 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) => void
|
||||
type MessageCallback = (topic: string, payload: Buffer, packet: any) => void
|
||||
|
||||
// A DataSource should automatically reconnect if connection was broken
|
||||
interface DataSource<DataSourceOptions> {
|
||||
|
||||
@@ -12,12 +12,19 @@ export interface MqttOptions {
|
||||
tls: boolean
|
||||
certValidation: boolean
|
||||
clientId?: string
|
||||
subscriptions: Array<string>
|
||||
subscriptions: Array<Subscription>
|
||||
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
|
||||
@@ -74,7 +81,7 @@ export class MqttSource implements DataSource<MqttOptions> {
|
||||
client.on('connect', () => {
|
||||
this.stateMachine.setConnected(true)
|
||||
options.subscriptions.forEach(subscription => {
|
||||
client.subscribe(subscription, (err: Error) => {
|
||||
client.subscribe(subscription.topic, { qos: subscription.qos }, (err: Error) => {
|
||||
if (err) {
|
||||
this.stateMachine.setError(err)
|
||||
}
|
||||
|
||||
@@ -70,9 +70,11 @@ 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 =>
|
||||
jsonToPropertyPaths(property.value, previousPath.slice().concat([property.key.value]))
|
||||
)
|
||||
children = ast.children.map(property => {
|
||||
const path = property.key.value.replace('.', '\\.')
|
||||
|
||||
return jsonToPropertyPaths(property.value, previousPath.slice().concat([path]))
|
||||
})
|
||||
}
|
||||
|
||||
return children.reduce((a, b) => a.concat(b), [])
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { Base64Message } from './Base64Message'
|
||||
import { QoS } from '../DataSource/MqttSource'
|
||||
|
||||
export interface Message {
|
||||
value?: Base64Message
|
||||
// mqtt based info
|
||||
payload: Base64Message | null
|
||||
messageId?: number
|
||||
retain: boolean
|
||||
qos: QoS
|
||||
|
||||
// meta info
|
||||
length: number
|
||||
received: Date
|
||||
|
||||
// Global message counter, not mqtt related
|
||||
messageNumber: number
|
||||
}
|
||||
|
||||
@@ -75,13 +75,7 @@ export class Tree<ViewModel extends Destroyable> extends TreeNode<ViewModel> {
|
||||
|
||||
public applyUnmergedChanges() {
|
||||
this.unmergedMessages.popAll().forEach(bufferedItem => {
|
||||
const edges = bufferedItem.message.topic.split('/')
|
||||
const node = TreeNodeFactory.fromEdgesAndValue<ViewModel>(
|
||||
edges,
|
||||
bufferedItem.message.payload,
|
||||
bufferedItem.received
|
||||
)
|
||||
node.mqttMessage = bufferedItem.message
|
||||
const node = TreeNodeFactory.fromMessage<ViewModel>(bufferedItem.message, bufferedItem.received)
|
||||
|
||||
if (!this.nodeFilter || this.nodeFilter(node)) {
|
||||
this.updateWithNode(node.firstNode())
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { Edge, Message, RingBuffer, MessageHistory } from './'
|
||||
import { EventDispatcher, MqttMessage } from '../../../events'
|
||||
import { EventDispatcher } 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> } = {}
|
||||
@@ -105,7 +104,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
}
|
||||
|
||||
public hasMessage() {
|
||||
return this.message && this.message.value && this.message.value.length !== 0
|
||||
return this.message && this.message.payload && this.message.length !== 0
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
@@ -131,7 +130,6 @@ 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
|
||||
@@ -203,7 +201,6 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
if (node.message) {
|
||||
this.setMessage(node.message)
|
||||
this.onMessage.dispatch(node.message)
|
||||
this.mqttMessage = node.mqttMessage
|
||||
}
|
||||
|
||||
this.removeFromTreeIfEmpty()
|
||||
@@ -236,7 +233,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
|
||||
public childTopics(): Array<TreeNode<ViewModel>> {
|
||||
if (this.cachedChildTopics === undefined) {
|
||||
const initialValue = this.message && this.message.value ? [this] : []
|
||||
const initialValue = this.message && this.message.payload ? [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,21 +20,23 @@ export abstract class TreeNodeFactory {
|
||||
node.sourceEdge!.target = node
|
||||
}
|
||||
|
||||
public static fromEdgesAndValue<ViewModel extends Destroyable>(
|
||||
edgeNames: Array<string>,
|
||||
value?: Base64Message | null,
|
||||
public static fromMessage<ViewModel extends Destroyable>(
|
||||
mqttMessage: MqttMessage,
|
||||
receiveDate: Date = new Date()
|
||||
): TreeNode<ViewModel> {
|
||||
const node = new TreeNode<ViewModel>()
|
||||
const edges = mqttMessage.topic.split('/')
|
||||
|
||||
mqttMessage.retain
|
||||
node.setMessage({
|
||||
value: value || undefined,
|
||||
length: value ? value.length : 0,
|
||||
...mqttMessage,
|
||||
length: mqttMessage.payload?.length ?? 0,
|
||||
received: receiveDate,
|
||||
messageNumber: this.messageCounter,
|
||||
})
|
||||
this.messageCounter += 1
|
||||
|
||||
this.insertNodeAtPosition<ViewModel>(edgeNames, node)
|
||||
this.insertNodeAtPosition<ViewModel>(edges, node)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Edge, TreeNodeFactory } from '../'
|
||||
import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { Edge } from '../'
|
||||
import { makeTreeNode } from './makeTreeNode'
|
||||
|
||||
describe('Edge', () => {
|
||||
it('should contain a name', () => {
|
||||
@@ -9,8 +10,7 @@ describe('Edge', () => {
|
||||
})
|
||||
|
||||
it('firstEdge should retrieve the first edge', () => {
|
||||
const topics = 'foo/bar/baz'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
|
||||
const leaf = makeTreeNode('foo/bar/baz')
|
||||
const bazEdge = leaf.sourceEdge
|
||||
|
||||
if (!bazEdge) {
|
||||
@@ -34,11 +34,8 @@ describe('Edge', () => {
|
||||
})
|
||||
|
||||
it('hash should include change if parents are different', () => {
|
||||
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
|
||||
const bazEdge1 = makeTreeNode('foo/bar/baz').sourceEdge
|
||||
const bazEdge2 = makeTreeNode('foo/foo/baz').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,18 +1,16 @@
|
||||
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 topics = 'foo/bar'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
|
||||
const leaf = makeTreeNode('foo/bar')
|
||||
|
||||
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 topics = 'foo/bar/baz'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
|
||||
const leaf = makeTreeNode('foo/bar/baz')
|
||||
|
||||
const root = leaf.firstNode()
|
||||
expect(root.sourceEdge).to.eq(undefined)
|
||||
|
||||
@@ -1,55 +1,51 @@
|
||||
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 topics = 'foo/bar'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics, undefined)
|
||||
const leaf = makeTreeNode('foo/bar')
|
||||
|
||||
expect(leaf.firstNode().edges['foo']).to.not.eq(undefined)
|
||||
})
|
||||
|
||||
it('updateWithNode should update value', () => {
|
||||
const topics = 'foo/bar'.split('/')
|
||||
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 leaf = makeTreeNode('foo/bar', '3')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
|
||||
|
||||
const updateLeave = makeTreeNode('foo/bar', '5')
|
||||
|
||||
const root = leaf.firstNode()
|
||||
root.updateWithNode(updateLeave.firstNode())
|
||||
|
||||
expect(root.sourceEdge).to.eq(undefined)
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.value!)).to.eq('5')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('5')
|
||||
})
|
||||
|
||||
it('updateWithNode should update intermediate nodes', () => {
|
||||
const topics1 = 'foo/bar/baz'.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(topics1, Base64Message.fromString('3'))
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.value!)).to.eq('3')
|
||||
const leaf = makeTreeNode('foo/bar/baz', '3')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
|
||||
|
||||
const topics2 = 'foo/bar'.split('/')
|
||||
const updateLeave = TreeNodeFactory.fromEdgesAndValue(topics2, Base64Message.fromString('5'))
|
||||
const updateLeave = makeTreeNode('foo/bar', '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!.value!)).to.eq('5')
|
||||
expect(Base64Message.toUnicodeString(barNode!.message!.payload!)).to.eq('5')
|
||||
|
||||
expect(leaf.sourceEdge && leaf.sourceEdge.name).to.eq('baz')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.value!)).to.eq('3')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
|
||||
})
|
||||
|
||||
it('updateWithNode should add nodes to the tree', () => {
|
||||
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'))
|
||||
const leaf1 = makeTreeNode('foo/bar', 'foo')
|
||||
const leaf2 = makeTreeNode('foo/bar/baz', 'bar')
|
||||
|
||||
leaf1.firstNode().updateWithNode(leaf2.firstNode())
|
||||
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
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 topic = 'foo/bar'
|
||||
const edges = topic.split('/')
|
||||
const leaf = TreeNodeFactory.fromEdgesAndValue(edges, undefined)
|
||||
const leaf = makeTreeNode('foo/bar')
|
||||
|
||||
expect(leaf.firstNode().sourceEdge).to.eq(undefined)
|
||||
})
|
||||
|
||||
it('should create node', () => {
|
||||
const topic = 'foo/bar'
|
||||
const edges = topic.split('/')
|
||||
const node = TreeNodeFactory.fromEdgesAndValue(edges, Base64Message.fromString('5'))
|
||||
const node = makeTreeNode('foo/bar', '5')
|
||||
|
||||
if (!node.sourceEdge || !node.sourceEdge.source || !node.message) {
|
||||
expect.fail('should not happen')
|
||||
@@ -25,23 +20,21 @@ describe('TreeNodeFactory', () => {
|
||||
|
||||
expect(node).to.not.eq(undefined)
|
||||
expect(node.sourceEdge.name).to.eq('bar')
|
||||
expect(Base64Message.toUnicodeString(node.message.value!)).to.eq('5')
|
||||
expect(Base64Message.toUnicodeString(node.message.payload!)).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 topic = 'foo/bar/baz'
|
||||
const edges = topic.split('/')
|
||||
const node = TreeNodeFactory.fromEdgesAndValue(edges, Base64Message.fromString('5'))
|
||||
const node = makeTreeNode('foo/bar/baz', '5')
|
||||
|
||||
if (!node.sourceEdge || !node.sourceEdge.source || !node.message) {
|
||||
expect.fail('should not happen')
|
||||
return
|
||||
}
|
||||
|
||||
expect(Base64Message.toUnicodeString(node.message.value!)).to.eq('5')
|
||||
expect(Base64Message.toUnicodeString(node.message.payload!)).to.eq('5')
|
||||
expect(node.sourceEdge.name).to.eq('baz')
|
||||
|
||||
const barNode = node.sourceEdge.source
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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,11 +45,13 @@ 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,6 +34,8 @@ 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> {
|
||||
|
||||
+22
-21
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "MQTT-Explorer",
|
||||
"version": "0.3.6-no-delete-limit",
|
||||
"version": "0.4.0-beta1",
|
||||
"description": "Explore your message queues",
|
||||
"main": "dist/src/electron.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"test": "yarn run test-backend",
|
||||
"test": "yarn test:app && yarn test:backend",
|
||||
"test:app": "cd app && yarn test",
|
||||
"test:backend": "cd backend && yarn test",
|
||||
"install": "cd app && yarn && cd ..",
|
||||
"dev": "npm-run-all --parallel dev:*",
|
||||
"dev:app": "cd app && npm run dev",
|
||||
@@ -15,7 +17,6 @@
|
||||
"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",
|
||||
@@ -68,48 +69,48 @@
|
||||
"license": "CC-BY-ND-4.0",
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/fs-extra": "^7.0.0",
|
||||
"@types/fs-extra": "8",
|
||||
"@types/lowdb": "^1.0.6",
|
||||
"@types/mime": "^2.0.0",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/mustache": "^0.8.32",
|
||||
"@types/node": "^12.0.4",
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/mustache": "4",
|
||||
"@types/node": "^12.6.8",
|
||||
"@types/semver": "7",
|
||||
"@types/sha1": "^1.1.1",
|
||||
"builder-util-runtime": "^8.2.5",
|
||||
"chai": "^4.2.0",
|
||||
"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",
|
||||
"cspell": "^4.0.28",
|
||||
"electron": "8",
|
||||
"electron-builder": "^22.1",
|
||||
"mocha": "7.1",
|
||||
"mustache": "4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^14.1.1",
|
||||
"prettier": "1.18.2",
|
||||
"nyc": "15",
|
||||
"prettier": "2",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"source-map-support": "^0.5.9",
|
||||
"spectron": "^6.0.0",
|
||||
"spectron": "10",
|
||||
"ts-node": "^8.2.0",
|
||||
"tslint": "^5.15.0",
|
||||
"tslint": "6",
|
||||
"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": "5.5"
|
||||
"webdriverio": "6"
|
||||
},
|
||||
"dependencies": {
|
||||
"about-window": "^1.12.1",
|
||||
"axios": "^0.19.0",
|
||||
"dot-prop": "^5.0.0",
|
||||
"electron-log": "^3.0.6",
|
||||
"electron-log": "4.1.1",
|
||||
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
|
||||
"electron-updater": "^4.0.6",
|
||||
"fs-extra": "^8.0.1",
|
||||
"fs-extra": "9",
|
||||
"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,4 +4,5 @@ module.exports = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
arrowParens: 'avoid',
|
||||
};
|
||||
|
||||
Binary file not shown.
@@ -13,7 +13,7 @@ interface Context {
|
||||
targets: [Target]
|
||||
}
|
||||
|
||||
export default async function(context: Context) {
|
||||
export default async function (context: Context) {
|
||||
console.log(context)
|
||||
const isLinux = context.targets.find(target => target.name === 'appImage')
|
||||
if (!isLinux) {
|
||||
|
||||
@@ -28,7 +28,7 @@ async function prepareRelease() {
|
||||
|
||||
// Clean up
|
||||
await fs.remove('node_modules')
|
||||
// await exec('yarn', ['install', '--production']) // Do not clean up, electron version detection will fail otherwise
|
||||
await exec('yarn', ['install', '--production']) // Do not clean up, electron version detection will fail otherwise
|
||||
await fs.remove(path.join('app', 'node_modules'))
|
||||
|
||||
chdir(originalDir)
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ const applicationMenu: MenuItemConstructorOptions = {
|
||||
{
|
||||
label: 'Dev Tools',
|
||||
accelerator: 'CmdOrCtrl+Alt+I',
|
||||
role: 'toggledevtools' as 'toggledevtools',
|
||||
role: 'toggleDevTools',
|
||||
},
|
||||
{
|
||||
label: 'Quit',
|
||||
@@ -69,7 +69,7 @@ const editMenu: MenuItemConstructorOptions = {
|
||||
{
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
role: 'selectall',
|
||||
role: 'selectAll',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user