Merge pull request #795 from thomasnordquist/tnordquist/decode-data-in-frontend

decode data in frontend
This commit is contained in:
Thomas Nordquist
2024-05-25 16:28:53 +02:00
committed by GitHub
49 changed files with 1467 additions and 646 deletions

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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}

View File

@@ -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'

View File

@@ -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

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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()]
)
}

View 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}</>
)
}

View File

@@ -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))

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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) => ({

View 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
}

View File

@@ -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)
}

View File

@@ -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'

View File

@@ -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(() => {

View File

@@ -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) {

View 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]
)
}

View 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])
}

View 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,
}
},
}

View 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
}

View 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
}

View 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,
}
}
},
}

View 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 }
},
}

View 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

View File

@@ -1,5 +1,4 @@
import { ConnectionOptions, createEmptyConnection } from './ConnectionOptions'
import { v4 } from 'uuid'
interface LegacyConnectionSettings {
host: string

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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',
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -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'

View File

@@ -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,
})

View File

@@ -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}`
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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('/')

View File

@@ -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,
})

View File

@@ -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'

View File

@@ -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
}
},
}

View File

@@ -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', () => {

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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')

View File

@@ -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
View File

@@ -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"