Add clear chart button and improve chart menu look&feel

This commit is contained in:
Thomas Nordquist
2019-07-17 12:59:25 +02:00
parent 1c52aced63
commit 8ae1528064
10 changed files with 122 additions and 48 deletions

View File

@@ -1,4 +1,4 @@
import * as React from 'react' import React, { useRef } from 'react'
import Play from '@material-ui/icons/PlayArrow' import Play from '@material-ui/icons/PlayArrow'
import Pause from '@material-ui/icons/PauseCircleFilled' import Pause from '@material-ui/icons/PauseCircleFilled'
import Clear from '@material-ui/icons/Clear' import Clear from '@material-ui/icons/Clear'
@@ -11,16 +11,24 @@ export function ChartActions(props: {
togglePause: () => void togglePause: () => void
parameters: ChartParameters parameters: ChartParameters
onRemove: () => void onRemove: () => void
resetDataAction: () => void
}) { }) {
const menuAnchor = useRef()
return ( return (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<CustomIconButton tooltip={props.paused ? 'Resume chart' : 'Pause chart'} onClick={props.togglePause}> <CustomIconButton tooltip={props.paused ? 'Resume chart' : 'Pause chart'} onClick={props.togglePause}>
{props.paused ? <Play /> : <Pause />} {props.paused ? <Play /> : <Pause />}
</CustomIconButton> </CustomIconButton>
<SettingsButton parameters={props.parameters} /> <SettingsButton menuAnchor={menuAnchor} parameters={props.parameters} resetDataAction={props.resetDataAction} />
<CustomIconButton tooltip="Remove chart" onClick={props.onRemove}> <CustomIconButton tooltip="Remove chart" onClick={props.onRemove}>
<Clear data-test-type="RemoveChart" data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`} /> <Clear data-test-type="RemoveChart" data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`} />
</CustomIconButton> </CustomIconButton>
<div style={{ width: 0, overflow: 'hidden' }}>
{/* Helper element to provide an anchor element for the menu,
* so the menu prefers not to overlap with the chart
*/}
<div style={{ marginLeft: '11px' }} ref={menuAnchor as any} />
</div>
</div> </div>
) )
} }

View File

