Merge pull request #795 from thomasnordquist/tnordquist/decode-data-in-frontend
decode data in frontend
This commit is contained in:
@@ -2,7 +2,7 @@ import { Action, ActionTypes } from '../reducers/Publish'
|
||||
import { AppState } from '../reducers'
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Dispatch } from 'redux'
|
||||
import { makePublishEvent, rendererEvents } from '../../../events'
|
||||
import { MqttMessage, makePublishEvent, rendererEvents } from '../../../events'
|
||||
|
||||
export const setTopic = (topic?: string): Action => {
|
||||
return {
|
||||
@@ -41,7 +41,7 @@ export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, ge
|
||||
}
|
||||
|
||||
const publishEvent = makePublishEvent(connectionId)
|
||||
const mqttMessage = {
|
||||
const mqttMessage: Partial<MqttMessage> = {
|
||||
topic,
|
||||
payload: state.publish.payload ? Base64Message.fromString(state.publish.payload) : null,
|
||||
retain: state.publish.retain,
|
||||
|
||||
@@ -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'
|
||||
@@ -68,13 +68,14 @@ 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,
|
||||
})
|
||||
dispatch(storeSettings())
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
@@ -117,7 +118,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
|
||||
const messageMatches =
|
||||
node.message &&
|
||||
node.message.payload &&
|
||||
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1
|
||||
node.message.payload.toUnicodeString().toLowerCase().indexOf(filterStr) !== -1
|
||||
|
||||
return Boolean(messageMatches)
|
||||
}
|
||||
|
||||
@@ -33,13 +33,8 @@ const debouncedSelectTopic = debounce(
|
||||
setTopicDispatch = setTopic(topic.path())
|
||||
}
|
||||
|
||||
if (previouslySelectedTopic && previouslySelectedTopic.viewModel) {
|
||||
previouslySelectedTopic.viewModel.setSelected(false)
|
||||
}
|
||||
|
||||
if (topic.viewModel) {
|
||||
topic.viewModel.setSelected(true)
|
||||
}
|
||||
previouslySelectedTopic?.viewModel?.setSelected(false)
|
||||
topic.viewModel?.setSelected(true)
|
||||
|
||||
const selectTreeTopicDispatch = {
|
||||
selectedTopic: topic,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
|
||||
return null
|
||||
}
|
||||
|
||||
const str = node.message.payload ? Base64Message.toUnicodeString(node.message.payload) : ''
|
||||
const str = node.message.payload ? node.message.payload.toUnicodeString() : ''
|
||||
let value = node.message && node.message.payload ? parseFloat(str) : NaN
|
||||
value = !isNaN(value) ? abbreviate(value) : str
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ function ChartPreview(props: Props) {
|
||||
<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>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore'
|
||||
import NodeStats from './NodeStats'
|
||||
import ValuePanel from './ValueRenderer/ValuePanel'
|
||||
import { AppState } from '../../reducers'
|
||||
import { Badge, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography } from '@material-ui/core'
|
||||
import { ExpansionPanelDetails } from '@material-ui/core'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { settingsActions, sidebarActions } from '../../actions'
|
||||
@@ -28,7 +27,7 @@ interface Props {
|
||||
}
|
||||
|
||||
function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
|
||||
const [lastUpdate, setLastUpdate] = useState(0)
|
||||
const [, setLastUpdate] = useState(0)
|
||||
const updateNode = useCallback(
|
||||
throttle(() => {
|
||||
setLastUpdate(node ? node.lastUpdate : 0)
|
||||
@@ -52,7 +51,6 @@ function Sidebar(props: Props) {
|
||||
const { classes, tree, nodePath } = props
|
||||
const node = usePollingToFetchTreeNode(tree, nodePath || '')
|
||||
useUpdateNodeWhenNodeReceivesUpdates(node)
|
||||
// console.log(node && node.path(), tree, nodePath)
|
||||
|
||||
return (
|
||||
<div id="Sidebar" className={classes.drawer}>
|
||||
|
||||
@@ -6,19 +6,19 @@ 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)
|
||||
}, [])
|
||||
|
||||
@@ -29,11 +29,12 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
|
||||
Topic {copyTopic}
|
||||
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
|
||||
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
|
||||
<TopicTypeButton node={node} />
|
||||
</span>
|
||||
<Topic node={node} />
|
||||
</Panel>
|
||||
),
|
||||
[node, node && node.childTopicCount()]
|
||||
[node, node?.childTopicCount()]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
103
app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx
Normal file
103
app/src/components/Sidebar/TopicPanel/TopicTypeButton.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
|
||||
import Grow from '@material-ui/core/Grow'
|
||||
import Button from '@material-ui/core/Button'
|
||||
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'
|
||||
import WarningRounded from '@material-ui/icons/WarningRounded'
|
||||
import { MessageDecoder, decoders } from '../../../decoders'
|
||||
import { Tooltip } from '@material-ui/core'
|
||||
|
||||
export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
|
||||
const { node } = props
|
||||
if (!node || !node.message || !node.message.payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
const options = decoders.flatMap(decoder => decoder.formats.map(format => [decoder, format] as const))
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const selectOption = useCallback(
|
||||
(decoder: MessageDecoder, format: string) => {
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
node.viewModel.decoder = { decoder, format }
|
||||
setOpen(false)
|
||||
},
|
||||
[node]
|
||||
)
|
||||
|
||||
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?.viewModel.decoder?.format ?? props.node?.type}
|
||||
<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(([decoder, format], index) => (
|
||||
<MenuItem
|
||||
key={format}
|
||||
selected={node && format === node.type}
|
||||
onClick={() => selectOption(decoder, format)}
|
||||
>
|
||||
<DecoderStatus decoder={decoder} format={format} node={node} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function DecoderStatus({ node, decoder, format }: { node: q.TreeNode<any>; decoder: MessageDecoder; format: string }) {
|
||||
const decoded = useMemo(() => {
|
||||
return node.message?.payload && decoder.decode(node.message?.payload, format)
|
||||
}, [node.message, decoder, format])
|
||||
|
||||
return decoded?.error ? (
|
||||
<Tooltip title={decoded.error}>
|
||||
<div>
|
||||
{format} <WarningRounded />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>{format}</>
|
||||
)
|
||||
}
|
||||
@@ -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,56 +26,38 @@ 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)
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.node && this.props.node.onMessage.subscribe(this.updateNode)
|
||||
}
|
||||
|
||||
public componentWillUnMount() {
|
||||
this.props.node && this.props.node.onMessage.unsubscribe(this.updateNode)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { node } = this.props
|
||||
const { node } = props
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
@@ -82,9 +65,10 @@ 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 = node.message ? decodeMessage(message)?.message?.format()[0] ?? null : null
|
||||
|
||||
const element = {
|
||||
value,
|
||||
value: value ?? '',
|
||||
key: `${message.messageNumber}-${message.received}`,
|
||||
title: (
|
||||
<span>
|
||||
@@ -102,18 +86,19 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
<MessageId message={message} />
|
||||
</span>
|
||||
<div style={{ float: 'right' }}>
|
||||
<Copy value={value} />
|
||||
<Copy value={value ?? ''} />
|
||||
</div>
|
||||
</span>
|
||||
),
|
||||
selected: message && message === this.props.selected,
|
||||
selected: message && message === props.selected,
|
||||
}
|
||||
previousMessage = message
|
||||
return element
|
||||
})
|
||||
|
||||
const isMessagePlottable =
|
||||
node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload))
|
||||
const value = node.message ? decodeMessage(node.message)?.message?.format()[0] ?? null : null
|
||||
|
||||
const isMessagePlottable = isPlottable(value)
|
||||
return (
|
||||
<div>
|
||||
<History
|
||||
@@ -122,20 +107,19 @@ class MessageHistory extends React.PureComponent<Props, State> {
|
||||
isMessagePlottable ? (
|
||||
<CustomIconButton
|
||||
style={{ height: '22px', width: '22px' }}
|
||||
onClick={this.addNodeToCharts}
|
||||
onClick={addNodeToCharts}
|
||||
tooltip="Add to chart panel"
|
||||
>
|
||||
<ShowChart style={{ marginTop: '-5px' }} />
|
||||
</CustomIconButton>
|
||||
) : undefined
|
||||
}
|
||||
onClick={this.displayMessage}
|
||||
onClick={displayMessage}
|
||||
>
|
||||
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null}
|
||||
{isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
|
||||
</History>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
@@ -144,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)?.message?.toUnicodeString()
|
||||
}, [node, decodeMessage])
|
||||
|
||||
function messageMetaInfo() {
|
||||
if (!props.node || !props.node.message) {
|
||||
return null
|
||||
@@ -85,10 +90,9 @@ function ValuePanel(props: Props) {
|
||||
[compareMessage]
|
||||
)
|
||||
|
||||
const copyValue =
|
||||
node && node.message && node.message.payload ? (
|
||||
<Copy value={Base64Message.toUnicodeString(node.message.payload)} />
|
||||
) : null
|
||||
const [value] =
|
||||
node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
|
||||
const copyValue = value ? <Copy getValue={getDecodedValue} /> : null
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import * as React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import CodeDiff from '../CodeDiff'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
import { connect } from 'react-redux'
|
||||
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
|
||||
import { Fade } from '@material-ui/core'
|
||||
import { Decoder } from '../../../../../backend/src/Model/Decoder'
|
||||
import { useDecoder } from '../../hooks/useDecoder'
|
||||
import { TopicViewModel } from '../../../model/TopicViewModel'
|
||||
|
||||
interface Props {
|
||||
message: q.Message
|
||||
@@ -15,103 +16,114 @@ interface Props {
|
||||
renderMode: ValueRendererDisplayMode
|
||||
}
|
||||
|
||||
interface State {
|
||||
width: number
|
||||
}
|
||||
type Language = 'json'
|
||||
|
||||
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(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
compareWithPreviousMessage: boolean,
|
||||
current: string = '',
|
||||
previous: string = '',
|
||||
title?: string,
|
||||
language?: Language
|
||||
) {
|
||||
return (
|
||||
<CodeDiff
|
||||
treeNode={this.props.treeNode}
|
||||
treeNode={treeNode}
|
||||
previous={previous}
|
||||
current={current}
|
||||
title={title}
|
||||
language={language}
|
||||
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'}
|
||||
nameOfCompareMessage={compareWithPreviousMessage ? 'selected' : 'previous'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] {
|
||||
if (!msg) {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
function renderDiffMode(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
currentStr: string | undefined,
|
||||
compareStr: string | undefined,
|
||||
currentType: Language | undefined,
|
||||
compareType: Language | undefined,
|
||||
compareWithPreviousMessage: boolean
|
||||
) {
|
||||
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined
|
||||
|
||||
const str = Base64Message.toUnicodeString(msg)
|
||||
try {
|
||||
JSON.parse(str)
|
||||
} catch (error) {
|
||||
return [str, undefined]
|
||||
}
|
||||
|
||||
return [this.messageToPrettyJson(str), 'json']
|
||||
}
|
||||
|
||||
private messageToPrettyJson(str: string): string | undefined {
|
||||
try {
|
||||
const json = JSON.parse(str)
|
||||
return JSON.stringify(json, undefined, ' ')
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
private renderRawMode(message: q.Message, 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]
|
||||
return <div>{renderDiff(treeNode, compareWithPreviousMessage, currentStr, compareStr, undefined, language)}</div>
|
||||
}
|
||||
|
||||
function renderRawMode(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
currentStr: string | undefined,
|
||||
compareStr: string | undefined,
|
||||
currentType: Language | undefined,
|
||||
compareType: Language | undefined,
|
||||
compareWithPreviousMessage: boolean
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
{this.renderDiff(value, value, undefined, valueLanguage)}
|
||||
{renderDiff(treeNode, compareWithPreviousMessage, currentStr, currentStr, undefined, currentType)}
|
||||
<Fade in={Boolean(compareStr)} timeout={400}>
|
||||
<div>
|
||||
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null}
|
||||
{Boolean(compareStr)
|
||||
? renderDiff(treeNode, compareWithPreviousMessage, 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>
|
||||
)
|
||||
}
|
||||
export const ValueRenderer: React.FC<Props> = ({ treeNode, compareWith: compare, message, renderMode }) => {
|
||||
const decodeMessage = useDecoder(treeNode)
|
||||
const decodedMessage = useMemo(() => decodeMessage(message), [decodeMessage, message])
|
||||
|
||||
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
|
||||
const compareMessage = compare || previousMessage || message
|
||||
const compareWithPreviousMessage = !!compare
|
||||
|
||||
if (renderMode === 'raw') {
|
||||
return this.renderRawMode(message, compareWith)
|
||||
}
|
||||
if (!message.payload) {
|
||||
const [currentStr, currentType] = useMemo(
|
||||
() => decodedMessage?.message?.format(treeNode.type) ?? [],
|
||||
[decodedMessage, treeNode.type]
|
||||
)
|
||||
const [compareStr, compareType] = useMemo(
|
||||
() => decodeMessage(compareMessage)?.message?.format(treeNode.type) ?? [],
|
||||
[compareMessage, decodeMessage, treeNode.type]
|
||||
)
|
||||
|
||||
function renderValue(
|
||||
treeNode: q.TreeNode<TopicViewModel>,
|
||||
currentStr: string | undefined,
|
||||
compareStr: string | undefined,
|
||||
currentType: Language | undefined,
|
||||
compareType: Language | undefined,
|
||||
renderMode: string,
|
||||
compareWithPreviousMessage: boolean
|
||||
) {
|
||||
if (!decodedMessage) {
|
||||
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 renderDiffMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
|
||||
default:
|
||||
return renderRawMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const renderedValue = useMemo(
|
||||
() =>
|
||||
renderValue(treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage),
|
||||
[treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
|
||||
{decodedMessage?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
|
||||
{renderedValue}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
|
||||
@@ -2,12 +2,14 @@ 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 {
|
||||
node?: q.TreeNode<any>
|
||||
history: q.MessageHistory
|
||||
dotPath?: string
|
||||
timeInterval?: string
|
||||
@@ -25,21 +27,27 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
|
||||
return data
|
||||
}
|
||||
|
||||
function nodeToHistory(startTime: number | undefined, history: q.MessageHistory) {
|
||||
function nodeToHistory(decodeMessage: DecoderFunction, startTime: number | undefined, history: q.MessageHistory) {
|
||||
return filterUsingTimeRange(startTime, history.toArray())
|
||||
.map((message: q.Message) => {
|
||||
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN
|
||||
return { x: message.received.getTime(), y: toPlottableValue(value) }
|
||||
const decoded = decodeMessage(message)?.message?.toUnicodeString()
|
||||
return { x: message.received.getTime(), y: toPlottableValue(decoded) }
|
||||
})
|
||||
.filter(data => !isNaN(data.y as any)) as any
|
||||
}
|
||||
|
||||
function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageHistory, dotPath: string) {
|
||||
function nodeDotPathToHistory(
|
||||
decodeMessage: DecoderFunction,
|
||||
startTime: number | undefined,
|
||||
history: q.MessageHistory,
|
||||
dotPath: string
|
||||
) {
|
||||
return filterUsingTimeRange(startTime, history.toArray())
|
||||
.map((message: q.Message) => {
|
||||
let json: any = {}
|
||||
try {
|
||||
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {}
|
||||
const decoded = decodeMessage(message)?.message
|
||||
json = decoded ? JSON.parse(decoded.toUnicodeString()) : {}
|
||||
} catch (ignore) {}
|
||||
|
||||
const value = dotProp.get(json, dotPath)
|
||||
@@ -50,14 +58,17 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
|
||||
}
|
||||
|
||||
function TopicPlot(props: Props) {
|
||||
const decodeMessage = useDecoder(props.node)
|
||||
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
|
||||
const data = React.useMemo(
|
||||
() =>
|
||||
props.dotPath
|
||||
? nodeDotPathToHistory(startOffset, props.history, props.dotPath)
|
||||
: nodeToHistory(startOffset, props.history),
|
||||
[props.history.last(), startOffset, props.dotPath]
|
||||
)
|
||||
const data = React.useMemo(() => {
|
||||
if (!props.node) {
|
||||
return []
|
||||
}
|
||||
|
||||
return props.dotPath
|
||||
? nodeDotPathToHistory(decodeMessage, startOffset, props.history, props.dotPath)
|
||||
: nodeToHistory(decodeMessage, startOffset, props.history)
|
||||
}, [props.history.last(), startOffset, props.dotPath])
|
||||
|
||||
return (
|
||||
<PlotHistory
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as q from '../../../../../backend/src/Model'
|
||||
import React, { memo } from 'react'
|
||||
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
|
||||
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>
|
||||
@@ -14,67 +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 = ''] = decodeMessage(props.treeNode.message)?.message?.format(props.treeNode.type) ?? []
|
||||
|
||||
const str = Base64Message.toUnicodeString(this.props.treeNode.message.payload)
|
||||
return str.length > limit ? `${str.slice(0, limit)}…` : str
|
||||
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)
|
||||
|
||||
const selectionDidChange = useCallback(() => {
|
||||
const selected = viewModel && viewModel.isSelected()
|
||||
viewModel && setSelected(Boolean(selected))
|
||||
|
||||
if (selected && nodeRef && nodeRef.current) {
|
||||
nodeRef.current.focus({ preventScroll: false })
|
||||
}
|
||||
}
|
||||
}, [viewModel])
|
||||
|
||||
const expandedDidChange = () => {
|
||||
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded())
|
||||
}
|
||||
const expandedDidChange = useCallback(() => {
|
||||
viewModel && setCollapsedOverride(!viewModel.isExpanded())
|
||||
}, [viewModel])
|
||||
|
||||
function addSubscriber() {
|
||||
treeNode.viewModel = new TopicViewModel()
|
||||
treeNode.viewModel.selectionChange.subscribe(selectionDidChange)
|
||||
treeNode.viewModel.expandedChange.subscribe(expandedDidChange)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as compareVersions from 'compare-versions'
|
||||
import * as electron from 'electron'
|
||||
import * as os from 'os'
|
||||
import * as React from 'react'
|
||||
import compareVersions from 'compare-versions'
|
||||
import electron from 'electron'
|
||||
import os from 'os'
|
||||
import React from 'react'
|
||||
import axios from 'axios'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import CloudDownload from '@material-ui/icons/CloudDownload'
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as moment from 'moment'
|
||||
import * as React from 'react'
|
||||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import { AppState } from '../../reducers'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
@@ -12,6 +12,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const unitMapping = {
|
||||
ms: 'milliseconds',
|
||||
s: 'seconds',
|
||||
m: 'minutes',
|
||||
h: 'hours',
|
||||
@@ -21,7 +22,7 @@ class DateFormatter extends React.PureComponent<Props, {}> {
|
||||
private intervalSince(intervalSince: Date) {
|
||||
const interval = intervalSince.getTime() - this.props.date.getTime()
|
||||
const unit = this.unitForInterval(interval)
|
||||
return `${Math.round(moment.duration(interval).as(unit) * 100) / 100} ${unitMapping[unit]}`
|
||||
return `${moment.duration(interval).as(unit).toFixed(3)} ${unitMapping[unit]}`
|
||||
}
|
||||
|
||||
private legacyDate() {
|
||||
@@ -31,10 +32,11 @@ class DateFormatter extends React.PureComponent<Props, {}> {
|
||||
private localizedDate(locale: string) {
|
||||
return moment(this.props.date)
|
||||
.locale(locale)
|
||||
.format(this.props.timeFirst ? 'LTS L' : 'L LTS')
|
||||
.format(this.props.timeFirst ? 'LTS.SSS L' : 'L LTS.SSS')
|
||||
}
|
||||
|
||||
private unitForInterval(milliseconds: number) {
|
||||
const oneSecond = 1000 * 1
|
||||
const oneMinute = 1000 * 60
|
||||
const oneHour = oneMinute * 60
|
||||
|
||||
@@ -46,9 +48,13 @@ class DateFormatter extends React.PureComponent<Props, {}> {
|
||||
return 'm'
|
||||
}
|
||||
|
||||
if (milliseconds > oneSecond * 0.5) {
|
||||
return 's'
|
||||
}
|
||||
|
||||
return 'ms'
|
||||
}
|
||||
|
||||
public render() {
|
||||
const locale = this.props.overrideLocale || this.props.locale
|
||||
if (this.props.intervalSince) {
|
||||
|
||||
31
app/src/components/hooks/useDecoder.ts
Normal file
31
app/src/components/hooks/useDecoder.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as q from '../../../../backend/src/Model'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { TopicViewModel } from '../../model/TopicViewModel'
|
||||
import { useSubscription } from './useSubscription'
|
||||
import { useViewModel } from '../Tree/TreeNode/effects/useViewModel'
|
||||
import { DecoderEnvelope } from '../../decoders/DecoderEnvelope'
|
||||
import { Decoder } from '../../../../backend/src/Model/Decoder'
|
||||
|
||||
export type DecoderFunction = (message: q.Message) => DecoderEnvelope | undefined
|
||||
|
||||
/**
|
||||
* 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: message.payload ?? undefined, decoder: Decoder.NONE }
|
||||
},
|
||||
[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])
|
||||
}
|
||||
56
app/src/decoders/BinaryDecoder.ts
Normal file
56
app/src/decoders/BinaryDecoder.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
import { DecoderEnvelope } from './DecoderEnvelope'
|
||||
import { MessageDecoder } from './MessageDecoder'
|
||||
|
||||
type BinaryFormats =
|
||||
| 'int8'
|
||||
| 'int16'
|
||||
| 'int32'
|
||||
| 'int64'
|
||||
| 'uint8'
|
||||
| 'uint16'
|
||||
| 'uint32'
|
||||
| 'uint64'
|
||||
| 'float'
|
||||
| 'double'
|
||||
|
||||
/**
|
||||
* Binary decode primitive binary data type and arrays of these
|
||||
*/
|
||||
export const BinaryDecoder: MessageDecoder<BinaryFormats> = {
|
||||
formats: ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float', 'double'],
|
||||
decode(input: Base64Message, format: BinaryFormats): DecoderEnvelope {
|
||||
const decodingOption = {
|
||||
int8: [Buffer.prototype.readInt8, 1],
|
||||
int16: [Buffer.prototype.readInt16LE, 2],
|
||||
int32: [Buffer.prototype.readInt32LE, 4],
|
||||
int64: [Buffer.prototype.readBigInt64LE, 8],
|
||||
uint8: [Buffer.prototype.readUint8, 1],
|
||||
uint16: [Buffer.prototype.readUint16LE, 2],
|
||||
uint32: [Buffer.prototype.readUint32LE, 4],
|
||||
uint64: [Buffer.prototype.readBigUint64LE, 8],
|
||||
float: [Buffer.prototype.readFloatLE, 4],
|
||||
double: [Buffer.prototype.readDoubleLE, 8],
|
||||
} as const
|
||||
|
||||
const [readNumber, bytesToRead] = decodingOption[format]
|
||||
|
||||
const buf = input.toBuffer()
|
||||
let str: String[] = []
|
||||
if (buf.length % bytesToRead !== 0) {
|
||||
return {
|
||||
error: 'Data type does not align with message',
|
||||
decoder: Decoder.NONE,
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < buf.length; index += bytesToRead) {
|
||||
str.push((readNumber as any).apply(buf, [index]).toString())
|
||||
}
|
||||
|
||||
return {
|
||||
message: Base64Message.fromString(JSON.stringify(str.length === 1 ? str[0] : str)),
|
||||
decoder: Decoder.NONE,
|
||||
}
|
||||
},
|
||||
}
|
||||
8
app/src/decoders/DecoderEnvelope.ts
Normal file
8
app/src/decoders/DecoderEnvelope.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
|
||||
export interface DecoderEnvelope {
|
||||
message?: Base64Message
|
||||
error?: string
|
||||
decoder: Decoder
|
||||
}
|
||||
13
app/src/decoders/MessageDecoder.ts
Normal file
13
app/src/decoders/MessageDecoder.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { DecoderEnvelope } from './DecoderEnvelope'
|
||||
|
||||
export interface MessageDecoder<T = string> {
|
||||
/**
|
||||
* Can be used to
|
||||
* @param topic
|
||||
*/
|
||||
formats: T[]
|
||||
canDecodeTopic?(topic: string): boolean
|
||||
canDecodeData?(data: Base64Message): boolean
|
||||
decode(input: Base64Message, format: T | string | undefined): DecoderEnvelope
|
||||
}
|
||||
28
app/src/decoders/SparkplugBDecoder.ts
Normal file
28
app/src/decoders/SparkplugBDecoder.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
import { get } from 'sparkplug-payload'
|
||||
import { MessageDecoder } from './MessageDecoder'
|
||||
var sparkplug = get('spBv1.0')
|
||||
|
||||
export const SparkplugDecoder: MessageDecoder = {
|
||||
formats: ['Sparkplug'],
|
||||
canDecodeTopic(topic: string) {
|
||||
return !!topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u)
|
||||
},
|
||||
decode(input) {
|
||||
try {
|
||||
const message = Base64Message.fromString(
|
||||
JSON.stringify(
|
||||
// @ts-ignore
|
||||
sparkplug.decodePayload(new Uint8Array(input.toBuffer()))
|
||||
)
|
||||
)
|
||||
return { message, decoder: Decoder.SPARKPLUG }
|
||||
} catch {
|
||||
return {
|
||||
error: 'Failed to decode sparkplugb payload',
|
||||
decoder: Decoder.NONE,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
10
app/src/decoders/StringDecoder.ts
Normal file
10
app/src/decoders/StringDecoder.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Base64Message } from '../../../backend/src/Model/Base64Message'
|
||||
import { Decoder } from '../../../backend/src/Model/Decoder'
|
||||
import { MessageDecoder } from './MessageDecoder'
|
||||
|
||||
export const StringDecoder: MessageDecoder = {
|
||||
formats: ['string'],
|
||||
decode(input: Base64Message) {
|
||||
return { message: input, decoder: Decoder.NONE }
|
||||
},
|
||||
}
|
||||
6
app/src/decoders/index.ts
Normal file
6
app/src/decoders/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { StringDecoder } from './StringDecoder'
|
||||
import { BinaryDecoder } from './BinaryDecoder'
|
||||
import { SparkplugDecoder } from './SparkplugBDecoder'
|
||||
export * from './MessageDecoder'
|
||||
|
||||
export const decoders = [SparkplugDecoder, BinaryDecoder, StringDecoder] as const
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConnectionOptions, createEmptyConnection } from './ConnectionOptions'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
interface LegacyConnectionSettings {
|
||||
host: string
|
||||
|
||||
@@ -1,19 +1,77 @@
|
||||
import * as q from '../../../backend/src/Model'
|
||||
import { Destroyable } from '../../../backend/src/Model/Destroyable'
|
||||
import { MessageDecoder, decoders } from '../decoders'
|
||||
import { EventDispatcher } from '../../../events'
|
||||
|
||||
function findDecoder<T extends Destroyable>(node: q.TreeNode<T>): TopicDecoder | undefined {
|
||||
const decoder = decoders.find(
|
||||
decoder =>
|
||||
decoder.canDecodeTopic?.(node.path()) || (node.message?.payload && decoder.canDecodeData?.(node.message?.payload))
|
||||
)
|
||||
|
||||
return decoder
|
||||
? {
|
||||
decoder,
|
||||
format: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
type TopicDecoder = { decoder: MessageDecoder; format: string | undefined }
|
||||
|
||||
export class TopicViewModel implements Destroyable {
|
||||
private selected: boolean
|
||||
private expanded: boolean
|
||||
private owner: q.TreeNode<TopicViewModel> | undefined
|
||||
private _decoder?: TopicDecoder
|
||||
/**
|
||||
* Reference counter for useViewModel hook
|
||||
*/
|
||||
private referenceCounter = 0
|
||||
public selectionChange = new EventDispatcher<void>()
|
||||
public expandedChange = new EventDispatcher<void>()
|
||||
public onDecoderChange = new EventDispatcher<TopicDecoder | undefined>()
|
||||
|
||||
public constructor() {
|
||||
get decoder(): TopicDecoder | undefined {
|
||||
if (!this._decoder) {
|
||||
this._decoder = this.owner && findDecoder(this.owner)
|
||||
}
|
||||
|
||||
return this._decoder
|
||||
}
|
||||
|
||||
set decoder(override: TopicDecoder | undefined) {
|
||||
this._decoder = override
|
||||
|
||||
this.onDecoderChange.dispatch(override)
|
||||
}
|
||||
|
||||
public constructor(treeNode: q.TreeNode<TopicViewModel>) {
|
||||
this.owner = treeNode
|
||||
this.selected = false
|
||||
this.expanded = false
|
||||
}
|
||||
|
||||
public retain() {
|
||||
this.referenceCounter += 1
|
||||
}
|
||||
|
||||
public release() {
|
||||
this.referenceCounter -= 1
|
||||
if (this.referenceCounter <= 0) {
|
||||
this.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
console.log('destroy', this.referenceCounter)
|
||||
if (this.owner) {
|
||||
this.owner.viewModel = undefined
|
||||
this.owner = undefined
|
||||
}
|
||||
this.selectionChange.removeAllListeners()
|
||||
this.onDecoderChange.removeAllListeners()
|
||||
this.expandedChange.removeAllListeners()
|
||||
}
|
||||
|
||||
public isSelected() {
|
||||
|
||||
@@ -4,35 +4,23 @@
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["es2019", "dom"],
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build/",
|
||||
"sourceMap": true,
|
||||
"module": "esnext",
|
||||
"target": "es2017",
|
||||
"target": "ES2017",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"react": [
|
||||
"./node_modules/@types/react"
|
||||
]
|
||||
"react": ["./node_modules/@types/react"]
|
||||
},
|
||||
"types": [
|
||||
"react"
|
||||
],
|
||||
"types": ["react"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.d.ts",
|
||||
".src/**/*.png",
|
||||
"./node_modules"
|
||||
],
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["**/*.d.ts", ".src/**/*.png", "./node_modules"],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
"useCache": true,
|
||||
"transpileModule": true,
|
||||
|
||||
@@ -54,7 +54,15 @@ module.exports = {
|
||||
// All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
// options: {
|
||||
// configFile: './tsconfig.json',
|
||||
// },
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
|
||||
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
|
||||
@@ -96,4 +104,7 @@ module.exports = {
|
||||
// "react": "React",
|
||||
// "react-dom": "ReactDOM"
|
||||
},
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
},
|
||||
}
|
||||
|
||||
671
app/yarn.lock
671
app/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import * as FileAsync from 'lowdb/adapters/FileAsync'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as lowdb from 'lowdb'
|
||||
import * as path from 'path'
|
||||
import FileAsync from 'lowdb/adapters/FileAsync'
|
||||
import fs from 'fs-extra'
|
||||
import lowdb from 'lowdb'
|
||||
import path from 'path'
|
||||
import { backendRpc } from '../../events'
|
||||
import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents'
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ export class MqttSource implements DataSource<MqttOptions> {
|
||||
|
||||
public publish(msg: MqttMessage) {
|
||||
if (this.client) {
|
||||
this.client.publish(msg.topic, msg.payload ? Base64Message.toUnicodeString(msg.payload) : '', {
|
||||
this.client.publish(msg.topic, (msg.payload && new Base64Message(msg.payload))?.toBuffer() ?? '', {
|
||||
qos: msg.qos,
|
||||
retain: msg.retain,
|
||||
})
|
||||
|
||||
@@ -1,31 +1,93 @@
|
||||
import { Base64 } from 'js-base64'
|
||||
import { Decoder } from './Decoder'
|
||||
import { TopicDataType } from './TreeNode'
|
||||
|
||||
export type Base64MessageDTO = Pick<Base64Message, 'base64Message'>
|
||||
|
||||
export class Base64Message {
|
||||
private base64Message: string
|
||||
private unicodeValue: string
|
||||
public decoder: Decoder
|
||||
public length: number
|
||||
public base64Message: string
|
||||
private _unicodeValue: string | undefined
|
||||
|
||||
private constructor(base64Str: string) {
|
||||
this.base64Message = base64Str
|
||||
this.unicodeValue = Base64.decode(base64Str)
|
||||
this.length = base64Str.length
|
||||
this.decoder = Decoder.NONE
|
||||
// Todo: Rename to `encodedLength`
|
||||
public get length(): number {
|
||||
return this.base64Message.length
|
||||
}
|
||||
|
||||
public static toUnicodeString(message: Base64Message) {
|
||||
return message.unicodeValue || ''
|
||||
private get unicodeValue(): string {
|
||||
if (!this._unicodeValue) {
|
||||
this._unicodeValue = Base64.decode(this.base64Message ?? '')
|
||||
}
|
||||
|
||||
return this._unicodeValue
|
||||
}
|
||||
|
||||
constructor(base64Str?: string | Base64MessageDTO, error?: string) {
|
||||
if (typeof base64Str === 'string' || typeof base64Str === 'undefined') {
|
||||
this.base64Message = base64Str ?? ''
|
||||
} else {
|
||||
if (typeof base64Str.base64Message !== 'string') {
|
||||
throw new Error('Received unexpected type in copy constructor')
|
||||
}
|
||||
this.base64Message = base64Str.base64Message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override default JSON serialization behavior to only return the DTO
|
||||
* @returns
|
||||
*/
|
||||
public toJSON(): Base64MessageDTO {
|
||||
return { base64Message: this.base64Message }
|
||||
}
|
||||
|
||||
public toUnicodeString() {
|
||||
return this.unicodeValue || ''
|
||||
}
|
||||
|
||||
public static fromBuffer(buffer: Buffer) {
|
||||
return new Base64Message(buffer.toString('base64'))
|
||||
}
|
||||
|
||||
public toBuffer(): Buffer {
|
||||
return Buffer.from(this.base64Message, 'base64')
|
||||
}
|
||||
|
||||
public static fromString(str: string) {
|
||||
return new Base64Message(Base64.encode(str))
|
||||
}
|
||||
|
||||
public format(type: TopicDataType = 'string'): [string, 'json' | undefined] {
|
||||
try {
|
||||
switch (type) {
|
||||
case 'json': {
|
||||
const json = JSON.parse(this.toUnicodeString())
|
||||
return [JSON.stringify(json, undefined, ' '), 'json']
|
||||
}
|
||||
case 'hex': {
|
||||
const hex = Base64Message.toHex(this)
|
||||
return [hex, undefined]
|
||||
}
|
||||
default: {
|
||||
const str = this.toUnicodeString()
|
||||
return [str, undefined]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const str = this.toUnicodeString()
|
||||
return [str, undefined]
|
||||
}
|
||||
}
|
||||
|
||||
public static toHex(message: Base64Message) {
|
||||
const buf = Buffer.from(message.base64Message, 'base64')
|
||||
|
||||
let str: string = ''
|
||||
buf.forEach(element => {
|
||||
let hex = element.toString(16).toUpperCase()
|
||||
str += `0x${hex.length < 2 ? '0' + hex : hex} `
|
||||
})
|
||||
return str.trimRight()
|
||||
}
|
||||
|
||||
public static toDataUri(message: Base64Message, mimeType: string) {
|
||||
return `data:${mimeType};base64,${message.base64Message}`
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ChangeBuffer {
|
||||
public push(val: MqttMessage) {
|
||||
if (!this.isFull()) {
|
||||
this.buffer.push({ message: val, received: new Date() })
|
||||
this.size += this.estimatedMessageOverhead + (val.payload ? val.payload.length : 0)
|
||||
this.size += this.estimatedMessageOverhead + (val.payload?.base64Message.length ?? 0)
|
||||
this.length += 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Base64Message } from './Base64Message'
|
||||
import { QoS } from '../DataSource/MqttSource'
|
||||
import { MemoryConsumptionExpressedByLength } from './RingBuffer'
|
||||
|
||||
export interface Message {
|
||||
export interface Message extends MemoryConsumptionExpressedByLength {
|
||||
// mqtt based info
|
||||
payload: Base64Message | null
|
||||
messageId?: number
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Destroyable } from './Destroyable'
|
||||
import { Edge, Message, RingBuffer, MessageHistory } from './'
|
||||
import { EventDispatcher } from '../../../events'
|
||||
|
||||
export type TopicDataType = 'string' | 'json' | 'hex'
|
||||
|
||||
export class TreeNode<ViewModel extends Destroyable> {
|
||||
public sourceEdge?: Edge<ViewModel>
|
||||
public message?: Message
|
||||
@@ -17,6 +19,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
public onMessage = new EventDispatcher<Message>()
|
||||
public onDestroy = new EventDispatcher<TreeNode<ViewModel>>()
|
||||
public isTree = false
|
||||
public type: TopicDataType = 'json'
|
||||
|
||||
private cachedPath?: string
|
||||
private cachedChildTopics?: Array<TreeNode<ViewModel>>
|
||||
@@ -153,7 +156,7 @@ export class TreeNode<ViewModel extends Destroyable> {
|
||||
|
||||
public path(): string {
|
||||
if (!this.cachedPath) {
|
||||
return this.branch()
|
||||
this.cachedPath = this.branch()
|
||||
.map(node => node.sourceEdge && node.sourceEdge.name)
|
||||
.filter(name => name !== undefined)
|
||||
.join('/')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Destroyable } from './Destroyable'
|
||||
import { Edge, Tree, TreeNode } from './'
|
||||
import { MqttMessage } from '../../../events'
|
||||
import { Base64Message } from './Base64Message'
|
||||
|
||||
export abstract class TreeNodeFactory {
|
||||
private static messageCounter = 0
|
||||
@@ -30,7 +31,8 @@ export abstract class TreeNodeFactory {
|
||||
mqttMessage.retain
|
||||
node.setMessage({
|
||||
...mqttMessage,
|
||||
length: mqttMessage.payload?.length ?? 0,
|
||||
payload: mqttMessage.payload && new Base64Message(mqttMessage.payload?.base64Message),
|
||||
length: mqttMessage.payload?.base64Message.length ?? 0,
|
||||
received: receiveDate,
|
||||
messageNumber: this.messageCounter,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { Edge } from './Edge'
|
||||
export { TreeNode } from './TreeNode'
|
||||
export { TreeNode, TopicDataType } from './TreeNode'
|
||||
export { Message } from './Message'
|
||||
export { TreeNodeFactory } from './TreeNodeFactory'
|
||||
export { Tree } from './Tree'
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Base64Message } from './Base64Message'
|
||||
import { Decoder } from './Decoder'
|
||||
import { get } from 'sparkplug-payload'
|
||||
var sparkplug = get("spBv1.0")
|
||||
|
||||
export const SparkplugDecoder = {
|
||||
decode(input: Buffer): Base64Message {
|
||||
try {
|
||||
const message = Base64Message.fromString(JSON.stringify(
|
||||
// @ts-ignore
|
||||
sparkplug.decodePayload(new Uint8Array(input)))
|
||||
)
|
||||
message.decoder = Decoder.SPARKPLUG
|
||||
return message
|
||||
} catch {
|
||||
const message = Base64Message.fromString("Failed to decode sparkplugb payload")
|
||||
message.decoder = Decoder.NONE
|
||||
return message
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'mocha'
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { Base64Message } from '../Base64Message'
|
||||
import { makeTreeNode } from './makeTreeNode'
|
||||
|
||||
describe('TreeNode', () => {
|
||||
@@ -14,7 +13,7 @@ describe('TreeNode', () => {
|
||||
it('updateWithNode should update value', () => {
|
||||
const topics = 'foo/bar'.split('/')
|
||||
const leaf = makeTreeNode('foo/bar', '3')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
|
||||
expect(leaf.message!.payload!.toUnicodeString()).to.eq('3')
|
||||
|
||||
const updateLeave = makeTreeNode('foo/bar', '5')
|
||||
|
||||
@@ -22,13 +21,13 @@ describe('TreeNode', () => {
|
||||
root.updateWithNode(updateLeave.firstNode())
|
||||
|
||||
expect(root.sourceEdge).to.eq(undefined)
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('5')
|
||||
expect(leaf.message!.payload!.toUnicodeString()).to.eq('5')
|
||||
})
|
||||
|
||||
it('updateWithNode should update intermediate nodes', () => {
|
||||
const topics1 = 'foo/bar/baz'.split('/')
|
||||
const leaf = makeTreeNode('foo/bar/baz', '3')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
|
||||
expect(leaf.message!.payload!.toUnicodeString()).to.eq('3')
|
||||
|
||||
const topics2 = 'foo/bar'.split('/')
|
||||
const updateLeave = makeTreeNode('foo/bar', '5')
|
||||
@@ -37,10 +36,10 @@ describe('TreeNode', () => {
|
||||
|
||||
const barNode = leaf.firstNode().findNode('foo/bar')
|
||||
expect(barNode && barNode.sourceEdge && barNode.sourceEdge.name).to.eq('bar')
|
||||
expect(Base64Message.toUnicodeString(barNode!.message!.payload!)).to.eq('5')
|
||||
expect(barNode!.message!.payload!.toUnicodeString()).to.eq('5')
|
||||
|
||||
expect(leaf.sourceEdge && leaf.sourceEdge.name).to.eq('baz')
|
||||
expect(Base64Message.toUnicodeString(leaf.message!.payload!)).to.eq('3')
|
||||
expect(leaf.message!.payload!.toUnicodeString()).to.eq('3')
|
||||
})
|
||||
|
||||
it('updateWithNode should add nodes to the tree', () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { Base64Message } from '../Base64Message'
|
||||
import { makeTreeNode } from './makeTreeNode'
|
||||
|
||||
describe('TreeNodeFactory', () => {
|
||||
@@ -20,7 +19,7 @@ describe('TreeNodeFactory', () => {
|
||||
|
||||
expect(node).to.not.eq(undefined)
|
||||
expect(node.sourceEdge.name).to.eq('bar')
|
||||
expect(Base64Message.toUnicodeString(node.message.payload!)).to.eq('5')
|
||||
expect(node.message.payload!.toUnicodeString()).to.eq('5')
|
||||
|
||||
const foo = node.firstNode().findNode('foo')
|
||||
expect(foo && foo.sourceEdge && foo.sourceEdge.name).to.eq('foo')
|
||||
@@ -34,7 +33,7 @@ describe('TreeNodeFactory', () => {
|
||||
return
|
||||
}
|
||||
|
||||
expect(Base64Message.toUnicodeString(node.message.payload!)).to.eq('5')
|
||||
expect(node.message.payload!.toUnicodeString()).to.eq('5')
|
||||
expect(node.sourceEdge.name).to.eq('baz')
|
||||
|
||||
const barNode = node.sourceEdge.source
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
makePublishEvent,
|
||||
removeConnection,
|
||||
} from '../../events'
|
||||
import { SparkplugDecoder } from './Model/sparkplugb'
|
||||
|
||||
export class ConnectionManager {
|
||||
private connections: { [s: string]: DataSource<any> } = {}
|
||||
@@ -48,12 +47,7 @@ export class ConnectionManager {
|
||||
}
|
||||
|
||||
let decoded_payload = null
|
||||
// spell-checker: disable-next-line
|
||||
if (topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u)) {
|
||||
decoded_payload = SparkplugDecoder.decode(buffer)
|
||||
} else {
|
||||
decoded_payload = Base64Message.fromBuffer(buffer)
|
||||
}
|
||||
|
||||
backendEvents.emit(messageEvent, {
|
||||
topic,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Base64Message } from '../backend/src/Model/Base64Message'
|
||||
import { Base64MessageDTO } from '../backend/src/Model/Base64Message'
|
||||
import { DataSourceState, MqttOptions } from '../backend/src/DataSource'
|
||||
import { UpdateInfo } from 'builder-util-runtime'
|
||||
import { RpcEvent } from './EventSystem/Rpc'
|
||||
@@ -32,7 +32,7 @@ export const updateAvailable: Event<UpdateInfo> = {
|
||||
|
||||
export interface MqttMessage {
|
||||
topic: string
|
||||
payload: Base64Message | null
|
||||
payload: Base64MessageDTO | null
|
||||
qos: 0 | 1 | 2
|
||||
retain: boolean
|
||||
// Set if QoS is > 0 on received messages
|
||||
|
||||
@@ -19,7 +19,8 @@ registerCrashReporter()
|
||||
// const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions)
|
||||
// }
|
||||
|
||||
app.commandLine.appendSwitch('--no-sandbox')
|
||||
// disable-dev-shm-usage is required to run the debug console
|
||||
app.commandLine.appendSwitch('--no-sandbox --disable-dev-shm-usage')
|
||||
app.whenReady().then(() => {
|
||||
backendRpc.on(makeOpenDialogRpc(), async request => {
|
||||
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request)
|
||||
@@ -69,8 +70,6 @@ async function createWindow() {
|
||||
}
|
||||
})
|
||||
|
||||
console.log('icon path', iconPath)
|
||||
|
||||
// Load the index.html of the app.
|
||||
if (isDev()) {
|
||||
mainWindow.loadURL('http://localhost:8080')
|
||||
|
||||
@@ -9,15 +9,11 @@
|
||||
"moduleResolution": "node",
|
||||
"sourceRoot": "src/",
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["ES2017", "dom"],
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"skipLibCheck": true
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"src/electron.ts",
|
||||
@@ -26,7 +22,5 @@
|
||||
"src/spec/leakTest.ts",
|
||||
"scripts/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
260
yarn.lock
260
yarn.lock
@@ -659,6 +659,38 @@
|
||||
minimatch "^3.0.4"
|
||||
plist "^3.0.4"
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@eslint-community/regexpp@^4.6.1":
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
|
||||
integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
|
||||
|
||||
"@eslint/eslintrc@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6"
|
||||
integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==
|
||||
dependencies:
|
||||
ajv "^6.12.4"
|
||||
debug "^4.3.2"
|
||||
espree "^10.0.1"
|
||||
globals "^14.0.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.1.0"
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.3.0":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.3.0.tgz#2e8f65c9c55227abc4845b1513c69c32c679d8fe"
|
||||
integrity sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==
|
||||
|
||||
"@fastify/busboy@^2.0.0":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
|
||||
@@ -683,6 +715,30 @@
|
||||
reflect-metadata "^0.1.12"
|
||||
tslib "^1.8.1"
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
version "0.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
|
||||
integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^2.0.3"
|
||||
debug "^4.3.1"
|
||||
minimatch "^3.0.5"
|
||||
|
||||
"@humanwhocodes/module-importer@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
|
||||
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
|
||||
|
||||
"@humanwhocodes/object-schema@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
|
||||
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
|
||||
|
||||
"@humanwhocodes/retry@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570"
|
||||
integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"
|
||||
@@ -786,7 +842,7 @@
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
|
||||
"@nodelib/fs.walk@^1.2.3":
|
||||
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
|
||||
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
|
||||
@@ -1514,12 +1570,17 @@ about-window@^1.12.1:
|
||||
resolved "https://registry.npmjs.org/about-window/-/about-window-1.15.2.tgz"
|
||||
integrity sha512-31mDAnLUfKm4uShfMzeEoS6a3nEto2tUt4zZn7qyAKedaTV4p0dGiW1n+YG8vtRh78mZiewghWJmoxDY+lHyYg==
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz"
|
||||
integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==
|
||||
|
||||
acorn@^8.4.1:
|
||||
acorn@^8.11.3, acorn@^8.4.1:
|
||||
version "8.11.3"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
|
||||
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
|
||||
@@ -1559,7 +1620,7 @@ ajv-keywords@^3.4.1:
|
||||
resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz"
|
||||
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
||||
|
||||
ajv@^6.10.0, ajv@^6.12.0:
|
||||
ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
|
||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||
@@ -2475,7 +2536,7 @@ cross-spawn@^6.0.5:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.3:
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
@@ -2674,6 +2735,11 @@ deep-extend@^0.6.0:
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
||||
default-require-extensions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz"
|
||||
@@ -3069,16 +3135,112 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
|
||||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
||||
|
||||
eslint-plugin-react-hooks@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596"
|
||||
integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==
|
||||
|
||||
eslint-scope@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc"
|
||||
integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
eslint-visitor-keys@^3.3.0:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
|
||||
eslint-visitor-keys@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
|
||||
integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
|
||||
|
||||
eslint@^9.3.0:
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.3.0.tgz#36a96db84592618d6ed9074d677e92f4e58c08b9"
|
||||
integrity sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.6.1"
|
||||
"@eslint/eslintrc" "^3.1.0"
|
||||
"@eslint/js" "9.3.0"
|
||||
"@humanwhocodes/config-array" "^0.13.0"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@humanwhocodes/retry" "^0.3.0"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
ajv "^6.12.4"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.2"
|
||||
debug "^4.3.2"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^8.0.1"
|
||||
eslint-visitor-keys "^4.0.0"
|
||||
espree "^10.0.1"
|
||||
esquery "^1.4.2"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^8.0.0"
|
||||
find-up "^5.0.0"
|
||||
glob-parent "^6.0.2"
|
||||
ignore "^5.2.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
is-path-inside "^3.0.3"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
lodash.merge "^4.6.2"
|
||||
minimatch "^3.1.2"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
espree@^10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f"
|
||||
integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==
|
||||
dependencies:
|
||||
acorn "^8.11.3"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^4.0.0"
|
||||
|
||||
esprima@^4.0.0, esprima@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.4.2:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
|
||||
integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
|
||||
dependencies:
|
||||
estraverse "^5.1.0"
|
||||
|
||||
esrecurse@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
|
||||
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
|
||||
dependencies:
|
||||
estraverse "^5.2.0"
|
||||
|
||||
estraverse@^5.1.0, estraverse@^5.2.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
|
||||
esutils@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz"
|
||||
integrity sha512-RG1ZkUT7iFJG9LSHr7KDuuMSlujfeTtMNIcInURxKAxhMtwQhI3NrQhz26gZQYlsYZQKzsnwtpKrFKj9K9Qu1A==
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
event-stream@=3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz"
|
||||
@@ -3161,7 +3323,7 @@ extsprintf@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
|
||||
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
@@ -3187,6 +3349,11 @@ fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
fast-levenshtein@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fastest-levenshtein@^1.0.16:
|
||||
version "1.0.16"
|
||||
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
|
||||
@@ -3255,7 +3422,7 @@ find-up-simple@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz"
|
||||
integrity sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==
|
||||
|
||||
find-up@5.0.0:
|
||||
find-up@5.0.0, find-up@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz"
|
||||
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
|
||||
@@ -3557,6 +3724,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob-parent@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
|
||||
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz"
|
||||
@@ -3626,6 +3800,11 @@ globals@^11.1.0:
|
||||
resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
|
||||
globals@^14.0.0:
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
|
||||
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
|
||||
|
||||
globalthis@^1.0.1, globalthis@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz"
|
||||
@@ -3897,12 +4076,12 @@ ignore-walk@^6.0.4:
|
||||
dependencies:
|
||||
minimatch "^9.0.0"
|
||||
|
||||
ignore@^5.2.4:
|
||||
ignore@^5.2.0, ignore@^5.2.4:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
|
||||
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
|
||||
|
||||
import-fresh@^3.3.0:
|
||||
import-fresh@^3.2.1, import-fresh@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"
|
||||
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
||||
@@ -4104,7 +4283,7 @@ is-fullwidth-code-point@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-glob@^4.0.1, is-glob@~4.0.1:
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
@@ -4138,6 +4317,11 @@ is-obj@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz"
|
||||
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
|
||||
|
||||
is-path-inside@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
||||
|
||||
is-plain-obj@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz"
|
||||
@@ -4438,6 +4622,11 @@ json-schema-traverse@^0.4.1:
|
||||
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json-stringify-nice@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67"
|
||||
@@ -4509,6 +4698,14 @@ leven@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz"
|
||||
integrity sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
libnpmaccess@^8.0.1:
|
||||
version "8.0.5"
|
||||
resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-8.0.5.tgz#ef14fecab8385669e91d6be27971ae448064211f"
|
||||
@@ -4703,6 +4900,11 @@ lodash.isstring@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
|
||||
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash.uniqby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
|
||||
@@ -4924,7 +5126,7 @@ minimatch@5.0.1:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
@@ -5126,6 +5328,11 @@ mz@^2.4.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
negotiator@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
@@ -5499,6 +5706,18 @@ onetime@^6.0.0:
|
||||
dependencies:
|
||||
mimic-fn "^4.0.0"
|
||||
|
||||
optionator@^0.9.3:
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
|
||||
integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
|
||||
dependencies:
|
||||
deep-is "^0.1.3"
|
||||
fast-levenshtein "^2.0.6"
|
||||
levn "^0.4.1"
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.5"
|
||||
|
||||
p-cancelable@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz"
|
||||
@@ -5908,6 +6127,11 @@ postcss-selector-parser@^6.0.10:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@^3.2.5:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz"
|
||||
@@ -6892,7 +7116,7 @@ strip-final-newline@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
|
||||
integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==
|
||||
|
||||
strip-json-comments@3.1.1:
|
||||
strip-json-comments@3.1.1, strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
@@ -7010,7 +7234,7 @@ text-extensions@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.4.0.tgz#a1cfcc50cf34da41bfd047cc744f804d1680ea34"
|
||||
integrity sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==
|
||||
|
||||
text-table@~0.2.0:
|
||||
text-table@^0.2.0, text-table@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
|
||||
@@ -7228,6 +7452,13 @@ tunnel@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
||||
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
|
||||
type-detect@^4.0.0, type-detect@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
|
||||
@@ -7534,6 +7765,11 @@ which@^4.0.0:
|
||||
dependencies:
|
||||
isexe "^3.1.1"
|
||||
|
||||
word-wrap@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wordwrap@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
|
||||
Reference in New Issue
Block a user