Add numeric chart panel

This commit is contained in:
Thomas Nordquist
2019-06-16 19:10:37 +02:00
parent 4ec8cdf0ff
commit 209899c3b8
28 changed files with 719 additions and 219 deletions

View File

@@ -1,6 +1,7 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"files.exclude": { "files.exclude": {
"**/node_modules": true "**/node_modules": true,
"build/": true
} }
} }

View File

@@ -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>

View File

@@ -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
View 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,
}
}

View File

@@ -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) {

View File

@@ -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))
} }
} }

View File

@@ -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',

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)',
}, },

View 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))

View 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))

View File

@@ -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',

View File

@@ -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>
) )
} }

View 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)

View File

@@ -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' }}

View File

@@ -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>

View File

@@ -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',

View File

@@ -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'

View File

@@ -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() {

View File

@@ -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') {

View File

@@ -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} />
} }

View File

@@ -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 (

View File

@@ -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>

View 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))
}

View File

@@ -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'

View File

@@ -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,

View File

@@ -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==