chore: refactor
This commit is contained in:
@@ -7,7 +7,7 @@ import { connect } from 'react-redux'
|
||||
import { connectionManagerActions } from '../../../actions'
|
||||
import { ConnectionOptions } from '../../../model/ConnectionOptions'
|
||||
import { KeyCodes } from '../../../utils/KeyCodes'
|
||||
import { List, ListSubheader } from '@material-ui/core'
|
||||
import { List } from '@material-ui/core'
|
||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
|
||||
@@ -35,7 +34,7 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
|
||||
<Topic node={node} />
|
||||
</Panel>
|
||||
),
|
||||
[node, node && node.childTopicCount()]
|
||||
[node, node?.childTopicCount()]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,6 @@ import WarningRounded from '@material-ui/icons/WarningRounded'
|
||||
import { IDecoder, decoders } from '../../../../../backend/src/Model/sparkplugb'
|
||||
import { Tooltip } from '@material-ui/core'
|
||||
|
||||
// const options: q.TopicDataType[] = ['json', 'string', 'hex', 'integer', 'unsigned int', 'floating point']
|
||||
const options: q.TopicDataType[] = [
|
||||
'json',
|
||||
'string',
|
||||
'hex',
|
||||
'uint8',
|
||||
'uint16',
|
||||
'uint32',
|
||||
'uint64',
|
||||
'int8',
|
||||
'int16',
|
||||
'int32',
|
||||
'int64',
|
||||
'float',
|
||||
'double',
|
||||
]
|
||||
|
||||
export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
|
||||
const { node } = props
|
||||
if (!node || !node.message || !node.message.payload) {
|
||||
@@ -39,34 +22,40 @@ export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const selectOption = useCallback((decoder: IDecoder, format: string) => {
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
node.decoder = decoder
|
||||
node.decoderFormat = format
|
||||
setOpen(false)
|
||||
}, [])
|
||||
const selectOption = useCallback(
|
||||
(decoder: IDecoder, format: string) => {
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleToggle = (event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation()
|
||||
if (open === true) {
|
||||
return
|
||||
}
|
||||
setAnchorEl(event.currentTarget)
|
||||
setOpen(prevOpen => !prevOpen)
|
||||
}
|
||||
node.viewModel.decoder = { decoder, format }
|
||||
setOpen(false)
|
||||
},
|
||||
[node]
|
||||
)
|
||||
|
||||
const handleClose = (event: React.MouseEvent<Document, MouseEvent>) => {
|
||||
const handleToggle = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation()
|
||||
if (open === true) {
|
||||
return
|
||||
}
|
||||
setAnchorEl(event.currentTarget)
|
||||
setOpen(prevOpen => !prevOpen)
|
||||
},
|
||||
[open]
|
||||
)
|
||||
|
||||
const handleClose = useCallback((event: React.MouseEvent<Document, MouseEvent>) => {
|
||||
if (anchorEl && anchorEl.contains(event.target as HTMLElement)) {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Button onClick={handleToggle}>
|
||||
{props.node?.decoderFormat ?? props.node?.type}
|
||||
{props.node?.viewModel.decoder?.format ?? props.node?.type}
|
||||
<Popper open={open} anchorEl={anchorEl} role={undefined} transition>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
|
||||
@@ -5,7 +5,6 @@ import Copy from '../../helper/Copy'
|
||||
import DateFormatter from '../../helper/DateFormatter'
|
||||
import History from '../HistoryDrawer'
|
||||
import TopicPlot from '../../TopicPlot'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { isPlottable } from '../CodeDiff/util'
|
||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||
import { bindActionCreators } from 'redux'
|
||||
@@ -13,6 +12,8 @@ import { chartActions } from '../../../actions'
|
||||
import { connect } from 'react-redux'
|
||||
import CustomIconButton from '../../helper/CustomIconButton'
|
||||
import { MessageId } from '../MessageId'
|
||||
import { useSubscription } from '../../hooks/useSubscription'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
|
||||
const throttle = require('lodash.throttle')
|
||||
|
||||
@@ -25,119 +26,100 @@ interface Props {
|
||||
}
|
||||
}
|
||||
|
||||
interface State {
|
||||
displayMessage?: q.Message
|
||||
anchorEl?: HTMLElement
|
||||
lastUpdate: number
|
||||
}
|
||||
export const MessageHistory: React.FC<Props> = props => {
|
||||
const [, setLastUpdate] = React.useState(Date.now())
|
||||
const updateNodeThrottled = React.useCallback(
|
||||
throttle(() => {
|
||||
setLastUpdate
|
||||
}, 300),
|
||||
[]
|
||||
)
|
||||
|
||||
class MessageHistory extends React.PureComponent<Props, State> {
|
||||
private updateNode = throttle(() => {
|
||||
this.setState({ lastUpdate: Date.now() })
|
||||
}, 300)
|
||||
useSubscription(props.node?.onMessage, updateNodeThrottled)
|
||||
const decodeMessage = useDecoder(props.node)
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { lastUpdate: 0 }
|
||||
}
|
||||
|
||||
private addNodeToCharts = (event: React.MouseEvent) => {
|
||||
function addNodeToCharts(event: React.MouseEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const { node } = this.props
|
||||
const { node } = props
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.props.actions.charts.addChart({ topic: node.path() })
|
||||
props.actions.charts.addChart({ topic: node.path() })
|
||||
}
|
||||
|
||||
private displayMessage = (index: number, eventTarget: EventTarget) => {
|
||||
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index]
|
||||
function displayMessage(index: number, eventTarget: EventTarget) {
|
||||
const message = props.node && props.node.messageHistory.toArray().reverse()[index]
|
||||
if (message) {
|
||||
this.props.onSelect(message)
|
||||
props.onSelect(message)
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
|
||||
nextProps.node && nextProps.node.onMessage.subscribe(this.updateNode)
|
||||
const { node } = props
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.node && this.props.node.onMessage.subscribe(this.updateNode)
|
||||
}
|
||||
const history = node.messageHistory.toArray()
|
||||
let previousMessage: q.Message | undefined = node.message
|
||||
const historyElements = [...history].reverse().map((message, idx) => {
|
||||
const value = node.message ? decodeMessage(node.message)?.format()[0] ?? null : null
|
||||
|
||||
public componentWillUnMount() {
|
||||
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { node } = this.props
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
const history = node.messageHistory.toArray()
|
||||
let previousMessage: q.Message | undefined = node.message
|
||||
const historyElements = [...history].reverse().map((message, idx) => {
|
||||
const value = node.message ? node.decodeMessage(node.message)?.format()[0] ?? null : null
|
||||
|
||||
const element = {
|
||||
value: value ?? '',
|
||||
key: `${message.messageNumber}-${message.received}`,
|
||||
title: (
|
||||
const element = {
|
||||
value: value ?? '',
|
||||
key: `${message.messageNumber}-${message.received}`,
|
||||
title: (
|
||||
<span>
|
||||
<div style={{ float: 'left' }}>
|
||||
<DateFormatter date={message.received} />
|
||||
{previousMessage && previousMessage !== message ? (
|
||||
<i>
|
||||
(-
|
||||
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
|
||||
</i>
|
||||
) : null}
|
||||
</div>
|
||||
<span>
|
||||
<div style={{ float: 'left' }}>
|
||||
<DateFormatter date={message.received} />
|
||||
{previousMessage && previousMessage !== message ? (
|
||||
<i>
|
||||
(-
|
||||
<DateFormatter date={message.received} intervalSince={previousMessage.received} />)
|
||||
</i>
|
||||
) : null}
|
||||
</div>
|
||||
<span>
|
||||
|
||||
<MessageId message={message} />
|
||||
</span>
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value ?? ''} />
|
||||
</div>
|
||||
|
||||
<MessageId message={message} />
|
||||
</span>
|
||||
),
|
||||
selected: message && message === this.props.selected,
|
||||
}
|
||||
previousMessage = message
|
||||
return element
|
||||
})
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value ?? ''} />
|
||||
</div>
|
||||
</span>
|
||||
),
|
||||
selected: message && message === props.selected,
|
||||
}
|
||||
previousMessage = message
|
||||
return element
|
||||
})
|
||||
|
||||
const value = node.message ? node.decodeMessage(node.message)?.format()[0] ?? null : null
|
||||
const value = node.message ? decodeMessage(node.message)?.format()[0] ?? null : null
|
||||
|
||||
const isMessagePlottable = isPlottable(value)
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
items={historyElements}
|
||||
contentTypeIndicator={
|
||||
isMessagePlottable ? (
|
||||
<CustomIconButton
|
||||
style={{ height: '22px', width: '22px' }}
|
||||
onClick={this.addNodeToCharts}
|
||||
tooltip="Add to chart panel"
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : undefined
|
||||
}
|
||||
onClick={this.displayMessage}
|
||||
>
|
||||
{isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
|
||||
</History>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const isMessagePlottable = isPlottable(value)
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
items={historyElements}
|
||||
contentTypeIndicator={
|
||||
isMessagePlottable ? (
|
||||
<CustomIconButton
|
||||
style={{ height: '22px', width: '22px' }}
|
||||
onClick={addNodeToCharts}
|
||||
tooltip="Add to chart panel"
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : undefined
|
||||
}
|
||||
onClick={displayMessage}
|
||||
>
|
||||
{isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
|
||||
</History>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
@@ -146,4 +128,4 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(MessageHistory)
|
||||
export default connect(null, mapDispatchToProps)(React.memo(MessageHistory))
|
||||
|
||||
@@ -7,13 +7,13 @@ import Panel from '../Panel'
|
||||
import React, { useCallback } from 'react'
|
||||
import ValueRenderer from './ValueRenderer'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Theme, Typography, withStyles } from '@material-ui/core'
|
||||
import { connect } from 'react-redux'
|
||||
import { sidebarActions } from '../../../actions'
|
||||
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
|
||||
import { MessageId } from '../MessageId'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
|
||||
interface Props {
|
||||
node?: q.TreeNode<any>
|
||||
@@ -35,6 +35,7 @@ function RenderedValue(props: { node?: q.TreeNode<any>; compareMessage?: q.Messa
|
||||
|
||||
function ValuePanel(props: Props) {
|
||||
const { node, compareMessage } = props
|
||||
const decodeMessage = useDecoder(node)
|
||||
|
||||
function renderViewOptions() {
|
||||
if (!props.node || !props.node.message) {
|
||||
@@ -54,6 +55,10 @@ function ValuePanel(props: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
const getDecodedValue = useCallback(() => {
|
||||
return node?.message && decodeMessage(node.message)?.toUnicodeString()
|
||||
}, [node, decodeMessage])
|
||||
|
||||
function messageMetaInfo() {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
@@ -87,7 +92,7 @@ function ValuePanel(props: Props) {
|
||||
|
||||
const [value] =
|
||||
node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
|
||||
const copyValue = value ? <Copy value={value} /> : null
|
||||
const copyValue = value ? <Copy getValue={getDecodedValue} /> : null
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { connect } from 'react-redux'
|
||||
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
|
||||
import { Fade } from '@material-ui/core'
|
||||
import { Decoder } from '../../../../../backend/src/Model/Decoder'
|
||||
import { DecoderFunction, useDecoder } from '../../hooks/useDecoder'
|
||||
|
||||
interface Props {
|
||||
message: q.Message
|
||||
@@ -14,30 +15,28 @@ interface Props {
|
||||
renderMode: ValueRendererDisplayMode
|
||||
}
|
||||
|
||||
interface State {
|
||||
width: number
|
||||
}
|
||||
export const ValueRenderer: React.FC<Props> = props => {
|
||||
const decodeMessage = useDecoder(props.treeNode)
|
||||
|
||||
class ValueRenderer extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { width: 0 }
|
||||
}
|
||||
|
||||
private renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') {
|
||||
function renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') {
|
||||
return (
|
||||
<CodeDiff
|
||||
treeNode={this.props.treeNode}
|
||||
treeNode={props.treeNode}
|
||||
previous={previous}
|
||||
current={current}
|
||||
title={title}
|
||||
language={language}
|
||||
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
|
||||
nameOfCompareMessage={props.compareWith ? 'selected' : 'previous'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private renderDiffMode(message: q.Message, treeNode: q.TreeNode<any>, compare?: q.Message) {
|
||||
function renderDiffMode(
|
||||
decodeMessage: DecoderFunction,
|
||||
message: q.Message,
|
||||
treeNode: q.TreeNode<any>,
|
||||
compare?: q.Message
|
||||
) {
|
||||
if (!message.payload) {
|
||||
return
|
||||
}
|
||||
@@ -46,55 +45,58 @@ class ValueRenderer extends React.Component<Props, State> {
|
||||
const previousMessage = previousMessages[previousMessages.length - 2]
|
||||
const compareMessage = compare || previousMessage || message
|
||||
|
||||
const [currentStr, currentType] = treeNode.decodeMessage(message)?.format(treeNode.type) ?? []
|
||||
const [compareStr, compareType] = treeNode.decodeMessage(compareMessage)?.format(treeNode.type) ?? []
|
||||
const [currentStr, currentType] = decodeMessage(message)?.format(treeNode.type) ?? []
|
||||
const [compareStr, compareType] = decodeMessage(compareMessage)?.format(treeNode.type) ?? []
|
||||
|
||||
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined
|
||||
|
||||
return <div>{this.renderDiff(currentStr, compareStr, undefined, language)}</div>
|
||||
return <div>{renderDiff(currentStr, compareStr, undefined, language)}</div>
|
||||
}
|
||||
|
||||
private renderRawMode(message: q.Message, treeNode: q.TreeNode<any>, compare?: q.Message) {
|
||||
function renderRawMode(
|
||||
decodeMessage: DecoderFunction,
|
||||
message: q.Message,
|
||||
treeNode: q.TreeNode<any>,
|
||||
compare?: q.Message
|
||||
) {
|
||||
if (!message.payload) {
|
||||
return
|
||||
}
|
||||
|
||||
const [currentStr, currentType] = treeNode.decodeMessage(message)?.format(treeNode.type) ?? []
|
||||
const [currentStr, currentType] = decodeMessage(message)?.format(treeNode.type) ?? []
|
||||
const [compareStr, compareType] =
|
||||
compare && compare.payload ? treeNode.decodeMessage(compare)?.format(treeNode.type) ?? [] : []
|
||||
compare && compare.payload ? decodeMessage(compare)?.format(treeNode.type) ?? [] : []
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.renderDiff(currentStr, currentStr, undefined, currentType)}
|
||||
{renderDiff(currentStr, currentStr, undefined, currentType)}
|
||||
<Fade in={Boolean(compareStr)} timeout={400}>
|
||||
<div>{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareType) : null}</div>
|
||||
<div>{Boolean(compareStr) ? renderDiff(compareStr, compareStr, 'selected', compareType) : null}</div>
|
||||
</Fade>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
|
||||
{this.props.message?.payload?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
|
||||
{this.renderValue()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
public renderValue() {
|
||||
const { message, treeNode, compareWith, renderMode } = this.props
|
||||
function renderValue(decodeMessage: DecoderFunction) {
|
||||
const { message, treeNode, compareWith, renderMode } = props
|
||||
if (!message.payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (renderMode) {
|
||||
case 'diff':
|
||||
return this.renderDiffMode(message, treeNode, compareWith)
|
||||
return renderDiffMode(decodeMessage, message, treeNode, compareWith)
|
||||
default:
|
||||
return this.renderRawMode(message, treeNode, compareWith)
|
||||
return renderRawMode(decodeMessage, message, treeNode, compareWith)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
|
||||
{props.message?.payload?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
|
||||
{renderValue(decodeMessage)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
|
||||
@@ -2,9 +2,10 @@ import * as dotProp from 'dot-prop'
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import * as React from 'react'
|
||||
import PlotHistory from './Chart/Chart'
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { toPlottableValue } from './Sidebar/CodeDiff/util'
|
||||
import { PlotCurveTypes } from '../reducers/Charts'
|
||||
import { DecoderFunction, useDecoder } from './hooks/useDecoder'
|
||||
|
||||
const parseDuration = require('parse-duration')
|
||||
|
||||
interface Props {
|
||||
@@ -26,26 +27,26 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
|
||||
return data
|
||||
}
|
||||
|
||||
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory, node: q.TreeNode<any>) {
|
||||
function nodeToHistory(decodeMessage: DecoderFunction, startTime: number | undefined, history: q.MessageHistory) {
|
||||
return filterUsingTimeRange(startTime, history.toArray())
|
||||
.map((message: q.Message) => {
|
||||
const decoded = node.decodeMessage(message)?.toUnicodeString()
|
||||
const decoded = decodeMessage(message)?.toUnicodeString()
|
||||
return { x: message.received.getTime(), y: toPlottableValue(decoded) }
|
||||
})
|
||||
.filter(data => !isNaN(data.y as any)) as any
|
||||
}
|
||||
|
||||
function nodeDotPathToHistory(
|
||||
decodeMessage: DecoderFunction,
|
||||
startTime: number | undefined,
|
||||
history: q.MessageHistory,
|
||||
dotPath: string,
|
||||
node: q.TreeNode<any>
|
||||
dotPath: string
|
||||
) {
|
||||
return filterUsingTimeRange(startTime, history.toArray())
|
||||
.map((message: q.Message) => {
|
||||
let json: any = {}
|
||||
try {
|
||||
const decoded = node.decodeMessage(message)
|
||||
const decoded = decodeMessage(message)
|
||||
json = decoded ? JSON.parse(decoded.toUnicodeString()) : {}
|
||||
} catch (ignore) {}
|
||||
|
||||
@@ -57,6 +58,7 @@ function nodeDotPathToHistory(
|
||||
}
|
||||
|
||||
function TopicPlot(props: Props) {
|
||||
const decodeMessage = useDecoder(props.node)
|
||||
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
|
||||
const data = React.useMemo(() => {
|
||||
if (!props.node) {
|
||||
@@ -64,8 +66,8 @@ function TopicPlot(props: Props) {
|
||||
}
|
||||
|
||||
return props.dotPath
|
||||
? nodeDotPathToHistory(startOffset, props.history, props.dotPath, props.node)
|
||||
: nodeToHistory(startOffset, props.history, props.node)
|
||||
? nodeDotPathToHistory(decodeMessage, startOffset, props.history, props.dotPath)
|
||||
: nodeToHistory(decodeMessage, startOffset, props.history)
|
||||
}, [props.history.last(), startOffset, props.dotPath])
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as q from '../../../../../backend/src/Model'
|
||||
import React, { memo } from 'react'
|
||||
import { Theme, withStyles } from '@material-ui/core'
|
||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
|
||||
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
|
||||
treeNode: q.TreeNode<TopicViewModel>
|
||||
@@ -13,68 +14,72 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
|
||||
classes: any
|
||||
}
|
||||
|
||||
class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> {
|
||||
private renderSourceEdge() {
|
||||
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
|
||||
export const TreeNodeTitle = (props: TreeNodeProps) => {
|
||||
const decodeMessage = useDecoder(props.treeNode)
|
||||
|
||||
function renderSourceEdge() {
|
||||
const name = props.name || (props.treeNode.sourceEdge && props.treeNode.sourceEdge.name)
|
||||
|
||||
return (
|
||||
<span key="edge" className={this.props.classes.sourceEdge} data-test-topic={name}>
|
||||
<span key="edge" className={props.classes.sourceEdge} data-test-topic={name}>
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private truncatedMessage() {
|
||||
function truncatedMessage() {
|
||||
const limit = 400
|
||||
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) {
|
||||
if (!props.treeNode.message || !props.treeNode.message.payload) {
|
||||
return ''
|
||||
}
|
||||
const [value = ''] =
|
||||
this.props.treeNode.decodeMessage(this.props.treeNode.message)?.format(this.props.treeNode.type) ?? []
|
||||
const [value = ''] = decodeMessage(props.treeNode.message)?.format(props.treeNode.type) ?? []
|
||||
|
||||
return value.length > limit ? `${value.slice(0, limit)}…` : value
|
||||
}
|
||||
|
||||
private renderValue() {
|
||||
return this.props.treeNode.message &&
|
||||
this.props.treeNode.message.payload &&
|
||||
this.props.treeNode.message.length > 0 ? (
|
||||
<span key="value" className={this.props.classes.value}>
|
||||
function renderValue() {
|
||||
return props.treeNode.message && props.treeNode.message.payload && props.treeNode.message.length > 0 ? (
|
||||
<span key="value" className={props.classes.value}>
|
||||
{' '}
|
||||
= {this.truncatedMessage()}
|
||||
= {truncatedMessage()}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
|
||||
private renderExpander() {
|
||||
if (this.props.treeNode.edgeCount() === 0) {
|
||||
function renderExpander() {
|
||||
if (props.treeNode.edgeCount() === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span key="expander" className={this.props.classes.expander} onClick={this.props.toggleCollapsed}>
|
||||
{this.props.collapsed ? '▶' : '▼'}
|
||||
<span key="expander" className={props.classes.expander} onClick={props.toggleCollapsed}>
|
||||
{props.collapsed ? '▶' : '▼'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private renderMetadata() {
|
||||
if (this.props.treeNode.edgeCount() === 0 || !this.props.collapsed) {
|
||||
function renderMetadata() {
|
||||
if (props.treeNode.edgeCount() === 0 || !props.collapsed) {
|
||||
return null
|
||||
}
|
||||
|
||||
const messages = this.props.treeNode.leafMessageCount()
|
||||
const topicCount = this.props.treeNode.childTopicCount()
|
||||
const messages = props.treeNode.leafMessageCount()
|
||||
const topicCount = props.treeNode.childTopicCount()
|
||||
return (
|
||||
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${
|
||||
<span key="metadata" className={props.classes.collapsedSubnodes}>{` (${topicCount} ${
|
||||
topicCount === 1 ? 'topic' : 'topics'
|
||||
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return [this.renderExpander(), this.renderSourceEdge(), this.renderMetadata(), this.renderValue()]
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{renderExpander()}
|
||||
{renderSourceEdge()}
|
||||
{renderMetadata()}
|
||||
{renderValue()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
|
||||
18
app/src/components/Tree/TreeNode/effects/useViewModel.tsx
Normal file
18
app/src/components/Tree/TreeNode/effects/useViewModel.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as q from '../../../../../../backend/src/Model'
|
||||
import { useEffect } from 'react'
|
||||
import { TopicViewModel } from '../../../../model/TopicViewModel'
|
||||
|
||||
export function useViewModel(treeNode: q.TreeNode<TopicViewModel> | undefined) {
|
||||
useEffect(() => {
|
||||
if (treeNode && !treeNode?.viewModel) {
|
||||
treeNode.viewModel = new TopicViewModel(treeNode)
|
||||
}
|
||||
treeNode?.viewModel?.retain()
|
||||
|
||||
return function cleanup() {
|
||||
treeNode?.viewModel?.release()
|
||||
}
|
||||
}, [treeNode])
|
||||
|
||||
return treeNode?.viewModel
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as q from '../../../../../../backend/src/Model'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { TopicViewModel } from '../../../../model/TopicViewModel'
|
||||
import { useSubscription } from '../../../hooks/useSubscription'
|
||||
import { useViewModel } from './useViewModel'
|
||||
|
||||
export function useViewModelSubscriptions(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
@@ -8,37 +10,21 @@ export function useViewModelSubscriptions(
|
||||
setSelected: (value: boolean) => void,
|
||||
setCollapsedOverride: (value: boolean) => void
|
||||
) {
|
||||
useEffect(() => {
|
||||
const selectionDidChange = () => {
|
||||
const selected = treeNode.viewModel && treeNode.viewModel.isSelected()
|
||||
treeNode.viewModel && setSelected(Boolean(selected))
|
||||
const viewModel = useViewModel(treeNode)
|
||||
|
||||
if (selected && nodeRef && nodeRef.current) {
|
||||
nodeRef.current.focus({ preventScroll: false })
|
||||
}
|
||||
}
|
||||
const selectionDidChange = useCallback(() => {
|
||||
const selected = viewModel && viewModel.isSelected()
|
||||
viewModel && setSelected(Boolean(selected))
|
||||
|
||||
const expandedDidChange = () => {
|
||||
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded())
|
||||
if (selected && nodeRef && nodeRef.current) {
|
||||
nodeRef.current.focus({ preventScroll: false })
|
||||
}
|
||||
}, [viewModel])
|
||||
|
||||
function addSubscriber() {
|
||||
treeNode.viewModel = new TopicViewModel()
|
||||
treeNode.viewModel.selectionChange.subscribe(selectionDidChange)
|
||||
treeNode.viewModel.expandedChange.subscribe(expandedDidChange)
|
||||
}
|
||||
const expandedDidChange = useCallback(() => {
|
||||
viewModel && setCollapsedOverride(!viewModel.isExpanded())
|
||||
}, [viewModel])
|
||||
|
||||
function removeSubscriber() {
|
||||
if (treeNode.viewModel) {
|
||||
treeNode.viewModel.selectionChange.unsubscribe(selectionDidChange)
|
||||
treeNode.viewModel.expandedChange.unsubscribe(expandedDidChange)
|
||||
treeNode.viewModel = undefined
|
||||
}
|
||||
}
|
||||
|
||||
addSubscriber()
|
||||
return function cleanup() {
|
||||
removeSubscriber()
|
||||
}
|
||||
}, [treeNode])
|
||||
useSubscription(viewModel?.selectionChange, selectionDidChange)
|
||||
useSubscription(viewModel?.expandedChange, expandedDidChange)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import { globalActions } from '../../actions'
|
||||
const copy = require('copy-text-to-clipboard')
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
value?: string
|
||||
getValue?: () => string | undefined
|
||||
actions: {
|
||||
global: typeof globalActions
|
||||
}
|
||||
@@ -28,7 +29,7 @@ class Copy extends React.PureComponent<Props, State> {
|
||||
private handleClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
|
||||
copy(this.props.value)
|
||||
copy(this.props.value ?? this.props.getValue?.())
|
||||
this.props.actions.global.showNotification('Copied to clipboard')
|
||||
this.setState({ didCopy: true })
|
||||
setTimeout(() => {
|
||||
|
||||
28
app/src/components/hooks/useDecoder.ts
Normal file
28
app/src/components/hooks/useDecoder.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
|
||||
import { useSubscription } from './useSubscription'
|
||||
import { useViewModel } from '../Tree/TreeNode/effects/useViewModel'
|
||||
|
||||
export type DecoderFunction = (message: q.Message) => Base64Message | null
|
||||
|
||||
/**
|
||||
* Provides the latest decoder for a topic
|
||||
*
|
||||
* @param treeNode
|
||||
* @returns
|
||||
*/
|
||||
export function useDecoder(treeNode: q.TreeNode<TopicViewModel> | undefined): DecoderFunction {
|
||||
const viewModel = useViewModel(treeNode)
|
||||
const [decoder, setDecoder] = useState(viewModel?.decoder)
|
||||
|
||||
useSubscription(viewModel?.onDecoderChange, setDecoder)
|
||||
|
||||
return useCallback(
|
||||
message => {
|
||||
return decoder && message.payload ? decoder.decoder.decode(message.payload, decoder.format) : message.payload
|
||||
},
|
||||
[decoder]
|
||||
)
|
||||
}
|
||||
10
app/src/components/hooks/useSubscription.ts
Normal file
10
app/src/components/hooks/useSubscription.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useEffect } from 'react'
|
||||
import { EventDispatcher } from '../../../../events'
|
||||
|
||||
export function useSubscription<T>(dispatcher: EventDispatcher<T> | undefined, callback: (value: T) => void) {
|
||||
useEffect(() => {
|
||||
dispatcher?.subscribe(callback)
|
||||
|
||||
return () => dispatcher?.unsubscribe(callback)
|
||||
}, [dispatcher, callback])
|
||||
}
|
||||
Reference in New Issue
Block a user