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

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,