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 Pause from '@material-ui/icons/PauseCircleFilled'
import Clear from '@material-ui/icons/Clear'
@@ -11,16 +11,24 @@ export function ChartActions(props: {
togglePause: () => void
parameters: ChartParameters
onRemove: () => void
resetDataAction: () => void
}) {
const menuAnchor = useRef()
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} />
<SettingsButton menuAnchor={menuAnchor} parameters={props.parameters} resetDataAction={props.resetDataAction} />
<CustomIconButton tooltip="Remove chart" onClick={props.onRemove}>
<Clear data-test-type="RemoveChart" data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`} />
</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>
)
}

View File

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

View File

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

View File

@@ -4,9 +4,12 @@ import CustomIconButton from '../../helper/CustomIconButton'
import MoreVertIcon from '@material-ui/icons/Settings'
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 settingsRef = React.useRef()
const toggleSettings = React.useCallback(() => {
setVisible(!visible)
}, [visible])
@@ -17,10 +20,15 @@ export function SettingsButton(props: { parameters: ChartParameters }) {
return (
<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}>
<MoreVertIcon
ref={settingsRef as any}
data-test-type="ChartSettings"
data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`}
/>

View File

@@ -24,16 +24,16 @@ function Size(props: {
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
<MenuItem selected={props.chart.width === undefined} onClick={setChartWidth()}>
auto
<Typography variant="inherit">auto</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'big'} onClick={setChartWidth('big')}>
100% width
<Typography variant="inherit">100% width</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'medium'} onClick={setChartWidth('medium')}>
50% width
<Typography variant="inherit">50% width</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'small'} onClick={setChartWidth('small')}>
33% width
<Typography variant="inherit">33% width</Typography>
</MenuItem>
</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 InterpolationSettings from './InterpolationSettings'
import MoveUp from './MoveUp'
import MultiLineChart from '@material-ui/icons/MultiLineChart'
import RangeSettings from './RangeSettings'
import React, { memo } from 'react'
import Size from './Size'
import Sort from '@material-ui/icons/Sort'
import TimeRangeSettings from './TimeRangeSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem } from '@material-ui/core'
import { Menu, MenuItem, ListItemIcon, Typography } from '@material-ui/core'
function ChartSettings(props: {
open: boolean
close: () => void
resetDataAction: () => void
chart: ChartParameters
anchorEl: React.MutableRefObject<undefined>
}) {
@@ -59,19 +65,40 @@ function ChartSettings(props: {
<span>
<Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}>
<MenuItem key="range" onClick={toggleRange}>
Set range
<ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">Y-Axis range (Values)</Typography>
</MenuItem>
<MenuItem key="timeRange" onClick={toggleTimeRange}>
Time range
<ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">X-Axis range (Time)</Typography>
</MenuItem>
<MenuItem key="interpolation" onClick={toggleInterpolation}>
<ListItemIcon>
<MultiLineChart />
</ListItemIcon>{' '}
Curve interpolation
</MenuItem>
<MenuItem key="size" onClick={toggleSize}>
Size
<ListItemIcon>
<Sort />
</ListItemIcon>
<Typography variant="inherit">Size</Typography>
</MenuItem>
<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>
<MoveUp chart={props.chart} close={props.close} />
</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 React, { useState } from 'react'
import ChartTitle from './ChartTitle'
import React, { useState, useCallback, memo, useRef } from 'react'
import TopicPlot from '../TopicPlot'
import { bindActionCreators } from 'redux'
import { ChartActions } from './ChartActions'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
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')
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 {
parameters: ChartParameters
treeNode?: q.TreeNode<any>
@@ -24,14 +39,14 @@ interface Props {
*/
function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
const [lastUpdated, setLastUpdate] = useState(0)
const [messageHistory, setMessageHistory] = useState<q.MessageHistory | undefined>()
const [messageHistory, setMessageHistory] = useState<ClearableMessageBuffer | undefined>()
let amendMessageCallback: any
function subscribeToMessageUpdates() {
const throttledUpdate = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
if (treeNode) {
const newMessageHistory = treeNode.messageHistory.clone()
const newMessageHistory = ClearableMessageBuffer.fromMessageBuffer(treeNode.messageHistory)
newMessageHistory.setCapacity(500, 2 * 500 * 10000)
amendMessageCallback = (message: q.Message) => {
@@ -52,12 +67,21 @@ function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
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) {
const { parameters, treeNode } = props
const [frozenHistory, setFrozenHistory] = React.useState<q.MessageHistory | undefined>()
const [frozenHistory, setFrozenHistory] = useState<q.MessageHistory | undefined>()
const messageHistory = useMessageSubscriptionToUpdate(treeNode)
const togglePause = React.useCallback(() => {
const togglePause = useCallback(() => {
if (!treeNode) {
return
}
@@ -69,6 +93,8 @@ function TopicChart(props: Props) {
props.actions.chart.removeChart(props.parameters)
}, [props.parameters])
const resetData = useResetDataCallback(messageHistory)
return (
<Paper
style={{ padding: '8px' }}
@@ -79,6 +105,7 @@ function TopicChart(props: Props) {
<div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
<ChartTitle parameters={parameters} />
<ChartActions
resetDataAction={resetData}
parameters={parameters}
onRemove={onRemove}
paused={Boolean(frozenHistory)}
@@ -91,7 +118,7 @@ function TopicChart(props: Props) {
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : 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}
/>
</Paper>
@@ -109,4 +136,4 @@ const mapDispatchToProps = (dispatch: any) => {
export default connect(
undefined,
mapDispatchToProps
)(TopicChart)
)(memo(TopicChart))

View File

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

View File

@@ -1,15 +1,15 @@
interface Lengthwise {
export interface MemoryConsumptionExpressedByLength {
length: number
}
export class RingBuffer<T extends Lengthwise> {
private capacity: number
private maxItems: number
export class RingBuffer<T extends MemoryConsumptionExpressedByLength> {
public capacity: number
public maxItems: number
public compactionFactor: number
protected items: Array<T> = []
protected start: number = 0
protected end: 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>) {
this.capacity = capacity

View File

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