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,
"files.exclude": {
"**/node_modules": true
"**/node_modules": true,
"build/": true
}
}

View File

@@ -1,125 +1,181 @@
<!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;
}
<style>
body,
html {
margin: 0;
padding: 0;
}
[tabindex] {
outline: none;
}
[tabindex] {
outline: none;
}
@keyframes updateDark {
0% {background-color: none;}
25% {background-color: #3f51b5;}
50% {background-color: #3f51b5;}
100% {background-color: none;}
@keyframes updateDark {
0% {
background-color: none;
}
25% {
background-color: #3f51b5;
}
50% {
background-color: #3f51b5;
}
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;
}
25% {
background-color: #bfc9c8;
color: #000;
}
50% {
background-color: #bfc9c8;
color: #000;
}
100% {
background-color: none;
color: inherit;
}
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0.0);
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60,60,60,0.5);
background-color: rgba(140,140,140,0.1);
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
background-color: rgba(140, 140, 140, 0.1);
}
::-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-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:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.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%;
}
.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.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.5);
border-bottom: 5px solid rgba(120, 120, 120, 0.5);
}
.Resizer.horizontal::before {
content: '•••';
display: inline-block;
vertical-align: middle;
text-align: center;
width: 100%;
margin-top: -22px;
color: #aaa;
opacity: 1;
}
.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.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.3);
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
}
.Resizer.vertical:hover {
border-left: 4px solid rgba(130, 130, 130, 1);
border-right: 4px solid rgba(140, 140, 140, 1);
}
.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.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.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;
}
</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>
.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>
</html>

View File

