Add numeric chart panel
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/node_modules": true
|
"**/node_modules": true,
|
||||||
|
"build/": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
app/index.html
116
app/index.html
@@ -2,15 +2,13 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
|
||||||
name="viewport"
|
|
||||||
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
|
||||||
>
|
|
||||||
<title>MQTT Explorer</title>
|
<title>MQTT Explorer</title>
|
||||||
<script src="./bugtracking.bundle.js"></script>
|
<script src="./bugtracking.bundle.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body, html {
|
body,
|
||||||
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -20,17 +18,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes updateDark {
|
@keyframes updateDark {
|
||||||
0% {background-color: none;}
|
0% {
|
||||||
25% {background-color: #3f51b5;}
|
background-color: none;
|
||||||
50% {background-color: #3f51b5;}
|
}
|
||||||
100% {background-color: none;}
|
25% {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes updateLight {
|
@keyframes updateLight {
|
||||||
0% {background-color: none; color: inherit}
|
0% {
|
||||||
25% {background-color: #bfc9c8; color: #000}
|
background-color: none;
|
||||||
50% {background-color: #bfc9c8; color: #000}
|
color: inherit;
|
||||||
100% {background-color: none; color: inherit}
|
}
|
||||||
|
25% {
|
||||||
|
background-color: #bfc9c8;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: #bfc9c8;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -39,23 +57,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
::-webkit-scrollbar-corner {
|
||||||
background-color: rgba(0, 0, 0, 0.0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(60,60,60,0.5);
|
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
|
||||||
background-color: rgba(140,140,140,0.1);
|
background-color: rgba(140, 140, 140, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(140,140,140,0.8);
|
background-color: rgba(140, 140, 140, 0.8);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
.Resizer {
|
.Resizer {
|
||||||
background: #eee;
|
background: rgba(200, 200, 200, 0);
|
||||||
opacity: .2;
|
z-index: 10;
|
||||||
z-index: 1;
|
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -70,17 +87,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Resizer.horizontal {
|
.Resizer.horizontal {
|
||||||
height: 11px;
|
height: 10px;
|
||||||
margin: -5px 0;
|
margin: -10px 0 0 0;
|
||||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||||
cursor: row-resize;
|
cursor: row-resize;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.Resizer.horizontal:hover {
|
||||||
border-top: 5px solid rgba(120, 120, 120, 0.5);
|
border-top: 5px solid rgba(120, 120, 120, 0.3);
|
||||||
border-bottom: 5px solid rgba(120, 120, 120, 0.5);
|
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Resizer.vertical {
|
.Resizer.vertical {
|
||||||
@@ -91,9 +119,22 @@
|
|||||||
cursor: col-resize;
|
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 {
|
.Resizer.vertical:hover {
|
||||||
border-left: 4px solid rgba(130, 130, 130, 1);
|
border-left: 4px solid rgba(130, 130, 130, 0.3);
|
||||||
border-right: 4px solid rgba(140, 140, 140, 1);
|
border-right: 4px solid rgba(140, 140, 140, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Resizer.disabled {
|
.Resizer.disabled {
|
||||||
@@ -103,23 +144,38 @@
|
|||||||
border-color: transparent;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" style="font:-webkit-control"></div>
|
<div id="app" style="font:-webkit-control"></div>
|
||||||
<script>
|
<script>
|
||||||
function loadScript(path) {
|
function loadScript(path) {
|
||||||
var script = document.createElement("script");
|
var script = document.createElement('script')
|
||||||
script.src = path
|
script.src = path
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', onLoad(), false);
|
document.addEventListener('DOMContentLoaded', onLoad(), false)
|
||||||
function onLoad() {
|
function onLoad() {
|
||||||
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
|
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
|
||||||
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
|
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script><% }); %>
|
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script
|
||||||
|
><% }); %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@material-ui/icons": "^4",
|
"@material-ui/icons": "^4",
|
||||||
"@material-ui/lab": "^4.0.0-alpha",
|
"@material-ui/lab": "^4.0.0-alpha",
|
||||||
"@material-ui/styles": "^4",
|
"@material-ui/styles": "^4",
|
||||||
|
"@types/react-transition-group": "^2.9.2",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"brace": "^0.11.1",
|
"brace": "^0.11.1",
|
||||||
"compare-versions": "^3.4.0",
|
"compare-versions": "^3.4.0",
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
"react-redux": "^7.0.3",
|
"react-redux": "^7.0.3",
|
||||||
"react-resize-detector": "^4.1.4",
|
"react-resize-detector": "^4.1.4",
|
||||||
"react-split-pane": "^0.1.85",
|
"react-split-pane": "^0.1.85",
|
||||||
|
"react-transition-group": "^4.1.1",
|
||||||
"react-vis": "^1.11.6",
|
"react-vis": "^1.11.6",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-batched-actions": "^0.4.1",
|
"redux-batched-actions": "^0.4.1",
|
||||||
|
|||||||
101
app/src/actions/Charts.ts
Normal file
101
app/src/actions/Charts.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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 } 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
|
||||||
|
) => {
|
||||||
|
let chartExists = Boolean(
|
||||||
|
getState()
|
||||||
|
.charts.get('charts')
|
||||||
|
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
|
||||||
|
)
|
||||||
|
if (chartExists) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.CHARTS_ADD,
|
||||||
|
chart: 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 setCharts = (charts: Array<ChartParameters>): Action => {
|
||||||
|
return {
|
||||||
|
charts,
|
||||||
|
type: ActionTypes.CHARTS_SET,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ export const connect = (options: MqttOptions, connectionId: string) => (
|
|||||||
const host = url.parse(options.url).hostname
|
const host = url.parse(options.url).hostname
|
||||||
|
|
||||||
rendererEvents.subscribe(event, dataSourceState => {
|
rendererEvents.subscribe(event, dataSourceState => {
|
||||||
console.log(dataSourceState)
|
|
||||||
if (dataSourceState.connected) {
|
if (dataSourceState.connected) {
|
||||||
const didReconnect = Boolean(getState().connection.tree)
|
const didReconnect = Boolean(getState().connection.tree)
|
||||||
if (!didReconnect) {
|
if (!didReconnect) {
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ import * as path from 'path'
|
|||||||
|
|
||||||
import { ActionTypes, Action } from '../reducers/ConnectionManager'
|
import { ActionTypes, Action } from '../reducers/ConnectionManager'
|
||||||
|
|
||||||
const storedConnectionsIdentifier: StorageIdentifier<{
|
type ConnectionDictionary = { [s: string]: ConnectionOptions }
|
||||||
[s: string]: ConnectionOptions
|
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
|
||||||
}> = {
|
|
||||||
id: 'ConnectionManager_connections',
|
id: 'ConnectionManager_connections',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,14 +46,12 @@ export const selectCertificate = (connectionId: string) => async (
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const certificate = await openCertificate()
|
const certificate = await openCertificate()
|
||||||
console.log(certificate)
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateConnection(connectionId, {
|
updateConnection(connectionId, {
|
||||||
selfSignedCertificate: certificate,
|
selfSignedCertificate: certificate,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
|
||||||
dispatch(showError(error))
|
dispatch(showError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import * as q from '../../../backend/src/Model'
|
import * as q from '../../../backend/src/Model'
|
||||||
|
import { ActionTypes, SettingsState, TopicOrder } from '../reducers/Settings'
|
||||||
import { AppState } from '../reducers'
|
import { AppState } from '../reducers'
|
||||||
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
|
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
|
||||||
|
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||||
import { batchActions } from 'redux-batched-actions'
|
import { batchActions } from 'redux-batched-actions'
|
||||||
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
|
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
import { globalActions } from './'
|
||||||
import { showError } from './Global'
|
import { showError } from './Global'
|
||||||
import { showTree } from './Tree'
|
import { showTree } from './Tree'
|
||||||
import { TopicViewModel } from '../model/TopicViewModel'
|
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>> = {
|
const settingsIdentifier: StorageIdentifier<Partial<SettingsState>> = {
|
||||||
id: 'Settings',
|
id: 'Settings',
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicC
|
|||||||
.filter(topic => Boolean(topic.message && topic.message.value))
|
.filter(topic => Boolean(topic.message && topic.message.value))
|
||||||
.slice(0, subtopicClearLimit)
|
.slice(0, subtopicClearLimit)
|
||||||
.forEach(topic => {
|
.forEach(topic => {
|
||||||
console.log('deleting', topic.path())
|
|
||||||
const mqttMessage = {
|
const mqttMessage = {
|
||||||
topic: topic.path(),
|
topic: topic.path(),
|
||||||
payload: null,
|
payload: null,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as chartActions from './Charts'
|
||||||
import * as connectionActions from './Connection'
|
import * as connectionActions from './Connection'
|
||||||
import * as connectionManagerActions from './ConnectionManager'
|
import * as connectionManagerActions from './ConnectionManager'
|
||||||
import * as globalActions from './Global'
|
import * as globalActions from './Global'
|
||||||
@@ -10,6 +11,7 @@ import * as updateNotifierActions from './UpdateNotifier'
|
|||||||
export {
|
export {
|
||||||
settingsActions,
|
settingsActions,
|
||||||
treeActions,
|
treeActions,
|
||||||
|
chartActions,
|
||||||
publishActions,
|
publishActions,
|
||||||
updateNotifierActions,
|
updateNotifierActions,
|
||||||
connectionActions,
|
connectionActions,
|
||||||
|
|||||||
@@ -97,19 +97,16 @@ const styles = (theme: Theme) => {
|
|||||||
const drawerWidth = 300
|
const drawerWidth = 300
|
||||||
const contentBaseStyle = {
|
const contentBaseStyle = {
|
||||||
width: '100vw',
|
width: '100vw',
|
||||||
overflow: 'hidden' as 'hidden',
|
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
heightProperty: {
|
heightProperty: {
|
||||||
height: 'calc(100vh - 64px) !important',
|
height: '100%', // 'calc(100vh - 64px) !important',
|
||||||
},
|
},
|
||||||
paneDefaults: {
|
paneDefaults: {
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
overflowY: 'scroll' as 'scroll',
|
|
||||||
overflowX: 'hidden' as 'hidden',
|
|
||||||
display: 'block' as 'block',
|
display: 'block' as 'block',
|
||||||
height: 'calc(100vh - 64px)',
|
height: 'calc(100vh - 64px)',
|
||||||
},
|
},
|
||||||
|
|||||||
107
app/src/components/ChartPanel/Chart.tsx
Normal file
107
app/src/components/ChartPanel/Chart.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
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, Fade } from '@material-ui/core'
|
||||||
|
|
||||||
|
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)
|
||||||
|
const [lastUpdate, setLastUpdate] = React.useState(0)
|
||||||
|
|
||||||
|
/** If a node is not available when the plot is shown, keep polling until it has been created */
|
||||||
|
function pollForTreeNode() {
|
||||||
|
const onUpdateCallback = () => setLastUpdate(treeNode ? treeNode.lastUpdate : 0)
|
||||||
|
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)
|
||||||
|
|
||||||
|
const onClick = React.useCallback(() => {
|
||||||
|
props.actions.chart.removeChart(props.parameters)
|
||||||
|
}, [props.parameters])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper style={{ padding: '8px' }}>
|
||||||
|
<div style={{ float: 'right' }}>
|
||||||
|
<CustomIconButton tooltip="Remove chart" onClick={onClick}>
|
||||||
|
<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 history={treeNode.messageHistory} dotPath={parameters.dotPath} /> : <span>No data</span>}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
return {
|
||||||
|
tree: state.tree.get('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))
|
||||||
124
app/src/components/ChartPanel/index.tsx
Normal file
124
app/src/components/ChartPanel/index.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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 FadingChart(props: { chartParameters: ChartParameters; chartsInView: number; key: any }) {
|
||||||
|
// const { chartsInView, chartParameters } = props
|
||||||
|
// const [spacing, setSpacing] = React.useState(spacingForChartCount(chartsInView))
|
||||||
|
|
||||||
|
// // Update spacing after animations have completed
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// const newSpacing = spacingForChartCount(chartsInView)
|
||||||
|
// if (spacing !== newSpacing) {
|
||||||
|
// setSpacing(newSpacing)
|
||||||
|
// // setTimeout(() => , 500)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return (
|
||||||
|
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
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={spacing}>
|
||||||
|
<Chart parameters={chartParameters} />
|
||||||
|
</Grid>
|
||||||
|
</CSSTransition>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', height: '100%', padding: '8px', borderTop: '1px solid #999' }}>
|
||||||
|
<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))
|
||||||
@@ -104,6 +104,7 @@ const styles = (theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
textColor: {
|
textColor: {
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: 'all' as 'all',
|
||||||
},
|
},
|
||||||
centered: {
|
centered: {
|
||||||
textAlign: 'center' as 'center',
|
textAlign: 'center' as 'center',
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ReactSplitPane from 'react-split-pane'
|
import ReactSplitPane from 'react-split-pane'
|
||||||
import { Sidebar } from '../Sidebar'
|
|
||||||
import Tree from '../Tree/Tree'
|
import Tree from '../Tree/Tree'
|
||||||
|
import ChartPanel from '../ChartPanel'
|
||||||
|
import { Sidebar } from '../Sidebar'
|
||||||
|
|
||||||
export default function ContentView(props: { heightProperty: any; paneDefaults: any; connectionId: any }) {
|
export default function ContentView(props: { heightProperty: any; paneDefaults: any; connectionId: any }) {
|
||||||
|
const [height, setHeight] = React.useState(0)
|
||||||
return (
|
return (
|
||||||
|
<div className={props.paneDefaults}>
|
||||||
|
<ReactSplitPane
|
||||||
|
step={10}
|
||||||
|
split="horizontal"
|
||||||
|
minSize={0}
|
||||||
|
defaultSize={'100%'}
|
||||||
|
allowResize={true}
|
||||||
|
style={{ height: 'calc(100vh - 64px)' }}
|
||||||
|
pane1Style={{ maxHeight: '100%' }}
|
||||||
|
pane2Style={{ maxWidth: '100%', overflow: 'hidden auto' }}
|
||||||
|
onChange={setHeight}
|
||||||
|
>
|
||||||
<ReactSplitPane
|
<ReactSplitPane
|
||||||
step={20}
|
step={20}
|
||||||
primary="second"
|
primary="second"
|
||||||
@@ -13,15 +27,17 @@ export default function ContentView(props: { heightProperty: any; paneDefaults:
|
|||||||
minSize={250}
|
minSize={250}
|
||||||
defaultSize={500}
|
defaultSize={500}
|
||||||
allowResize={true}
|
allowResize={true}
|
||||||
style={{ position: 'relative' }}
|
style={{ height: '100%' }}
|
||||||
pane1Style={{ overflow: 'hidden' }}
|
pane1Style={{ overflowX: 'hidden' }}
|
||||||
|
resizerStyle={{ height: '100%' }}
|
||||||
>
|
>
|
||||||
<div className={props.paneDefaults}>
|
|
||||||
<Tree />
|
<Tree />
|
||||||
</div>
|
<div className={props.paneDefaults} style={{ height: '100%', overflowY: 'auto', overflowX: 'hidden' }}>
|
||||||
<div className={props.paneDefaults}>
|
|
||||||
<Sidebar connectionId={props.connectionId} />
|
<Sidebar connectionId={props.connectionId} />
|
||||||
</div>
|
</div>
|
||||||
</ReactSplitPane>
|
</ReactSplitPane>
|
||||||
|
<ChartPanel />
|
||||||
|
</ReactSplitPane>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
82
app/src/components/Sidebar/CodeDiff/ChartPreview.tsx
Normal file
82
app/src/components/Sidebar/CodeDiff/ChartPreview.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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,
|
||||||
|
})
|
||||||
|
}, [props.literal.path, props.treeNode])
|
||||||
|
|
||||||
|
const mouseOver = React.useCallback(() => {
|
||||||
|
setOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const mouseOut = React.useCallback(() => {
|
||||||
|
setOpen(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const hasEnoughDataToDisplayDiagrams = props.treeNode.messageHistory.count() > 1
|
||||||
|
|
||||||
|
let preview = hasEnoughDataToDisplayDiagrams ? (
|
||||||
|
<Tooltip title="Click to add to chart panel">
|
||||||
|
<ShowChart
|
||||||
|
ref={chartIconRef}
|
||||||
|
className={props.classes.icon}
|
||||||
|
onMouseEnter={mouseOver}
|
||||||
|
onMouseLeave={mouseOut}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip title="Click to add to chart panel, not enough data for preview">
|
||||||
|
<ShowChart onClick={onClick} className={props.classes.icon} style={{ color: '#aaa' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{preview}
|
||||||
|
<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)
|
||||||
@@ -2,10 +2,10 @@ import * as diff from 'diff'
|
|||||||
import * as q from '../../../../../backend/src/Model'
|
import * as q from '../../../../../backend/src/Model'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Add from '@material-ui/icons/Add'
|
import Add from '@material-ui/icons/Add'
|
||||||
|
import ChartPreview from './ChartPreview'
|
||||||
import Remove from '@material-ui/icons/Remove'
|
import Remove from '@material-ui/icons/Remove'
|
||||||
import ShowChart from '@material-ui/icons/ShowChart'
|
import ShowChart from '@material-ui/icons/ShowChart'
|
||||||
import TopicPlot from '../TopicPlot'
|
import { Theme, Tooltip } from '@material-ui/core'
|
||||||
import { Fade, Paper, Popper, Theme, Tooltip } from '@material-ui/core'
|
|
||||||
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
|
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
|
||||||
import { lineChangeStyle, trimNewlineRight } from './util'
|
import { lineChangeStyle, trimNewlineRight } from './util'
|
||||||
import { withStyles } from '@material-ui/styles'
|
import { withStyles } from '@material-ui/styles'
|
||||||
@@ -15,7 +15,7 @@ interface Props {
|
|||||||
literalPositions: Array<JsonPropertyLocation>
|
literalPositions: Array<JsonPropertyLocation>
|
||||||
classes: any
|
classes: any
|
||||||
className: string
|
className: string
|
||||||
messageHistory: q.MessageHistory
|
treeNode: q.TreeNode<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = (theme: Theme) => {
|
const style = (theme: Theme) => {
|
||||||
@@ -29,12 +29,9 @@ const style = (theme: Theme) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
icon,
|
icon,
|
||||||
iconDisabled: {
|
|
||||||
...icon,
|
|
||||||
color: theme.palette.text.disabled,
|
|
||||||
},
|
|
||||||
iconButton: {
|
iconButton: {
|
||||||
...icon,
|
...icon,
|
||||||
|
marginTop: '0px',
|
||||||
width: '16px',
|
width: '16px',
|
||||||
height: '16px',
|
height: '16px',
|
||||||
padding: '2px',
|
padding: '2px',
|
||||||
@@ -52,66 +49,24 @@ const style = (theme: Theme) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChartIcon(props: { messageHistory: q.MessageHistory; classes: any; literal: JsonPropertyLocation }) {
|
|
||||||
const chartIconRef = React.useRef(null)
|
|
||||||
const [open, setOpen] = React.useState(false)
|
|
||||||
|
|
||||||
const mouseOver = React.useCallback(
|
|
||||||
(event: React.MouseEvent<Element>) => {
|
|
||||||
setOpen(true)
|
|
||||||
},
|
|
||||||
[props.literal.path]
|
|
||||||
)
|
|
||||||
|
|
||||||
const mouseOut = React.useCallback(() => {
|
|
||||||
setOpen(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<ShowChart ref={chartIconRef} className={props.classes.icon} onMouseEnter={mouseOver} onMouseLeave={mouseOut} />
|
|
||||||
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
|
|
||||||
<Fade in={open} timeout={300}>
|
|
||||||
<Paper style={{ width: '300px' }}>
|
|
||||||
{open ? <TopicPlot history={props.messageHistory} dotPath={props.literal.path} /> : <span />}
|
|
||||||
</Paper>
|
|
||||||
</Fade>
|
|
||||||
</Popper>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokensForLine(change: diff.Change, line: number, props: Props) {
|
function tokensForLine(change: diff.Change, line: number, props: Props) {
|
||||||
const { classes, literalPositions } = props
|
const { classes, literalPositions } = props
|
||||||
const hasEnoughDataToDisplayDiagrams = props.messageHistory.count() > 1
|
|
||||||
const literal = literalPositions[line]
|
const literal = literalPositions[line]
|
||||||
|
|
||||||
let chartIcon = null
|
let chartPreview = null
|
||||||
if (literal) {
|
if (literal) {
|
||||||
if (hasEnoughDataToDisplayDiagrams) {
|
chartPreview = (
|
||||||
chartIcon = (
|
<ChartPreview treeNode={props.treeNode} classes={{ icon: props.classes.iconButton }} literal={literal} />
|
||||||
<ChartIcon
|
|
||||||
messageHistory={props.messageHistory}
|
|
||||||
classes={{ icon: props.classes.iconButton }}
|
|
||||||
literal={literal}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
chartIcon = (
|
|
||||||
<Tooltip title="Not enough data">
|
|
||||||
<ShowChart className={props.classes.iconDisabled} style={{ color: '#aaa' }} />
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (change.added) {
|
if (change.added) {
|
||||||
return [chartIcon, <Add key="add" className={classes.icon} />]
|
return [chartPreview, <Add key="add" className={classes.icon} />]
|
||||||
} else if (change.removed) {
|
} else if (change.removed) {
|
||||||
return [<Remove key="remove" className={classes.icon} />]
|
return [<Remove key="remove" className={classes.icon} />]
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
chartIcon,
|
chartPreview,
|
||||||
<div
|
<div
|
||||||
key="placeholder"
|
key="placeholder"
|
||||||
style={{ width: '12px', display: 'inline-block' }}
|
style={{ width: '12px', display: 'inline-block' }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { withStyles } from '@material-ui/core'
|
|||||||
import 'prismjs/components/prism-json'
|
import 'prismjs/components/prism-json'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messageHistory: q.MessageHistory
|
treeNode: q.TreeNode<any>
|
||||||
previous: string
|
previous: string
|
||||||
current: string
|
current: string
|
||||||
nameOfCompareMessage: string
|
nameOfCompareMessage: string
|
||||||
@@ -93,7 +93,7 @@ class CodeDiff extends React.Component<Props, State> {
|
|||||||
<Gutters
|
<Gutters
|
||||||
className={this.props.classes.gutters}
|
className={this.props.classes.gutters}
|
||||||
changes={changes}
|
changes={changes}
|
||||||
messageHistory={this.props.messageHistory}
|
treeNode={this.props.treeNode}
|
||||||
literalPositions={literalPositions}
|
literalPositions={literalPositions}
|
||||||
/>
|
/>
|
||||||
<pre className={this.props.classes.codeBlock}>{code}</pre>
|
<pre className={this.props.classes.codeBlock}>{code}</pre>
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||||||
const styles = (theme: Theme) => ({
|
const styles = (theme: Theme) => ({
|
||||||
drawer: {
|
drawer: {
|
||||||
display: 'block' as 'block',
|
display: 'block' as 'block',
|
||||||
height: '100%',
|
|
||||||
},
|
},
|
||||||
badge: {
|
badge: {
|
||||||
top: '3px',
|
top: '3px',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import BarChart from '@material-ui/icons/BarChart'
|
|||||||
import Copy from '../../helper/Copy'
|
import Copy from '../../helper/Copy'
|
||||||
import DateFormatter from '../../helper/DateFormatter'
|
import DateFormatter from '../../helper/DateFormatter'
|
||||||
import History from '../HistoryDrawer'
|
import History from '../HistoryDrawer'
|
||||||
import TopicPlot from '../TopicPlot'
|
import TopicPlot from '../../TopicPlot'
|
||||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||||
import { isPlottable } from '../CodeDiff/util'
|
import { isPlottable } from '../CodeDiff/util'
|
||||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||||
|
|||||||
@@ -53,13 +53,7 @@ class ValuePanel extends React.Component<Props, State> {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <ValueRenderer treeNode={node} message={node.message} compareWith={this.props.compareMessage} />
|
||||||
<ValueRenderer
|
|
||||||
message={node.message}
|
|
||||||
messageHistory={node.messageHistory}
|
|
||||||
compareWith={this.props.compareMessage}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderViewOptions() {
|
private renderViewOptions() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { ValueRendererDisplayMode } from '../../../reducers/Settings'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: q.Message
|
message: q.Message
|
||||||
messageHistory: q.MessageHistory
|
treeNode: q.TreeNode<any>
|
||||||
compareWith?: q.Message
|
compareWith?: q.Message
|
||||||
renderMode: ValueRendererDisplayMode
|
renderMode: ValueRendererDisplayMode
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ class ValueRenderer extends React.Component<Props, State> {
|
|||||||
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
|
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
|
||||||
return (
|
return (
|
||||||
<CodeDiff
|
<CodeDiff
|
||||||
messageHistory={this.props.messageHistory}
|
treeNode={this.props.treeNode}
|
||||||
previous={previous}
|
previous={previous}
|
||||||
current={current}
|
current={current}
|
||||||
language={language}
|
language={language}
|
||||||
@@ -65,9 +65,8 @@ class ValueRenderer extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public renderValue() {
|
public renderValue() {
|
||||||
const { message, messageHistory, compareWith, renderMode } = this.props
|
const { message, treeNode, compareWith, renderMode } = this.props
|
||||||
|
const previousMessages = treeNode.messageHistory.toArray()
|
||||||
const previousMessages = messageHistory.toArray()
|
|
||||||
const previousMessage = previousMessages[previousMessages.length - 2]
|
const previousMessage = previousMessages[previousMessages.length - 2]
|
||||||
let compareMessage = compareWith || previousMessage || message
|
let compareMessage = compareWith || previousMessage || message
|
||||||
if (renderMode === 'raw') {
|
if (renderMode === 'raw') {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as dotProp from 'dot-prop'
|
import * as dotProp from 'dot-prop'
|
||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../backend/src/Model'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import PlotHistory from './PlotHistory'
|
import PlotHistory from './Sidebar/PlotHistory'
|
||||||
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
|
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||||
import { toPlottableValue } from './CodeDiff/util'
|
import { toPlottableValue } from './Sidebar/CodeDiff/util'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
history: q.MessageHistory
|
history: q.MessageHistory
|
||||||
@@ -38,7 +38,6 @@ function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
|
|||||||
|
|
||||||
function render(props: Props) {
|
function render(props: Props) {
|
||||||
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
|
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
|
||||||
console.log(props.dotPath, data)
|
|
||||||
return <PlotHistory data={data} />
|
return <PlotHistory data={data} />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +107,10 @@ class TreeComponent extends React.PureComponent<Props, State> {
|
|||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
lineHeight: '1.1',
|
lineHeight: '1.1',
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
|
overflowY: 'scroll',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
height: '100%',
|
||||||
|
paddingBottom: '16px', // avoid conflict with chart panel Resizer
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ const styles = (theme: Theme) => ({
|
|||||||
width: '32px',
|
width: '32px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
marginTop: '-2px',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
class CustomIconButton extends React.Component<Props, {}> {
|
class CustomIconButton extends React.Component<Props, {}> {
|
||||||
@@ -30,7 +33,7 @@ class CustomIconButton extends React.Component<Props, {}> {
|
|||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<IconButton className={this.props.classes.button} onClick={this.onClick}>
|
<IconButton className={this.props.classes.button} onClick={this.onClick}>
|
||||||
<Tooltip title={this.props.tooltip}>
|
<Tooltip title={this.props.tooltip} className={this.props.classes.label}>
|
||||||
<span>{this.props.children}</span>
|
<span>{this.props.children}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
61
app/src/reducers/Charts.ts
Normal file
61
app/src/reducers/Charts.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Action } from 'redux'
|
||||||
|
import { createReducer } from './lib'
|
||||||
|
import { Record, List } from 'immutable'
|
||||||
|
|
||||||
|
export interface ChartParameters {
|
||||||
|
topic: string
|
||||||
|
dotPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartsStateModel {
|
||||||
|
charts: List<ChartParameters>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChartsState = Record<ChartsStateModel>
|
||||||
|
|
||||||
|
export type Action = AddChart | RemoveChart | SetCharts
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
CHARTS_ADD = 'CHARTS_ADD',
|
||||||
|
CHARTS_REMOVE = 'CHARTS_REMOVE',
|
||||||
|
CHARTS_SET = 'CHARTS_SET',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddChart {
|
||||||
|
type: ActionTypes.CHARTS_ADD
|
||||||
|
chart: 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,
|
||||||
|
})
|
||||||
|
|
||||||
|
function addChart(state: ChartsState, action: AddChart) {
|
||||||
|
return state.set('charts', state.get('charts').push(action.chart))
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChart(state: ChartsState, action: RemoveChart) {
|
||||||
|
const charts = state.get('charts')
|
||||||
|
const newCharts = charts.filter(chart => chart.topic !== action.chart.topic || chart.dotPath !== action.chart.dotPath)
|
||||||
|
return state.set('charts', newCharts)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCharts(state: ChartsState, action: SetCharts) {
|
||||||
|
return state.set('charts', List(action.charts))
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as moment from 'moment'
|
|
||||||
import { createReducer } from './lib'
|
import { createReducer } from './lib'
|
||||||
import { Record } from 'immutable'
|
import { Record } from 'immutable'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { chartsReducer, ChartsState } from './Charts'
|
||||||
import { combineReducers } from 'redux'
|
import { combineReducers } from 'redux'
|
||||||
import { connectionManagerReducer, ConnectionManagerState } from './ConnectionManager'
|
import { connectionManagerReducer, ConnectionManagerState } from './ConnectionManager'
|
||||||
import { connectionReducer, ConnectionState } from './Connection'
|
import { connectionReducer, ConnectionState } from './Connection'
|
||||||
@@ -13,6 +14,7 @@ export interface AppState {
|
|||||||
tree: TreeState
|
tree: TreeState
|
||||||
settings: Record<SettingsState>
|
settings: Record<SettingsState>
|
||||||
publish: PublishState
|
publish: PublishState
|
||||||
|
charts: ChartsState
|
||||||
sidebar: SidebarState
|
sidebar: SidebarState
|
||||||
connection: ConnectionState
|
connection: ConnectionState
|
||||||
connectionManager: ConnectionManagerState
|
connectionManager: ConnectionManagerState
|
||||||
@@ -20,6 +22,7 @@ export interface AppState {
|
|||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
globalState,
|
globalState,
|
||||||
|
charts: chartsReducer,
|
||||||
publish: publishReducer,
|
publish: publishReducer,
|
||||||
sidebar: sidebarReducer,
|
sidebar: sidebarReducer,
|
||||||
connection: connectionReducer,
|
connection: connectionReducer,
|
||||||
|
|||||||
@@ -414,7 +414,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-transition-group@^2.0.16":
|
"@types/react-transition-group@^2.0.16", "@types/react-transition-group@^2.9.2":
|
||||||
version "2.9.2"
|
version "2.9.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.9.2.tgz#c48cf2a11977c8b4ff539a1c91d259eaa627028d"
|
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.9.2.tgz#c48cf2a11977c8b4ff539a1c91d259eaa627028d"
|
||||||
integrity sha512-5Fv2DQNO+GpdPZcxp2x/OQG/H19A01WlmpjVD9cKvVFmoVLOZ9LvBgSWG6pSXIU4og5fgbvGPaCV5+VGkWAEHA==
|
integrity sha512-5Fv2DQNO+GpdPZcxp2x/OQG/H19A01WlmpjVD9cKvVFmoVLOZ9LvBgSWG6pSXIU4og5fgbvGPaCV5+VGkWAEHA==
|
||||||
@@ -4729,7 +4729,7 @@ react-style-proptype@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.5.4"
|
prop-types "^15.5.4"
|
||||||
|
|
||||||
react-transition-group@^4.0.0:
|
react-transition-group@^4.0.0, react-transition-group@^4.1.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.1.1.tgz#16efe9ac8c68306f6bef59c7da5a96b4dfd9fb32"
|
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.1.1.tgz#16efe9ac8c68306f6bef59c7da5a96b4dfd9fb32"
|
||||||
integrity sha512-K/N1wqJ2GRP2yj3WBqEUYa0KV5fiaAWpUfU9SpHOHefeKvyrO+VrnMBML21M19QZoVbDZKmuQFHZYoMMi1xuJA==
|
integrity sha512-K/N1wqJ2GRP2yj3WBqEUYa0KV5fiaAWpUfU9SpHOHefeKvyrO+VrnMBML21M19QZoVbDZKmuQFHZYoMMi1xuJA==
|
||||||
|
|||||||
Reference in New Issue
Block a user