Refactor
This commit is contained in:
@@ -1,35 +1,51 @@
|
|||||||
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 Clear from '@material-ui/icons/Clear'
|
|
||||||
import CustomIconButton from '../helper/CustomIconButton'
|
|
||||||
import TopicPlot from '../TopicPlot'
|
import TopicPlot from '../TopicPlot'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { chartActions } from '../../actions'
|
import { chartActions } from '../../actions'
|
||||||
import { ChartParameters } from '../../reducers/Charts'
|
import { ChartParameters } from '../../reducers/Charts'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { Paper, Theme, Typography, withStyles } from '@material-ui/core'
|
import { Paper } from '@material-ui/core'
|
||||||
import { SettingsButton } from './ChartSettings/SettingsButton'
|
import ChartTitle from './ChartTitle'
|
||||||
|
import { ChartActions } from './ChartActions'
|
||||||
const throttle = require('lodash.throttle')
|
const throttle = require('lodash.throttle')
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
parameters: ChartParameters
|
parameters: ChartParameters
|
||||||
tree?: q.Tree<any>
|
treeNode?: q.TreeNode<any>
|
||||||
classes: any
|
|
||||||
actions: {
|
actions: {
|
||||||
chart: typeof chartActions
|
chart: typeof chartActions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Chart(props: Props) {
|
/**
|
||||||
if (!props.tree) {
|
* Subscribes to onMessage of treeNode
|
||||||
return null
|
*/
|
||||||
}
|
function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
|
||||||
|
const [lastUpdated, setLastUpdate] = React.useState(0)
|
||||||
|
function subscribeToMessageUpdates() {
|
||||||
|
const onUpdateCallback = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
|
||||||
|
treeNode && treeNode.onMessage.subscribe(onUpdateCallback)
|
||||||
|
|
||||||
const { tree, parameters } = props
|
return function cleanup() {
|
||||||
const initialTreeNode = tree.findNode(parameters.topic)
|
treeNode && treeNode.onMessage.unsubscribe(onUpdateCallback)
|
||||||
const [treeNode, setTreeNode] = React.useState<q.TreeNode<any> | undefined>(initialTreeNode)
|
}
|
||||||
usePollingToFetchTreeNode(treeNode, tree, parameters, setTreeNode)
|
}
|
||||||
|
React.useEffect(subscribeToMessageUpdates, [treeNode])
|
||||||
|
}
|
||||||
|
|
||||||
|
function Chart(props: Props) {
|
||||||
|
const { parameters, treeNode } = props
|
||||||
|
const [freezedHistory, setHistory] = React.useState<q.MessageHistory | undefined>()
|
||||||
|
useMessageSubscriptionToUpdate(treeNode)
|
||||||
|
|
||||||
|
const togglePause = React.useCallback(() => {
|
||||||
|
if (!props.treeNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setHistory(freezedHistory ? undefined : props.treeNode.messageHistory.clone())
|
||||||
|
}, [props.treeNode, freezedHistory])
|
||||||
|
|
||||||
const onRemove = React.useCallback(() => {
|
const onRemove = React.useCallback(() => {
|
||||||
props.actions.chart.removeChart(props.parameters)
|
props.actions.chart.removeChart(props.parameters)
|
||||||
@@ -37,26 +53,23 @@ function Chart(props: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper style={{ padding: '8px' }}>
|
<Paper style={{ padding: '8px' }}>
|
||||||
<div style={{ float: 'right' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<SettingsButton parameters={parameters} />
|
<div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
|
||||||
<CustomIconButton tooltip="Remove chart" onClick={onRemove}>
|
<ChartTitle parameters={parameters} />
|
||||||
<Clear />
|
<ChartActions
|
||||||
</CustomIconButton>
|
parameters={parameters}
|
||||||
|
onRemove={onRemove}
|
||||||
|
paused={Boolean(freezedHistory)}
|
||||||
|
togglePause={togglePause}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Typography variant="caption" className={props.classes.topic}>
|
</div>
|
||||||
{parameters.dotPath ? parameters.dotPath : ''}
|
{props.treeNode ? (
|
||||||
</Typography>
|
|
||||||
<br />
|
|
||||||
<Typography variant="caption" className={props.classes.topic}>
|
|
||||||
{parameters.topic}
|
|
||||||
</Typography>
|
|
||||||
<br />
|
|
||||||
{treeNode ? (
|
|
||||||
<TopicPlot
|
<TopicPlot
|
||||||
color={props.parameters.color}
|
color={props.parameters.color}
|
||||||
interpolation={props.parameters.interpolation}
|
interpolation={props.parameters.interpolation}
|
||||||
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
|
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
|
||||||
history={treeNode.messageHistory}
|
history={freezedHistory ? freezedHistory : props.treeNode.messageHistory}
|
||||||
dotPath={parameters.dotPath}
|
dotPath={parameters.dotPath}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -66,12 +79,6 @@ function Chart(props: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
return {
|
|
||||||
tree: state.connection.tree,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: any) => {
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
return {
|
return {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -80,50 +87,7 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
export default connect(
|
||||||
mapStateToProps,
|
undefined,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(withStyles(styles)(Chart))
|
)(Chart)
|
||||||
|
|
||||||
/**
|
|
||||||
* If a node is not available when the plot is shown, keep polling until it has been created
|
|
||||||
*/
|
|
||||||
function usePollingToFetchTreeNode(
|
|
||||||
treeNode: q.TreeNode<any> | undefined,
|
|
||||||
tree: q.Tree<any>,
|
|
||||||
parameters: ChartParameters,
|
|
||||||
setTreeNode: React.Dispatch<React.SetStateAction<q.TreeNode<any> | undefined>>
|
|
||||||
) {
|
|
||||||
const [lastUpdate, setLastUpdate] = React.useState(0)
|
|
||||||
|
|
||||||
function pollForTreeNode() {
|
|
||||||
const onUpdateCallback = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
|
|
||||||
let intervalTimer: any
|
|
||||||
if (!treeNode) {
|
|
||||||
intervalTimer = setInterval(() => {
|
|
||||||
const node = tree.findNode(parameters.topic)
|
|
||||||
if (node) {
|
|
||||||
setTreeNode(node)
|
|
||||||
node.onMessage.subscribe(onUpdateCallback)
|
|
||||||
clearInterval(intervalTimer)
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
} else {
|
|
||||||
treeNode.onMessage.subscribe(onUpdateCallback)
|
|
||||||
}
|
|
||||||
return function cleanup() {
|
|
||||||
treeNode && treeNode.onMessage.unsubscribe(onUpdateCallback)
|
|
||||||
intervalTimer && clearInterval(intervalTimer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
React.useEffect(pollForTreeNode)
|
|
||||||
}
|
|
||||||
|
|||||||
26
app/src/components/ChartPanel/ChartActions.tsx
Normal file
26
app/src/components/ChartPanel/ChartActions.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import Play from '@material-ui/icons/PlayArrow'
|
||||||
|
import Pause from '@material-ui/icons/PauseCircleFilled'
|
||||||
|
import Clear from '@material-ui/icons/Clear'
|
||||||
|
import CustomIconButton from '../helper/CustomIconButton'
|
||||||
|
import { ChartParameters } from '../../reducers/Charts'
|
||||||
|
import { SettingsButton } from './ChartSettings/SettingsButton'
|
||||||
|
|
||||||
|
export function ChartActions(props: {
|
||||||
|
paused: boolean
|
||||||
|
togglePause: () => void
|
||||||
|
parameters: ChartParameters
|
||||||
|
onRemove: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<CustomIconButton tooltip={props.paused ? 'Resume chart' : 'Pause chart'} onClick={props.togglePause}>
|
||||||
|
{props.paused ? <Play /> : <Pause />}
|
||||||
|
</CustomIconButton>
|
||||||
|
<SettingsButton parameters={props.parameters} />
|
||||||
|
<CustomIconButton tooltip="Remove chart" onClick={props.onRemove}>
|
||||||
|
<Clear />
|
||||||
|
</CustomIconButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
app/src/components/ChartPanel/ChartTitle.tsx
Normal file
29
app/src/components/ChartPanel/ChartTitle.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { ChartParameters } from '../../reducers/Charts'
|
||||||
|
import { Typography, Theme, withStyles } from '@material-ui/core'
|
||||||
|
|
||||||
|
function ChartTitle(props: { parameters: ChartParameters; classes: any }) {
|
||||||
|
const { classes, parameters } = props
|
||||||
|
return (
|
||||||
|
<div style={{ flexGrow: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
|
<Typography variant="caption" className={classes.topic}>
|
||||||
|
{parameters.dotPath ? parameters.dotPath : parameters.topic}
|
||||||
|
</Typography>
|
||||||
|
<br />
|
||||||
|
<Typography variant="caption" className={classes.topic}>
|
||||||
|
{parameters.dotPath ? parameters.topic : ''}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 withStyles(styles)(ChartTitle)
|
||||||
50
app/src/components/ChartPanel/ChartWithTreeNode.tsx
Normal file
50
app/src/components/ChartPanel/ChartWithTreeNode.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import * as q from '../../../../backend/src/Model'
|
||||||
|
import * as React from 'react'
|
||||||
|
import Chart from './Chart'
|
||||||
|
import { ChartParameters } from '../../reducers/Charts'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tree?: q.Tree<any>
|
||||||
|
parameters: ChartParameters
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChartWithTreeNode(props: Props) {
|
||||||
|
const { tree, parameters } = props
|
||||||
|
if (!tree) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialTreeNode = tree.findNode(parameters.topic)
|
||||||
|
const [treeNode, setTreeNode] = React.useState<q.TreeNode<any> | undefined>(initialTreeNode)
|
||||||
|
|
||||||
|
usePollingToFetchTreeNode(treeNode, tree, parameters.topic, setTreeNode)
|
||||||
|
return <Chart treeNode={treeNode} parameters={parameters} />
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a node is not available when the plot is shown, keep polling until it has been created
|
||||||
|
*/
|
||||||
|
function usePollingToFetchTreeNode(
|
||||||
|
treeNode: q.TreeNode<any> | undefined,
|
||||||
|
tree: q.Tree<any>,
|
||||||
|
path: string,
|
||||||
|
setTreeNode: React.Dispatch<React.SetStateAction<q.TreeNode<any> | undefined>>
|
||||||
|
) {
|
||||||
|
function pollUntilTreeNodeHasBeenFound() {
|
||||||
|
let intervalTimer: any
|
||||||
|
if (!treeNode) {
|
||||||
|
intervalTimer = setInterval(() => {
|
||||||
|
const node = tree.findNode(path)
|
||||||
|
if (node) {
|
||||||
|
setTreeNode(node)
|
||||||
|
clearInterval(intervalTimer)
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
return function cleanup() {
|
||||||
|
intervalTimer && clearInterval(intervalTimer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(pollUntilTreeNodeHasBeenFound, [])
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import * as q from '../../../../backend/src/Model'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Chart from './Chart'
|
|
||||||
import ShowChart from '@material-ui/icons/ShowChart'
|
import ShowChart from '@material-ui/icons/ShowChart'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { chartActions } from '../../actions'
|
import { chartActions } from '../../actions'
|
||||||
import { ChartParameters } from '../../reducers/Charts'
|
import { ChartParameters } from '../../reducers/Charts'
|
||||||
|
import { ChartWithTreeNode } from './ChartWithTreeNode'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { Grid, Theme, Typography, withStyles } from '@material-ui/core'
|
import { Grid, Theme, Typography, withStyles } from '@material-ui/core'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
@@ -13,6 +14,7 @@ const { TransitionGroup, CSSTransition } = require('react-transition-group/esm')
|
|||||||
interface Props {
|
interface Props {
|
||||||
charts: List<ChartParameters>
|
charts: List<ChartParameters>
|
||||||
connectionId?: string
|
connectionId?: string
|
||||||
|
tree?: q.Tree<any>
|
||||||
actions: {
|
actions: {
|
||||||
chart: typeof chartActions
|
chart: typeof chartActions
|
||||||
}
|
}
|
||||||
@@ -67,7 +69,7 @@ function ChartPanel(props: Props) {
|
|||||||
classNames="example"
|
classNames="example"
|
||||||
>
|
>
|
||||||
<Grid item xs={mapWidth(chartParameters.width, spacing)}>
|
<Grid item xs={mapWidth(chartParameters.width, spacing)}>
|
||||||
<Chart parameters={chartParameters} />
|
<ChartWithTreeNode tree={props.tree} parameters={chartParameters} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
))
|
))
|
||||||
@@ -100,6 +102,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
return {
|
return {
|
||||||
charts: state.charts.get('charts'),
|
charts: state.charts.get('charts'),
|
||||||
connectionId: state.connection.connectionId,
|
connectionId: state.connection.connectionId,
|
||||||
|
tree: state.connection.tree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ function ContentView(props: Props) {
|
|||||||
setDetectedHeight(newHeight)
|
setDetectedHeight(newHeight)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const closeDrawerCompletelyIfItSitsOnTheEdge = React.useCallback(() => {
|
||||||
|
if (detectedHeight < 30) {
|
||||||
|
setHeight('100%')
|
||||||
|
}
|
||||||
|
}, [detectedHeight])
|
||||||
|
|
||||||
// Open chart panel on start and when a new chart is added but the panel is closed
|
// Open chart panel on start and when a new chart is added but the panel is closed
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const almostClosed = !isNaN(height as any) && detectedHeight < 30
|
const almostClosed = !isNaN(height as any) && detectedHeight < 30
|
||||||
@@ -48,6 +54,7 @@ function ContentView(props: Props) {
|
|||||||
pane1Style={{ maxHeight: '100%' }}
|
pane1Style={{ maxHeight: '100%' }}
|
||||||
pane2Style={{ borderTop: '1px solid #999', display: 'flex' }}
|
pane2Style={{ borderTop: '1px solid #999', display: 'flex' }}
|
||||||
onChange={setHeight}
|
onChange={setHeight}
|
||||||
|
onDragFinished={closeDrawerCompletelyIfItSitsOnTheEdge}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<ReactSplitPane
|
<ReactSplitPane
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as React from 'react'
|
|||||||
import * as q from '../../../../backend/src/Model'
|
import * as q from '../../../../backend/src/Model'
|
||||||
import CustomIconButton from '../helper/CustomIconButton'
|
import CustomIconButton from '../helper/CustomIconButton'
|
||||||
import Pause from '@material-ui/icons/PauseCircleFilled'
|
import Pause from '@material-ui/icons/PauseCircleFilled'
|
||||||
import Resume from '@material-ui/icons/PlayCircleFilled'
|
import Resume from '@material-ui/icons/PlayArrow'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
|||||||
Reference in New Issue
Block a user