@@ -15,6 +15,7 @@
"@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",
"brace": "^0.11.1",
"compare-versions": "^3.4.0",
@@ -41,6 +42,7 @@
"react-redux": "^7.0.3",
"react-resize-detector": "^4.1.4",
"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",

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
rendererEvents.subscribe(event, dataSourceState => {
console.log(dataSourceState)
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {

View File

@@ -15,9 +15,8 @@ import * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager'
const storedConnectionsIdentifier: StorageIdentifier<{
[s: string]: ConnectionOptions
}> = {
type ConnectionDictionary = { [s: string]: ConnectionOptions }
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
id: 'ConnectionManager_connections',
}
@@ -47,14 +46,12 @@ export const selectCertificate = (connectionId: string) => async (
) => {
try {
const certificate = await openCertificate()
console.log(certificate)
dispatch(
updateConnection(connectionId, {
selfSignedCertificate: certificate,
})
)
} catch (error) {
console.log(error)
dispatch(showError(error))
}
}

View File

@@ -1,15 +1,15 @@
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',

View File

@@ -44,7 +44,6 @@ export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicC
.filter(topic => Boolean(topic.message && topic.message.value))
.slice(0, subtopicClearLimit)
.forEach(topic => {
console.log('deleting', topic.path())
const mqttMessage = {
topic: topic.path(),
payload: null,

View File

@@ -1,3 +1,4 @@
import * as chartActions from './Charts'
import * as connectionActions from './Connection'
import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global'
@@ -10,6 +11,7 @@ import * as updateNotifierActions from './UpdateNotifier'
export {
settingsActions,
treeActions,
chartActions,
publishActions,
updateNotifierActions,
connectionActions,

View File

@@ -97,19 +97,16 @@ const styles = (theme: Theme) => {
const drawerWidth = 300
const contentBaseStyle = {
width: '100vw',
overflow: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
}
return {
heightProperty: {
height: 'calc(100vh - 64px) !important',
height: '100%', // '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)',
},

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: {
color: theme.palette.text.primary,
userSelect: 'all' as 'all',
},
centered: {
textAlign: 'center' as 'center',

View File

@@ -1,27 +1,43 @@
import * as React from 'react'
import ReactSplitPane from 'react-split-pane'
import { Sidebar } from '../Sidebar'
import Tree from '../Tree/Tree'
import ChartPanel from '../ChartPanel'
import { Sidebar } from '../Sidebar'
export default function ContentView(props: { heightProperty: any; paneDefaults: any; connectionId: any }) {
const [height, setHeight] = React.useState(0)
return (
<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>
<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
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>
<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 React from 'react'
import Add from '@material-ui/icons/Add'
import ChartPreview from './ChartPreview'
import Remove from '@material-ui/icons/Remove'
import ShowChart from '@material-ui/icons/ShowChart'
import TopicPlot from '../TopicPlot'
import { Fade, Paper, Popper, Theme, Tooltip } from '@material-ui/core'
import { Theme, Tooltip } from '@material-ui/core'
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
import { lineChangeStyle, trimNewlineRight } from './util'
import { withStyles } from '@material-ui/styles'
@@ -15,7 +15,7 @@ interface Props {
literalPositions: Array<JsonPropertyLocation>
classes: any
className: string
messageHistory: q.MessageHistory
treeNode: q.TreeNode<any>
}
const style = (theme: Theme) => {
@@ -29,12 +29,9 @@ const style = (theme: Theme) => {
return {
icon,
iconDisabled: {
...icon,
color: theme.palette.text.disabled,
},
iconButton: {
...icon,
marginTop: '0px',
width: '16px',
height: '16px',
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) {
const { classes, literalPositions } = props
const hasEnoughDataToDisplayDiagrams = props.messageHistory.count() > 1
const literal = literalPositions[line]
let chartIcon = null
let chartPreview = null
if (literal) {
if (hasEnoughDataToDisplayDiagrams) {
chartIcon = (
<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>
)
}
chartPreview = (
<ChartPreview treeNode={props.treeNode} classes={{ icon: props.classes.iconButton }} literal={literal} />
)
}
if (change.added) {
return [chartIcon, <Add key="add" className={classes.icon} />]
return [chartPreview, <Add key="add" className={classes.icon} />]
} else if (change.removed) {
return [<Remove key="remove" className={classes.icon} />]
} else {
return [
chartIcon,
chartPreview,
<div
key="placeholder"
style={{ width: '12px', display: 'inline-block' }}

View File

@@ -12,7 +12,7 @@ import { withStyles } from '@material-ui/core'
import 'prismjs/components/prism-json'
interface Props {
messageHistory: q.MessageHistory
treeNode: q.TreeNode<any>
previous: string
current: string
nameOfCompareMessage: string
@@ -93,7 +93,7 @@ class CodeDiff extends React.Component<Props, State> {
<Gutters
className={this.props.classes.gutters}
changes={changes}
messageHistory={this.props.messageHistory}
treeNode={this.props.treeNode}
literalPositions={literalPositions}
/>
<pre className={this.props.classes.codeBlock}>{code}</pre>

View File

@@ -188,7 +188,6 @@ const mapDispatchToProps = (dispatch: any) => {
const styles = (theme: Theme) => ({
drawer: {
display: 'block' as 'block',
height: '100%',
},
badge: {
top: '3px',

View File

@@ -4,7 +4,7 @@ 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 TopicPlot from '../../TopicPlot'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { isPlottable } from '../CodeDiff/util'
import { TopicViewModel } from '../../../model/TopicViewModel'

View File

@@ -53,13 +53,7 @@ class ValuePanel extends React.Component<Props, State> {
return null
}
return (
<ValueRenderer
message={node.message}
messageHistory={node.messageHistory}
compareWith={this.props.compareMessage}
/>
)
return <ValueRenderer treeNode={node} message={node.message} compareWith={this.props.compareMessage} />
}
private renderViewOptions() {

View File

@@ -9,7 +9,7 @@ import { ValueRendererDisplayMode } from '../../../reducers/Settings'
interface Props {
message: q.Message
messageHistory: q.MessageHistory
treeNode: q.TreeNode<any>
compareWith?: q.Message
renderMode: ValueRendererDisplayMode
}
@@ -27,7 +27,7 @@ class ValueRenderer extends React.Component<Props, State> {
private renderDiff(current: string = '', previous: string = '', language?: 'json') {
return (
<CodeDiff
messageHistory={this.props.messageHistory}
treeNode={this.props.treeNode}
previous={previous}
current={current}
language={language}
@@ -65,9 +65,8 @@ class ValueRenderer extends React.Component<Props, State> {
}
public renderValue() {
const { message, messageHistory, compareWith, renderMode } = this.props
const previousMessages = messageHistory.toArray()
const { message, treeNode, compareWith, renderMode } = this.props
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
let compareMessage = compareWith || previousMessage || message
if (renderMode === 'raw') {

View File

@@ -1,9 +1,9 @@
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 PlotHistory from './PlotHistory'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './CodeDiff/util'
import PlotHistory from './Sidebar/PlotHistory'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './Sidebar/CodeDiff/util'
interface Props {
history: q.MessageHistory
@@ -38,7 +38,6 @@ function nodeDotPathToHistory(history: q.MessageHistory, dotPath: string) {
function render(props: Props) {
const data = props.dotPath ? nodeDotPathToHistory(props.history, props.dotPath) : nodeToHistory(props.history)
console.log(props.dotPath, data)
return <PlotHistory data={data} />
}

View File

@@ -107,6 +107,10 @@ 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 (

View File

@@ -15,6 +15,9 @@ const styles = (theme: Theme) => ({
width: '32px',
height: '32px',
},
label: {
marginTop: '-2px',
},
})
class CustomIconButton extends React.Component<Props, {}> {
@@ -30,7 +33,7 @@ class CustomIconButton extends React.Component<Props, {}> {
public render() {
return (
<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>
</Tooltip>
</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 { Record } from 'immutable'

View File

@@ -1,3 +1,4 @@
import { chartsReducer, ChartsState } from './Charts'
import { combineReducers } from 'redux'
import { connectionManagerReducer, ConnectionManagerState } from './ConnectionManager'
import { connectionReducer, ConnectionState } from './Connection'
@@ -13,6 +14,7 @@ export interface AppState {
tree: TreeState
settings: Record<SettingsState>
publish: PublishState
charts: ChartsState
sidebar: SidebarState
connection: ConnectionState
connectionManager: ConnectionManagerState
@@ -20,6 +22,7 @@ export interface AppState {
export default combineReducers({
globalState,
charts: chartsReducer,
publish: publishReducer,
sidebar: sidebarReducer,
connection: connectionReducer,

View File

@@ -414,7 +414,7 @@
dependencies:
"@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"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.9.2.tgz#c48cf2a11977c8b4ff539a1c91d259eaa627028d"
integrity sha512-5Fv2DQNO+GpdPZcxp2x/OQG/H19A01WlmpjVD9cKvVFmoVLOZ9LvBgSWG6pSXIU4og5fgbvGPaCV5+VGkWAEHA==
@@ -4729,7 +4729,7 @@ react-style-proptype@^3.0.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.1.1.tgz#16efe9ac8c68306f6bef59c7da5a96b4dfd9fb32"
integrity sha512-K/N1wqJ2GRP2yj3WBqEUYa0KV5fiaAWpUfU9SpHOHefeKvyrO+VrnMBML21M19QZoVbDZKmuQFHZYoMMi1xuJA==