Add numeric chart panel
This commit is contained in:
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
|
||||
|
||||
rendererEvents.subscribe(event, dataSourceState => {
|
||||
console.log(dataSourceState)
|
||||
if (dataSourceState.connected) {
|
||||
const didReconnect = Boolean(getState().connection.tree)
|
||||
if (!didReconnect) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
|
||||
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: {
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: 'all' as 'all',
|
||||
},
|
||||
centered: {
|
||||
textAlign: 'center' as 'center',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
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 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' }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -188,7 +188,6 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
const styles = (theme: Theme) => ({
|
||||
drawer: {
|
||||
display: 'block' as 'block',
|
||||
height: '100%',
|
||||
},
|
||||
badge: {
|
||||
top: '3px',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
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 { Record } from 'immutable'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user