mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-11 17:13:53 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d824c1618e | ||
|
|
e7a325b675 | ||
|
|
b0961be86d | ||
|
|
e39fcddc21 | ||
|
|
e3f5d22b1f | ||
|
|
3e2e08e2d4 | ||
|
|
d40565af05 | ||
|
|
4d9f556946 | ||
|
|
2ac272fc4c | ||
|
|
82a5fb1987 |
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"language": "en",
|
||||
"words": [
|
||||
"subheader",
|
||||
"basepath",
|
||||
"webdriverio"
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
build
|
||||
@@ -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
|
||||
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"files.exclude": {
|
||||
"**/node_modules": true,
|
||||
"build/": true
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
| | | |
|
||||
|:---:|:---:|:---:|
|
||||
|[](https://mqtt-explorer.com/img/screen-composite.png)|[](https://mqtt-explorer.com/img/screen2.png)|[](https://mqtt-explorer.com/img/screen3.png)|
|
||||
|[](https://user-images.githubusercontent.com/7721625/53954364-52551f00-40d6-11e9-93cf-d5a9601897ea.png)|[](https://user-images.githubusercontent.com/7721625/53954365-52551f00-40d6-11e9-823f-afd66f19ed01.png)|[](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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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 }
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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 />
|
||||
Abort
|
||||
<ConnectionHealthIndicator /> 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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ function writeHeapdump(path?: string) {
|
||||
return path
|
||||
}
|
||||
|
||||
;(window as any).demo = {
|
||||
(window as any).demo = {
|
||||
writeHeapdump,
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
<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:
|
||||
<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: ' ' }}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
|
||||
@@ -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: ' ' }} />}
|
||||
</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') {
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 />
|
||||
{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 /> {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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export enum ActionTypes {
|
||||
}
|
||||
|
||||
export interface SetConnecting {
|
||||
type: ActionTypes.CONNECTION_SET_CONNECTING
|
||||
type: ActionTypes.CONNECTION_SET_CONNECTING,
|
||||
connectionId: string
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ build_script:
|
||||
- yarn package appx
|
||||
|
||||
test_script:
|
||||
- yarn lint
|
||||
- yarn test
|
||||
|
||||
artifacts:
|
||||
|
||||
Generated
+2677
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface Destroyable {
|
||||
destroy(): void
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user