Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: virtualize rendering #798

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Added support for binary data types
- Select data type (string, json, hex, uint, int, float) for each topic individually
- Default data type is 'string'
- Show milliseconds in message received timestamp
  • Loading branch information
mhorsche committed Jan 11, 2021
commit 567f6d2d5083e77cc84033d0a4cfc097517a7a7a
4 changes: 2 additions & 2 deletions app/src/actions/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as q from '../../../backend/src/Model'
import { ActionTypes, SettingsStateModel, TopicOrder } from '../reducers/Settings'
import { ActionTypes, SettingsStateModel, TopicOrder, ValueRendererDisplayMode } from '../reducers/Settings'
import { AppState } from '../reducers'
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
Expand Down Expand Up @@ -66,7 +66,7 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat
dispatch(storeSettings())
}

export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>) => {
export const setValueDisplayMode = (valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
Expand Down
1 change: 1 addition & 0 deletions app/src/components/ChartPanel/TopicChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function TopicChart(props: Props) {
</div>
</div>
<TopicPlot
node={props.treeNode ? props.treeNode : undefined}
color={props.parameters.color}
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
Expand Down
22 changes: 11 additions & 11 deletions app/src/components/Sidebar/CodeDiff/ChartPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,24 @@ function ChartPreview(props: Props) {
/>
</Tooltip>
) : (
<Tooltip title="Add to chart panel, not enough data for preview">
<ShowChart
onClick={onClick}
className={props.classes.icon}
style={{ color: '#aaa' }}
data-test-type="ShowChart"
data-test={props.literal.path}
/>
</Tooltip>
)
<Tooltip title="Add to chart panel, not enough data for preview">
<ShowChart
onClick={onClick}
className={props.classes.icon}
style={{ color: '#aaa' }}
data-test-type="ShowChart"
data-test={props.literal.path}
/>
</Tooltip>
)

return (
<span>
{addChartToPanelButton}
<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 />}
{open ? <TopicPlot node={props.treeNode} history={props.treeNode.messageHistory} dotPath={props.literal.path} /> : <span />}
</Paper>
</Fade>
</Popper>
Expand Down
13 changes: 11 additions & 2 deletions app/src/components/Sidebar/TopicPanel/TopicPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,38 @@ import Topic from './Topic'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
import { sidebarActions } from '../../../actions'
import { TopicDeleteButton } from './TopicDeleteButton'
import { TopicTypeButton } from './TopicTypeButton'
import { sidebarActions } from '../../../actions'

const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions }) => {
const { node } = props
console.log(node && node.path())

const copyTopic = node ? <Copy value={node.path()} /> : null

const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
if (!topic) {
return
}

props.actions.clearTopic(topic, recursive)
}, [])

const setTopicType = useCallback((node?: q.TreeNode<any>, type: q.TopicDataType = 'string') => {
if (!node) {
return
}
node.type = type
}, [])

