Compare commits

..
10 Commits
155 changed files with 5741 additions and 6608 deletions
-8
View File
@@ -1,8 +0,0 @@
{
"language": "en",
"words": [
"subheader",
"basepath",
"webdriverio"
]
}
-2
View File
@@ -1,2 +0,0 @@
node_modules
build
-1
View File
@@ -28,7 +28,6 @@ install:
script:
- yarn run build
- yarn lint
- yarn test
- 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
-7
View File
@@ -1,7 +0,0 @@
{
"editor.formatOnSave": true,
"files.exclude": {
"**/node_modules": true,
"build/": true
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
| | | |
|:---:|:---:|:---:|
|[![screen_composite](https://mqtt-explorer.com/img/screen-composite_small.png)](https://mqtt-explorer.com/img/screen-composite.png)|[![screen2_small](https://mqtt-explorer.com/img/screen2_small.png)](https://mqtt-explorer.com/img/screen2.png)|[![screen3_small](https://mqtt-explorer.com/img/screen3_small.png)](https://mqtt-explorer.com/img/screen3.png)|
|[![screen1_small](https://user-images.githubusercontent.com/7721625/53954800-84b34c00-40d7-11e9-842d-bf655c569600.jpg)](https://user-images.githubusercontent.com/7721625/53954364-52551f00-40d6-11e9-93cf-d5a9601897ea.png)|[![screen2_small](https://user-images.githubusercontent.com/7721625/53954801-84b34c00-40d7-11e9-913a-42572e675620.jpg)](https://user-images.githubusercontent.com/7721625/53954365-52551f00-40d6-11e9-823f-afd66f19ed01.png)|[![screen3_small](https://user-images.githubusercontent.com/7721625/53954802-854be280-40d7-11e9-973c-08f23c8dbf89.jpg)](https://user-images.githubusercontent.com/7721625/53954366-52551f00-40d6-11e9-9738-74db830d03ac.png)|
# The App has moved to [mqtt-explorer.com](https://mqtt-explorer.com)
MQTT Explorer is a comprehensive and easy-to-use MQTT Client.
+105 -161
View File
@@ -1,181 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
<title>MQTT Explorer</title>
<script src="./bugtracking.bundle.js"></script>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
>
<title>MQTT Explorer</title>
<script src="./bugtracking.bundle.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
}
[tabindex] {
outline: none;
}
@keyframes updateDark {
0% {
background-color: none;
<style>
body, html {
margin: 0;
padding: 0;
}
25% {
background-color: #3f51b5;
[tabindex] {
outline: none;
}
50% {
background-color: #3f51b5;
@keyframes updateDark {
0% {background-color: none;}
25% {background-color: #3f51b5;}
50% {background-color: #3f51b5;}
100% {background-color: none;}
}
100% {
background-color: none;
@keyframes updateLight {
0% {background-color: none; color: inherit}
25% {background-color: #bfc9c8; color: #000}
50% {background-color: #bfc9c8; color: #000}
100% {background-color: none; color: inherit}
}
}
@keyframes updateLight {
0% {
background-color: none;
color: inherit;
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
25% {
background-color: #bfc9c8;
color: #000;
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0.0);
}
50% {
background-color: #bfc9c8;
color: #000;
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60,60,60,0.5);
background-color: rgba(140,140,140,0.1);
}
100% {
background-color: none;
color: inherit;
::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8);
}
</style>
<style>
.Resizer {
background: #eee;
opacity: .2;
z-index: 1;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
}
.Resizer.horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
background-color: rgba(140, 140, 140, 0.1);
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.5);
border-bottom: 5px solid rgba(120, 120, 120, 0.5);
}
::-webkit-scrollbar-thumb {
background-color: rgba(140, 140, 140, 0.8);
}
</style>
<style>
.Resizer {
background: rgba(200, 200, 200, 0);
z-index: 10;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
.Resizer.vertical {
width: 2px;
margin: 0px -8px 0px 0px;
border-left: 4px solid rgba(128, 128, 128, 0);
border-right: 4px solid rgba(128, 128, 128, 0);
cursor: col-resize;
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.Resizer.vertical:hover {
border-left: 4px solid rgba(130, 130, 130, 1);
border-right: 4px solid rgba(140, 140, 140, 1);
}
.Resizer.horizontal {
height: 10px;
margin: -10px 0 0 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.Resizer.horizontal::before {
content: '•••';
display: inline-block;
vertical-align: middle;
text-align: center;
width: 100%;
margin-top: -22px;
color: #aaa;
opacity: 1;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.3);
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
}
.Resizer.vertical {
width: 2px;
margin: 0px -8px 0px 0px;
border-left: 4px solid rgba(128, 128, 128, 0);
border-right: 4px solid rgba(128, 128, 128, 0);
cursor: col-resize;
}
.Resizer.vertical::before {
content: '•••';
margin-left: -6px;
height: 100%;
display: inline-block;
vertical-align: middle;
text-align: center;
color: #aaa;
opacity: 1;
writing-mode: vertical-lr;
text-orientation: sideways;
}
.Resizer.vertical:hover {
border-left: 4px solid rgba(130, 130, 130, 0.3);
border-right: 4px solid rgba(140, 140, 140, 0.3);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.example-enter {
opacity: 0;
}
.example-enter-active {
opacity: 1;
transition: opacity 300ms ease-in;
}
.example-exit {
opacity: 1;
}
.example-exit-active {
opacity: 0;
transition: opacity 300ms ease-in;
}
</style>
</head>
<body>
<div id="app" style="font:-webkit-control"></div>
<script>
function loadScript(path) {
var script = document.createElement('script')
script.src = path
document.head.appendChild(script)
}
document.addEventListener('DOMContentLoaded', onLoad(), false)
function onLoad() {
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script
><% }); %>
</body>
</style>
</head>
<body>
<div id="app" style="font:-webkit-control"></div>
<script>
function loadScript(path) {
var script = document.createElement("script");
script.src = path
document.head.appendChild(script);
}
document.addEventListener('DOMContentLoaded', onLoad(), false);
function onLoad() {
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script><% }); %>
</body>
</html>
+14 -24
View File
@@ -6,26 +6,21 @@
"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.4 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
"rebuild": "cd node_modules/heapdump && node-gyp rebuild --target=5.0.0 --arch=x64 --dist-url=https://atom.io/download/electron || echo Could not build heapdump; cd -"
},
"author": "",
"license": "CC-BY-ND-4.0",
"dependencies": {
"@material-ui/core": "^4",
"@material-ui/icons": "^4",
"@material-ui/lab": "^4.0.0-alpha",
"@material-ui/styles": "^4",
"@types/react-transition-group": "^2.9.2",
"axios": "^0.19.0",
"@material-ui/core": "^4.0.0-alpha.6",
"@material-ui/icons": "^4.0.0-alpha.1",
"@material-ui/lab": "^4.0.0-alpha.6",
"@material-ui/styles": "^4.0.0-alpha.6",
"brace": "^0.11.1",
"compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^2.1.0",
"d3": "^5.9.2",
"d3-shape": "^1.3.5",
"copy-text-to-clipboard": "^1.0.4",
"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": "^3.0.1",
"get-value": "^3.0.1",
"immutable": "^4.0.0-rc.12",
"js-base64": "^2.5.1",
@@ -37,12 +32,11 @@
"number-abbreviate": "^2.0.0",
"prismjs": "^1.15.0",
"react": "16.8",
"react-ace": "^7.0.1",
"react-ace": "^6.3.2",
"react-dom": "^16.7.0",
"react-redux": "^7.0.3",
"react-resize-detector": "^4.1.4",
"react-redux": "^6.0.0",
"react-resize-detector": "^3.4.0",
"react-split-pane": "^0.1.85",
"react-transition-group": "^4.1.1",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "^0.4.1",
@@ -52,21 +46,20 @@
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/d3": "^5.7.2",
"@types/diff": "^4.0.1",
"@types/get-value": "^3.0.1",
"@types/node": "^12.0.4",
"@types/node": "^10.12.18",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^7.0.9",
"@types/react-resize-detector": "^4.0.1",
"@types/react-redux": "^6.0.12",
"@types/react-resize-detector": "^3.1.0",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^1.4.32",
"@types/uuid": "^3.4.4",
"@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1",
"css-loader": "^3.0.0",
"css-loader": "^2.1.0",
"hard-source-webpack-plugin": "^0.13.1",
"heapdump": "^0.3.12",
"html-webpack-plugin": "^4.0.0-beta.5",
@@ -78,8 +71,5 @@
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
},
"peerDependencies": {
"electron": "^5.0.4"
}
}
-128
View File
@@ -1,128 +0,0 @@
import { Action, ActionTypes, ChartParameters } from '../reducers/Charts'
import { AppState } from '../reducers'
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux'
import { showError, showNotification } from './Global'
interface ConnectionViewState {
charts: Array<ChartParameters>
}
interface ConnectionViewStateDictionary {
[s: string]: ConnectionViewState
}
const connectionViewStateIdentifier: StorageIdentifier<ConnectionViewStateDictionary> = {
id: 'connection_view_state',
}
export const loadCharts = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionId = getState().connection.connectionId
if (!connectionId) {
return
}
let viewStates: ConnectionViewStateDictionary | undefined
try {
viewStates = await persistentStorage.load(connectionViewStateIdentifier)
} catch (error) {
dispatch(showError(error))
}
if (!viewStates || !viewStates[connectionId]) {
dispatch(setCharts([]))
return
}
const viewState = viewStates[connectionId]
if (viewState) {
dispatch(setCharts(viewState.charts))
}
}
export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionId = getState().connection.connectionId
if (!connectionId) {
return
}
const charts = getState()
.charts.get('charts')
.toArray()
let viewStates: ConnectionViewStateDictionary | undefined
try {
viewStates = (await persistentStorage.load(connectionViewStateIdentifier)) || {}
const state: ConnectionViewState = viewStates[connectionId] || { charts: [] }
state.charts = charts
viewStates[connectionId] = state
await persistentStorage.store(connectionViewStateIdentifier, viewStates)
} catch (error) {
dispatch(showError(error))
}
}
export const addChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
const chartExists = Boolean(
getState()
.charts.get('charts')
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
)
if (chartExists) {
dispatch(showNotification('Already added'))
return
}
dispatch({
type: ActionTypes.CHARTS_ADD,
chart: chartParameters,
})
dispatch(saveCharts())
dispatch(showNotification('Added to chart panel'))
}
export const updateChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
type: ActionTypes.CHARTS_UPDATE,
topic: chartParameters.topic,
dotPath: chartParameters.dotPath,
parameters: chartParameters,
})
dispatch(saveCharts())
}
export const removeChart = (chartParameters: ChartParameters) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
chart: chartParameters,
type: ActionTypes.CHARTS_REMOVE,
})
dispatch(saveCharts())
}
export const moveChartUp = (parameters: { topic: string; dotPath?: string }) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
dispatch({
topic: parameters.topic,
dotPath: parameters.dotPath,
type: ActionTypes.CHARTS_MOVE_UP,
})
dispatch(saveCharts())
}
export const setCharts = (charts: Array<ChartParameters>): Action => {
return {
charts,
type: ActionTypes.CHARTS_SET,
}
}
+10 -10
View File
@@ -9,18 +9,21 @@ import { globalActions } from '.'
import { resetStore as resetTreeStore, showTree } from './Tree'
import { showError } from './Global'
import { TopicViewModel } from '../model/TopicViewModel'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../events'
import {
addMqttConnectionEvent,
makeConnectionStateEvent,
removeConnection,
rendererEvents,
} from '../../../events'
export const connect = (options: MqttOptions, connectionId: string) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
rendererEvents.subscribe(event, dataSourceState => {
rendererEvents.subscribe(event, (dataSourceState) => {
console.log(dataSourceState)
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
@@ -56,10 +59,7 @@ const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<a
})
}
export const connected: (tree: q.Tree<TopicViewModel>, host: string) => Action = (
tree: q.Tree<TopicViewModel>,
host: string
) => ({
export const connected: (tree: q.Tree<TopicViewModel>, host: string) => Action = (tree: q.Tree<TopicViewModel>, host: string) => ({
tree,
host,
type: ActionTypes.CONNECTION_SET_CONNECTED,
+36 -75
View File
@@ -1,11 +1,6 @@
import { AppState } from '../reducers'
import { clearLegacyConnectionOptions, loadLegacyConnectionOptions } from '../model/LegacyConnectionSettings'
import {
ConnectionOptions,
createEmptyConnection,
makeDefaultConnections,
CertificateParameters,
} from '../model/ConnectionOptions'
import { ConnectionOptions, createEmptyConnection, makeDefaultConnections, CertificateParameters } from '../model/ConnectionOptions'
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux'
import { showError } from './Global'
@@ -13,12 +8,12 @@ import { remote } from 'electron'
import * as fs from 'fs'
import * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager'
import {
ActionTypes,
Action,
} from '../reducers/ConnectionManager'
interface ConnectionDictionary {
[s: string]: ConnectionOptions
}
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
const storedConnectionsIdentifier: StorageIdentifier<{[s: string]: ConnectionOptions}> = {
id: 'ConnectionManager_connections',
}
@@ -42,18 +37,15 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
}
}
export const selectCertificate = (connectionId: string) => async (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
export const selectCertificate = (connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const certificate = await openCertificate()
dispatch(
updateConnection(connectionId, {
selfSignedCertificate: certificate,
})
)
console.log(certificate)
dispatch(updateConnection(connectionId, {
selfSignedCertificate: certificate,
}))
} catch (error) {
console.log(error)
dispatch(showError(error))
}
}
@@ -65,33 +57,30 @@ async function openCertificate(): Promise<CertificateParameters> {
}
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)
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
}
fs.readFile(selectedFile, (error, data) => {
if (error) {
reject(error)
return
}
if (data.length > 16_384 || data.length < 128) {
reject(rejectReasons.certificateSizeDoesNotMatch)
return
}
if (data.length > 16_384 || data.length < 128) {
reject(rejectReasons.certificateSizeDoesNotMatch)
return
}
resolve({
data: data.toString('base64'),
name: path.basename(selectedFile),
})
resolve({
data: data.toString('base64'),
name: path.basename(selectedFile),
})
}
)
})
})
})
}
@@ -128,7 +117,7 @@ export const createConnection = () => (dispatch: Dispatch<any>) => {
dispatch(selectConnection(newConnection.id))
}
export const setConnections = (connections: { [s: string]: ConnectionOptions }): Action => ({
export const setConnections = (connections: {[s: string]: ConnectionOptions}): Action => ({
connections,
type: ActionTypes.CONNECTION_MANAGER_SET_CONNECTIONS,
})
@@ -143,7 +132,7 @@ export const addConnection = (connection: ConnectionOptions): Action => ({
type: ActionTypes.CONNECTION_MANAGER_ADD_CONNECTION,
})
export const toggleAdvancedSettings = (): Action => ({
export const toggleAdvancedSettings = (): Action => ({
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS,
})
@@ -166,44 +155,16 @@ export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<an
}
async function ensureConnectionsHaveBeenInitialized() {
let connections = await persistentStorage.load(storedConnectionsIdentifier)
const connections = await persistentStorage.load(storedConnectionsIdentifier)
const requiresInitialization = !connections
if (requiresInitialization) {
const migratedConnection = loadLegacyConnectionOptions()
const defaultConnections = makeDefaultConnections()
connections = {
persistentStorage.store(storedConnectionsIdentifier, {
...migratedConnection,
...defaultConnections,
}
await persistentStorage.store(storedConnectionsIdentifier, connections)
})
clearLegacyConnectionOptions()
}
// Migrate connections, rewrite dictionary to "keep" it "ordered" (dictionaries do not have a guaranteed order)
const mayNeedMigrations = connections && connections['iot.eclipse.org']
if (connections && mayNeedMigrations) {
let newConnections = {}
for (const connection of Object.values(connections)) {
addMigratedConnection(newConnections, connection)
}
await persistentStorage.store(storedConnectionsIdentifier, newConnections)
}
}
function addMigratedConnection(newConnections: { [key: string]: ConnectionOptions }, connection: ConnectionOptions) {
// The host has been renamed, only change the host if it has not been changed
// Also check for ssl since SSL is not yet working
if (
connection.id === 'iot.eclipse.org' &&
connection.host === 'iot.eclipse.org' &&
connection.port === 1883 &&
!connection.encryption
) {
connection.id = 'mqtt.eclipse.org'
connection.host = 'mqtt.eclipse.org'
connection.name = 'mqtt.eclipse.org'
}
newConnections[connection.id] = connection
}
+1 -1
View File
@@ -32,7 +32,7 @@ export const setEditorMode = (editorMode: string): Action => {
}
}
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const state = getState()
const topic = state.publish.topic
+16 -23
View File
@@ -1,15 +1,19 @@
import * as q from '../../../backend/src/Model'
import { ActionTypes, SettingsState, TopicOrder } from '../reducers/Settings'
import { AppState } from '../reducers'
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { batchActions } from 'redux-batched-actions'
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux'
import { globalActions } from './'
import { showError } from './Global'
import { showTree } from './Tree'
import { TopicViewModel } from '../model/TopicViewModel'
import {
ActionTypes,
SettingsState,
TopicOrder,
} from '../reducers/Settings'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { globalActions } from '.'
const settingsIdentifier: StorageIdentifier<Partial<SettingsState>> = {
id: 'Settings',
@@ -17,7 +21,7 @@ const settingsIdentifier: StorageIdentifier<Partial<SettingsState>> = {
export const loadSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const settings = (await persistentStorage.load(settingsIdentifier)) || {}
const settings = await persistentStorage.load(settingsIdentifier) || {}
dispatch({
settings: getState().settings.merge(settings),
type: ActionTypes.SETTINGS_DID_LOAD_SETTINGS,
@@ -98,34 +102,26 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
})
if (!filterStr || !tree) {
dispatch(batchActions([setAutoExpandLimit(0), showTree(tree) as any]))
dispatch(batchActions([setAutoExpandLimit(0), (showTree(tree) as any)]))
return
}
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
const messageMatches = node.message
&& node.message.value
&& Base64Message.toUnicodeString(node.message.value).toLowerCase().indexOf(filterStr) !== -1
return Boolean(messageMatches)
}
const resultTree = tree
.childTopics()
const resultTree = tree.childTopics()
.filter(nodeFilter)
.map((node: q.TreeNode<TopicViewModel>) => {
const clone = node.unconnectedClone()
@@ -142,7 +138,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
nextTree.updateWithConnection(tree.updateSource, tree.connectionId, nodeFilter)
}
dispatch(batchActions([setAutoExpandLimit(autoExpandLimitForTree(nextTree)), showTree(nextTree) as any]))
dispatch(batchActions([setAutoExpandLimit(autoExpandLimitForTree(nextTree)), (showTree(nextTree) as any)]))
}
function autoExpandLimitForTree(tree: q.Tree<TopicViewModel>) {
@@ -162,10 +158,7 @@ function autoExpandLimitForTree(tree: q.Tree<TopicViewModel>) {
export const toggleTheme = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
type:
getState().settings.get('theme') === 'light'
? ActionTypes.SETTINGS_SET_THEME_DARK
: ActionTypes.SETTINGS_SET_THEME_LIGHT,
type: getState().settings.get('theme') === 'light' ? ActionTypes.SETTINGS_SET_THEME_DARK : ActionTypes.SETTINGS_SET_THEME_LIGHT,
})
dispatch(storeSettings())
}
+4 -7
View File
@@ -20,10 +20,7 @@ export const setCompareMessage = (message?: q.Message) => (dispatch: Dispatch<an
})
}
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId } = getState().connection
if (!connectionId) {
return
@@ -39,11 +36,11 @@ export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicC
rendererEvents.emit(publishEvent, mqttMessage)
if (recursive) {
topic
.childTopics()
topic.childTopics()
.filter(topic => Boolean(topic.message && topic.message.value))
.slice(0, subtopicClearLimit)
.forEach(topic => {
.forEach((topic) => {
console.log('deleting', topic.path())
const mqttMessage = {
topic: topic.path(),
payload: null,
+34 -43
View File
@@ -9,55 +9,49 @@ import { TopicViewModel } from '../model/TopicViewModel'
import { globalActions } from '.'
const debounce = require('lodash.debounce')
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (
dispatch: Dispatch<any>,
getState: () => AppState
) => {
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
debouncedSelectTopic(topic, dispatch, getState)
}
const debouncedSelectTopic = debounce(
(topic: q.TreeNode<TopicViewModel>, dispatch: Dispatch<any>, getState: () => AppState) => {
const previouslySelectedTopic = getState().tree.get('selectedTopic')
const debouncedSelectTopic = debounce((topic: q.TreeNode<TopicViewModel>, dispatch: Dispatch<any>, getState: () => AppState) => {
const previouslySelectedTopic = getState().tree.get('selectedTopic')
if (previouslySelectedTopic === topic) {
return
}
if (previouslySelectedTopic === topic) {
return
}
// Update publish topic
let setTopicDispatch: any | undefined
if (!getState().publish.topic) {
setTopicDispatch = setTopic(topic.path())
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.topic) {
setTopicDispatch = setTopic(topic.path())
}
// Update publish topic
let setTopicDispatch: any | undefined
if (!getState().publish.topic) {
setTopicDispatch = setTopic(topic.path())
} else if (previouslySelectedTopic && (previouslySelectedTopic.path() === getState().publish.topic)) {
setTopicDispatch = setTopic(topic.path())
}
if (previouslySelectedTopic && previouslySelectedTopic.viewModel) {
previouslySelectedTopic.viewModel.setSelected(false)
}
if (previouslySelectedTopic && previouslySelectedTopic.viewModel) {
previouslySelectedTopic.viewModel.setSelected(false)
}
if (topic.viewModel) {
topic.viewModel.setSelected(true)
}
if (topic.viewModel) {
topic.viewModel.setSelected(true)
}
const selectTreeTopicDispatch = {
selectedTopic: topic,
type: ActionTypes.TREE_SELECT_TOPIC,
}
const selectTreeTopicDispatch = {
selectedTopic: topic,
type: ActionTypes.TREE_SELECT_TOPIC,
}
dispatch({
type: SidebarActionTypes.SIDEBAR_SET_COMPARE_MESSAGE,
message: undefined,
})
dispatch({
type: SidebarActionTypes.SIDEBAR_SET_COMPARE_MESSAGE,
message: undefined,
})
if (setTopicDispatch) {
dispatch(batchActions([selectTreeTopicDispatch, setTopicDispatch]))
} else {
dispatch(selectTreeTopicDispatch)
}
},
70
)
if (setTopicDispatch) {
dispatch(batchActions([selectTreeTopicDispatch, setTopicDispatch]))
} else {
dispatch(selectTreeTopicDispatch)
}
}, 70)
function destroyUnreferencedTree(state: AppState) {
const visibleTree = state.tree.get('tree')
@@ -79,10 +73,7 @@ export const resetStore = () => (dispatch: Dispatch<any>, getState: () => AppSta
})
}
export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (
dispatch: Dispatch<any>,
getState: () => AppState
): AnyAction => {
export const showTree = (tree: q.Tree<TopicViewModel> | undefined) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
+1 -12
View File
@@ -1,4 +1,3 @@
import * as chartActions from './Charts'
import * as connectionActions from './Connection'
import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global'
@@ -8,14 +7,4 @@ import * as sidebarActions from './Sidebar'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
export {
settingsActions,
treeActions,
chartActions,
publishActions,
updateNotifierActions,
connectionActions,
sidebarActions,
connectionManagerActions,
globalActions,
}
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActions, connectionManagerActions, globalActions }
+9 -17
View File
@@ -28,7 +28,7 @@ interface Props {
class App extends React.PureComponent<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
this.state = { }
}
private renderNotification() {
@@ -41,9 +41,7 @@ class App extends React.PureComponent<Props, {}> {
<Notification
message={str}
type={isError ? 'error' : 'notification'}
onClose={() => {
isError ? this.props.actions.showError(undefined) : this.props.actions.showNotification(undefined)
}}
onClose={() => { isError ? this.props.actions.showError(undefined) : this.props.actions.showNotification(undefined) }}
/>
)
}
@@ -67,7 +65,7 @@ class App extends React.PureComponent<Props, {}> {
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
{this.renderNotification()}
{this.renderNotification()}
<React.Suspense fallback={<div></div>}>
<Settings />
</React.Suspense>
@@ -77,11 +75,7 @@ class App extends React.PureComponent<Props, {}> {
</div>
<div className={settingsVisible ? contentShift : content}>
<React.Suspense fallback={<div></div>}>
<ContentView
heightProperty={heightProperty}
connectionId={this.props.connectionId}
paneDefaults={paneDefaults}
/>
<ContentView heightProperty={heightProperty} connectionId={this.props.connectionId} paneDefaults={paneDefaults} />
</React.Suspense>
</div>
</div>
@@ -97,16 +91,19 @@ const styles = (theme: Theme) => {
const drawerWidth = 300
const contentBaseStyle = {
width: '100vw',
overflow: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
}
return {
heightProperty: {
height: '100%', // 'calc(100vh - 64px) !important',
height: 'calc(100vh - 64px) !important',
},
paneDefaults: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
overflowY: 'scroll' as 'scroll',
overflowX: 'hidden' as 'hidden',
display: 'block' as 'block',
height: 'calc(100vh - 64px)',
},
@@ -152,9 +149,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(App)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
-129
View File
@@ -1,129 +0,0 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import CustomIconButton from '../helper/CustomIconButton'
import TopicPlot from '../TopicPlot'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { Paper, Theme, Typography, withStyles } from '@material-ui/core'
import { SettingsButton } from './ChartSettings/SettingsButton'
const throttle = require('lodash.throttle')
interface Props {
parameters: ChartParameters
tree?: q.Tree<any>
classes: any
actions: {
chart: typeof chartActions
}
}
function Chart(props: Props) {
if (!props.tree) {
return null
}
const { tree, parameters } = props
const initialTreeNode = tree.findNode(parameters.topic)
const [treeNode, setTreeNode] = React.useState<q.TreeNode<any> | undefined>(initialTreeNode)
usePollingToFetchTreeNode(treeNode, tree, parameters, setTreeNode)
const onRemove = React.useCallback(() => {
props.actions.chart.removeChart(props.parameters)
}, [props.parameters])
return (
<Paper style={{ padding: '8px' }}>
<div style={{ float: 'right' }}>
<SettingsButton parameters={parameters} />
<CustomIconButton tooltip="Remove chart" onClick={onRemove}>
<Clear />
</CustomIconButton>
</div>
<Typography variant="caption" className={props.classes.topic}>
{parameters.dotPath ? parameters.dotPath : ''}
</Typography>
<br />
<Typography variant="caption" className={props.classes.topic}>
{parameters.topic}
</Typography>
<br />
{treeNode ? (
<TopicPlot
color={props.parameters.color}
interpolation={props.parameters.interpolation}
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
history={treeNode.messageHistory}
dotPath={parameters.dotPath}
/>
) : (
<span>No data</span>
)}
</Paper>
)
}
const mapStateToProps = (state: AppState) => {
return {
tree: state.connection.tree,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({
topic: {
wordBreak: 'break-all' as 'break-all',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
textOverflow: 'ellipsis' as 'ellipsis',
},
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Chart))
/**
* If a node is not available when the plot is shown, keep polling until it has been created
*/
function usePollingToFetchTreeNode(
treeNode: q.TreeNode<any> | undefined,
tree: q.Tree<any>,
parameters: ChartParameters,
setTreeNode: React.Dispatch<React.SetStateAction<q.TreeNode<any> | undefined>>
) {
const [lastUpdate, setLastUpdate] = React.useState(0)
function pollForTreeNode() {
const onUpdateCallback = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
let intervalTimer: any
if (!treeNode) {
intervalTimer = setInterval(() => {
const node = tree.findNode(parameters.topic)
if (node) {
setTreeNode(node)
node.onMessage.subscribe(onUpdateCallback)
clearInterval(intervalTimer)
}
}, 500)
} else {
treeNode.onMessage.subscribe(onUpdateCallback)
}
return function cleanup() {
treeNode && treeNode.onMessage.unsubscribe(onUpdateCallback)
intervalTimer && clearInterval(intervalTimer)
}
}
React.useEffect(pollForTreeNode)
}
@@ -1,71 +0,0 @@
import * as React from 'react'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { Menu, MenuItem } from '@material-ui/core'
import { colors as createColors } from './colors'
function chartParametersForColor(chart: ChartParameters, color?: string) {
return {
color,
topic: chart.topic,
dotPath: chart.dotPath,
}
}
const colors: Array<string> = createColors()
function ColorSettings(props: {
chart: ChartParameters
actions: {
chart: typeof chartActions
}
anchorEl?: Element
open: boolean
close: () => void
}) {
const setColor = React.useCallback(
(color?: string) => props.actions.chart.updateChart(chartParametersForColor(props.chart, color)),
[props.chart]
)
const menuItems = React.useMemo(() => {
return colors.map(color => (
<MenuItem
style={{ minWidth: '8em', minHeight: '36px', backgroundColor: color, textAlign: 'center' }}
key={color}
onClick={() => setColor(color)}
>
{props.chart.color === color ? 'X' : ''}
</MenuItem>
))
}, [colors, props.chart])
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
<MenuItem
style={{ minWidth: '8em', minHeight: '36px', textAlign: 'center' }}
key="none"
onClick={() => setColor()}
selected={props.chart.color === undefined}
>
default
</MenuItem>
{menuItems}
</Menu>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(ColorSettings)
@@ -1,66 +0,0 @@
import * as React from 'react'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters, PlotCurveTypes } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { Menu, MenuItem } from '@material-ui/core'
function chartParametersForAction(chart: ChartParameters, action: string) {
return {
topic: chart.topic,
dotPath: chart.dotPath,
interpolation: action as any,
}
}
const curves: Array<PlotCurveTypes> = ['curve', 'linear', 'step_after', 'step_before', 'cubic_basis_spline']
function InterpolationSettings(props: {
chart: ChartParameters
actions: {
chart: typeof chartActions
}
anchorEl?: Element
open: boolean
close: () => void
}) {
const callbacks = React.useMemo(() => {
const createCurveCallback = (curve: PlotCurveTypes) => () => {
props.actions.chart.updateChart(chartParametersForAction(props.chart, curve))
}
const callbacks: { [key: string]: () => void } = {}
for (const curve of curves) {
callbacks[curve] = createCurveCallback(curve)
}
return callbacks
}, [curves])
const menuItems = React.useMemo(() => {
return curves.map(curve => (
<MenuItem key={curve} onClick={callbacks[curve]} selected={props.chart.interpolation === curve}>
{curve.replace(/_/g, ' ')}
</MenuItem>
))
}, [curves, props.chart])
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
{menuItems}
</Menu>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(InterpolationSettings)
@@ -1,35 +0,0 @@
import * as React from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartParameters; close: () => void }) {
const moveUp = React.useCallback(() => {
props.actions.chart.moveChartUp({
topic: props.chart.topic,
dotPath: props.chart.dotPath,
})
props.close()
}, [props.chart])
return (
<MenuItem key="size" onClick={moveUp}>
Move up
</MenuItem>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(MoveUp)
@@ -1,72 +0,0 @@
import * as React from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
function RangeSettings(props: {
actions: { chart: typeof chartActions }
chart: ChartParameters
anchorEl?: Element
open: boolean
onClose: () => void
}) {
const dismissClick = React.useCallback((e: React.MouseEvent) => e.stopPropagation(), [])
const [rangeFrom, setRangeFrom] = React.useState<string | number | undefined>(
props.chart.range && props.chart.range.from
)
const [rangeTo, setRangeTo] = React.useState<string | number | undefined>(props.chart.range && props.chart.range.to)
React.useEffect(() => {
const from = parseFloat(rangeFrom as any)
const to = parseFloat(rangeTo as any)
props.actions.chart.updateChart({
topic: props.chart.topic,
dotPath: props.chart.dotPath,
range: {
from: isNaN(from) ? undefined : from,
to: isNaN(to) ? undefined : to,
},
})
}, [rangeFrom, rangeTo])
const setFromHandler = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => setRangeFrom(e.target.value), [])
const setToHandler = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => setRangeTo(e.target.value), [])
return (
<Menu style={{ textAlign: 'center' }} anchorEl={props.anchorEl} open={props.open} onClose={props.onClose}>
<Typography>Define custom ranges for the Y-Axis</Typography>
<div style={{ padding: '0 16px' }}>
<TextField
style={{ marginTop: '0' }}
onClick={dismissClick}
label="from"
value={rangeFrom}
onChange={setFromHandler}
margin="normal"
/>
<TextField
style={{ marginLeft: '8px', marginTop: '0' }}
onClick={dismissClick}
label="to"
value={rangeTo}
onChange={setToHandler}
margin="normal"
/>
</div>
</Menu>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(RangeSettings)
@@ -1,26 +0,0 @@
import * as React from 'react'
import ChartSettings from '.'
import CustomIconButton from '../../helper/CustomIconButton'
import MoreVertIcon from '@material-ui/icons/Settings'
import { ChartParameters } from '../../../reducers/Charts'
export function SettingsButton(props: { parameters: ChartParameters }) {
const [visible, setVisible] = React.useState(false)
const settingsRef = React.useRef()
const toggleSettings = React.useCallback(() => {
setVisible(!visible)
}, [visible])
const close = React.useCallback(() => {
setVisible(false)
}, [])
return (
<span>
<ChartSettings open={visible} close={close} anchorEl={settingsRef} chart={props.parameters} />
<CustomIconButton tooltip="Chart settings" onClick={toggleSettings}>
<MoreVertIcon ref={settingsRef as any} />
</CustomIconButton>
</span>
)
}
@@ -1,53 +0,0 @@
import * as React from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
function Size(props: {
actions: { chart: typeof chartActions }
chart: ChartParameters
anchorEl?: Element
open: boolean
close: () => void
}) {
const setChartWidth = (width?: 'big' | 'medium' | 'small') => () => {
props.actions.chart.updateChart({
width,
topic: props.chart.topic,
dotPath: props.chart.dotPath,
})
props.close()
}
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
<MenuItem selected={props.chart.width === undefined} onClick={setChartWidth()}>
auto
</MenuItem>
<MenuItem selected={props.chart.width === 'big'} onClick={setChartWidth('big')}>
100% width
</MenuItem>
<MenuItem selected={props.chart.width === 'medium'} onClick={setChartWidth('medium')}>
50% width
</MenuItem>
<MenuItem selected={props.chart.width === 'small'} onClick={setChartWidth('small')}>
33% width
</MenuItem>
</Menu>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(Size)
@@ -1,46 +0,0 @@
import {
amber,
orange,
pink,
purple,
deepPurple,
teal,
red,
green,
lime,
indigo,
yellow,
brown,
blueGrey,
} from '@material-ui/core/colors'
export function colors() {
function colorToInt(color: string): [number, number, number] {
const str = color.replace('#', '')
return [parseInt(str.slice(0, 2), 16), parseInt(str.slice(2, 4), 16), parseInt(str.slice(4, 6), 16)]
}
function colorCompare(colorA: string, colorB: string) {
const a = colorToInt(colorA)
const b = colorToInt(colorB)
return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2))
}
const colors: Array<string> = [
brown,
blueGrey,
amber,
orange,
pink,
purple,
deepPurple,
teal,
red,
green,
lime,
indigo,
yellow,
]
.map(color => [color[200], color[500], color[700]])
.reduce((a, b) => a.concat(b), [])
.sort((a, b) => colorCompare(a, b))
return colors
}
@@ -1,79 +0,0 @@
import * as React from 'react'
import InterpolationSettings from './InterpolationSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem } from '@material-ui/core'
import RangeSettings from './RangeSettings'
import Size from './Size'
import MoveUp from './MoveUp'
import ColorSettings from './ColorSettings'
function ChartSettings(props: {
open: boolean
close: () => void
chart: ChartParameters
anchorEl: React.MutableRefObject<undefined>
}) {
const [rangeVisible, setRangeVisible] = React.useState(false)
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
const [sizeVisible, setSizeVisible] = React.useState(false)
const [colorVisible, setColorVisible] = React.useState(false)
const toggleRange = React.useCallback(() => {
if (open) {
props.close()
}
setRangeVisible(!rangeVisible)
}, [rangeVisible, open])
const toggleInterpolation = React.useCallback(() => {
if (open) {
props.close()
}
setInterpolationVisible(!interpolationVisible)
}, [interpolationVisible, open])
const toggleSize = React.useCallback(() => {
if (open) {
props.close()
}
setSizeVisible(!sizeVisible)
}, [sizeVisible, open])
const toggleColor = React.useCallback(() => {
if (open) {
props.close()
}
setColorVisible(!colorVisible)
}, [colorVisible, open])
return (
<span>
<Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}>
<MenuItem key="range" onClick={toggleRange}>
Set range
</MenuItem>
<MenuItem key="interpolation" onClick={toggleInterpolation}>
Curve interpolation
</MenuItem>
<MenuItem key="size" onClick={toggleSize}>
Size
</MenuItem>
<MenuItem key="color" onClick={toggleColor}>
Color
</MenuItem>
<MoveUp chart={props.chart} close={props.close} />
</Menu>
<RangeSettings chart={props.chart} anchorEl={props.anchorEl.current} open={rangeVisible} onClose={toggleRange} />
<InterpolationSettings
chart={props.chart}
anchorEl={props.anchorEl.current}
open={interpolationVisible}
close={toggleInterpolation}
/>
<Size chart={props.chart} anchorEl={props.anchorEl.current} open={sizeVisible} close={toggleSize} />
<ColorSettings chart={props.chart} anchorEl={props.anchorEl.current} open={colorVisible} close={toggleColor} />
</span>
)
}
export default ChartSettings
-119
View File
@@ -1,119 +0,0 @@
import * as React from 'react'
import Chart from './Chart'
import ShowChart from '@material-ui/icons/ShowChart'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { Grid, Theme, Typography, withStyles } from '@material-ui/core'
import { List } from 'immutable'
const { TransitionGroup, CSSTransition } = require('react-transition-group/esm')
interface Props {
charts: List<ChartParameters>
connectionId?: string
actions: {
chart: typeof chartActions
}
}
function spacingForChartCount(count: number): 4 | 6 | 12 {
if (count >= 5) {
return 4
} else if (count >= 2) {
return 6
} else {
return 12
}
}
function mapWidth(width: 'big' | 'medium' | 'small' | undefined, calculatedSpacing: 4 | 6 | 12): 4 | 6 | 12 {
switch (width) {
case 'big':
return 12
case 'medium':
return 6
case 'small':
return 4
default:
return calculatedSpacing
}
}
function ChartPanel(props: Props) {
const chartsInView = props.charts.count()
const [spacing, setSpacing] = React.useState(spacingForChartCount(chartsInView))
React.useEffect(() => {
props.actions.chart.loadCharts()
}, [props.connectionId])
// Update spacing after animations have completed
React.useEffect(() => {
const newSpacing = spacingForChartCount(chartsInView)
if (newSpacing > spacing) {
setTimeout(() => setSpacing(newSpacing), 500)
} else {
setSpacing(newSpacing)
}
}, [chartsInView])
const charts = props.charts.map(chartParameters => (
<CSSTransition
key={`${chartParameters.topic}-${chartParameters.dotPath || ''}`}
timeout={{ enter: 500, exit: 500 }}
classNames="example"
>
<Grid item xs={mapWidth(chartParameters.width, spacing)}>
<Chart parameters={chartParameters} />
</Grid>
</CSSTransition>
))
return (
<div style={{ width: '100%', height: '100%', padding: '8px' }}>
<Grid container spacing={1}>
<TransitionGroup component={null} className="example">
{charts}
</TransitionGroup>
{chartsInView === 0 ? <NoCharts key="noCharts" /> : null}
</Grid>
</div>
)
}
function NoCharts() {
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<Typography variant="h2">No charts selected</Typography>
<Typography>Select a numeric values from the value preview.</Typography>
<Typography>
Click on <ShowChart /> to add a topic / value to this panel.
</Typography>
</div>
)
}
const mapStateToProps = (state: AppState) => {
return {
charts: state.charts.get('charts'),
connectionId: state.connection.connectionId,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({})
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ChartPanel))
@@ -14,9 +14,18 @@ const styles = (theme: Theme) => ({
},
})
export const AddButton = withStyles(styles)((props: { classes: any; action: any }) => {
export const AddButton = withStyles(styles)((props: {
classes: any,
action: any,
}) => {
return (
<Fab size="small" color="secondary" aria-label="Add" className={props.classes.addButton} onClick={props.action}>
<Fab
size="small"
color="secondary"
aria-label="Add"
className={props.classes.addButton}
onClick={props.action}
>
<Add className={props.classes.addIcon} />
</Fab>
)
@@ -8,7 +8,7 @@ import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
@@ -56,8 +56,8 @@ class ConnectionSettings extends React.Component<Props, State> {
<ClearAdornment action={this.clearCertificate} value={this.props.connection.selfSignedCertificate.name} />
{this.props.connection.selfSignedCertificate.name}
</Typography>
</Tooltip>
</span>
</Tooltip>
</span>
)
}
@@ -89,26 +89,27 @@ class ConnectionSettings extends React.Component<Props, State> {
className={classes.fullWidth}
label="Subscription"
margin="normal"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ subscription: event.target.value })
}
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)
}
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
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}>
@@ -150,15 +151,17 @@ class ConnectionSettings extends React.Component<Props, State> {
}
}
const Subscription = (props: { subscription: string; deleteAction: any }) => {
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>
{props.subscription}</ListItemText>
</ListItem>
)
}
@@ -169,34 +172,33 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
const styles = (theme: Theme) => ({
fullWidth: {
width: '100%',
},
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',
},
certificateName: {
width: '100%',
height: 'calc(1em + 4px)',
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.hint,
},
})
const styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
fullWidth: {
width: '100%',
},
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',
},
certificateName: {
width: '100%',
height: 'calc(1em + 4px)',
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.hint,
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(ConnectionSettings))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
@@ -10,7 +10,7 @@ import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import {
Button,
@@ -29,14 +29,17 @@ import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
interface Props {
connection: ConnectionOptions
classes: { [s: string]: string }
actions: typeof connectionActions
classes: {[s: string]: string}
actions: typeof connectionActions,
managerActions: typeof connectionManagerActions
connected: boolean
connecting: boolean
}
const protocols = ['mqtt', 'ws']
const protocols = [
'mqtt',
'ws',
]
interface State {
showPassword: boolean
@@ -124,12 +127,20 @@ class ConnectionSettings extends React.Component<Props, State> {
const { classes, connection } = this.props
const certSwitch = (
<Switch checked={connection.certValidation} onChange={this.toggleCertValidation} color="primary" />
<Switch
checked={connection.certValidation}
onChange={this.toggleCertValidation}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel control={certSwitch} label="Validate certificate" labelPlacement="bottom" />
<FormControlLabel
control={certSwitch}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
)
}
@@ -143,11 +154,21 @@ class ConnectionSettings extends React.Component<Props, State> {
private renderTlsSwitch() {
const { classes, connection } = this.props
const tlsSwitch = <Switch checked={connection.encryption} onChange={this.toggleTls} color="primary" />
const tlsSwitch = (
<Switch
checked={connection.encryption}
onChange={this.toggleTls}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel control={tlsSwitch} label="Encryption (tls)" labelPlacement="bottom" />
<FormControlLabel
control={tlsSwitch}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
)
}
@@ -164,13 +185,12 @@ class ConnectionSettings extends React.Component<Props, State> {
if (this.props.connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
<ConnectionHealthIndicator />
&nbsp;&nbsp;Abort
<ConnectionHealthIndicator />&nbsp;&nbsp;Abort
</Button>
)
}
return (
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
<PowerSettingsNew /> Connect
</Button>
)
@@ -192,7 +212,10 @@ class ConnectionSettings extends React.Component<Props, State> {
const passwordVisibilityButton = (
<InputAdornment position="end">
<IconButton aria-label="Toggle password visibility" onClick={this.handleClickShowPassword}>
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
>
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
@@ -264,28 +287,15 @@ class ConnectionSettings extends React.Component<Props, State> {
<br />
<div>
<div style={{ float: 'left' }}>
<Button
variant="contained"
className={classes.button}
onClick={() => this.props.managerActions.deleteConnection(this.props.connection.id)}
>
<Button variant="contained" className={classes.button} onClick={() => this.props.managerActions.deleteConnection(this.props.connection.id)}>
Delete <Delete />
</Button>
<Button
variant="contained"
className={classes.button}
onClick={this.props.managerActions.toggleAdvancedSettings}
>
<Button variant="contained" className={classes.button} onClick={this.props.managerActions.toggleAdvancedSettings}>
<Settings /> Advanced
</Button>
</div>
<div style={{ float: 'right' }}>
<Button
variant="contained"
color="secondary"
className={classes.button}
onClick={this.props.managerActions.saveConnectionSettings}
>
<div style={{ float : 'right' }}>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.managerActions.saveConnectionSettings}>
<Save /> Save
</Button>
{this.renderConnectButton()}
@@ -311,22 +321,21 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
const styles = (theme: Theme) => ({
textField: {
width: '100%',
},
switch: {
marginTop: 0,
},
button: {
margin: theme.spacing(1),
},
inputFormControl: {
marginTop: '16px',
},
})
const styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
textField: {
width: '100%',
},
switch: {
marginTop: 0,
},
button: {
margin: theme.spacing(1),
},
inputFormControl: {
marginTop: '16px',
},
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSettings))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
@@ -7,7 +7,13 @@ import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Modal, Paper, Toolbar, Typography, Collapse } from '@material-ui/core'
import {
Modal,
Paper,
Toolbar,
Typography,
Collapse,
} from '@material-ui/core'
import AdvancedConnectionSettings from './AdvancedConnectionSettings'
interface Props {
@@ -31,12 +37,8 @@ class ConnectionSetup extends React.Component<Props, {}> {
return (
<div>
<Collapse in={!showAdvancedSettings}>
<ConnectionSettings connection={connection} />
</Collapse>
<Collapse in={showAdvancedSettings}>
<AdvancedConnectionSettings connection={connection} />
</Collapse>
<Collapse in={!showAdvancedSettings}><ConnectionSettings connection={connection} /></Collapse>
<Collapse in={showAdvancedSettings}><AdvancedConnectionSettings connection={connection} /></Collapse>
</div>
)
}
@@ -52,15 +54,13 @@ class ConnectionSetup extends React.Component<Props, {}> {
<div>
<Modal open={visible} disableAutoFocus={true}>
<Paper className={classes.root}>
<div className={classes.left}>
<ProfileList />
</div>
<div className={classes.left}><ProfileList /></div>
<div className={classes.right} key={connection && connection.id}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">
MQTT Connection
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
<Typography className={classes.connectionUri}>
{mqttConnection && mqttConnection.url}
</Typography>
<Typography className={classes.connectionUri}>{mqttConnection && mqttConnection.url}</Typography>
</Toolbar>
{this.renderSettings()}
</div>
@@ -115,9 +115,7 @@ const mapStateToProps = (state: AppState) => {
return {
visible: !state.connection.connected,
showAdvancedSettings: state.connectionManager.showAdvancedSettings,
connection: state.connectionManager.selected
? state.connectionManager.connections[state.connectionManager.selected]
: undefined,
connection: state.connectionManager.selected ? state.connectionManager.connections[state.connectionManager.selected] : undefined,
}
}
@@ -127,7 +125,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ConnectionSetup))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
@@ -6,12 +6,17 @@ import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { List, ListItem, ListSubheader, Typography } from '@material-ui/core'
import {
List,
ListItem,
ListSubheader,
Typography,
} from '@material-ui/core'
interface Props {
classes: any
selected?: string
connections: { [s: string]: ConnectionOptions }
connections: {[s: string]: ConnectionOptions}
actions: any
}
@@ -21,11 +26,7 @@ class ProfileList extends React.Component<Props, {}> {
}
private addConnectionButton() {
return (
<span id="addProfileButton" style={{ marginRight: '12px' }}>
<AddButton action={this.props.actions.createConnection} />
</span>
)
return <span id="addProfileButton" style={{ marginRight: '12px' }}><AddButton action={this.props.actions.createConnection} /></span>
}
public render() {
@@ -36,13 +37,7 @@ class ProfileList extends React.Component<Props, {}> {
subheader={<ListSubheader component="div">{this.addConnectionButton()}Connections</ListSubheader>}
>
<div className={this.props.classes.list}>
{Object.values(this.props.connections).map(connection => (
<ConnectionItem
connection={connection}
key={connection.id}
selected={this.props.selected === connection.id}
/>
))}
{Object.values(this.props.connections).map(connection => <ConnectionItem connection={connection} key={connection.id} selected={this.props.selected === connection.id} />)}
</div>
</List>
)
@@ -64,9 +59,9 @@ const mapDispatchToProps = (dispatch: any) => {
}
interface ConnectionItemProps {
connection: ConnectionOptions
actions: any
selected: boolean
connection: ConnectionOptions,
actions: any,
selected: boolean,
classes: any
}
@@ -96,16 +91,17 @@ const connectionItemRenderer = withStyles(connectionItemStyle)((props: Connectio
style={{ display: 'block' }}
onClick={() => props.actions.selectConnection(props.connection.id)}
>
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
<Typography className={props.classes.name}>
{props.connection.name || 'mqtt broker'}
</Typography>
<Typography className={props.classes.details}>
{connection && connection.url}
</Typography>
</ListItem>
)
})
const ConnectionItem = connect(
null,
mapDispatchToProps
)(connectionItemRenderer)
const ConnectionItem = connect(null, mapDispatchToProps)(connectionItemRenderer)
const mapStateToProps = (state: AppState) => {
return {
@@ -114,7 +110,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ProfileList))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
+1
View File
@@ -13,6 +13,7 @@ class Key extends React.Component<Props, {}> {
}
public render() {
return (
<div className={this.props.classes.keyStyle}>
<div className={this.props.classes.keyTextStyle}>{this.props.keyboardKey}</div>
+12 -10
View File
@@ -4,13 +4,13 @@ const cursor = require('./cursor.png')
interface State {
enabled: boolean
target: { x: number; y: number }
position: { x: number; y: number }
stepSizeX: number
stepSizeY: number
target: {x: number, y: number}
position: {x: number, y: number}
stepSizeX: number,
stepSizeY: number,
}
class Demo extends React.Component<{ classes: any }, State> {
class Demo extends React.Component<{classes: any}, State> {
private timer: any
private frameInterval = 20
@@ -32,8 +32,8 @@ class Demo extends React.Component<{ classes: any }, State> {
this.setState({
position: {
x: this.state.position.x + dirX * steSizeX,
y: this.state.position.y + dirY * steSizeY,
x: this.state.position.x + (dirX * steSizeX),
y: this.state.position.y + (dirY * steSizeY),
},
})
@@ -43,10 +43,10 @@ class Demo extends React.Component<{ classes: any }, State> {
}
public componentDidMount() {
;(window as any).demo.enableMouse = () => {
(window as any).demo.enableMouse = () => {
this.setState({ enabled: true })
}
;(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
@@ -64,7 +64,9 @@ class Demo extends React.Component<{ classes: any }, State> {
top: this.state.position.y + 2,
}
return <img src={cursor} style={cursorStyle} className={this.props.classes.cursor} />
return (
<img src={cursor} style={cursorStyle} className={this.props.classes.cursor} />
)
}
}
+7 -11
View File
@@ -8,7 +8,7 @@ interface State {
location: string
}
class Demo extends React.Component<{ classes: any }, State> {
class Demo extends React.Component<{classes: any}, State> {
private timer: any
constructor(props: any) {
super(props)
@@ -20,24 +20,20 @@ class Demo extends React.Component<{ classes: any }, State> {
}
public componentDidMount() {
;(window as any).demo.showMessage = (
message: string,
location: string,
duration: number,
keys: Array<string> = []
) => {
(window as any).demo.showMessage = (message: string, location: string, duration: number, keys: Array<string> = []) => {
this.clearTimer()
this.setState({ message, location, keys })
this.timer = setTimeout(() => this.setState({ message: undefined }), duration)
}
;(window as any).demo.hideMessage = () => {
(window as any).demo.hideMessage = () => {
this.clearTimer()
this.setState({ message: undefined })
}
}
public render() {
const positions: { [s: string]: number } = {
const positions: {[s: string]: number} = {
top: 0,
bottom: -65,
middle: -32,
@@ -56,6 +52,7 @@ class Demo extends React.Component<{ classes: any }, State> {
color: 'white',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: '16px',
}
if (!this.state.message) {
@@ -64,8 +61,7 @@ class Demo extends React.Component<{ classes: any }, State> {
let keys: Array<any> = []
if (this.state.keys.length > 0) {
keys = this.state.keys
.map(key => [<Key key={key} keyboardKey={key} />])
keys = this.state.keys.map(key => [<Key key={key} keyboardKey={key} />])
.reduce((prev, current) => {
return [prev, '+' as any, current]
})
+1 -1
View File
@@ -12,7 +12,7 @@ function writeHeapdump(path?: string) {
return path
}
;(window as any).demo = {
(window as any).demo = {
writeHeapdump,
}
+17 -24
View File
@@ -4,7 +4,13 @@ import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import Warning from '@material-ui/icons/Warning'
import { electronRendererTelementry } from 'electron-telemetry'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Button, Modal, Paper, Toolbar, Typography } from '@material-ui/core'
import {
Button,
Modal,
Paper,
Toolbar,
Typography,
} from '@material-ui/core'
interface State {
error?: Error
@@ -15,6 +21,7 @@ interface Props {
}
class ErrorBoundary extends React.Component<Props, State> {
public static getDerivedStateFromError(error: Error) {
return { error }
}
@@ -47,36 +54,23 @@ class ErrorBoundary extends React.Component<Props, State> {
<Modal open={true} disableAutoFocus={true}>
<Paper className={classes.root}>
<Toolbar style={{ padding: '0' }}>
<Typography className={classes.title} variant="h6" color="inherit">
<Warning /> Oooooops!
</Typography>
<Typography className={classes.title} variant="h6" color="inherit"><Warning /> Oooooops!</Typography>
</Toolbar>
<Typography className={classes.centered}>
I hoped that you would never see this window, but MQTT-Explorer had an unexpected error.
</Typography>
<Typography className={classes.centered}>
<SentimentDissatisfied />
</Typography>
<Typography className={classes.centered}>I hoped that you would never see this window, but MQTT-Explorer had an unexpected error.</Typography>
<Typography className={classes.centered}><SentimentDissatisfied /></Typography>
<pre className={classes.textColor} style={{ maxHeight: '35vh', overflow: 'scroll' }}>
<code className={classes.textColor}>{this.state.error.stack}</code>
<code className={classes.textColor}>
{this.state.error.stack}
</code>
</pre>
<Typography>
Please report this issue with a short description of what happened to
<span>
{' '}
<a className={classes.textColor} href="https://github.com/thomasnordquist/MQTT-Explorer/issues">
https://github.com/thomasnordquist/MQTT-Explorer/issues
</a>
</span>
<span> <a className={classes.textColor} href="https://github.com/thomasnordquist/MQTT-Explorer/issues">https://github.com/thomasnordquist/MQTT-Explorer/issues</a></span>
</Typography>
<div>
<div className={classes.buttonPositioning}>
<Button className={classes.button} variant="contained" color="secondary" onClick={this.clearStorage}>
Start Fresh
</Button>
<Button className={classes.button} variant="contained" color="primary" onClick={this.restart}>
Restart
</Button>
<Button className={classes.button} variant="contained" color="secondary" onClick={this.clearStorage}>Start Fresh</Button>
<Button className={classes.button} variant="contained" color="primary" onClick={this.restart}>Restart</Button>
</div>
</div>
</Paper>
@@ -104,7 +98,6 @@ const styles = (theme: Theme) => ({
},
textColor: {
color: theme.palette.text.primary,
userSelect: 'all' as 'all',
},
centered: {
textAlign: 'center' as 'center',
+20 -79
View File
@@ -1,86 +1,27 @@
import * as React from 'react'
import ChartPanel from '../ChartPanel'
import ReactSplitPane from 'react-split-pane'
import Tree from '../Tree/Tree'
import { AppState } from '../../reducers'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { List } from 'immutable'
import { Sidebar } from '../Sidebar'
import ReactResizeDetector from 'react-resize-detector'
interface Props {
heightProperty: any
paneDefaults: any
connectionId?: string
chartPanelItems: List<ChartParameters>
}
function ContentView(props: Props) {
const [height, setHeight] = React.useState<string | number>('100%')
const [detectedHeight, setDetectedHeight] = React.useState(0)
const detectSize = React.useCallback((width, newHeight) => {
setDetectedHeight(newHeight)
}, [])
// Open chart panel on start and when a new chart is added but the panel is closed
React.useEffect(() => {
const almostClosed = !isNaN(height as any) && detectedHeight < 30
if ((!height || height === '100%' || almostClosed) && props.chartPanelItems.count() > 0) {
setHeight('calc(100% - 250px)')
}
if (props.chartPanelItems.count() === 0) {
setHeight('100%')
}
}, [props.chartPanelItems])
import Tree from '../Tree/Tree'
export default function ContentView(props: {heightProperty: any, paneDefaults: any, connectionId: any}) {
return (
<div className={props.paneDefaults}>
<ReactSplitPane
step={10}
split="horizontal"
minSize={0}
size={height}
defaultSize={'100%'}
allowResize={true}
style={{ height: 'calc(100vh - 64px)' }}
pane1Style={{ maxHeight: '100%' }}
pane2Style={{ borderTop: '1px solid #999', overflow: 'hidden scroll' }}
onChange={setHeight}
>
<span>
<ReactSplitPane
step={20}
primary="second"
className={props.heightProperty}
split="vertical"
minSize={250}
defaultSize={500}
allowResize={true}
style={{ height: '100%' }}
pane1Style={{ overflowX: 'hidden' }}
resizerStyle={{ height: '100%' }}
>
<Tree />
<div className={props.paneDefaults} style={{ height: '100%', overflowY: 'auto', overflowX: 'hidden' }}>
<Sidebar connectionId={props.connectionId} />
</div>
</ReactSplitPane>
</span>
<div>
<ReactResizeDetector handleHeight={true} onResize={detectSize} />
<ChartPanel />
</div>
</ReactSplitPane>
</div>
<ReactSplitPane
step={20}
primary="second"
className={props.heightProperty}
split="vertical"
minSize={250}
defaultSize={500}
allowResize={true}
style={{ position: 'relative' }}
pane1Style={{ overflow: 'hidden' }}
>
<div className={props.paneDefaults}>
<Tree />
</div>
<div className={props.paneDefaults}>
<Sidebar connectionId={props.connectionId} />
</div>
</ReactSplitPane>
)
}
const mapStateToProps = (state: AppState) => {
return {
chartPanelItems: state.charts.get('charts'),
}
}
export default connect(mapStateToProps)(ContentView)
+9 -18
View File
@@ -7,9 +7,10 @@ import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { treeActions } from '../../actions'
import { withStyles, Theme } from '@material-ui/core/styles'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { Tooltip } from '@material-ui/core'
const styles = (theme: Theme) => ({
const styles: StyleRulesCallback = theme => ({
icon: {
color: theme.palette.primary.contrastText,
verticalAlign: 'middle' as 'middle',
@@ -22,13 +23,13 @@ const styles = (theme: Theme) => ({
interface Props {
classes: any
actions: {
tree: typeof treeActions
tree: typeof treeActions,
}
paused: boolean
tree?: q.Tree<any>
}
class PauseButton extends React.Component<Props, { changes: number }> {
class PauseButton extends React.Component<Props, {changes: number}> {
private timer?: any
constructor(props: Props) {
super(props)
@@ -42,8 +43,7 @@ class PauseButton extends React.Component<Props, { changes: number }> {
return (
<span className={this.props.classes.bufferStats}>
{this.state.changes} changes
<br />
{this.state.changes} changes<br />
buffer at {Math.round(this.props.tree.unmergedChanges().fillState() * 10000) / 100}%
</span>
)
@@ -67,18 +67,12 @@ class PauseButton extends React.Component<Props, { changes: number }> {
}
public render() {
const message = this.props.paused
? 'Resumes updating the tree, after applying all recorded changes'
: 'Stops all updates, records changes until the buffer is full.'
const message = this.props.paused ? 'Resumes updating the tree, after applying all recorded changes' : 'Stops all updates, records changes until the buffer is full.'
return (
<div style={{ display: 'inline-flex' }}>
<span>
<CustomIconButton onClick={this.props.actions.tree.togglePause} tooltip={message}>
{this.props.paused ? (
<Resume className={this.props.classes.icon} />
) : (
<Pause className={this.props.classes.icon} />
)}
{this.props.paused ? <Resume className={this.props.classes.icon} /> : <Pause className={this.props.classes.icon} />}
</CustomIconButton>
</span>
{this.props.paused ? this.renderBufferStats() : null}
@@ -102,7 +96,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(PauseButton))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
+31 -38
View File
@@ -3,26 +3,33 @@ import ClearAdornment from '../helper/ClearAdornment'
import CloudOff from '@material-ui/icons/CloudOff'
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
import Menu from '@material-ui/icons/Menu'
import PauseButton from './PauseButton'
import Search from '@material-ui/icons/Search'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, globalActions, settingsActions } from '../../actions'
import { connectionActions, settingsActions, globalActions } from '../../actions'
import { fade } from '@material-ui/core/styles/colorManipulator'
import { Theme, withStyles } from '@material-ui/core/styles'
import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import {
AppBar,
Button,
IconButton,
InputBase,
Toolbar,
Typography,
} from '@material-ui/core'
import PauseButton from './PauseButton'
const styles = (theme: Theme) => ({
const styles: StyleRulesCallback = theme => ({
title: {
display: 'none' as 'none',
display: 'none',
[theme.breakpoints.up(750)]: {
display: 'block' as 'block',
display: 'block',
},
whiteSpace: 'nowrap' as 'nowrap',
},
search: {
position: 'relative' as 'relative',
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
@@ -37,11 +44,11 @@ const styles = (theme: Theme) => ({
maxWidth: '30%',
marginLeft: theme.spacing(4),
width: 'auto' as 'auto',
width: 'auto',
},
[theme.breakpoints.up(750)]: {
marginLeft: theme.spacing(4),
width: 'auto' as 'auto',
width: 'auto',
},
},
disconnectIcon: {
@@ -54,14 +61,14 @@ const styles = (theme: Theme) => ({
searchIcon: {
width: theme.spacing(6),
height: '100%',
position: 'absolute' as 'absolute',
pointerEvents: 'none' as 'none',
display: 'flex' as 'flex',
alignItems: 'center' as 'center',
justifyContent: 'center' as 'center',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit' as 'inherit',
color: 'inherit',
width: '100%',
},
inputInput: {
@@ -85,9 +92,9 @@ const styles = (theme: Theme) => ({
interface Props {
classes: any
actions: {
settings: typeof settingsActions
connection: typeof connectionActions
global: typeof globalActions
settings: typeof settingsActions,
connection: typeof connectionActions,
global: typeof globalActions,
}
topicFilter?: string
}
@@ -95,7 +102,7 @@ interface Props {
class TitleBar extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
this.state = { }
}
private renderSearch() {
@@ -110,11 +117,7 @@ class TitleBar extends React.Component<Props, {}> {
value={topicFilter}
onChange={this.onFilterChange}
placeholder="Search…"
endAdornment={
<div style={{ width: '24px', paddingRight: '8px' }}>
<ClearAdornment variant="primary" action={this.clearFilter} value={topicFilter} />
</div>
}
endAdornment={<div style={{ width: '24px', paddingRight: '8px' }}><ClearAdornment variant="primary" action={this.clearFilter} value={topicFilter} /></div>}
classes={{ root: classes.inputRoot, input: classes.inputInput }}
/>
</div>
@@ -135,17 +138,10 @@ class TitleBar extends React.Component<Props, {}> {
return (
<AppBar position="static">
<Toolbar>
<IconButton
className={classes.menuButton}
color="inherit"
aria-label="Menu"
onClick={actions.global.toggleSettingsVisibility}
>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={actions.global.toggleSettingsVisibility}>
<Menu />
</IconButton>
<Typography className={classes.title} variant="h6" color="inherit">
MQTT Explorer
</Typography>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Explorer</Typography>
{this.renderSearch()}
<PauseButton />
<Button className={classes.disconnect} onClick={actions.connection.disconnect}>
@@ -174,7 +170,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(TitleBar))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TitleBar))
@@ -1,9 +1,14 @@
import * as React from 'react'
import { InputLabel, Switch, Theme, Tooltip } from '@material-ui/core'
import {
InputLabel,
Switch,
Theme,
Tooltip
} from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
const sha1 = require('sha1')
function BooleanSwitch(props: { title: string; value: boolean; tooltip: string; action: () => void; classes: any }) {
function BooleanSwitch(props: {title: string, value: boolean, tooltip: string, action: () => void, classes: any}) {
const { tooltip, value, action, title, classes } = props
const clickHandler = (e: React.MouseEvent) => {
@@ -15,21 +20,34 @@ function BooleanSwitch(props: { title: string; value: boolean; tooltip: string;
return (
<div style={{ padding: '8px', display: 'flex' }}>
<Tooltip title={tooltip}>
<InputLabel htmlFor={`toggle-${sha1(title)}`} onClick={clickHandler} className={classes.label}>
<InputLabel
htmlFor={`toggle-${sha1(title)}`}
onClick={clickHandler}
className={classes.label}
>
{title}
</InputLabel>
</Tooltip>
<Tooltip title={tooltip}>
<Switch name={`toggle-${sha1(title)}`} checked={value} onChange={action} color="primary" />
<Switch
name={`toggle-${sha1(title)}`}
checked={value}
onChange={action}
color="primary"
classes={{ switchBase: classes.switchBase }}
/>
</Tooltip>
</div>
)
}
const styles = (theme: Theme) => ({
switchBase: {
height: theme.spacing(4),
},
label: {
flex: '1',
paddingTop: theme.spacing(1.5),
paddingTop: '8px',
},
})
@@ -2,7 +2,7 @@ import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { AppState } from '../../reducers'
import { connect } from 'react-redux'
import { StyleRulesCallback, withStyles, Theme } from '@material-ui/core/styles'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
@@ -15,7 +15,7 @@ interface Stats {
title: string
}
const styles = (theme: Theme) => ({
const styles: StyleRulesCallback = theme => ({
flex: {
display: 'flex',
width: '100%',
@@ -112,17 +112,13 @@ class BrokerStatistics extends React.Component<Props, {}> {
}
const str = node.message.value ? Base64Message.toUnicodeString(node.message.value) : ''
let value = node.message && node.message.value ? parseFloat(str) : NaN
let value = (node.message && node.message.value) ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str
return (
<div key={stat.title}>
<Typography>
<b>{stat.title}</b>
</Typography>
<Typography style={{ paddingLeft: '8px' }}>
<i>{value}</i>
</Typography>
<Typography><b>{stat.title}</b></Typography>
<Typography style={{ paddingLeft: '8px' }}><i>{value}</i></Typography>
</div>
)
}
+47 -82
View File
@@ -2,13 +2,12 @@ import * as React from 'react'
import BooleanSwitch from './BooleanSwitch'
import BrokerStatistics from './BrokerStatistics'
import ChevronRight from '@material-ui/icons/ChevronRight'
import TimeLocale from './TimeLocale'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../../actions'
import { shell } from 'electron'
import { Theme, withStyles } from '@material-ui/core/styles'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { TopicOrder } from '../../reducers/Settings'
import {
@@ -22,35 +21,29 @@ import {
Typography,
Tooltip,
} from '@material-ui/core'
import TimeLocale from './TimeLocale';
export const autoExpandLimitSet = [
{
limit: 0,
name: 'Collapsed',
},
{
limit: 2,
name: 'Few',
},
{
limit: 5,
name: 'Some',
},
{
limit: 15,
name: 'Most',
},
{
limit: 30,
name: 'Most',
},
{
limit: 1e6,
name: 'All',
},
]
export const autoExpandLimitSet = [{
limit: 0,
name: 'Collapsed',
}, {
limit: 2,
name: 'Few',
}, {
limit: 5,
name: 'Some',
}, {
limit: 15,
name: 'Most',
}, {
limit: 30,
name: 'Most',
}, {
limit: 1E6,
name: 'All',
}]
const styles = (theme: Theme) => ({
const styles: StyleRulesCallback = theme => ({
drawer: {
backgroundColor: theme.palette.background.default,
flexShrink: 0,
@@ -77,8 +70,8 @@ const styles = (theme: Theme) => ({
interface Props {
actions: {
settings: typeof settingsActions
global: typeof globalActions
settings: typeof settingsActions,
global: typeof globalActions,
}
autoExpandLimit: number
classes: any
@@ -103,56 +96,29 @@ class Settings extends React.Component<Props, {}> {
private renderHighlightTopicUpdates() {
const { highlightTopicUpdates, actions } = this.props
return (
<BooleanSwitch
title="Show Activity"
tooltip="Topics blink when a new message arrives"
value={highlightTopicUpdates}
action={actions.settings.toggleHighlightTopicUpdates}
/>
)
return <BooleanSwitch title="Show Activity" tooltip="Topics blink when a new message arrives" value={highlightTopicUpdates} action={actions.settings.toggleHighlightTopicUpdates}/>
}
private selectTopicsOnMouseOver() {
const { actions, selectTopicWithMouseOver } = this.props
const toggle = () => actions.settings.selectTopicWithMouseOver(!selectTopicWithMouseOver)
return (
<BooleanSwitch
title="Quick Preview"
tooltip="Select topics on mouse over"
value={selectTopicWithMouseOver}
action={toggle}
/>
)
return <BooleanSwitch title="Quick Preview" tooltip="Select topics on mouse over" value={selectTopicWithMouseOver} action={toggle} />
}
private toggleTheme() {
const { actions, theme } = this.props
return (
<BooleanSwitch
title="Dark Mode"
tooltip="Enable dark theme"
value={theme === 'dark'}
action={actions.settings.toggleTheme}
/>
)
return <BooleanSwitch title="Dark Mode" tooltip="Enable dark theme" value={theme === 'dark'} action={actions.settings.toggleTheme} />
}
private renderAutoExpand() {
const { classes, autoExpandLimit } = this.props
const limits = autoExpandLimitSet.map(limit => (
<MenuItem key={limit.limit} value={limit.limit}>
{limit.limit < 10000 && limit.limit > 0 ? `${limit.limit} topics` : limit.name}
</MenuItem>
))
const limits = autoExpandLimitSet.map(limit => <MenuItem key={limit.limit} value={limit.limit}>{limit.limit < 10000 && limit.limit > 0 ? `${limit.limit} topics` : limit.name}</MenuItem>)
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>
Auto Expand
</InputLabel>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Auto Expand</InputLabel>
<Select
value={autoExpandLimit}
onChange={this.onChangeAutoExpand}
@@ -167,8 +133,8 @@ class Settings extends React.Component<Props, {}> {
)
}
private onChangeAutoExpand = (e: React.ChangeEvent<{ value: unknown }>) => {
this.props.actions.settings.setAutoExpandLimit(parseInt(String(e.target.value), 10))
private onChangeAutoExpand = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.settings.setAutoExpandLimit(parseInt(e.target.value, 10))
}
private renderNodeOrder() {
@@ -176,9 +142,7 @@ class Settings extends React.Component<Props, {}> {
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>
Topic Order
</InputLabel>
<InputLabel htmlFor="auto-expand" style={{ flex: '1', marginTop: '8px' }}>Topic Order</InputLabel>
<Select
value={topicOrder}
onChange={this.onChangeSorting}
@@ -188,26 +152,32 @@ class Settings extends React.Component<Props, {}> {
className={classes.input}
style={{ flex: '1' }}
>
<MenuItem value={TopicOrder.none}>
<em>default</em>
</MenuItem>
<MenuItem value={TopicOrder.none}><em>default</em></MenuItem>
<MenuItem value={TopicOrder.abc}>a-z</MenuItem>
<MenuItem value={TopicOrder.messages}>{TopicOrder.messages}</MenuItem>
<MenuItem value={TopicOrder.topics}>{TopicOrder.topics}</MenuItem>
</Select>
</div>
)
</div>)
}
private onChangeSorting = (e: React.ChangeEvent<{ value: unknown }>) => {
private onChangeSorting = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.props.actions.settings.setTopicOrder(e.target.value as TopicOrder)
}
public render() {
const { classes, actions, visible } = this.props
return (
<Drawer anchor="left" className={classes.drawer} open={visible} variant="persistent">
<div className={classes.paper} tabIndex={0} role="button">
<Drawer
anchor="left"
className={classes.drawer}
open={visible}
variant="persistent"
>
<div
className={classes.paper}
tabIndex={0}
role="button"
>
<Typography className={classes.title} variant="h6" color="inherit">
<IconButton onClick={actions.global.toggleSettingsVisibility}>
<ChevronRight />
@@ -255,9 +225,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Settings)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Settings))
@@ -3,7 +3,13 @@ import DateFormatter from '../helper/DateFormatter'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { Input, InputLabel, MenuItem, Select, StyleRulesCallback, Theme } from '@material-ui/core'
import {
Input,
InputLabel,
MenuItem,
Select,
StyleRulesCallback
} from '@material-ui/core'
import { settingsActions } from '../../actions'
import { withStyles } from '@material-ui/styles'
const moment = require('moment/min/moment-with-locales')
@@ -11,7 +17,7 @@ const moment = require('moment/min/moment-with-locales')
interface Props {
actions: {
settings: typeof settingsActions
}
},
timeLocale: string
classes: any
}
@@ -22,33 +28,21 @@ function TimeLocaleSettings(props: Props) {
const date = new Date()
const localeMenuItems = locales.map((l: string) => (
<MenuItem key={l} value={l}>
<div>
Locale: <b>{l}</b>, Format:{' '}
<b>
<DateFormatter date={date} overrideLocale={l} />
</b>
</div>
<div>Locale: <b>{l}</b>, Format: <b><DateFormatter date={date} overrideLocale={l} /></b></div>
</MenuItem>
))
function updateLocale(e: React.ChangeEvent<{ value: unknown }>) {
const locale = e.target.value ? String(e.target.value) : ''
actions.settings.setTimeLocale(locale)
}
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="time-locale" style={{ flex: '1', marginTop: '8px' }}>
Time Locale
</InputLabel>
<InputLabel htmlFor="time-locale" style={{ flex: '1', marginTop: '8px' }}>Time Locale</InputLabel>
<Select
value={timeLocale}
onChange={updateLocale}
input={<Input name="time-locale" id="time-locale-label-placeholder" />}
name="time-locale"
className={classes.input}
renderValue={v => <span>{String(v)}</span>}
style={{ flex: '1' }}
value={timeLocale}
onChange={e => actions.settings.setTimeLocale(e.target.value)}
input={<Input name="time-locale" id="time-locale-label-placeholder" />}
name="time-locale"
className={classes.input}
renderValue={(v) => <span>{v}</span>}
style={{ flex: '1' }}
>
{localeMenuItems}
</Select>
@@ -70,21 +64,16 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
const styles = (theme: Theme) => ({
const styles: StyleRulesCallback = theme => ({
input: {
minWidth: '150px',
margin: `auto ${theme.spacing(1)} auto ${theme.spacing(2)}px`,
},
selected: {
'& div': {
display: 'none',
},
},
display: 'none'
}
}
})
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(TimeLocaleSettings)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(TimeLocaleSettings))
+199
View File
@@ -0,0 +1,199 @@
import * as diff from 'diff'
import * as Prism from 'prismjs'
import * as React from 'react'
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from './CodeBlockColors'
import { selectTextWithCtrlA } from '../../utils/handleTextSelectWithCtrlA'
import { Theme, withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json'
interface Props {
previous: string
current: string
nameOfCompareMessage: string
language?: 'json'
classes: any
}
class CodeDiff extends React.Component<Props, {}> {
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre ~ pre' })
constructor(props: Props) {
super(props)
}
private renderChangeAmount(changes: Array<Diff.Change>) {
const additions = changes.map(change => (change.added === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
const deletions = changes.map(change => (change.removed === true) ? (change.count || 0) : 0).reduce((a, b) => a + b)
if (additions === 0 && deletions === 0) {
return null
}
return (
<span style={{ display: 'block', marginBottom: '8px', float: 'right' }}>
<span>
Comparing with <b>{this.props.nameOfCompareMessage}</b> message:&nbsp;
<span className={this.props.classes.additions}>+ {additions} line{additions === 1 ? '' : 's'}</span>
, <span className={this.props.classes.deletions}>- {deletions} line{deletions === 1 ? '' : 's'}</span>
</span>
</span>
)
}
private trimNewlineRight(str: string) {
if (str.slice(-1) === '\n') {
return str.slice(0, -1)
}
return str
}
private cssClassForChange(change: diff.Change) {
if (change.added === true) {
return this.props.classes.addition
}
if (change.removed === true) {
return this.props.classes.deletion
}
return this.props.classes.noChange
}
public render() {
const changes = diff.diffLines(this.props.previous, this.props.current)
const styledLines = Prism.highlight(this.props.current, Prism.languages.json, 'json').split('\n')
let lineNumber = 0
const code = changes.map((change, key) => {
const hasStyledCode = change.removed !== true
const changedLines = change.count || 0
if (hasStyledCode && this.props.language === 'json') {
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
const lines = currentLines.map((html: string, idx: number) => {
return <div key={`${key}-${idx}`} className={`${this.props.classes.line} ${this.cssClassForChange(change)}`}><span dangerouslySetInnerHTML={{ __html: html }} /></div>
})
lineNumber += changedLines
return <div key={key}>{lines}</div>
}
return this.trimNewlineRight(change.value)
.split('\n')
.map((line, idx) => {
return <div key={`${key}-${idx}`} className={`${this.props.classes.line} ${this.cssClassForChange(change)}`}><span>{line}</span></div>
})
})
const gutters = changes.map((change, key) => {
return this.trimNewlineRight(change.value)
.split('\n')
.map((_, idx) => (
<div key={`${key}-${idx}`} className={`${this.cssClassForChange(change)} ${this.props.classes.gutterLine}`}>
{change.added ? '+' : null}{change.removed ? '-' : null}{!change.added && !change.removed ? ' ' : null}
</div>
))
})
return (
<div>
<div tabIndex={0} onKeyDown={this.handleCtrlA} className={this.props.classes.codeWrapper}>
<pre className={this.props.classes.gutters}>{gutters}</pre>
<pre className={this.props.classes.codeBlock}>{code}</pre>
</div>
{this.renderChangeAmount(changes)}
</div>
)
}
}
const style = (theme: Theme) => {
const codeBlockColors = theme.palette.type === 'light' ? CodeBlockColors : CodeBlockColorsBraceMonokai
const gutterBaseStyle = {
width: '100%',
}
const codeBaseStyle = {
font: "12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace",
display: 'inline-grid' as 'inline-grid',
margin: '0',
padding: '1px 0 2px 0',
}
return {
additions: {
color: 'rgb(10, 255, 10)',
},
deletions: {
color: 'rgb(255, 10, 10)',
},
line: {
lineHeight: 'normal' as 'normal',
paddingLeft: '4px',
},
gutterLine: {
textAlign: 'right' as 'right',
paddingRight: theme.spacing(0.5),
},
codeWrapper: {
maxHeight: '15em',
overflow: 'auto',
backgroundColor: `${codeBlockColors.background}`,
margin: '8px 0 0 0',
},
gutters: {
...codeBaseStyle,
width: '33px',
backgroundColor: codeBlockColors.gutters,
userSelect: 'none' as 'none',
},
codeBlock: {
...codeBaseStyle,
width: 'calc(100% - 33px)',
backgroundColor: 'inherit !important',
'& span': {
color: codeBlockColors.text,
},
'& .token.number': {
color: codeBlockColors.numeric,
},
'& .token.boolean': {
color: codeBlockColors.numeric,
},
'& .token.property': {
color: codeBlockColors.variable,
},
'& .token.string': {
color: codeBlockColors.string,
},
'& .token': {
color: codeBlockColors.text,
},
'& .token.operator': {
color: codeBlockColors.text,
},
'& .token.punctuation': {
color: codeBlockColors.text,
},
},
noChange: {
...gutterBaseStyle,
},
deletion: {
...gutterBaseStyle,
backgroundColor: 'rgba(255, 10, 10, 0.3)',
// '&:hover': {
// backgroundColor: 'rgba(255, 10, 10, 0.3)',
// },
},
addition: {
...gutterBaseStyle,
backgroundColor: 'rgba(10, 255, 10, 0.3)',
// '&:hover': {
// backgroundColor: 'rgba(10, 255, 10, 0.5)',
// },
},
}
}
export default withStyles(style)(CodeDiff)
@@ -1,83 +0,0 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ShowChart from '@material-ui/icons/ShowChart'
import TopicPlot from '../../TopicPlot'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { connect } from 'react-redux'
import { Fade, Paper, Popper, Tooltip } from '@material-ui/core'
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
interface Props {
treeNode: q.TreeNode<any>
classes: any
literal: JsonPropertyLocation
actions: {
chart: typeof chartActions
}
}
function ChartPreview(props: Props) {
const chartIconRef = React.useRef(null)
const [open, setOpen] = React.useState(false)
const onClick = React.useCallback(() => {
props.actions.chart.addChart({
topic: props.treeNode.path(),
dotPath: props.literal.path,
})
setOpen(false)
}, [props.literal.path, props.treeNode])
const mouseOver = React.useCallback(() => {
setOpen(true)
}, [])
const mouseOut = React.useCallback(() => {
setOpen(false)
}, [])
const hasEnoughDataToDisplayDiagrams = props.treeNode.messageHistory.count() > 1
const addChartToPanelButton = hasEnoughDataToDisplayDiagrams ? (
<Tooltip title="Add to chart panel">
<ShowChart
ref={chartIconRef}
className={props.classes.icon}
onMouseEnter={mouseOver}
onMouseLeave={mouseOut}
onClick={onClick}
/>
</Tooltip>
) : (
<Tooltip title="Add to chart panel, not enough data for preview">
<ShowChart onClick={onClick} className={props.classes.icon} style={{ color: '#aaa' }} />
</Tooltip>
)
return (
<span>
{addChartToPanelButton}
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
<Fade in={open} timeout={300}>
<Paper style={{ width: '300px' }}>
{open ? <TopicPlot history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
</Paper>
</Fade>
</Popper>
</span>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(
undefined,
mapDispatchToProps
)(ChartPreview)
@@ -1,45 +0,0 @@
import * as React from 'react'
import { Theme } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
interface Props {
changes: Array<Diff.Change>
classes: { [s: string]: any }
nameOfCompareMessage: string
}
function changeAmount(props: Props) {
const additions = props.changes.map(change => (change.added === true ? change.count || 0 : 0)).reduce((a, b) => a + b)
const deletions = props.changes
.map(change => (change.removed === true ? change.count || 0 : 0))
.reduce((a, b) => a + b)
if (additions === 0 && deletions === 0) {
return null
}
return (
<span style={{ display: 'block', marginBottom: '8px', float: 'right' }}>
<span>
Comparing with <b>{props.nameOfCompareMessage}</b> message:&nbsp;
<span className={props.classes.additions}>
+ {additions} line{additions === 1 ? '' : 's'}
</span>
,{' '}
<span className={props.classes.deletions}>
- {deletions} line{deletions === 1 ? '' : 's'}
</span>
</span>
</span>
)
}
const style = (theme: Theme) => ({
additions: {
color: 'rgb(10, 255, 10)',
},
deletions: {
color: 'rgb(255, 10, 10)',
},
})
export default withStyles(style)(changeAmount)
@@ -1,104 +0,0 @@
import * as diff from 'diff'
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import Add from '@material-ui/icons/Add'
import ChartPreview from './ChartPreview'
import Remove from '@material-ui/icons/Remove'
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
import { lineChangeStyle, trimNewlineRight } from './util'
import { Theme } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
interface Props {
changes: Array<diff.Change>
literalPositions: Array<JsonPropertyLocation>
classes: any
className: string
treeNode: q.TreeNode<any>
}
const style = (theme: Theme) => {
const icon = {
verticalAlign: 'top',
width: '12px',
height: '12px',
marginTop: '2px',
borderRadius: '50%',
}
return {
icon,
iconButton: {
...icon,
marginTop: '0px',
width: '16px',
height: '16px',
padding: '2px',
'&:hover': {
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.main,
},
},
gutterLine: {
textAlign: 'right' as 'right',
paddingRight: theme.spacing(0.5),
height: '16px',
width: '100%',
},
}
}
function tokensForLine(change: diff.Change, line: number, props: Props) {
const { classes, literalPositions } = props
const literal = literalPositions[line]
const chartPreview = Boolean(literal) ? (
<ChartPreview
key="chartPreview"
treeNode={props.treeNode}
classes={{ icon: props.classes.iconButton }}
literal={literal}
/>
) : null
if (change.added) {
return [chartPreview, <Add key="add" className={classes.icon} />]
} else if (change.removed) {
return [<Remove key="remove" className={classes.icon} />]
} else {
return [
chartPreview,
<div
key="placeholder"
style={{ width: '12px', display: 'inline-block' }}
dangerouslySetInnerHTML={{ __html: '&nbsp;' }}
/>,
]
}
}
function Gutters(props: Props) {
let currentLine = -1
const gutters = props.changes
.map((change, key) => {
return trimNewlineRight(change.value)
.split('\n')
.map((_, idx) => {
currentLine = !change.removed ? currentLine + 1 : currentLine
return (
<div key={`${key}-${idx}`} style={lineChangeStyle(change)} className={props.classes.gutterLine}>
{tokensForLine(change, currentLine, props)}
</div>
)
})
})
.reduce((a, b) => a.concat(b), [])
return (
<span>
<pre className={props.className}>{gutters}</pre>
</span>
)
}
export default withStyles(style)(Gutters)
@@ -1,107 +0,0 @@
import * as diff from 'diff'
import * as Prism from 'prismjs'
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import DiffCount from './DiffCount'
import Gutters from './Gutters'
import { isPlottable, lineChangeStyle, trimNewlineRight } from './util'
import { JsonPropertyLocation, literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA'
import { style } from './style'
import { withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json'
interface Props {
treeNode: q.TreeNode<any>
previous: string
current: string
nameOfCompareMessage: string
language?: 'json'
classes: any
}
interface State {}
class CodeDiff extends React.Component<Props, State> {
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre ~ pre' })
constructor(props: Props) {
super(props)
this.state = {}
}
private isValidJson(str: string) {
try {
JSON.parse(str)
return true
} catch (error) {
return false
}
}
private plottableLiteralsIndexedWithLineNumbers() {
const allLiterals = this.isValidJson(this.props.current) ? literalsMappedByLines(this.props.current) || [] : []
return allLiterals.map((l: JsonPropertyLocation) => (isPlottable(l.value) ? l : undefined)) as Array<
JsonPropertyLocation
>
}
private renderStyledCodeLines(changes: Array<Diff.Change>) {
const styledLines = Prism.highlight(this.props.current, Prism.languages.json, 'json').split('\n')
let lineNumber = 0
return changes
.map((change, key) => {
const hasStyledCode = change.removed !== true
const changedLines = change.count || 0
if (hasStyledCode && this.props.language === 'json') {
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
const lines = currentLines.map((html: string, idx: number) => {
return (
<div key={`${key}-${idx}`} style={lineChangeStyle(change)} className={this.props.classes.line}>
<span dangerouslySetInnerHTML={{ __html: html }} />
</div>
)
})
lineNumber += changedLines
return [<div key={key}>{lines}</div>]
}
return trimNewlineRight(change.value)
.split('\n')
.map((line, idx) => {
return (
<div key={`${key}-${idx}`} style={lineChangeStyle(change)} className={this.props.classes.line}>
<span>{line}</span>
</div>
)
})
})
.reduce((a, b) => a.concat(b), [])
}
public render() {
const changes = diff.diffLines(this.props.previous, this.props.current)
const literalPositions = this.plottableLiteralsIndexedWithLineNumbers()
const code = this.renderStyledCodeLines(changes)
return (
<div>
<div tabIndex={0} onKeyDown={this.handleCtrlA} className={this.props.classes.codeWrapper}>
<Gutters
className={this.props.classes.gutters}
changes={changes}
treeNode={this.props.treeNode}
literalPositions={literalPositions}
/>
<pre className={this.props.classes.codeBlock}>{code}</pre>
</div>
<DiffCount changes={changes} nameOfCompareMessage={this.props.nameOfCompareMessage} />
</div>
)
}
}
export default withStyles(style)(CodeDiff)
@@ -1,63 +0,0 @@
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from '../CodeBlockColors'
import { Theme } from '@material-ui/core'
export const style = (theme: Theme) => {
const codeBlockColors = theme.palette.type === 'light' ? CodeBlockColors : CodeBlockColorsBraceMonokai
const codeBaseStyle = {
font: "12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace",
display: 'inline-grid' as 'inline-grid',
margin: '0',
padding: '1px 0 0 0',
}
return {
line: {
lineHeight: 'normal' as 'normal',
paddingLeft: '4px',
width: '100%',
height: '16px',
},
codeWrapper: {
display: 'flex',
maxHeight: '15em',
overflow: 'auto',
backgroundColor: `${codeBlockColors.background}`,
margin: '8px 0 0 0',
},
gutters: {
...codeBaseStyle,
width: '33px',
backgroundColor: codeBlockColors.gutters,
userSelect: 'none' as 'none',
},
codeBlock: {
...codeBaseStyle,
width: 'calc(100% - 33px)',
backgroundColor: 'inherit !important',
'& span': {
color: codeBlockColors.text,
},
'& .token.number': {
color: codeBlockColors.numeric,
},
'& .token.boolean': {
color: codeBlockColors.numeric,
},
'& .token.property': {
color: codeBlockColors.variable,
},
'& .token.string': {
color: codeBlockColors.string,
},
'& .token': {
color: codeBlockColors.text,
},
'& .token.operator': {
color: codeBlockColors.text,
},
'& .token.punctuation': {
color: codeBlockColors.text,
},
},
}
}
@@ -1,69 +0,0 @@
export function trimNewlineRight(str: string) {
if (str.slice(-1) === '\n') {
return str.slice(0, -1)
}
return str
}
const gutterBaseStyle = {}
const additionStyle = {
...gutterBaseStyle,
backgroundColor: 'rgba(10, 255, 10, 0.3)',
}
const deletionStyle = {
...gutterBaseStyle,
backgroundColor: 'rgba(255, 10, 10, 0.3)',
}
export function lineChangeStyle(change: Diff.Change) {
if (change.added === true) {
return additionStyle
}
if (change.removed === true) {
return deletionStyle
}
return gutterBaseStyle
}
export function toPlottableValue(value: any): number | undefined {
if (typeof value === 'number') {
return value
}
if (typeof value === 'boolean') {
return value ? 1 : 0
}
const isNumber = !isNaN(value)
const floatVal = parseFloat(value)
if (isNumber && !isNaN(floatVal)) {
return floatVal
}
const intVal = parseInt(value, undefined)
if (isNumber && !isNaN(intVal)) {
return intVal
}
// Convert "on" / "off" to int 1 / 0
if (typeof value === 'string') {
if (/^(on$)|(off$)/i.test(value)) {
return value.toLowerCase() === 'on' ? 1 : 0
}
if (/^[0-9]*,[0-9]+$/.test(value)) {
let parsedFloat = parseFloat(value.replace(',', '.'))
if (!isNaN(parsedFloat)) {
return parsedFloat
}
}
}
}
export function isPlottable(value: any) {
return !isNaN(toPlottableValue(value) as any)
}
+18 -15
View File
@@ -34,13 +34,11 @@ class HistoryDrawer extends React.Component<Props, State> {
this.setState({ collapsed: !this.state.collapsed })
}
private handleCtrlA = selectTextWithCtrlA({ targetSelector: 'pre' })
private handleCtrlA = selectTextWithCtrlA({targetSelector: 'pre'})
public renderHistory() {
const style = (element: HistoryItem) => ({
backgroundColor: element.selected
? this.props.theme.palette.action.selected
: this.props.theme.palette.action.hover,
backgroundColor: element.selected ? this.props.theme.palette.action.selected : this.props.theme.palette.action.hover,
margin: '8px',
padding: '8px 8px 0 8px',
cursor: this.props.onClick ? 'pointer' : 'inherit',
@@ -52,16 +50,11 @@ class HistoryDrawer extends React.Component<Props, State> {
key={element.key}
style={style(element)}
onClick={(event: React.MouseEvent) => this.props.onClick && this.props.onClick(index, event.target)}
tabIndex={0}
onKeyDown={this.handleCtrlA}
tabIndex={0} onKeyDown={this.handleCtrlA}
>
<div>
<i>{element.title}</i>
</div>
<div><i>{element.title}</i></div>
<div style={messageStyle}>
<span>
<pre>{element.value}</pre>
</span>
<span><pre>{element.value}</pre></span>
</div>
</div>
))
@@ -70,7 +63,11 @@ class HistoryDrawer extends React.Component<Props, State> {
return (
<div className={this.props.classes.main}>
<Typography component={'span'} onClick={this.toggle} style={{ cursor: 'pointer' }}>
<Typography
component={'span'}
onClick={this.toggle}
style={{ cursor: 'pointer' }}
>
<Badge
classes={{ badge: this.props.classes.badge }}
invisible={!visible}
@@ -81,7 +78,7 @@ class HistoryDrawer extends React.Component<Props, State> {
</Badge>
<div style={{ float: 'right' }}>{this.state.collapsed ? this.props.contentTypeIndicator : null}</div>
</Typography>
<div style={{ maxHeight: '230px', overflowY: 'scroll' }}>
<div style={{ maxHeight: '230px', overflowY: 'scroll' }}>
{this.state.collapsed ? null : this.props.children}
{this.state.collapsed ? null : elements}
</div>
@@ -90,7 +87,13 @@ class HistoryDrawer extends React.Component<Props, State> {
}
public render() {
return <div style={{ display: 'block', width: '100%' }}>{this.renderHistory()}</div>
return (
<div
style={{ display: 'block', width: '100%' }}
>
{this.renderHistory()}
</div>
)
}
}
-111
View File
@@ -1,111 +0,0 @@
import * as React from 'react'
import DateFormatter from '../helper/DateFormatter'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { PlotCurveTypes } from '../../reducers/Charts'
import { Theme, withTheme } from '@material-ui/core'
import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, Hint, XAxis, YAxis, HorizontalGridLines } = require('react-vis')
const abbreviate = require('number-abbreviate')
interface Props {
data: Array<{ x: number; y: number }>
theme: Theme
interpolation?: PlotCurveTypes
range?: [number?, number?]
color?: string
}
function mapCurveType(type: PlotCurveTypes | undefined) {
switch (type) {
case 'curve':
return 'curveMonotoneX'
case 'linear':
return 'curveLinear'
case 'cubic_basis_spline':
return 'curveBasis'
case 'step_after':
return 'curveStepAfter'
case 'step_before':
return 'curveStepBefore'
default:
return 'curveMonotoneX'
}
}
export default withTheme((props: Props) => {
const [width, setWidth] = React.useState(300)
const [tooltip, setTooltip] = React.useState({ value: undefined })
const detectResize = React.useCallback(newWidth => setWidth(newWidth), [])
const hintFormatter = React.useCallback((point: any) => {
return [
{ title: <b>Time</b>, value: <DateFormatter date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: point.y },
]
}, [])
const hideTooltip = React.useCallback(() => {
setTooltip({ value: undefined })
}, [])
const showTooltip = React.useCallback((value: any) => {
setTooltip({ value })
}, [])
return React.useMemo(() => {
const data = props.data
const calculatedDomain = domainForData(data)
let yDomain: [number, number] = props.range
? [props.range[0] || calculatedDomain[0], props.range[1] || calculatedDomain[1]]
: calculatedDomain
let color: string =
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
if (props.color) {
color = props.color
}
return (
<div style={{ height: '150px', overflow: 'hidden' }}>
<XYPlot width={width} height={180} yDomain={yDomain}>
<HorizontalGridLines />
<XAxis />
<YAxis width={45} tickFormat={(num: number) => abbreviate(num)} />
<LineMarkSeries
color={color}
onValueMouseOver={showTooltip}
onValueMouseOut={hideTooltip}
size={3}
data={data}
curve={mapCurveType(props.interpolation)}
/>
{tooltip.value ? <Hint format={hintFormatter} value={tooltip.value} /> : null}
</XYPlot>
<ReactResizeDetector handleWidth={true} onResize={detectResize} />
</div>
)
}, [width, props.data, tooltip, props.interpolation, props.range])
})
function domainForData(data: Array<{ x: number; y: number }>): [number, number] {
let max = data[0].y
let min = data[0].y
data.forEach(d => {
if (max < d.y) {
max = d.y
}
if (min > d.y) {
min = d.y
}
})
if ((max === 1 || max === 0) && (min === 1 || min === 0)) {
return [0, 1]
}
if (min === max) {
return [min - 0.5 * min, min + 0.5 * min]
}
return [min, max]
}
@@ -1,39 +0,0 @@
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 'react-ace'
function Editor(props: {
editorMode: string
theme: Theme
value: string | undefined
onChange: (value: string) => void
}) {
const editorOptions = {
showLineNumbers: false,
tabSize: 2,
}
return (
<AceEditor
style={{}}
mode={props.editorMode}
theme={props.theme.palette.type === 'dark' ? 'monokai' : 'dawn'}
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
showGutter={true}
value={props.value}
onChange={props.onChange}
setOptions={editorOptions}
editorProps={{ $blockScrolling: true }}
/>
)
}
export default withTheme(Editor)
@@ -1,40 +0,0 @@
import * as React from 'react'
import { FormControlLabel, Radio, RadioGroup } from '@material-ui/core'
interface Props {
value: string
onChange: (event: React.ChangeEvent<{}>, value: string) => void
}
export function EditorModeSelect(props: Props) {
const labelStyle = { margin: '0 8px 0 8px' }
return (
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={props.value}
onChange={props.onChange}
row={true}
>
<FormControlLabel
value="text"
style={labelStyle}
control={<Radio color="primary" />}
label="raw"
labelPlacement="top"
/>
<FormControlLabel
value="xml"
style={labelStyle}
control={<Radio color="primary" />}
label="xml"
labelPlacement="top"
/>
<FormControlLabel
value="json"
style={labelStyle}
control={<Radio color="primary" />}
label="json"
labelPlacement="top"
/>
</RadioGroup>
)
}
+145 -42
View File
@@ -8,23 +8,32 @@ import Navigation from '@material-ui/icons/Navigation'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { default as AceEditor } from 'react-ace'
import { globalActions, publishActions } from '../../../actions'
import { TopicViewModel } from '../../../model/TopicViewModel'
import 'brace/mode/json'
import 'brace/theme/dawn'
import 'brace/theme/monokai'
import 'brace/mode/xml'
import 'brace/mode/text'
import 'react-ace'
import {
Button,
FormControlLabel,
Radio,
RadioGroup,
TextField,
FormControl,
InputLabel,
Input,
Checkbox,
MenuItem,
Tooltip,
Fab,
Theme,
withTheme,
} from '@material-ui/core'
import { EditorModeSelect } from './EditorModeSelect'
import QosSelect from './QosSelect'
import Editor from './Editor'
const sha1 = require('sha1')
@@ -36,6 +45,7 @@ interface Props {
globalActions: typeof globalActions
retain: boolean
editorMode: string
qos: 0 | 1 | 2
theme: Theme
}
@@ -44,12 +54,17 @@ interface State {
}
class Publish extends React.Component<Props, State> {
private editorOptions = {
showLineNumbers: false,
tabSize: 2,
}
constructor(props: any) {
super(props)
this.state = { history: [] }
}
private updatePayload = (payload: string) => {
private updatePayload = (payload: string, event?: any) => {
this.props.actions.setPayload(payload)
}
@@ -94,18 +109,18 @@ class Publish extends React.Component<Props, State> {
return (
<div>
<FormControl style={{ width: '100%' }}>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
<Input
id="publish-topic"
value={topicStr}
startAdornment={<span />}
endAdornment={<ClearAdornment action={this.clearTopic} value={topicStr} />}
onBlur={this.onTopicBlur}
onChange={this.updateTopic}
multiline={true}
placeholder="example/topic"
/>
</FormControl>
<InputLabel htmlFor="publish-topic">Topic</InputLabel>
<Input
id="publish-topic"
value={topicStr}
startAdornment={<span/>}
endAdornment={<ClearAdornment action={this.clearTopic} value={topicStr} />}
onBlur={this.onTopicBlur}
onChange={this.updateTopic}
multiline={true}
placeholder="example/topic"
/>
</FormControl>
</div>
)
}
@@ -118,7 +133,13 @@ class Publish extends React.Component<Props, State> {
private publishButton() {
return (
<Button variant="contained" size="small" color="primary" onClick={this.publish} id="publish-button">
<Button
variant="contained"
size="small"
color="primary"
onClick={this.publish}
id="publish-button"
>
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>
)
@@ -142,11 +163,7 @@ class Publish extends React.Component<Props, State> {
return (
<Tooltip title="Format JSON">
<Fab
style={{ width: '36px', height: '36px', marginLeft: '8px' }}
onClick={this.formatJson}
id="sidebar-publish-format-json"
>
<Fab style={{ width: '36px', height: '36px', marginLeft: '8px' }} onClick={this.formatJson} id="sidebar-publish-format-json">
<FormatAlignLeft style={{ fontSize: '20px' }} />
</Fab>
</Tooltip>
@@ -157,31 +174,96 @@ class Publish extends React.Component<Props, State> {
return (
<div style={{ marginTop: '16px' }}>
<div style={{ width: '100%', lineHeight: '64px' }}>
<EditorModeSelect value={this.props.editorMode} onChange={this.updateMode} />
{this.renderEditorModeSelection()}
{this.renderFormatJson()}
<div style={{ float: 'right', marginRight: '16px' }}>{this.publishButton()}</div>
<div style={{ float: 'right', marginRight: '16px' }}>
{this.publishButton()}
</div>
</div>
</div>
)
}
private renderEditorModeSelection() {
const labelStyle = { margin: '0 8px 0 8px' }
return (
<RadioGroup
style={{ display: 'inline-block', float: 'left' }}
value={this.props.editorMode}
onChange={this.updateMode}
row={true}
>
<FormControlLabel
value="text"
style={labelStyle}
control={<Radio color="primary" />}
label="raw"
labelPlacement="top"
/>
<FormControlLabel
value="xml"
style={labelStyle}
control={<Radio color="primary" />}
label="xml"
labelPlacement="top"
/>
<FormControlLabel
value="json"
style={labelStyle}
control={<Radio color="primary" />}
label="json"
labelPlacement="top"
/>
</RadioGroup>
)
}
private onChangeQoS = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
if (value !== 0 && value !== 1 && value !== 2) {
return
}
this.props.actions.setQoS(value)
}
private publishMode() {
const labelStyle = { margin: '0 8px 0 8px' }
const itemStyle = { padding: '0' }
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
const qosSelect = (
<TextField
select={true}
value={this.props.qos}
margin="normal"
style={{ margin: '8px 0 8px 8px' }}
onChange={this.onChangeQoS}
>
<MenuItem key={0} value={0} style={itemStyle}>
<Tooltip title="At most once"><div style={tooltipStyle}>0</div></Tooltip>
</MenuItem>
<MenuItem key={1} value={1} style={itemStyle}>
<Tooltip title="At least once"><div style={tooltipStyle}>1</div></Tooltip>
</MenuItem>
<MenuItem key={2} value={2} style={itemStyle}>
<Tooltip title="Exactly once"><div style={tooltipStyle}>2</div></Tooltip>
</MenuItem>
</TextField>
)
return (
<div style={{ marginTop: '8px', clear: 'both' }}>
<div style={{ width: '100%', textAlign: 'right' }}>
<FormControlLabel style={labelStyle} control={<QosSelect />} label="QoS" labelPlacement="start" />
<Tooltip
title="Retained messages only appear to be retained, when client subscribes after the initial publish."
placement="top"
>
<FormControlLabel
style={labelStyle}
control={qosSelect}
label="QoS"
labelPlacement="start"
/>
<Tooltip title="Retained messages only appear to be retained, when client subscribes after the initial publish." placement="top">
<FormControlLabel
value="retain"
style={labelStyle}
control={
<Checkbox color="primary" checked={this.props.retain} onChange={this.props.actions.toggleRetain} />
}
control={<Checkbox color="primary" checked={this.props.retain} onChange={this.props.actions.toggleRetain} />}
label="retain"
labelPlacement="end"
/>
@@ -206,15 +288,38 @@ class Publish extends React.Component<Props, State> {
this.props.actions.setPayload(message.payload)
}
private editor() {
return (
<div style={{ width: '100%', display: 'block' }}>
{this.editorMode()}
{this.renderEditor()}
{this.publishMode()}
</div>
)
}
private renderEditor() {
return (
<AceEditor
mode={this.props.editorMode}
theme={this.props.theme.palette.type === 'dark' ? 'monokai' : 'dawn'}
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
showGutter={true}
value={this.props.payload}
onChange={this.updatePayload}
setOptions={this.editorOptions}
editorProps={{ $blockScrolling: true }}
/>
)
}
public render() {
return (
<div style={{ flexGrow: 1, marginLeft: '8px' }}>
{this.topic()}
<div style={{ width: '100%', display: 'block' }}>
{this.editorMode()}
<Editor value={this.props.payload} editorMode={this.props.editorMode} onChange={this.updatePayload} />
{this.publishMode()}
</div>
{this.editor()}
{this.history()}
</div>
)
@@ -234,10 +339,8 @@ const mapStateToProps = (state: AppState) => {
payload: state.publish.payload,
editorMode: state.publish.editorMode,
retain: state.publish.retain,
qos: state.publish.qos,
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTheme(Publish))
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(Publish))
@@ -1,75 +0,0 @@
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'
interface Props {
qos: 0 | 1 | 2
actions: {
publish: typeof publishActions
}
}
function QosSelect(props: Props) {
const tooltipStyle = { textAlign: 'center' as 'center', width: '100%' }
const itemStyle = { padding: '0' }
const onChangeQos = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
if (value !== 0 && value !== 1 && value !== 2) {
return
}
props.actions.publish.setQoS(value)
},
[props.actions.publish]
)
return (
<TextField
select={true}
value={props.qos}
margin="normal"
style={{ margin: '8px 0 8px 8px' }}
onChange={onChangeQos}
>
<MenuItem key={0} value={0} style={itemStyle}>
<Tooltip title="At most once">
<div style={tooltipStyle}>0</div>
</Tooltip>
</MenuItem>
<MenuItem key={1} value={1} style={itemStyle}>
<Tooltip title="At least once">
<div style={tooltipStyle}>1</div>
</Tooltip>
</MenuItem>
<MenuItem key={2} value={2} style={itemStyle}>
<Tooltip title="Exactly once">
<div style={tooltipStyle}>2</div>
</Tooltip>
</MenuItem>
</TextField>
)
}
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)
+32 -32
View File
@@ -8,13 +8,20 @@ import NodeStats from './NodeStats'
import Topic from './Topic'
import ValuePanel from './ValueRenderer/ValuePanel'
import { AppState } from '../../reducers'
import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../actions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import {
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
Typography,
Badge,
} from '@material-ui/core'
const throttle = require('lodash.throttle')
const Publish = React.lazy(() => import('./Publish/Publish'))
@@ -69,7 +76,7 @@ class Sidebar extends React.Component<Props, State> {
private renderRecursiveTopicDeleteButton() {
const deleteLimit = 50
const topicCount = this.props.node ? this.props.node.childTopicCount() : 0
if (!this.props.node || topicCount === 0 || (this.props.node.message && topicCount === 1)) {
if ((!this.props.node || topicCount === 0) || (this.props.node.message && topicCount === 1)) {
return null
}
@@ -79,10 +86,7 @@ class Sidebar extends React.Component<Props, State> {
badgeContent={<span style={{ whiteSpace: 'nowrap' }}>{topicCount >= deleteLimit ? '50+' : topicCount}</span>}
color="secondary"
>
<CustomIconButton
onClick={() => this.deleteTopic(this.props.node, true, deleteLimit)}
tooltip={`Deletes up to ${deleteLimit} sub-topics with a single click`}
>
<CustomIconButton onClick={() => this.deleteTopic(this.props.node, true, deleteLimit)} tooltip={`Deletes up to ${deleteLimit} sub-topics with a single click`}>
<Delete style={{ marginTop: '-3px' }} color="action" />
</CustomIconButton>
</Badge>
@@ -108,9 +112,7 @@ class Sidebar extends React.Component<Props, State> {
<div>
<ExpansionPanel key="topic" defaultExpanded={true} disabled={!Boolean(this.props.node)}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} style={summaryStyle}>
<Typography className={classes.heading}>
Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}
</Typography>
<Typography className={classes.heading}>Topic {copyTopic} {deleteTopic} {deleteRecursiveTopic}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails style={this.detailsStyle}>
<Topic node={this.props.node} didSelectNode={this.updateNode} />
@@ -184,26 +186,24 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
const styles = (theme: Theme) => ({
drawer: {
display: 'block' as 'block',
},
badge: {
top: '3px',
right: '3px',
},
valuePaper: {
margin: theme.spacing(1),
},
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
})
const styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
drawer: {
display: 'block',
height: '100%',
},
badge: {
top: '3px',
right: '3px',
},
valuePaper: {
margin: theme.spacing(1),
},
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Sidebar))
+20 -22
View File
@@ -1,7 +1,7 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import Button from '@material-ui/core/Button'
import { withStyles, Theme } from '@material-ui/core/styles'
import { withStyles, Theme, StyleRulesCallback } from '@material-ui/core/styles'
import { treeActions } from '../../actions'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
@@ -16,9 +16,9 @@ interface Props {
didSelectNode: (node: q.TreeNode<TopicViewModel>) => void
}
const styles = (theme: Theme) => ({
const styles: StyleRulesCallback<string> = (theme: Theme) => ({
button: {
textTransform: 'none' as 'none',
textTransform: 'none',
padding: '3px 5px 3px 5px',
minWidth: '30px',
},
@@ -32,29 +32,30 @@ class Topic extends React.Component<Props, {}> {
}
let key = 0
const breadCrumps = node
.branch()
const breadCrumps = node.branch()
.map(node => node.sourceEdge)
.filter(edge => Boolean(edge))
.map(edge => [
<Button
onClick={() => this.props.actions.selectTopic(edge!.target)}
size="small"
variant={theme.palette.type === 'light' ? 'contained' : undefined}
color={theme.palette.type === 'light' ? 'primary' : 'secondary'}
className={this.props.classes.button}
key={edge!.hash()}
>
{edge!.name || <span dangerouslySetInnerHTML={{ __html: '&nbsp;' }} />}
</Button>,
])
.map(edge =>
[(
<Button
onClick={() => this.props.actions.selectTopic(edge!.target)}
size="small"
variant={theme.palette.type === 'light' ? 'contained' : undefined}
color={theme.palette.type === 'light' ? 'primary' : 'secondary'}
className={this.props.classes.button}
key={edge!.hash()}
>
{edge!.name}
</Button>
)]
)
if (breadCrumps.length === 0) {
return null
}
const joinedBreadCrumps = breadCrumps.reduce((prev, current) =>
prev.concat([<span key={(key += 1)}> / </span>]).concat(current)
prev.concat([<span key={key += 1}> / </span>]).concat(current)
)
return <span style={{ lineHeight: '2.2em' }}>{joinedBreadCrumps}</span>
@@ -67,7 +68,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))
@@ -1,17 +1,13 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ShowChart from '@material-ui/icons/ShowChart'
import BarChart from '@material-ui/icons/BarChart'
import Copy from '../../helper/Copy'
import DateFormatter from '../../helper/DateFormatter'
import History from '../HistoryDrawer'
import TopicPlot from '../../TopicPlot'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { isPlottable } from '../CodeDiff/util'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { connect } from 'react-redux'
import CustomIconButton from '../../helper/CustomIconButton'
const PlotHistory = React.lazy(() => import('./PlotHistory'))
const throttle = require('lodash.throttle')
@@ -19,13 +15,10 @@ interface Props {
node?: q.TreeNode<TopicViewModel>
selected?: q.Message
onSelect: (message: q.Message) => void
actions: {
charts: typeof chartActions
}
}
interface State {
displayMessage?: q.Message
displayMessage?: q.Message,
anchorEl?: HTMLElement
}
@@ -39,18 +32,6 @@ class MessageHistory extends React.Component<Props, State> {
this.state = {}
}
private addNodeToCharts = (event: React.MouseEvent) => {
event.preventDefault()
event.stopPropagation()
const { node } = this.props
if (!node) {
return null
}
this.props.actions.charts.addChart({ topic: node.path() })
}
private displayMessage = (index: number, eventTarget: EventTarget) => {
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
if (message) {
@@ -84,61 +65,44 @@ class MessageHistory extends React.Component<Props, State> {
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: 'right' }}>
<Copy value={value} />
</div>
</span>
),
title: (<span>
<DateFormatter date={message.received} />
{previousMessage ? <i>(-<DateFormatter date={message.received} intervalSince={previousMessage.received} />)</i> : null}
<div style={{ float: 'right' }}><Copy value={value} /></div>
</span>),
selected: message && message === this.props.selected,
}
previousMessage = message
return element
})
const isMessagePlottable =
node.message && node.message.value && isPlottable(Base64Message.toUnicodeString(node.message.value))
const numericMessages = history
.map((message: q.Message) => {
const value = message.value ? parseFloat(Base64Message.toUnicodeString(message.value)) : NaN
return { x: message.received.getTime(), y: value }
}).filter(data => !isNaN(data.y))
const showPlot = numericMessages.length >= 2
return (
<div>
<History
items={historyElements}
contentTypeIndicator={
isMessagePlottable ? (
<CustomIconButton
style={{ height: '22px', width: '22px' }}
onClick={this.addNodeToCharts}
tooltip="Add to chart panel"
>
<ShowChart style={{ marginTop: '-5px' }} />
</CustomIconButton>
) : (
undefined
)
}
contentTypeIndicator={showPlot ? <BarChart /> : undefined}
onClick={this.displayMessage}
>
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null}
{showPlot ? this.renderPlot(numericMessages) : null}
</History>
</div>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: { charts: bindActionCreators(chartActions, dispatch) },
public renderPlot(data: Array<{x: number, y: number}>) {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<PlotHistory data={data} />
</React.Suspense>
)
}
}
export default connect(
null,
mapDispatchToProps
)(MessageHistory)
export default MessageHistory
@@ -0,0 +1,72 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import DateFormatter from '../../helper/DateFormatter'
import { default as ReactResizeDetector } from 'react-resize-detector'
import 'react-vis/dist/style.css'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
const { XYPlot, LineMarkSeries, Hint, YAxis, HorizontalGridLines } = require('react-vis')
const abbreviate = require('number-abbreviate')
interface Props {
data: Array<{x: number, y: number}>
}
interface Stats {
width: number
value?: any
}
class PlotHistory extends React.Component<Props, Stats> {
constructor(props: Props) {
super(props)
this.state = { width: 300 }
}
private resize = (width: number, height: number) => {
this.setState({ width: width - 12 })
}
private hintFormatter = (point: any) => {
return [
{ title: <b>Time</b>, value: <DateFormatter date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: point.y },
]
}
private _forgetValue = () => {
this.setState({
value: undefined,
})
}
private _rememberValue = (value: any) => {
this.setState({ value })
}
public render() {
const data = this.props.data
return (
<div>
<XYPlot width={this.state.width} height={150}>
<HorizontalGridLines />
<YAxis
width={45}
tickFormat={(num: number) => abbreviate(num)}
/>
<LineMarkSeries
onValueMouseOver={this._rememberValue}
onValueMouseOut={this._forgetValue}
size={3}
data={data}
curve={data.length < 50 ? 'curveCardinal' : undefined}
/>
{this.state.value ? <Hint format={this.hintFormatter} value={this.state.value} /> : null}
</XYPlot>
<ReactResizeDetector handleWidth={true} onResize={this.resize} />
</div>
)
}
}
export default PlotHistory
@@ -44,7 +44,7 @@ interface State {}
class ValuePanel extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {}
this.state = { }
}
private renderValue() {
@@ -53,7 +53,13 @@ class ValuePanel extends React.Component<Props, State> {
return null
}
return <ValueRenderer treeNode={node} message={node.message} compareWith={this.props.compareMessage} />
return (
<ValueRenderer
message={node.message}
messageHistory={node.messageHistory}
compareWith={this.props.compareMessage}
/>
)
}
private renderViewOptions() {
@@ -78,7 +84,9 @@ class ValuePanel extends React.Component<Props, State> {
return (
<div style={{ width: '100%', display: 'flex', paddingLeft: '8px' }}>
<span style={{ marginTop: '2px', flexGrow: 1 }}>{this.renderActionButtons()}</span>
<div style={{ flex: 6, textAlign: 'right' }}>{this.props.node.mqttMessage.retain ? retainedButton : null}</div>
<div style={{ flex: 6, textAlign: 'right' }}>
{this.props.node.mqttMessage.retain ? retainedButton : null}
</div>
{this.messageMetaInfo()}
</div>
)
@@ -92,11 +100,7 @@ class ValuePanel extends React.Component<Props, State> {
return (
<span style={{ width: '100%', paddingLeft: '8px', flex: 6 }}>
<Typography style={{ textAlign: 'right' }}>QoS: {this.props.node.mqttMessage.qos}</Typography>
<Typography style={{ textAlign: 'right' }}>
<i>
<DateFormatter date={this.props.node.message.received} />
</i>
</Typography>
<Typography style={{ textAlign: 'right' }}><i><DateFormatter date={this.props.node.message.received} /></i></Typography>
</span>
)
}
@@ -110,24 +114,15 @@ class ValuePanel extends React.Component<Props, State> {
}
return (
<ToggleButtonGroup
id="valueRendererDisplayMode"
value={this.props.valueRendererDisplayMode}
exclusive={true}
onChange={handleValue}
>
<ToggleButtonGroup id="valueRendererDisplayMode" value={this.props.valueRendererDisplayMode} exclusive={true} onChange={handleValue}>
<ToggleButton className={this.props.classes.toggleButton} value="diff" id="valueRendererDisplayMode-diff">
<Tooltip title="Show difference between the current and the last message">
<span>
<Code className={this.props.classes.toggleButtonIcon} />
</span>
<span><Code className={this.props.classes.toggleButtonIcon} /></span>
</Tooltip>
</ToggleButton>
<ToggleButton className={this.props.classes.toggleButton} value="raw" id="valueRendererDisplayMode-raw">
<Tooltip title="Raw value">
<span>
<Reorder className={this.props.classes.toggleButtonIcon} />
</span>
<span><Reorder className={this.props.classes.toggleButtonIcon} /></span>
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
@@ -153,10 +148,7 @@ class ValuePanel extends React.Component<Props, State> {
const { node, classes } = this.props
const { detailsStyle, summaryStyle } = this.panelStyle()
const copyValue =
node && node.message && node.message.value ? (
<Copy value={Base64Message.toUnicodeString(node.message.value)} />
) : null
const copyValue = (node && node.message && node.message.value) ? <Copy value={Base64Message.toUnicodeString(node.message.value)} /> : null
return (
<ExpansionPanel key="value" defaultExpanded={true}>
@@ -166,15 +158,11 @@ class ValuePanel extends React.Component<Props, State> {
<ExpansionPanelDetails style={detailsStyle}>
{this.renderViewOptions()}
<div>
<React.Suspense fallback={<div>Loading...</div>}>{this.renderValue()}</React.Suspense>
</div>
<div>
<MessageHistory
onSelect={this.handleMessageHistorySelect}
selected={this.props.compareMessage}
node={this.props.node}
/>
<React.Suspense fallback={<div>Loading...</div>}>
{this.renderValue()}
</React.Suspense>
</div>
<div><MessageHistory onSelect={this.handleMessageHistorySelect} selected={this.props.compareMessage} node={this.props.node} /></div>
</ExpansionPanelDetails>
</ExpansionPanel>
)
@@ -196,20 +184,19 @@ const mapStateToProps = (state: AppState) => {
}
}
const styles = (theme: Theme) => ({
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
toggleButton: {
height: '36px',
},
toggleButtonIcon: {
verticalAlign: 'middle',
},
})
const styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
heading: {
fontSize: theme.typography.pxToRem(15),
fontWeight: theme.typography.fontWeightRegular,
},
toggleButton: {
height: '36px',
},
toggleButtonIcon: {
verticalAlign: 'middle',
},
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(ValuePanel))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
@@ -9,7 +9,7 @@ import { ValueRendererDisplayMode } from '../../../reducers/Settings'
interface Props {
message: q.Message
treeNode: q.TreeNode<any>
messageHistory: q.RingBuffer<q.Message>
compareWith?: q.Message
renderMode: ValueRendererDisplayMode
}
@@ -27,7 +27,6 @@ class ValueRenderer extends React.Component<Props, State> {
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
return (
<CodeDiff
treeNode={this.props.treeNode}
previous={previous}
current={current}
language={language}
@@ -65,8 +64,9 @@ class ValueRenderer extends React.Component<Props, State> {
}
public renderValue() {
const { message, treeNode, compareWith, renderMode } = this.props
const previousMessages = treeNode.messageHistory.toArray()
const { message, messageHistory, compareWith, renderMode } = this.props
const previousMessages = messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {
-48
View File
@@ -1,48 +0,0 @@
import * as dotProp from 'dot-prop'
import * as q from '../../../backend/src/Model'
import * as React from 'react'
import PlotHistory from './Sidebar/PlotHistory'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './Sidebar/CodeDiff/util'
import { PlotCurveTypes } from '../reducers/Charts'
interface Props {
history: q.MessageHistory
dotPath?: string
interpolation?: PlotCurveTypes
range?: [number?, number?]
color?: string
}
function nodeToHistory(history: q.MessageHistory) {
return history
.toArray()
.map((message: q.Message) => {
const value = message.value ? toPlottableValue(Base64Message.toUnicodeString(message.value)) : NaN
return { x: message.received.getTime(), y: toPlottableValue(value) }
})
.filter(data => !isNaN(data.y as any)) as any
}
function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
return history
.toArray()
.map((message: q.Message) => {
let json = {}
try {
json = message.value ? JSON.parse(Base64Message.toUnicodeString(message.value)) : {}
} catch (ignore) {}
let value = dotProp.get(json, dotPath)
return { x: message.received.getTime(), y: toPlottableValue(value) }
})
.filter(data => !isNaN(data.y as any)) as any
}
function render(props: Props) {
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
return <PlotHistory color={props.color} range={props.range} interpolation={props.interpolation} data={data} />
}
export default render
+12 -25
View File
@@ -77,24 +77,18 @@ class TreeComponent extends React.PureComponent<Props, State> {
const timeUntilNextUpdate = updateInterval - (performance.now() - this.renderTime)
this.updateTimer = setTimeout(() => {
window.requestIdleCallback(
() => {
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.renderTime = performance.now()
window.requestIdleCallback(() => {
this.updateTimer && clearTimeout(this.updateTimer)
this.updateTimer = undefined
this.renderTime = performance.now()
if (!this.props.paused) {
this.props.tree && this.props.tree.applyUnmergedChanges()
}
window.requestIdleCallback(
() => {
this.setState({ lastUpdate: this.renderTime })
},
{ timeout: 100 }
)
},
{ timeout: 500 }
)
if (!this.props.paused) {
this.props.tree && this.props.tree.applyUnmergedChanges()
}
window.requestIdleCallback(() => {
this.setState({ lastUpdate: this.renderTime })
}, { timeout: 100 })
}, { timeout: 500 })
}, Math.max(0, timeUntilNextUpdate))
}
@@ -107,10 +101,6 @@ class TreeComponent extends React.PureComponent<Props, State> {
const style: React.CSSProperties = {
lineHeight: '1.1',
cursor: 'default',
overflowY: 'scroll',
overflowX: 'hidden',
height: '100%',
paddingBottom: '16px', // avoid conflict with chart panel Resizer
}
return (
@@ -147,7 +137,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TreeComponent)
export default connect(mapStateToProps, mapDispatchToProps)(TreeComponent)
+14 -30
View File
@@ -97,8 +97,8 @@ class TreeNodeComponent extends React.Component<Props, State> {
treeNode.viewModel.change.subscribe(this.viewStateHasChanged)
}
private viewStateHasChanged = () => {
this.props.treeNode.viewModel && this.setState({ selected: this.props.treeNode.viewModel.isSelected() })
private viewStateHasChanged = (msg: void) => {
this.setState({ selected: this.props.treeNode.viewModel!.isSelected() })
}
private removeSubscriber(treeNode: q.TreeNode<TopicViewModel>) {
@@ -107,11 +107,9 @@ class TreeNodeComponent extends React.Component<Props, State> {
}
private stateHasChanged(newState: State) {
return (
this.state.collapsedOverride !== newState.collapsedOverride ||
this.state.mouseOver !== newState.mouseOver ||
this.state.selected !== newState.selected
)
return this.state.collapsedOverride !== newState.collapsedOverride
|| this.state.mouseOver !== newState.mouseOver
|| this.state.selected !== newState.selected
}
private toggle() {
@@ -139,12 +137,7 @@ class TreeNodeComponent extends React.Component<Props, State> {
private mouseOver = (event: React.MouseEvent) => {
event.stopPropagation()
this.setHover(true)
if (
this.props.settings.get('selectTopicWithMouseOver') &&
this.props.treeNode &&
this.props.treeNode.message &&
this.props.treeNode.message.value
) {
if (this.props.settings.get('selectTopicWithMouseOver') && this.props.treeNode && this.props.treeNode.message && this.props.treeNode.message.value) {
this.props.didSelectTopic(this.props.treeNode)
}
}
@@ -188,13 +181,11 @@ class TreeNodeComponent extends React.Component<Props, State> {
public shouldComponentUpdate(nextProps: Props, nextState: State) {
const shouldRenderToRemoveCssAnimation = this.cssAnimationWasSetAt !== undefined
return (
this.stateHasChanged(nextState) ||
this.props.settings !== nextProps.settings ||
this.props.lastUpdate !== nextProps.lastUpdate ||
this.animationDirty ||
shouldRenderToRemoveCssAnimation
)
return this.stateHasChanged(nextState)
|| this.props.settings !== nextProps.settings
|| (this.props.lastUpdate !== nextProps.lastUpdate)
|| this.animationDirty
|| shouldRenderToRemoveCssAnimation
}
public componentDidUpdate() {
@@ -213,19 +204,12 @@ class TreeNodeComponent extends React.Component<Props, State> {
public render() {
const { classes } = this.props
const shouldStartAnimation =
!this.animationDirty && !this.props.isRoot && this.props.settings.get('highlightTopicUpdates')
const shouldStartAnimation = (!this.animationDirty) && !this.props.isRoot && this.props.settings.get('highlightTopicUpdates')
const animationName = this.props.theme.palette.type === 'light' ? 'updateLight' : 'updateDark'
const animation = shouldStartAnimation
? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` }
: {}
const animation = shouldStartAnimation ? { willChange: 'auto', translateZ: 0, animation: `${animationName} 0.5s` } : {}
this.animationDirty = shouldStartAnimation
const highlightClass = this.state.selected
? this.props.classes.selected
: this.state.mouseOver
? this.props.classes.hover
: ''
const highlightClass = this.state.selected ? this.props.classes.selected : (this.state.mouseOver ? this.props.classes.hover : '')
return (
<div>
+10 -9
View File
@@ -49,16 +49,13 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
}
private renderMore() {
this.renderMoreAnimationFrame = (window as any).requestIdleCallback(
() => {
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
},
{ timeout: 500 }
)
this.renderMoreAnimationFrame = (window as any).requestIdleCallback(() => {
this.setState({ ...this.state, alreadyAdded: this.state.alreadyAdded * 1.5 })
}, { timeout: 500 })
}
public componentWillUnmount() {
;(window as any).cancelIdleCallback(this.renderMoreAnimationFrame)
(window as any).cancelIdleCallback(this.renderMoreAnimationFrame)
}
public render() {
@@ -72,7 +69,7 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
}
const nodes = this.sortedNodes().slice(0, this.state.alreadyAdded)
const listItems = nodes.map(node => {
const listItems = nodes.map((node) => {
return (
<TreeNode
key={`${node.hash()}-${this.props.filter}`}
@@ -85,7 +82,11 @@ class TreeNodeSubnodes extends React.Component<Props, State> {
)
})
return <span className={this.props.classes.list}>{listItems}</span>
return (
<span className={this.props.classes.list}>
{listItems}
</span>
)
}
}
+13 -25
View File
@@ -14,25 +14,17 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
}
class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
private renderSourceEdge() {
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
return (
<span key="edge" className={this.props.classes.sourceEdge}>
{name}
</span>
)
return <span key="edge" className={this.props.classes.sourceEdge}>{name}</span>
}
private renderValue() {
return this.props.treeNode.message &&
this.props.treeNode.message.value &&
this.props.treeNode.message.length > 0 ? (
<span key="value" className={this.props.classes.value}>
{' '}
= {Base64Message.toUnicodeString(this.props.treeNode.message.value).slice(0, 120)}
</span>
) : null
return this.props.treeNode.message && this.props.treeNode.message.value && this.props.treeNode.message.length > 0
? <span key="value" className={this.props.classes.value}> = {Base64Message.toUnicodeString(this.props.treeNode.message.value).slice(0, 120)}</span>
: null
}
private renderExpander() {
@@ -40,11 +32,7 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
return null
}
return (
<span key="expander" className={this.props.classes.expander} onClick={this.props.toggleCollapsed}>
{this.props.collapsed ? '▶' : '▼'}
</span>
)
return <span key="expander" className={this.props.classes.expander} onClick={this.props.toggleCollapsed}>{this.props.collapsed ? '▶' : '▼'}</span>
}
private renderMetadata() {
@@ -53,16 +41,16 @@ class TreeNodeTitle extends React.Component<TreeNodeProps, {}> {
}
const messages = this.props.treeNode.leafMessageCount()
return (
<span
key="metadata"
className={this.props.classes.collapsedSubnodes}
>{`(${this.props.treeNode.childTopicCount()} topics, ${messages} messages)`}</span>
)
return <span key="metadata" className={this.props.classes.collapsedSubnodes}>{`(${this.props.treeNode.childTopicCount()} topics, ${messages} messages)`}</span>
}
public render() {
return [this.renderExpander(), this.renderSourceEdge(), this.renderMetadata(), this.renderValue()]
return ([
this.renderExpander(),
this.renderSourceEdge(),
this.renderMetadata(),
this.renderValue(),
])
}
}
+50 -38
View File
@@ -12,7 +12,15 @@ import { green } from '@material-ui/core/colors'
import { Theme, withStyles } from '@material-ui/core/styles'
import { updateNotifierActions } from '../actions'
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@material-ui/core'
import {
Button,
IconButton,
Modal,
Paper,
Snackbar,
SnackbarContent,
Typography,
} from '@material-ui/core'
interface Props {
showUpdateNotification: boolean
@@ -22,7 +30,7 @@ interface Props {
}
interface GithubRelease {
url: string
url: string,
assets?: Array<GithubAsset>
published_at: string // "2019-01-25T20:14:39Z"
body_html: string
@@ -51,7 +59,7 @@ class UpdateNotifier extends React.Component<Props, State> {
this.state = { newerVersions: [] }
const ownVersion = electron.remote.app.getVersion()
this.fetchReleases().then(releases => {
this.fetchReleases().then((releases) => {
const newerVersions = releases
.filter(release => this.allowPrereleaseIfOwnVersionIsBeta(release, ownVersion))
.filter(release => compareVersions(release.tag_name, ownVersion) > 0)
@@ -123,19 +131,21 @@ class UpdateNotifier extends React.Component<Props, State> {
}
private notificationActions() {
return [
<Button key="undo" size="small" onClick={this.showDetails}>
Details
</Button>,
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={this.props.classes.close}
onClick={this.closeNotification}
>
<Close />
</IconButton>,
return [(
<Button key="undo" size="small" onClick={this.showDetails}>
Details
</Button>
), (
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={this.props.classes.close}
onClick={this.closeNotification}
>
<Close />
</IconButton>
),
]
}
@@ -149,20 +159,23 @@ class UpdateNotifier extends React.Component<Props, State> {
.join('<hr />')
return (
<Modal open={this.props.showUpdateDetails} disableAutoFocus={true} onClose={this.hideDetails}>
<Modal
open={this.props.showUpdateDetails}
disableAutoFocus={true}
onClose={this.hideDetails}
>
<Paper className={this.props.classes.root}>
<Typography variant="h6" className={this.props.classes.title}>
Version {latestUpdate.tag_name}
</Typography>
<Typography variant="h6" className={this.props.classes.title}>Version {latestUpdate.tag_name}</Typography>
<Typography className={this.props.classes.title}>Changelog</Typography>
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: releaseNotes }} />
{this.renderDownloads()}
<Button className={this.props.classes.download} onClick={this.openHomePage}>
<Button
className={this.props.classes.download}
onClick={this.openHomePage}
>
Github Page
</Button>
<Button className={this.props.classes.closeButton} color="secondary" onClick={this.hideDetails}>
Close
</Button>
<Button className={this.props.classes.closeButton} color="secondary" onClick={this.hideDetails}>Close</Button>
</Paper>
</Modal>
)
@@ -195,14 +208,18 @@ class UpdateNotifier extends React.Component<Props, State> {
return null
}
return latestUpdate.assets.filter(this.assetForCurrentPlatform).map(asset => (
<div>
<Button className={this.props.classes.download} onClick={() => this.openUrl(asset.browser_download_url)}>
<CloudDownload />
&nbsp;{asset.name}
</Button>
</div>
))
return latestUpdate.assets
.filter(this.assetForCurrentPlatform)
.map(asset => (
<div>
<Button
className={this.props.classes.download}
onClick={() => this.openUrl(asset.browser_download_url)}
>
<CloudDownload />&nbsp;{asset.name}
</Button>
</div>
))
}
public render() {
@@ -266,9 +283,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default withStyles(styles)(
connect(
mapStateToProps,
mapDispatchToProps
)(UpdateNotifier)
)
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(UpdateNotifier))
@@ -4,10 +4,10 @@ import { AppState } from '../../reducers'
import { connect } from 'react-redux'
import { ConnectionHealth } from '../../reducers/Connection'
import { green, orange, red } from '@material-ui/core/colors'
import { withStyles, Theme } from '@material-ui/core/styles'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { Tooltip } from '@material-ui/core'
const styles = (theme: Theme) => ({
const styles: StyleRulesCallback = theme => ({
offline: {
color: red[700],
},
@@ -18,10 +18,7 @@ const styles = (theme: Theme) => ({
color: orange[600],
},
icon: {
boxShadow: theme.shadows[2]
.split('),')
.map(s => `inset ${s}`)
.join('),'),
boxShadow: theme.shadows[2].split('),').map(s => `inset ${s}`).join('),'),
padding: '6px',
borderRadius: '50%',
backgroundColor: '#eee',
+4 -9
View File
@@ -37,11 +37,9 @@ class Copy extends React.Component<Props, State> {
}
public render() {
const icon = !this.state.didCopy ? (
<FileCopy fontSize="inherit" />
) : (
<Check fontSize="inherit" style={{ cursor: 'default' }} />
)
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" />
: <Check fontSize="inherit" style={{ cursor: 'default' }} />
return (
<span>
@@ -63,7 +61,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(
undefined,
mapDispatchToProps
)(Copy)
export default connect(undefined, mapDispatchToProps)(Copy)
@@ -6,7 +6,6 @@ interface Props {
onClick: any
tooltip: string
classes: any
style?: React.CSSProperties
}
const styles = (theme: Theme) => ({
@@ -16,9 +15,6 @@ const styles = (theme: Theme) => ({
width: '32px',
height: '32px',
},
label: {
marginTop: '-2px',
},
})
class CustomIconButton extends React.Component<Props, {}> {
@@ -33,8 +29,8 @@ class CustomIconButton extends React.Component<Props, {}> {
public render() {
return (
<IconButton className={this.props.classes.button} style={this.props.style} onClick={this.onClick}>
<Tooltip title={this.props.tooltip} className={this.props.classes.label}>
<IconButton className={this.props.classes.button} onClick={this.onClick}>
<Tooltip title={this.props.tooltip}>
<span>{this.props.children}</span>
</Tooltip>
</IconButton>
+2 -4
View File
@@ -20,7 +20,7 @@ class DateFormatter extends React.Component<Props, {}> {
private intervalSince(intervalSince: Date) {
const interval = intervalSince.getTime() - this.props.date.getTime()
const unit = this.unitForInterval(interval)
return `${Math.round(moment.duration(interval).as(unit) * 100) / 100} ${unitMapping[unit]}`
return `${moment.duration(interval).as(unit)} ${unitMapping[unit]}`
}
private legacyDate() {
@@ -28,9 +28,7 @@ class DateFormatter extends React.Component<Props, {}> {
}
private localizedDate(locale: string) {
return moment(this.props.date)
.locale(locale)
.format('L LTS')
return moment(this.props.date).locale(locale).format('L LTS')
}
private unitForInterval(milliseconds: number) {
@@ -6,7 +6,7 @@ export function isElementInViewport(el: any) {
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) /*or $(window).height() */ &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
)
}
+10 -2
View File
@@ -12,9 +12,17 @@ import './utils/tracking'
import { themes } from './theme'
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(reducers, composeEnhancers(applyMiddleware(reduxThunk, batchDispatchMiddleware)))
const store = createStore(
reducers,
composeEnhancers(
applyMiddleware(
reduxThunk,
batchDispatchMiddleware
)
)
)
function ApplicationRenderer(props: { theme: 'light' | 'dark' }) {
function ApplicationRenderer(props: {theme: 'light' | 'dark'}) {
return (
<ThemeProvider theme={props.theme === 'light' ? themes.lightTheme : themes.darkTheme}>
<App />
+4 -5
View File
@@ -65,12 +65,11 @@ export function createEmptyConnection(): ConnectionOptions {
export function makeDefaultConnections() {
return {
// remember: there was also iot.eclipse.org once
'mqtt.eclipse.org': {
'iot.eclipse.org': {
...createEmptyConnection(),
id: 'mqtt.eclipse.org',
name: 'mqtt.eclipse.org',
host: 'mqtt.eclipse.org',
id: 'iot.eclipse.org',
name: 'iot.eclipse.org',
host: 'iot.eclipse.org',
},
'test.mosquitto.org': {
...createEmptyConnection(),
+2 -2
View File
@@ -17,7 +17,7 @@ export function clearLegacyConnectionOptions() {
window.localStorage.setItem('connectionSettings', '')
}
export function loadLegacyConnectionOptions(): { [s: string]: ConnectionOptions } | {} {
export function loadLegacyConnectionOptions(): {[s: string]: ConnectionOptions} | {} {
const legacySettingsString = window.localStorage.getItem('connectionSettings')
if (!legacySettingsString) {
return {}
@@ -30,7 +30,7 @@ export function loadLegacyConnectionOptions(): { [s: string]: ConnectionOptions
return {}
}
const protocolMap: { [s: string]: string } = {
const protocolMap: {[s: string]: string} = {
'tcp://': 'mqtt',
'ws://': 'ws',
'mqtt://': 'mqtt',
+1 -6
View File
@@ -1,7 +1,6 @@
import { Destroyable } from '../../../backend/src/Model/Destroyable'
import { EventDispatcher } from '../../../events'
export class TopicViewModel implements Destroyable {
export class TopicViewModel {
private selected: boolean
public change = new EventDispatcher<void, TopicViewModel>()
@@ -9,10 +8,6 @@ export class TopicViewModel implements Destroyable {
this.selected = false
}
public destroy() {
this.change.removeAllListeners()
}
public isSelected() {
return this.selected
}
-109
View File
@@ -1,109 +0,0 @@
import { Action } from 'redux'
import { createReducer } from './lib'
import { Record, List } from 'immutable'
import MoveUp from '../components/ChartPanel/ChartSettings/MoveUp'
export type PlotCurveTypes = 'curve' | 'linear' | 'cubic_basis_spline' | 'step_after' | 'step_before'
export interface ChartParameters {
topic: string
dotPath?: string
interpolation?: PlotCurveTypes
range?: {
from?: number
to?: number
}
width?: 'big' | 'medium' | 'small'
color?: string
}
export interface ChartsStateModel {
charts: List<ChartParameters>
}
export type ChartsState = Record<ChartsStateModel>
export type Action = AddChart | RemoveChart | SetCharts | UpdateChart | MoveUp
export enum ActionTypes {
CHARTS_ADD = 'CHARTS_ADD',
CHARTS_REMOVE = 'CHARTS_REMOVE',
CHARTS_SET = 'CHARTS_SET',
CHARTS_UPDATE = 'CHARTS_UPDATE',
CHARTS_MOVE_UP = 'CHARTS_MOVE_UP',
}
export interface AddChart {
type: ActionTypes.CHARTS_ADD
chart: ChartParameters
}
export interface MoveUp {
type: ActionTypes.CHARTS_ADD
topic: string
dotPath?: string
}
export interface UpdateChart {
type: ActionTypes.CHARTS_ADD
topic: string
dotPath?: string
parameters: Partial<ChartParameters>
}
export interface RemoveChart {
type: ActionTypes.CHARTS_REMOVE
chart: ChartParameters
}
export interface SetCharts {
type: ActionTypes.CHARTS_SET
charts: Array<ChartParameters>
}
const initialState = Record<ChartsStateModel>({
charts: List<ChartParameters>(),
})
export const chartsReducer = createReducer(initialState(), {
CHARTS_ADD: addChart,
CHARTS_REMOVE: removeChart,
CHARTS_SET: setCharts,
CHARTS_UPDATE: updateChart,
CHARTS_MOVE_UP: moveUp,
})
function addChart(state: ChartsState, action: AddChart) {
return state.set('charts', state.get('charts').push(action.chart))
}
function moveUp(state: ChartsState, action: MoveUp) {
const charts = state.get('charts')
const idx = charts.findIndex(chart => chart.topic === action.topic && chart.dotPath === action.dotPath)
const item = charts.get(idx)
const previousItem = charts.get(idx - 1)
if (idx === 0 || !item || !previousItem) {
return
}
const newlyOrderedCharts = charts.set(idx - 1, item).set(idx, previousItem)
return state.set('charts', newlyOrderedCharts)
}
function updateChart(state: ChartsState, action: UpdateChart) {
const charts = state.get('charts')
const chartIdx = charts.findIndex(chart => chart.topic === action.topic && chart.dotPath === action.dotPath)
const chart = charts.get(chartIdx)
return state.set('charts', chart ? charts.set(chartIdx, { ...chart, ...action.parameters }) : charts)
}
function removeChart(state: ChartsState, action: RemoveChart) {
const charts = state.get('charts')
const newCharts = charts.filter(chart => chart.topic !== action.chart.topic || chart.dotPath !== action.chart.dotPath)
return state.set('charts', newCharts)
}
function setCharts(state: ChartsState, action: SetCharts) {
return state.set('charts', List(action.charts))
}
+1 -1
View File
@@ -27,7 +27,7 @@ export enum ActionTypes {
}
export interface SetConnecting {
type: ActionTypes.CONNECTION_SET_CONNECTING
type: ActionTypes.CONNECTION_SET_CONNECTING,
connectionId: string
}
+3 -11
View File
@@ -3,7 +3,7 @@ import { ConnectionOptions } from '../model/ConnectionOptions'
import { createReducer } from './lib'
export interface ConnectionManagerState {
connections: { [s: string]: ConnectionOptions }
connections: {[s: string]: ConnectionOptions},
selected?: string
showAdvancedSettings: boolean
}
@@ -14,15 +14,7 @@ const initialState: ConnectionManagerState = {
showAdvancedSettings: false,
}
export type Action =
| SetConnections
| SelectConnection
| UpdateConnection
| AddConnection
| DeleteConnection
| ToggleAdvancedSettings
| DeleteSubscription
| AddSubscription
export type Action = SetConnections | SelectConnection | UpdateConnection | AddConnection | DeleteConnection | ToggleAdvancedSettings | DeleteSubscription | AddSubscription
export enum ActionTypes {
CONNECTION_MANAGER_SET_CONNECTIONS = 'CONNECTION_MANAGER_SET_CONNECTIONS',
@@ -37,7 +29,7 @@ export enum ActionTypes {
export interface SetConnections {
type: ActionTypes.CONNECTION_MANAGER_SET_CONNECTIONS
connections: { [s: string]: ConnectionOptions }
connections: {[s: string]: ConnectionOptions}
}
export interface SelectConnection {
+19 -22
View File
@@ -12,7 +12,7 @@ export enum ActionTypes {
}
export interface GlobalAction extends Action {
type: ActionTypes
type: ActionTypes,
showUpdateNotification?: boolean
showUpdateDetails?: boolean
error?: string
@@ -39,36 +39,33 @@ const initialStateFactory = Record<GlobalStateInterface>({
settingsVisible: false,
})
export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> = (
state = initialStateFactory(),
action
): GlobalState => {
export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> = (state = initialStateFactory(), action): GlobalState => {
trackEvent(action.type)
console.log(action.type)
switch (action.type) {
case ActionTypes.showUpdateNotification:
return state.set('showUpdateNotification', action.showUpdateNotification)
case ActionTypes.showUpdateNotification:
return state.set('showUpdateNotification', action.showUpdateNotification)
case ActionTypes.toggleSettingsVisibility:
return state.set('settingsVisible', !state.get('settingsVisible'))
case ActionTypes.toggleSettingsVisibility:
return state.set('settingsVisible', !state.get('settingsVisible'))
case ActionTypes.showError:
return state.set('error', action.error)
case ActionTypes.showError:
return state.set('error', action.error)
case ActionTypes.showNotification:
return state.set('notification', action.notification)
case ActionTypes.showNotification:
return state.set('notification', action.notification)
case ActionTypes.didLaunch:
return state.set('launching', false)
case ActionTypes.didLaunch:
return state.set('launching', false)
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
}
return state.set('showUpdateDetails', action.showUpdateDetails)
default:
case ActionTypes.showUpdateDetails:
if (action.showUpdateDetails === undefined) {
return state
}
return state.set('showUpdateDetails', action.showUpdateDetails)
default:
return state
}
}
+11 -12
View File
@@ -1,3 +1,4 @@
import * as moment from 'moment'
import { createReducer } from './lib'
import { Record } from 'immutable'
@@ -21,15 +22,15 @@ export interface SettingsState {
theme: 'light' | 'dark'
}
export type Actions = SetAutoExpandLimitAction &
DidLoadSettingsAction &
SetTopicOrderAction &
FilterTopicsAction &
ToggleHighlightTopicUpdatesAction &
SetValueRendererDisplayModeAction &
SetTheme &
SetSelectTopicWithMouseOverAction &
SetTimeLocale
export type Actions = SetAutoExpandLimitAction
& DidLoadSettingsAction
& SetTopicOrderAction
& FilterTopicsAction
& ToggleHighlightTopicUpdatesAction
& SetValueRendererDisplayModeAction
& SetTheme
& SetSelectTopicWithMouseOverAction
& SetTimeLocale
export enum ActionTypes {
SETTINGS_SET_AUTO_EXPAND_LIMIT = 'SETTINGS_SET_AUTO_EXPAND_LIMIT',
@@ -59,9 +60,7 @@ const setTheme = (theme: 'light' | 'dark') => (state: Record<SettingsState>) =>
return state.set('theme', theme)
}
const reducerActions: {
[s: string]: (state: Record<SettingsState>, action: Actions) => Record<SettingsState>
} = {
const reducerActions: {[s: string]: (state: Record<SettingsState>, action: Actions) => Record<SettingsState>} = {
SETTINGS_SET_AUTO_EXPAND_LIMIT: setAutoExpandLimit,
SETTINGS_SET_TOPIC_ORDER: setTopicOrder,
SETTINGS_FILTER_TOPICS: filterTopics,
+1 -3
View File
@@ -19,9 +19,7 @@ export enum ActionTypes {
export type SidebarState = Record<SidebarModel>
const actions: {
[s: string]: (state: SidebarState, action: Action) => SidebarState
} = {
const actions: {[s: string]: (state: SidebarState, action: Action) => SidebarState} = {
SIDEBAR_SET_COMPARE_MESSAGE: setCompareMessage,
SIDEBAR_RESET_STORE: resetStore,
}
+1 -3
View File
@@ -49,9 +49,7 @@ const setPaused = (pause: boolean) => (state: TreeState, action: ShowTree): Tree
return state.set('paused', pause)
}
const actions: {
[s: string]: (state: TreeState, action: Action) => TreeState
} = {
const actions: {[s: string]: (state: TreeState, action: Action) => TreeState} = {
TREE_SHOW_TREE: showTree,
TREE_SELECT_TOPIC: selectTopic,
TREE_PAUSE_UPDATES: setPaused(true),
+2 -5
View File
@@ -1,4 +1,3 @@
import { chartsReducer, ChartsState } from './Charts'
import { combineReducers } from 'redux'
import { connectionManagerReducer, ConnectionManagerState } from './ConnectionManager'
import { connectionReducer, ConnectionState } from './Connection'
@@ -12,17 +11,15 @@ import { treeReducer, TreeState } from './Tree'
export interface AppState {
globalState: GlobalState
tree: TreeState
settings: Record<SettingsState>
settings: Record<SettingsState>,
publish: PublishState
charts: ChartsState
sidebar: SidebarState
sidebar: SidebarState,
connection: ConnectionState
connectionManager: ConnectionManagerState
}
export default combineReducers({
globalState,
charts: chartsReducer,
publish: publishReducer,
sidebar: sidebarReducer,
connection: connectionReducer,
+1 -5
View File
@@ -46,11 +46,7 @@ class RemoteStorage implements PersistentStorage {
public store<Model>(identifier: StorageIdentifier<Model>, data: Model): Promise<void> {
const transactionId = v4()
const expectation = this.expectAck(transactionId)
rendererEvents.emit(storageStoreEvent, {
data,
transactionId,
store: identifier.id,
})
rendererEvents.emit(storageStoreEvent, { data, transactionId, store: identifier.id })
return expectation
}
+3 -5
View File
@@ -1,6 +1,4 @@
export const selectTextWithCtrlA = (options?: { targetSelector: string }) => (
e: React.KeyboardEvent<HTMLDivElement>
) => {
export const selectTextWithCtrlA = (options?: {targetSelector: string}) => (e: React.KeyboardEvent<HTMLDivElement>) => {
const isCtrlA = (e.metaKey || e.ctrlKey) && e.key === 'a'
if (isCtrlA && window.getSelection) {
@@ -9,8 +7,8 @@ export const selectTextWithCtrlA = (options?: { targetSelector: string }) => (
e.stopPropagation()
const selection = window.getSelection()
const range = document.createRange()
const eventTarget = e.target as HTMLElement
const target: HTMLElement | null = options ? eventTarget.querySelector(options.targetSelector) : eventTarget
const eventTarget = (e.target as HTMLElement)
const target: HTMLElement | null = (options) ? eventTarget.querySelector(options.targetSelector) : eventTarget
if (!target) {
console.error('Could not find matching target for Ctrl+A Event')
+4 -16
View File
@@ -4,18 +4,9 @@ import { electronRendererTelementry } from 'electron-telemetry'
function trackProcessStatistics() {
setInterval(() => {
try {
electronRendererTelementry.trackCustomEvent({
name: 'heapStatistics',
payload: process.getHeapStatistics(),
})
electronRendererTelementry.trackCustomEvent({
name: 'cpuUsage',
payload: process.getCPUUsage(),
})
electronRendererTelementry.trackCustomEvent({
name: 'runningSince',
payload: performance.now(),
})
electronRendererTelementry.trackCustomEvent({ name: 'heapStatistics', payload: process.getHeapStatistics() })
electronRendererTelementry.trackCustomEvent({ name: 'cpuUsage', payload: process.getCPUUsage() })
electronRendererTelementry.trackCustomEvent({ name: 'runningSince', payload: performance.now() })
} catch (error) {
console.error(error)
}
@@ -28,8 +19,5 @@ export function trackEvent(name: string) {
if (name.match(/^@@redux/)) {
return
}
const blacklist = ['CONNECTION_SET_HEALTH']
if (blacklist.indexOf(name) === -1) {
electronRendererTelementry.trackEvent(name)
}
electronRendererTelementry.trackEvent(name)
}
+1 -1
View File
@@ -48,7 +48,7 @@ module.exports = {
devtool: 'source-map',
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: ['.ts', '.mjs', '.m.js', '.tsx', '.js', '.json', '.node'],
extensions: ['.ts', '.tsx', '.js', '.json', '.node'],
},
module: {
rules: [
+655 -1172
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -16,7 +16,6 @@ build_script:
- yarn package appx
test_script:
- yarn lint
- yarn test
artifacts:
+2677
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -34,11 +34,5 @@
],
"sourceMap": true,
"instrument": true
},
"peerDependencies": {
"fs-extra": "^8.0.1",
"js-base64": "^2.5.1",
"lowdb": "^1.0.0",
"mqtt": "^3.0.0"
}
}
+9 -24
View File
@@ -8,8 +8,8 @@ import {
makeStorageResponseEvent,
storageClearEvent,
storageLoadEvent,
storageStoreEvent,
} from '../../events/StorageEvents'
storageStoreEvent
} from '../../events/StorageEvents'
export default class ConfigStorage {
private file: string
@@ -32,43 +32,31 @@ export default class ConfigStorage {
}
public async init() {
backendEvents.subscribe(storageStoreEvent, async event => {
backendEvents.subscribe(storageStoreEvent, async (event) => {
const ack = makeStorageAcknowledgementEvent(event.transactionId)
try {
const db = await this.getDb()
await db.set(event.store, event.data).write()
backendEvents.emit(ack, undefined)
} catch (error) {
backendEvents.emit(ack, {
error,
transactionId: event.transactionId,
store: event.store,
})
backendEvents.emit(ack, { error, transactionId: event.transactionId, store: event.store })
throw error
}
})
backendEvents.subscribe(storageLoadEvent, async event => {
backendEvents.subscribe(storageLoadEvent, async (event) => {
const responseEvent = makeStorageResponseEvent(event.transactionId)
try {
const db = await this.getDb()
const data = await db.get(event.store).value()
backendEvents.emit(responseEvent, {
data,
transactionId: event.transactionId,
store: event.store,
})
backendEvents.emit(responseEvent, { data, transactionId: event.transactionId, store: event.store })
} catch (error) {
backendEvents.emit(responseEvent, {
error,
transactionId: event.transactionId,
store: event.store,
})
backendEvents.emit(responseEvent, { error, transactionId: event.transactionId, store: event.store })
throw error
}
})
backendEvents.subscribe(storageClearEvent, async event => {
backendEvents.subscribe(storageClearEvent, async (event) => {
try {
const db = await this.getDb()
const keys = await db.keys().value()
@@ -77,10 +65,7 @@ export default class ConfigStorage {
}
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), undefined)
} catch (error) {
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), {
error,
transactionId: event.transactionId,
})
backendEvents.emit(makeStorageAcknowledgementEvent(event.transactionId), { error, transactionId: event.transactionId })
throw error
}
})
+5 -5
View File
@@ -68,7 +68,7 @@ export class MqttSource implements DataSource<MqttOptions> {
client.on('connect', () => {
this.stateMachine.setConnected(true)
options.subscriptions.forEach(subscription => {
options.subscriptions.forEach((subscription) => {
client.subscribe(subscription, (err: Error) => {
if (err) {
this.stateMachine.setError(err)
@@ -86,10 +86,10 @@ export class MqttSource implements DataSource<MqttOptions> {
public publish(msg: MqttMessage) {
if (this.client) {
this.client.publish(msg.topic, msg.payload ? Base64Message.toUnicodeString(msg.payload) : '', {
qos: msg.qos,
retain: msg.retain,
})
this.client.publish(
msg.topic,
msg.payload ? Base64Message.toUnicodeString(msg.payload) : '',
{ qos: msg.qos, retain: msg.retain })
}
}
-101
View File
@@ -1,101 +0,0 @@
const parse = require('json-to-ast')
export interface JsonPropertyLocation {
path: string
line: number
value: any
column: number
}
type JsonAst = JsonAstLiteral | JsonAstObject | JsonAstArray
interface JsonAstLocation {
start: {
line: number
column: number
offset: number
}
end: {
line: number
column: number
offset: number
}
source: null | string
}
interface JsonAstIdentifier {
type: 'Identifier'
value: string
raw: string
loc: JsonAstLocation
}
interface JsonAstObjectProperty {
type: 'Property'
key: JsonAstIdentifier
value: JsonAst
loc: JsonAstLocation
}
interface JsonAstLiteral {
type: 'Literal'
value: string | number | boolean | any
raw: string
loc: JsonAstLocation
}
interface JsonAstObject {
type: 'Object'
children: Array<JsonAstObjectProperty>
loc: JsonAstLocation
}
interface JsonAstArray {
type: 'Array'
children: Array<JsonAst>
loc: JsonAstLocation
}
function jsonToPropertyPaths(ast: JsonAst, previousPath: Array<string> = []): Array<JsonPropertyLocation> {
let children: Array<Array<JsonPropertyLocation>> = []
if (ast.type === 'Literal') {
return [
{
value: ast.value,
path: previousPath.join('.'),
line: ast.loc.start.line,
column: ast.loc.start.column,
},
]
} 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]))
)
}
return children.reduce((a, b) => a.concat(b), [])
}
// Used for testing only
export function parseJson(formattedJson: string): Array<JsonPropertyLocation> {
const parsedJson = parse(formattedJson) as JsonAst
return jsonToPropertyPaths(parsedJson, [])
}
export function literalsMappedByLines(formattedJson: string): Array<JsonPropertyLocation> | undefined {
try {
const parsedJson = parse(formattedJson) as JsonAst
const literals = jsonToPropertyPaths(parsedJson, [])
const lines = []
for (const literal of literals) {
lines[literal.line - 1] = literal
}
return lines
} catch (error) {
return undefined
}
}
+3 -8
View File
@@ -1,12 +1,7 @@
import { MqttMessage } from '../../../events'
interface BufferedMessage {
message: MqttMessage
received: Date
}
export class ChangeBuffer {
private buffer: Array<BufferedMessage> = []
private buffer: Array<MqttMessage> = []
private size = 0
private maxSize = 100_000_000 // ~100MB
public length = 0
@@ -14,7 +9,7 @@ export class ChangeBuffer {
public push(val: MqttMessage) {
if (!this.isFull()) {
this.buffer.push({ message: val, received: new Date() })
this.buffer.push(val)
this.size += this.estimatedMessageOverhead + (val.payload ? val.payload.length : 0)
this.length += 1
}
@@ -32,7 +27,7 @@ export class ChangeBuffer {
return this.size / this.maxSize
}
public popAll(): Array<BufferedMessage> {
public popAll(): Array<MqttMessage> {
const tmpBuffer = this.buffer
this.buffer = []
this.size = 0
-3
View File
@@ -1,3 +0,0 @@
export interface Destroyable {
destroy(): void
}
+1 -2
View File
@@ -1,8 +1,7 @@
import { Destroyable } from './Destroyable'
import { Hashable, TreeNode } from './'
const sha1 = require('sha1')
export class Edge<ViewModel extends Destroyable> implements Hashable {
export class Edge<ViewModel> implements Hashable {
public name: string
public target!: TreeNode<ViewModel>
-4
View File
@@ -1,4 +0,0 @@
import { Message } from './Message'
import { RingBuffer } from './RingBuffer'
export type MessageHistory = RingBuffer<Message>

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