@@ -1,9 +1,10 @@
import * as React from 'react' import * as React from 'react'
import { ChartParameters } from '../../../reducers/Charts' import ArrowUpward from '@material-ui/icons/ArrowUpward'
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions' import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { MenuItem, Typography, ListItemIcon } from '@material-ui/core'
function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartParameters; close: () => void }) { function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartParameters; close: () => void }) {
const moveUp = React.useCallback(() => { const moveUp = React.useCallback(() => {
@@ -16,7 +17,10 @@ function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartPa
return ( return (
<MenuItem key="size" onClick={moveUp}> <MenuItem key="size" onClick={moveUp}>
Move up <ListItemIcon>
<ArrowUpward />
</ListItemIcon>
<Typography variant="inherit">Move up</Typography>
</MenuItem> </MenuItem>
) )
} }

View File

@@ -56,8 +56,8 @@ function RangeSettings(props: Props) {
onClose={props.onClose} onClose={props.onClose}
onKeyDownCapture={handleKeyEvents} onKeyDownCapture={handleKeyEvents}
> >
<Typography>Define custom ranges for the Y-Axis</Typography> <div style={{ padding: '0 16px', width: '275px' }}>
<div style={{ padding: '0 16px' }}> <Typography>Define custom ranges for the Y-Axis</Typography>
<TextField <TextField
inputProps={{ inputProps={{
ref: rangeFromRef, ref: rangeFromRef,

View File

@@ -4,9 +4,12 @@ import CustomIconButton from '../../helper/CustomIconButton'
import MoreVertIcon from '@material-ui/icons/Settings' import MoreVertIcon from '@material-ui/icons/Settings'
import { ChartParameters } from '../../../reducers/Charts' import { ChartParameters } from '../../../reducers/Charts'
export function SettingsButton(props: { parameters: ChartParameters }) { export function SettingsButton(props: {
parameters: ChartParameters
resetDataAction: () => void
menuAnchor: React.MutableRefObject<undefined>
}) {
const [visible, setVisible] = React.useState(false) const [visible, setVisible] = React.useState(false)
const settingsRef = React.useRef()
const toggleSettings = React.useCallback(() => { const toggleSettings = React.useCallback(() => {
setVisible(!visible) setVisible(!visible)
}, [visible]) }, [visible])
@@ -17,10 +20,15 @@ export function SettingsButton(props: { parameters: ChartParameters }) {
return ( return (
<span> <span>
<ChartSettings open={visible} close={close} anchorEl={settingsRef} chart={props.parameters} /> <ChartSettings
open={visible}
close={close}
anchorEl={props.menuAnchor}
chart={props.parameters}
resetDataAction={props.resetDataAction}
/>
<CustomIconButton tooltip="Chart settings" onClick={toggleSettings}> <CustomIconButton tooltip="Chart settings" onClick={toggleSettings}>
<MoreVertIcon <MoreVertIcon
ref={settingsRef as any}
data-test-type="ChartSettings" data-test-type="ChartSettings"
data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`} data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`}
/> />

View File

@@ -24,16 +24,16 @@ function Size(props: {
return ( return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}> <Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
<MenuItem selected={props.chart.width === undefined} onClick={setChartWidth()}> <MenuItem selected={props.chart.width === undefined} onClick={setChartWidth()}>
auto <Typography variant="inherit">auto</Typography>
</MenuItem> </MenuItem>
<MenuItem selected={props.chart.width === 'big'} onClick={setChartWidth('big')}> <MenuItem selected={props.chart.width === 'big'} onClick={setChartWidth('big')}>
100% width <Typography variant="inherit">100% width</Typography>
</MenuItem> </MenuItem>
<MenuItem selected={props.chart.width === 'medium'} onClick={setChartWidth('medium')}> <MenuItem selected={props.chart.width === 'medium'} onClick={setChartWidth('medium')}>
50% width <Typography variant="inherit">50% width</Typography>
</MenuItem> </MenuItem>
<MenuItem selected={props.chart.width === 'small'} onClick={setChartWidth('small')}> <MenuItem selected={props.chart.width === 'small'} onClick={setChartWidth('small')}>
33% width <Typography variant="inherit">33% width</Typography>
</MenuItem> </MenuItem>
</Menu> </Menu>
) )

View File

@@ -1,16 +1,22 @@
import * as React from 'react' import BarChart from '@material-ui/icons/BarChart'
import Clear from '@material-ui/icons/Refresh'
import ColorLens from '@material-ui/icons/ColorLens'
import ColorSettings from './ColorSettings' import ColorSettings from './ColorSettings'
import InterpolationSettings from './InterpolationSettings' import InterpolationSettings from './InterpolationSettings'
import MoveUp from './MoveUp' import MoveUp from './MoveUp'
import MultiLineChart from '@material-ui/icons/MultiLineChart'
import RangeSettings from './RangeSettings' import RangeSettings from './RangeSettings'
import React, { memo } from 'react'
import Size from './Size' import Size from './Size'
import Sort from '@material-ui/icons/Sort'
import TimeRangeSettings from './TimeRangeSettings' import TimeRangeSettings from './TimeRangeSettings'
import { ChartParameters } from '../../../reducers/Charts' import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem } from '@material-ui/core' import { Menu, MenuItem, ListItemIcon, Typography } from '@material-ui/core'
function ChartSettings(props: { function ChartSettings(props: {
open: boolean open: boolean
close: () => void close: () => void
resetDataAction: () => void
chart: ChartParameters chart: ChartParameters
anchorEl: React.MutableRefObject<undefined> anchorEl: React.MutableRefObject<undefined>
}) { }) {
@@ -59,19 +65,40 @@ function ChartSettings(props: {
<span> <span>
<Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}> <Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}>
<MenuItem key="range" onClick={toggleRange}> <MenuItem key="range" onClick={toggleRange}>
Set range <ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">Y-Axis range (Values)</Typography>
</MenuItem> </MenuItem>
<MenuItem key="timeRange" onClick={toggleTimeRange}> <MenuItem key="timeRange" onClick={toggleTimeRange}>
Time range <ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">X-Axis range (Time)</Typography>
</MenuItem> </MenuItem>
<MenuItem key="interpolation" onClick={toggleInterpolation}> <MenuItem key="interpolation" onClick={toggleInterpolation}>
<ListItemIcon>
<MultiLineChart />
</ListItemIcon>{' '}
Curve interpolation Curve interpolation
</MenuItem> </MenuItem>
<MenuItem key="size" onClick={toggleSize}> <MenuItem key="size" onClick={toggleSize}>
Size <ListItemIcon>
<Sort />
</ListItemIcon>
<Typography variant="inherit">Size</Typography>
</MenuItem> </MenuItem>
<MenuItem key="color" onClick={toggleColor}> <MenuItem key="color" onClick={toggleColor}>
Color <ListItemIcon>
<ColorLens />
</ListItemIcon>
<Typography variant="inherit">Color</Typography>
</MenuItem>
<MenuItem key="clear" onClick={props.resetDataAction}>
<ListItemIcon>
<Clear />
</ListItemIcon>
<Typography variant="inherit">Clear data</Typography>
</MenuItem> </MenuItem>
<MoveUp chart={props.chart} close={props.close} /> <MoveUp chart={props.chart} close={props.close} />
</Menu> </Menu>
@@ -94,4 +121,4 @@ function ChartSettings(props: {
) )
} }
export default ChartSettings export default memo(ChartSettings)

View File

@@ -1,16 +1,31 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import React, { useState } from 'react' import ChartTitle from './ChartTitle'
import React, { useState, useCallback, memo, useRef } from 'react'
import TopicPlot from '../TopicPlot' import TopicPlot from '../TopicPlot'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { ChartActions } from './ChartActions'
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 } from '@material-ui/core' import { Paper } from '@material-ui/core'
import ChartTitle from './ChartTitle'
import { ChartActions } from './ChartActions'
import { RingBuffer } from '../../../../backend/src/Model'
const throttle = require('lodash.throttle') const throttle = require('lodash.throttle')
class ClearableMessageBuffer extends q.RingBuffer<q.Message> {
public clear() {
this.items = []
this.start = 0
this.end = 0
}
public static fromMessageBuffer(buffer: q.RingBuffer<q.Message>): ClearableMessageBuffer {
return new ClearableMessageBuffer(buffer.capacity, buffer.maxItems, buffer.compactionFactor, buffer)
}
public clone(): ClearableMessageBuffer {
return ClearableMessageBuffer.fromMessageBuffer(this)
}
}
interface Props { interface Props {
parameters: ChartParameters parameters: ChartParameters
treeNode?: q.TreeNode<any> treeNode?: q.TreeNode<any>
@@ -24,14 +39,14 @@ interface Props {
*/ */
function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) { function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
const [lastUpdated, setLastUpdate] = useState(0) const [lastUpdated, setLastUpdate] = useState(0)
const [messageHistory, setMessageHistory] = useState<q.MessageHistory | undefined>() const [messageHistory, setMessageHistory] = useState<ClearableMessageBuffer | undefined>()
let amendMessageCallback: any let amendMessageCallback: any
function subscribeToMessageUpdates() { function subscribeToMessageUpdates() {
const throttledUpdate = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300) const throttledUpdate = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
if (treeNode) { if (treeNode) {
const newMessageHistory = treeNode.messageHistory.clone() const newMessageHistory = ClearableMessageBuffer.fromMessageBuffer(treeNode.messageHistory)
newMessageHistory.setCapacity(500, 2 * 500 * 10000) newMessageHistory.setCapacity(500, 2 * 500 * 10000)
amendMessageCallback = (message: q.Message) => { amendMessageCallback = (message: q.Message) => {
@@ -52,12 +67,21 @@ function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
return messageHistory return messageHistory
} }
function useResetDataCallback(messageHistory: ClearableMessageBuffer | undefined) {
const [lastUpdated, setLastUpdate] = useState(0)
return React.useCallback(() => {
messageHistory && messageHistory.clear()
setLastUpdate(Date.now())
}, [messageHistory])
}
function TopicChart(props: Props) { function TopicChart(props: Props) {
const { parameters, treeNode } = props const { parameters, treeNode } = props
const [frozenHistory, setFrozenHistory] = React.useState<q.MessageHistory | undefined>() const [frozenHistory, setFrozenHistory] = useState<q.MessageHistory | undefined>()
const messageHistory = useMessageSubscriptionToUpdate(treeNode) const messageHistory = useMessageSubscriptionToUpdate(treeNode)
const togglePause = React.useCallback(() => { const togglePause = useCallback(() => {
if (!treeNode) { if (!treeNode) {
return return
} }
@@ -69,6 +93,8 @@ function TopicChart(props: Props) {
props.actions.chart.removeChart(props.parameters) props.actions.chart.removeChart(props.parameters)
}, [props.parameters]) }, [props.parameters])
const resetData = useResetDataCallback(messageHistory)
return ( return (
<Paper <Paper
style={{ padding: '8px' }} style={{ padding: '8px' }}
@@ -79,6 +105,7 @@ function TopicChart(props: Props) {
<div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}> <div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
<ChartTitle parameters={parameters} /> <ChartTitle parameters={parameters} />
<ChartActions <ChartActions
resetDataAction={resetData}
parameters={parameters} parameters={parameters}
onRemove={onRemove} onRemove={onRemove}
paused={Boolean(frozenHistory)} paused={Boolean(frozenHistory)}
@@ -91,7 +118,7 @@ function TopicChart(props: Props) {
interpolation={props.parameters.interpolation} interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined} timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
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={frozenHistory || messageHistory || new RingBuffer<q.Message>(1)} history={frozenHistory || messageHistory || new ClearableMessageBuffer(1)}
dotPath={parameters.dotPath} dotPath={parameters.dotPath}
/> />
</Paper> </Paper>
@@ -109,4 +136,4 @@ const mapDispatchToProps = (dispatch: any) => {
export default connect( export default connect(
undefined, undefined,
mapDispatchToProps mapDispatchToProps
)(TopicChart) )(memo(TopicChart))

View File

@@ -7,7 +7,7 @@ function ConfirmationDialog(props: { confirmationRequests: Array<ConfirmationReq
const request = props.confirmationRequests[0] const request = props.confirmationRequests[0]
const yesRef = useRef<HTMLButtonElement>() const yesRef = useRef<HTMLButtonElement>()
const noRef = useRef<HTMLButtonElement>() const noRef = useRef<HTMLButtonElement>()
const arrowKeyHandler = useCallback((event: KeyboardEvent) => { const arrowKeyHandler = useCallback((event: React.KeyboardEvent) => {
const isArrowKey = event.keyCode === KeyCodes.arrow_left || event.keyCode === KeyCodes.arrow_right const isArrowKey = event.keyCode === KeyCodes.arrow_left || event.keyCode === KeyCodes.arrow_right
if (!isArrowKey) { if (!isArrowKey) {
return return
@@ -45,10 +45,10 @@ function ConfirmationDialog(props: { confirmationRequests: Array<ConfirmationReq
<DialogContentText id="alert-dialog-description">{request.inquiry}</DialogContentText> <DialogContentText id="alert-dialog-description">{request.inquiry}</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button ref={yesRef} variant="contained" onClick={confirm} color="primary" autoFocus> <Button ref={yesRef as any} variant="contained" onClick={confirm} color="primary" autoFocus>
Yes Yes
</Button> </Button>
<Button ref={noRef} variant="contained" onClick={reject} color="secondary"> <Button ref={noRef as any} variant="contained" onClick={reject} color="secondary">
No No
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -1,15 +1,15 @@
interface Lengthwise { export interface MemoryConsumptionExpressedByLength {
length: number length: number
} }
export class RingBuffer<T extends Lengthwise> { export class RingBuffer<T extends MemoryConsumptionExpressedByLength> {
private capacity: number public capacity: number
private maxItems: number public maxItems: number
public compactionFactor: number
protected items: Array<T> = []
protected start: number = 0
protected end: number = 0
private usage: number = 0 private usage: number = 0
private items: Array<T> = []
private start: number = 0
private end: number = 0
private compactionFactor: number
constructor(capacity: number, maxItems = Infinity, compactionFactor: number = 10, ringBuffer?: RingBuffer<T>) { constructor(capacity: number, maxItems = Infinity, compactionFactor: number = 10, ringBuffer?: RingBuffer<T>) {
this.capacity = capacity this.capacity = capacity

View File

@@ -52,10 +52,6 @@ export class TreeNode<ViewModel extends Destroyable> {
return this.sourceEdge ? this.sourceEdge.source || undefined : undefined return this.sourceEdge ? this.sourceEdge.source || undefined : undefined
} }
public hasMessage() {
return this.message && this.message.value && this.message.value.length !== 0
}
private isTopicEmptyLeaf() { private isTopicEmptyLeaf() {
return !this.hasMessage() && this.isLeaf() return !this.hasMessage() && this.isLeaf()
} }
@@ -108,6 +104,10 @@ export class TreeNode<ViewModel extends Destroyable> {
} }
} }
public hasMessage() {
return this.message && this.message.value && this.message.value.length !== 0
}
public destroy() { public destroy() {
this.onDestroy.dispatch(this) this.onDestroy.dispatch(this)
this.onDestroy.removeAllListeners() this.onDestroy.removeAllListeners()