return useMemo(
() => (
<Panel disabled={!Boolean(node)}>
<span>
Topic {copyTopic}
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<TopicTypeButton node={node} setTopicType={setTopicType} />
</span>
<Topic node={node} />
</Panel>
Expand Down
83 changes: 83 additions & 0 deletions app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useCallback } from 'react'
import * as q from '../../../../../backend/src/Model'
import CustomIconButton from '../../helper/CustomIconButton'
import Code from '@material-ui/icons/Code'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import Grow from '@material-ui/core/Grow'
import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper'
import MenuItem from '@material-ui/core/MenuItem'
import MenuList from '@material-ui/core/MenuList'

const options: q.TopicDataType[] = ['string', 'json', 'hex', 'integer', 'unsigned int', 'floating point'];

export const TopicTypeButton = (props: {
node?: q.TreeNode<any>
setTopicType: (node: q.TreeNode<any>, type: q.TopicDataType) => void
}) => {
const { node } = props
if (!node || !node.message || !node.message.payload) {
return null
}

const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [open, setOpen] = React.useState(false)

const handleMenuItemClick = useCallback(
(mouseEvent: React.MouseEvent, element: q.TreeNode<any>, type: q.TopicDataType) => {
if (!element || !type) {
return
}
props.setTopicType(element, type as q.TopicDataType)
setOpen(false)
},
[props.setTopicType]
)

const handleToggle = (event: React.MouseEvent<HTMLElement>) => {
if (open === true) {
return
}
setAnchorEl(event.currentTarget)
setOpen((prevOpen) => !prevOpen)
}

const handleClose = (event: React.MouseEvent<Document, MouseEvent>) => {
if (anchorEl && anchorEl.contains(event.target as HTMLElement)) {
return
}
setOpen(false)
}

return (
<CustomIconButton tooltip="" onClick={handleToggle}>
<Code />
<Popper open={open} anchorEl={anchorEl} role={undefined} transition>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom',
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="topicTypeMode">
{options.map((option, index) => (
<MenuItem
key={option}
selected={node && option === node.type}
onClick={(event) => handleMenuItemClick(event, node, option)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</CustomIconButton>
)
}
10 changes: 5 additions & 5 deletions app/src/components/Sidebar/ValueRenderer/MessageHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
const history = node.messageHistory.toArray()
let previousMessage: q.Message | undefined = node.message
const historyElements = [...history].reverse().map((message, idx) => {
const value = message.payload ? Base64Message.toUnicodeString(message.payload) : ''
const [value, ignore] = Base64Message.format(message.payload, node.type)
const element = {
value,
key: `${message.messageNumber}-${message.received}`,
Expand All @@ -102,7 +102,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
<MessageId message={message} />
</span>
<div style={{ float: 'right' }}>
<Copy value={value} />
<Copy value={value ? value : ''} />
</div>
</span>
),
Expand All @@ -112,8 +112,8 @@ class MessageHistory extends React.PureComponent<Props, State> {
return element
})

const isMessagePlottable =
node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload))
const [value, ignore] = node.message && node.message.payload ? Base64Message.format(node.message.payload, node.type) : [null, undefined]
const isMessagePlottable = isPlottable(value)
return (
<div>
<History
Expand All @@ -131,7 +131,7 @@ class MessageHistory extends React.PureComponent<Props, State> {
}
onClick={this.displayMessage}
>
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null}
{isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
</History>
</div>
)
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ function ValuePanel(props: Props) {
[compareMessage]
)

const copyValue =
node && node.message && node.message.payload ? (
<Copy value={Base64Message.toUnicodeString(node.message.payload)} />
) : null
const [value, ignore] = node && node.message && node.message.payload ? Base64Message.format(node.message.payload, node.type) : [null, undefined]
const copyValue = value ? (
<Copy value={value} />
) : null

return (
<Panel>
Expand Down
69 changes: 29 additions & 40 deletions app/src/components/Sidebar/ValueRenderer/ValueRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import CodeDiff from '../CodeDiff'
import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import { Typography, Fade, Grow } from '@material-ui/core'
import { Fade } from '@material-ui/core'

interface Props {
message: q.Message
Expand Down Expand Up @@ -38,44 +37,42 @@ class ValueRenderer extends React.Component<Props, State> {
)
}

private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] {
if (!msg) {
return [undefined, undefined]
private renderDiffMode(message: q.Message, treeNode: q.TreeNode<any>, compare?: q.Message) {
if (!message.payload) {
return
}

const str = Base64Message.toUnicodeString(msg)
try {
JSON.parse(str)
} catch (error) {
return [str, undefined]
}
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compare || previousMessage || message

return [this.messageToPrettyJson(str), 'json']
}
const compareValue = compareMessage.payload || message.payload
const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type)
const [compareStr, compareType] = Base64Message.format(compareValue, treeNode.type)

private messageToPrettyJson(str: string): string | undefined {
try {
const json = JSON.parse(str)
return JSON.stringify(json, undefined, ' ')
} catch {
return undefined
}
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined

return (
<div>
{this.renderDiff(currentStr, compareStr, undefined, language)}
</div>
)
}

private renderRawMode(message: q.Message, compare?: q.Message) {
private renderRawMode(message: q.Message, treeNode: q.TreeNode<any>, compare?: q.Message) {
if (!message.payload) {
return
}
const [value, valueLanguage] = this.convertMessage(message.payload)
const [compareStr, compareStrLanguage] =
compare && compare.payload ? this.convertMessage(compare.payload) : [undefined, undefined]

const [currentStr, currentType] = Base64Message.format(message.payload, treeNode.type)
const [compareStr, compareType] = compare && compare.payload ? Base64Message.format(compare.payload, treeNode.type) : [undefined, undefined]

return (
<div>
{this.renderDiff(value, value, undefined, valueLanguage)}
{this.renderDiff(currentStr, currentStr, undefined, currentType)}
<Fade in={Boolean(compareStr)} timeout={400}>
<div>
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null}
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareType) : null}
</div>
</Fade>
</div>
Expand All @@ -88,24 +85,16 @@ class ValueRenderer extends React.Component<Props, State> {

public renderValue() {
const { message, treeNode, compareWith, renderMode } = this.props
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compareWith || previousMessage || message

if (renderMode === 'raw') {
return this.renderRawMode(message, compareWith)
}
if (!message.payload) {
return null
}

const compareValue = compareMessage.payload || message.payload
const [current, currentLanguage] = this.convertMessage(message.payload)
const [compare, compareLanguage] = this.convertMessage(compareValue)

const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined

return this.renderDiff(current, compare, undefined, language)
switch (renderMode) {
case 'diff':
return this.renderDiffMode(message, treeNode, compareWith)
default:
return this.renderRawMode(message, treeNode, compareWith)
}
}
}

Expand Down
14 changes: 8 additions & 6 deletions app/src/components/TopicPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PlotCurveTypes } from '../reducers/Charts'
const parseDuration = require('parse-duration')

interface Props {
node?: q.TreeNode<any>
history: q.MessageHistory
dotPath?: string
timeInterval?: string
Expand All @@ -25,22 +26,23 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
return data
}

function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory, type: q.TopicDataType) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN
const [value, ignore] = message.payload ? Base64Message.format(message.payload, type) : [NaN, undefined]
// const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN
return { x: message.received.getTime(), y: toPlottableValue(value) }
})
.filter(data => !isNaN(data.y as any)) as any
}

function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string, type: q.TopicDataType) {
return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => {
let json = {}
try {
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {}
} catch (ignore) {}
} catch (ignore) { }

const value = dotProp.get(json, dotPath)

Expand All @@ -54,8 +56,8 @@ function TopicPlot(props: Props) {
const data = React.useMemo(
() =>
props.dotPath
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
: nodeToHistory(startOffset, props.history),
? nodeDotPathToHistory(startOffset, props.history, props.dotPath, props.node ? props.node.type : 'string')
: nodeToHistory(startOffset, props.history, props.node ? props.node.type : 'string'),
[props.history.last(), startOffset, props.dotPath]
)

Expand Down
Loading