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 { AppState } from '../reducers'
import { Base64Message } from '../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events' import { MqttMessage, makePublishEvent, rendererEvents } from '../../../events'
export const setTopic = (topic?: string): Action => { export const setTopic = (topic?: string): Action => {
return { return {
@@ -41,7 +41,7 @@ export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, ge
} }
const publishEvent = makePublishEvent(connectionId) const publishEvent = makePublishEvent(connectionId)
const mqttMessage = { const mqttMessage: Partial<MqttMessage> = {
topic, topic,
payload: state.publish.payload ? Base64Message.fromString(state.publish.payload) : null, payload: state.publish.payload ? Base64Message.fromString(state.publish.payload) : null,
retain: state.publish.retain, retain: state.publish.retain,

View File

@@ -1,5 +1,5 @@
import * as q from '../../../backend/src/Model' 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 { AppState } from '../reducers'
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings' import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
import { Base64Message } from '../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../backend/src/Model/Base64Message'
@@ -68,13 +68,14 @@ export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispat
dispatch(storeSettings()) dispatch(storeSettings())
} }
export const setValueDisplayMode = (valueRendererDisplayMode: 'diff' | 'raw') => (dispatch: Dispatch<any>) => { export const setValueDisplayMode =
(valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => {
dispatch({ dispatch({
valueRendererDisplayMode, valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE, type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
}) })
dispatch(storeSettings()) dispatch(storeSettings())
} }
export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => { export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
dispatch({ dispatch({
@@ -117,7 +118,7 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
const messageMatches = const messageMatches =
node.message && node.message &&
node.message.payload && node.message.payload &&
Base64Message.toUnicodeString(node.message.payload).toLowerCase().indexOf(filterStr) !== -1 node.message.payload.toUnicodeString().toLowerCase().indexOf(filterStr) !== -1
return Boolean(messageMatches) return Boolean(messageMatches)
} }

View File

@@ -33,13 +33,8 @@ const debouncedSelectTopic = debounce(
setTopicDispatch = setTopic(topic.path()) setTopicDispatch = setTopic(topic.path())
} }
if (previouslySelectedTopic && previouslySelectedTopic.viewModel) { previouslySelectedTopic?.viewModel?.setSelected(false)
previouslySelectedTopic.viewModel.setSelected(false) topic.viewModel?.setSelected(true)
}
if (topic.viewModel) {
topic.viewModel.setSelected(true)
}
const selectTreeTopicDispatch = { const selectTreeTopicDispatch = {
selectedTopic: topic, selectedTopic: topic,

View File

@@ -114,6 +114,7 @@ function TopicChart(props: Props) {
</div> </div>
</div> </div>
<TopicPlot <TopicPlot
node={props.treeNode ? props.treeNode : undefined}
color={props.parameters.color} color={props.parameters.color}
interpolation={props.parameters.interpolation} interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined} 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 { connectionManagerActions } from '../../../actions'
import { ConnectionOptions } from '../../../model/ConnectionOptions' import { ConnectionOptions } from '../../../model/ConnectionOptions'
import { KeyCodes } from '../../../utils/KeyCodes' 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 { Theme, withStyles } from '@material-ui/core/styles'
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler' import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'

View File

@@ -123,7 +123,7 @@ function renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
return null 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 let value = node.message && node.message.payload ? parseFloat(str) : NaN
value = !isNaN(value) ? abbreviate(value) : str 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"> <Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
<Fade in={open} timeout={300}> <Fade in={open} timeout={300}>
<Paper style={{ width: '300px' }}> <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> </Paper>
</Fade> </Fade>
</Popper> </Popper>

View File

@@ -1,10 +1,9 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import React, { useState, useEffect, useCallback } from 'react' import React, { useState, useEffect, useCallback } from 'react'
import ExpandMore from '@material-ui/icons/ExpandMore'
import NodeStats from './NodeStats' import NodeStats from './NodeStats'
import ValuePanel from './ValueRenderer/ValuePanel' import ValuePanel from './ValueRenderer/ValuePanel'
import { AppState } from '../../reducers' 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 { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../actions' import { settingsActions, sidebarActions } from '../../actions'
@@ -28,7 +27,7 @@ interface Props {
} }
function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) { function useUpdateNodeWhenNodeReceivesUpdates(node?: q.TreeNode<any>) {
const [lastUpdate, setLastUpdate] = useState(0) const [, setLastUpdate] = useState(0)
const updateNode = useCallback( const updateNode = useCallback(
throttle(() => { throttle(() => {
setLastUpdate(node ? node.lastUpdate : 0) setLastUpdate(node ? node.lastUpdate : 0)
@@ -52,7 +51,6 @@ function Sidebar(props: Props) {
const { classes, tree, nodePath } = props const { classes, tree, nodePath } = props
const node = usePollingToFetchTreeNode(tree, nodePath || '') const node = usePollingToFetchTreeNode(tree, nodePath || '')
useUpdateNodeWhenNodeReceivesUpdates(node) useUpdateNodeWhenNodeReceivesUpdates(node)
// console.log(node && node.path(), tree, nodePath)
return ( return (
<div id="Sidebar" className={classes.drawer}> <div id="Sidebar" className={classes.drawer}>

View File

@@ -6,19 +6,19 @@ import Topic from './Topic'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton' import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
import { sidebarActions } from '../../../actions'
import { TopicDeleteButton } from './TopicDeleteButton' import { TopicDeleteButton } from './TopicDeleteButton'
import { TopicTypeButton } from './TopicTypeButton'
import { sidebarActions } from '../../../actions'
const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions }) => { const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActions }) => {
const { node } = props const { node } = props
console.log(node && node.path())
const copyTopic = node ? <Copy value={node.path()} /> : null const copyTopic = node ? <Copy value={node.path()} /> : null
const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => { const deleteTopic = useCallback((topic?: q.TreeNode<any>, recursive: boolean = false) => {
if (!topic) { if (!topic) {
return return
} }
props.actions.clearTopic(topic, recursive) props.actions.clearTopic(topic, recursive)
}, []) }, [])
@@ -29,11 +29,12 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
Topic {copyTopic} Topic {copyTopic}
<TopicDeleteButton node={node} deleteTopicAction={deleteTopic} /> <TopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} /> <RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<TopicTypeButton node={node} />
</span> </span>
<Topic node={node} /> <Topic node={node} />
</Panel> </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 DateFormatter from '../../helper/DateFormatter'
import History from '../HistoryDrawer' import History from '../HistoryDrawer'
import TopicPlot from '../../TopicPlot' import TopicPlot from '../../TopicPlot'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { isPlottable } from '../CodeDiff/util' import { isPlottable } from '../CodeDiff/util'
import { TopicViewModel } from '../../../model/TopicViewModel' import { TopicViewModel } from '../../../model/TopicViewModel'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
@@ -13,6 +12,8 @@ import { chartActions } from '../../../actions'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import CustomIconButton from '../../helper/CustomIconButton' import CustomIconButton from '../../helper/CustomIconButton'
import { MessageId } from '../MessageId' import { MessageId } from '../MessageId'
import { useSubscription } from '../../hooks/useSubscription'
import { useDecoder } from '../../hooks/useDecoder'
const throttle = require('lodash.throttle') const throttle = require('lodash.throttle')
@@ -25,56 +26,38 @@ interface Props {
} }
} }
interface State { export const MessageHistory: React.FC<Props> = props => {
displayMessage?: q.Message const [, setLastUpdate] = React.useState(Date.now())
anchorEl?: HTMLElement const updateNodeThrottled = React.useCallback(
lastUpdate: number throttle(() => {
} setLastUpdate
}, 300),
[]
)
class MessageHistory extends React.PureComponent<Props, State> { useSubscription(props.node?.onMessage, updateNodeThrottled)
private updateNode = throttle(() => { const decodeMessage = useDecoder(props.node)
this.setState({ lastUpdate: Date.now() })
}, 300)
constructor(props: any) { function addNodeToCharts(event: React.MouseEvent) {
super(props)
this.state = { lastUpdate: 0 }
}
private addNodeToCharts = (event: React.MouseEvent) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
const { node } = this.props const { node } = props
if (!node) { if (!node) {
return null return null
} }
this.props.actions.charts.addChart({ topic: node.path() }) props.actions.charts.addChart({ topic: node.path() })
} }
private displayMessage = (index: number, eventTarget: EventTarget) => { function displayMessage(index: number, eventTarget: EventTarget) {
const message = this.props.node && this.props.node.messageHistory.toArray().reverse()[index] const message = props.node && props.node.messageHistory.toArray().reverse()[index]
if (message) { if (message) {
this.props.onSelect(message) props.onSelect(message)
} }
} }
public componentWillReceiveProps(nextProps: Props) { const { node } = 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
if (!node) { if (!node) {
return null return null
} }
@@ -82,9 +65,10 @@ class MessageHistory extends React.PureComponent<Props, State> {
const history = node.messageHistory.toArray() const history = node.messageHistory.toArray()
let previousMessage: q.Message | undefined = node.message let previousMessage: q.Message | undefined = node.message
const historyElements = [...history].reverse().map((message, idx) => { 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 = { const element = {
value, value: value ?? '',
key: `${message.messageNumber}-${message.received}`, key: `${message.messageNumber}-${message.received}`,
title: ( title: (
<span> <span>
@@ -102,18 +86,19 @@ class MessageHistory extends React.PureComponent<Props, State> {
<MessageId message={message} /> <MessageId message={message} />
</span> </span>
<div style={{ float: 'right' }}> <div style={{ float: 'right' }}>
<Copy value={value} /> <Copy value={value ?? ''} />
</div> </div>
</span> </span>
), ),
selected: message && message === this.props.selected, selected: message && message === props.selected,
} }
previousMessage = message previousMessage = message
return element return element
}) })
const isMessagePlottable = const value = node.message ? decodeMessage(node.message)?.message?.format()[0] ?? null : null
node.message && node.message.payload && isPlottable(Base64Message.toUnicodeString(node.message.payload))
const isMessagePlottable = isPlottable(value)
return ( return (
<div> <div>
<History <History
@@ -122,20 +107,19 @@ class MessageHistory extends React.PureComponent<Props, State> {
isMessagePlottable ? ( isMessagePlottable ? (
<CustomIconButton <CustomIconButton
style={{ height: '22px', width: '22px' }} style={{ height: '22px', width: '22px' }}
onClick={this.addNodeToCharts} onClick={addNodeToCharts}
tooltip="Add to chart panel" tooltip="Add to chart panel"
> >
<ShowChart style={{ marginTop: '-5px' }} /> <ShowChart style={{ marginTop: '-5px' }} />
</CustomIconButton> </CustomIconButton>
) : undefined ) : undefined
} }
onClick={this.displayMessage} onClick={displayMessage}
> >
{isMessagePlottable ? <TopicPlot history={node.messageHistory} /> : null} {isMessagePlottable ? <TopicPlot node={node} history={node.messageHistory} /> : null}
</History> </History>
</div> </div>
) )
}
} }
const mapDispatchToProps = (dispatch: any) => { 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 React, { useCallback } from 'react'
import ValueRenderer from './ValueRenderer' import ValueRenderer from './ValueRenderer'
import { AppState } from '../../../reducers' import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { Theme, Typography, withStyles } from '@material-ui/core' import { Theme, Typography, withStyles } from '@material-ui/core'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions' import { sidebarActions } from '../../../actions'
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton' import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
import { MessageId } from '../MessageId' import { MessageId } from '../MessageId'
import { useDecoder } from '../../hooks/useDecoder'
interface Props { interface Props {
node?: q.TreeNode<any> node?: q.TreeNode<any>
@@ -35,6 +35,7 @@ function RenderedValue(props: { node?: q.TreeNode<any>; compareMessage?: q.Messa
function ValuePanel(props: Props) { function ValuePanel(props: Props) {
const { node, compareMessage } = props const { node, compareMessage } = props
const decodeMessage = useDecoder(node)
function renderViewOptions() { function renderViewOptions() {
if (!props.node || !props.node.message) { 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() { function messageMetaInfo() {
if (!props.node || !props.node.message) { if (!props.node || !props.node.message) {
return null return null
@@ -85,10 +90,9 @@ function ValuePanel(props: Props) {
[compareMessage] [compareMessage]
) )
const copyValue = const [value] =
node && node.message && node.message.payload ? ( node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined]
<Copy value={Base64Message.toUnicodeString(node.message.payload)} /> const copyValue = value ? <Copy getValue={getDecodedValue} /> : null
) : null
return ( return (
<Panel> <Panel>

View File

@@ -1,12 +1,13 @@
import * as q from '../../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import * as React from 'react' import React, { useMemo } from 'react'
import CodeDiff from '../CodeDiff' import CodeDiff from '../CodeDiff'
import { AppState } from '../../../reducers' import { AppState } from '../../../reducers'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { ValueRendererDisplayMode } from '../../../reducers/Settings' import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import { Fade } from '@material-ui/core' import { Fade } from '@material-ui/core'
import { Decoder } from '../../../../../backend/src/Model/Decoder' import { Decoder } from '../../../../../backend/src/Model/Decoder'
import { useDecoder } from '../../hooks/useDecoder'
import { TopicViewModel } from '../../../model/TopicViewModel'
interface Props { interface Props {
message: q.Message message: q.Message
@@ -15,103 +16,114 @@ interface Props {
renderMode: ValueRendererDisplayMode renderMode: ValueRendererDisplayMode
} }
interface State { type Language = 'json'
width: number
}
class ValueRenderer extends React.Component<Props, State> { function renderDiff(
constructor(props: Props) { treeNode: q.TreeNode<TopicViewModel>,
super(props) compareWithPreviousMessage: boolean,
this.state = { width: 0 } current: string = '',
} previous: string = '',
title?: string,
private renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') { language?: Language
) {
return ( return (
<CodeDiff <CodeDiff
treeNode={this.props.treeNode} treeNode={treeNode}
previous={previous} previous={previous}
current={current} current={current}
title={title} title={title}
language={language} language={language}
nameOfCompareMessage={this.props.compareWith ? 'selected' : 'previous'} nameOfCompareMessage={compareWithPreviousMessage ? 'selected' : 'previous'}
/> />
) )
} }
private convertMessage(msg?: Base64Message): [string | undefined, 'json' | undefined] { function renderDiffMode(
if (!msg) { treeNode: q.TreeNode<TopicViewModel>,
return [undefined, undefined] 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) return <div>{renderDiff(treeNode, compareWithPreviousMessage, currentStr, compareStr, undefined, language)}</div>
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]
function renderRawMode(
treeNode: q.TreeNode<TopicViewModel>,
currentStr: string | undefined,
compareStr: string | undefined,
currentType: Language | undefined,
compareType: Language | undefined,
compareWithPreviousMessage: boolean
) {
return ( return (
<div> <div>
{this.renderDiff(value, value, undefined, valueLanguage)} {renderDiff(treeNode, compareWithPreviousMessage, currentStr, currentStr, undefined, currentType)}
<Fade in={Boolean(compareStr)} timeout={400}> <Fade in={Boolean(compareStr)} timeout={400}>
<div> <div>
{Boolean(compareStr) ? this.renderDiff(compareStr, compareStr, 'selected', compareStrLanguage) : null} {Boolean(compareStr)
? renderDiff(treeNode, compareWithPreviousMessage, compareStr, compareStr, 'selected', compareType)
: null}
</div> </div>
</Fade> </Fade>
</div> </div>
) )
} }
public render() { export const ValueRenderer: React.FC<Props> = ({ treeNode, compareWith: compare, message, renderMode }) => {
return ( const decodeMessage = useDecoder(treeNode)
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}> const decodedMessage = useMemo(() => decodeMessage(message), [decodeMessage, message])
{this.props.message?.payload?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
{this.renderValue()}
</div>
)
}
public renderValue() {
const { message, treeNode, compareWith, renderMode } = this.props
const previousMessages = treeNode.messageHistory.toArray() const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2] const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compareWith || previousMessage || message const compareMessage = compare || previousMessage || message
const compareWithPreviousMessage = !!compare
if (renderMode === 'raw') { const [currentStr, currentType] = useMemo(
return this.renderRawMode(message, compareWith) () => decodedMessage?.message?.format(treeNode.type) ?? [],
} [decodedMessage, treeNode.type]
if (!message.payload) { )
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 return null
} }
const compareValue = compareMessage.payload || message.payload switch (renderMode) {
const [current, currentLanguage] = this.convertMessage(message.payload) case 'diff':
const [compare, compareLanguage] = this.convertMessage(compareValue) return renderDiffMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
default:
const language = currentLanguage === compareLanguage && compareLanguage === 'json' ? 'json' : undefined return renderRawMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
return this.renderDiff(current, compare, undefined, language)
} }
}
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) => { 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 q from '../../../backend/src/Model'
import * as React from 'react' import * as React from 'react'
import PlotHistory from './Chart/Chart' import PlotHistory from './Chart/Chart'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { toPlottableValue } from './Sidebar/CodeDiff/util' import { toPlottableValue } from './Sidebar/CodeDiff/util'
import { PlotCurveTypes } from '../reducers/Charts' import { PlotCurveTypes } from '../reducers/Charts'
import { DecoderFunction, useDecoder } from './hooks/useDecoder'
const parseDuration = require('parse-duration') const parseDuration = require('parse-duration')
interface Props { interface Props {
node?: q.TreeNode<any>
history: q.MessageHistory history: q.MessageHistory
dotPath?: string dotPath?: string
timeInterval?: string timeInterval?: string
@@ -25,21 +27,27 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
return data 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()) return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => { .map((message: q.Message) => {
const value = message.payload ? toPlottableValue(Base64Message.toUnicodeString(message.payload)) : NaN const decoded = decodeMessage(message)?.message?.toUnicodeString()
return { x: message.received.getTime(), y: toPlottableValue(value) } return { x: message.received.getTime(), y: toPlottableValue(decoded) }
}) })
.filter(data => !isNaN(data.y as any)) as any .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()) return filterUsingTimeRange(startTime, history.toArray())
.map((message: q.Message) => { .map((message: q.Message) => {
let json: any = {} let json: any = {}
try { try {
json = message.payload ? JSON.parse(Base64Message.toUnicodeString(message.payload)) : {} const decoded = decodeMessage(message)?.message
json = decoded ? JSON.parse(decoded.toUnicodeString()) : {}
} catch (ignore) {} } catch (ignore) {}
const value = dotProp.get(json, dotPath) const value = dotProp.get(json, dotPath)
@@ -50,14 +58,17 @@ function nodeDotPathToHistory(startTime: number | undefined, history: q.MessageH
} }
function TopicPlot(props: Props) { function TopicPlot(props: Props) {
const decodeMessage = useDecoder(props.node)
const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined const startOffset = props.timeInterval ? parseDuration(props.timeInterval) : undefined
const data = React.useMemo( const data = React.useMemo(() => {
() => if (!props.node) {
props.dotPath return []
? nodeDotPathToHistory(startOffset, props.history, props.dotPath) }
: nodeToHistory(startOffset, props.history),
[props.history.last(), startOffset, props.dotPath] return props.dotPath
) ? nodeDotPathToHistory(decodeMessage, startOffset, props.history, props.dotPath)
: nodeToHistory(decodeMessage, startOffset, props.history)
}, [props.history.last(), startOffset, props.dotPath])
return ( return (
<PlotHistory <PlotHistory

View File

@@ -1,8 +1,8 @@
import * as q from '../../../../../backend/src/Model' import * as q from '../../../../../backend/src/Model'
import React, { memo } from 'react' import React, { memo } from 'react'
import { Base64Message } from '../../../../../backend/src/Model/Base64Message'
import { Theme, withStyles } from '@material-ui/core' import { Theme, withStyles } from '@material-ui/core'
import { TopicViewModel } from '../../../model/TopicViewModel' import { TopicViewModel } from '../../../model/TopicViewModel'
import { useDecoder } from '../../hooks/useDecoder'
export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> { export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
treeNode: q.TreeNode<TopicViewModel> treeNode: q.TreeNode<TopicViewModel>
@@ -14,67 +14,72 @@ export interface TreeNodeProps extends React.HTMLAttributes<HTMLElement> {
classes: any classes: any
} }
class TreeNodeTitle extends React.PureComponent<TreeNodeProps, {}> { export const TreeNodeTitle = (props: TreeNodeProps) => {
private renderSourceEdge() { const decodeMessage = useDecoder(props.treeNode)
const name = this.props.name || (this.props.treeNode.sourceEdge && this.props.treeNode.sourceEdge.name)
function renderSourceEdge() {
const name = props.name || (props.treeNode.sourceEdge && props.treeNode.sourceEdge.name)
return ( 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} {name}
</span> </span>
) )
} }
private truncatedMessage() { function truncatedMessage() {
const limit = 400 const limit = 400
if (!this.props.treeNode.message || !this.props.treeNode.message.payload) { if (!props.treeNode.message || !props.treeNode.message.payload) {
return '' return ''
} }
const [value = ''] = decodeMessage(props.treeNode.message)?.message?.format(props.treeNode.type) ?? []
const str = Base64Message.toUnicodeString(this.props.treeNode.message.payload) return value.length > limit ? `${value.slice(0, limit)}` : value
return str.length > limit ? `${str.slice(0, limit)}` : str
} }
private renderValue() { function renderValue() {
return this.props.treeNode.message && return props.treeNode.message && props.treeNode.message.payload && props.treeNode.message.length > 0 ? (
this.props.treeNode.message.payload && <span key="value" className={props.classes.value}>
this.props.treeNode.message.length > 0 ? (
<span key="value" className={this.props.classes.value}>
{' '} {' '}
= {this.truncatedMessage()} = {truncatedMessage()}
</span> </span>
) : null ) : null
} }
private renderExpander() { function renderExpander() {
if (this.props.treeNode.edgeCount() === 0) { if (props.treeNode.edgeCount() === 0) {
return null return null
} }
return ( return (
<span key="expander" className={this.props.classes.expander} onClick={this.props.toggleCollapsed}> <span key="expander" className={props.classes.expander} onClick={props.toggleCollapsed}>
{this.props.collapsed ? '▶' : '▼'} {props.collapsed ? '▶' : '▼'}
</span> </span>
) )
} }
private renderMetadata() { function renderMetadata() {
if (this.props.treeNode.edgeCount() === 0 || !this.props.collapsed) { if (props.treeNode.edgeCount() === 0 || !props.collapsed) {
return null return null
} }
const messages = this.props.treeNode.leafMessageCount() const messages = props.treeNode.leafMessageCount()
const topicCount = this.props.treeNode.childTopicCount() const topicCount = props.treeNode.childTopicCount()
return ( return (
<span key="metadata" className={this.props.classes.collapsedSubnodes}>{` (${topicCount} ${ <span key="metadata" className={props.classes.collapsedSubnodes}>{` (${topicCount} ${
topicCount === 1 ? 'topic' : 'topics' topicCount === 1 ? 'topic' : 'topics'
}, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span> }, ${messages} ${messages === 1 ? 'message' : 'messages'})`}</span>
) )
} }
public render() { return (
return [this.renderExpander(), this.renderSourceEdge(), this.renderMetadata(), this.renderValue()] <>
} {renderExpander()}
{renderSourceEdge()}
{renderMetadata()}
{renderValue()}
</>
)
} }
const styles = (theme: Theme) => ({ 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 * as q from '../../../../../../backend/src/Model'
import React, { useEffect } from 'react' import React, { useCallback } from 'react'
import { TopicViewModel } from '../../../../model/TopicViewModel' import { TopicViewModel } from '../../../../model/TopicViewModel'
import { useSubscription } from '../../../hooks/useSubscription'
import { useViewModel } from './useViewModel'
export function useViewModelSubscriptions( export function useViewModelSubscriptions(
treeNode: q.TreeNode<TopicViewModel>, treeNode: q.TreeNode<TopicViewModel>,
@@ -8,37 +10,21 @@ export function useViewModelSubscriptions(
setSelected: (value: boolean) => void, setSelected: (value: boolean) => void,
setCollapsedOverride: (value: boolean) => void setCollapsedOverride: (value: boolean) => void
) { ) {
useEffect(() => { const viewModel = useViewModel(treeNode)
const selectionDidChange = () => {
const selected = treeNode.viewModel && treeNode.viewModel.isSelected() const selectionDidChange = useCallback(() => {
treeNode.viewModel && setSelected(Boolean(selected)) const selected = viewModel && viewModel.isSelected()
viewModel && setSelected(Boolean(selected))
if (selected && nodeRef && nodeRef.current) { if (selected && nodeRef && nodeRef.current) {
nodeRef.current.focus({ preventScroll: false }) nodeRef.current.focus({ preventScroll: false })
} }
} }, [viewModel])
const expandedDidChange = () => { const expandedDidChange = useCallback(() => {
treeNode.viewModel && setCollapsedOverride(!treeNode.viewModel.isExpanded()) viewModel && setCollapsedOverride(!viewModel.isExpanded())
} }, [viewModel])
function addSubscriber() { useSubscription(viewModel?.selectionChange, selectionDidChange)
treeNode.viewModel = new TopicViewModel() useSubscription(viewModel?.expandedChange, expandedDidChange)
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])
} }

View File

@@ -1,7 +1,7 @@
import * as compareVersions from 'compare-versions' import compareVersions from 'compare-versions'
import * as electron from 'electron' import electron from 'electron'
import * as os from 'os' import os from 'os'
import * as React from 'react' import React from 'react'
import axios from 'axios' import axios from 'axios'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
import CloudDownload from '@material-ui/icons/CloudDownload' import CloudDownload from '@material-ui/icons/CloudDownload'

View File

@@ -9,7 +9,8 @@ import { globalActions } from '../../actions'
const copy = require('copy-text-to-clipboard') const copy = require('copy-text-to-clipboard')
interface Props { interface Props {
value: string value?: string
getValue?: () => string | undefined
actions: { actions: {
global: typeof globalActions global: typeof globalActions
} }
@@ -28,7 +29,7 @@ class Copy extends React.PureComponent<Props, State> {
private handleClick = (event: React.MouseEvent) => { private handleClick = (event: React.MouseEvent) => {
event.stopPropagation() event.stopPropagation()
copy(this.props.value) copy(this.props.value ?? this.props.getValue?.())
this.props.actions.global.showNotification('Copied to clipboard') this.props.actions.global.showNotification('Copied to clipboard')
this.setState({ didCopy: true }) this.setState({ didCopy: true })
setTimeout(() => { setTimeout(() => {

View File

@@ -1,5 +1,5 @@
import * as moment from 'moment' import moment from 'moment'
import * as React from 'react' import React from 'react'
import { AppState } from '../../reducers' import { AppState } from '../../reducers'
import { connect } from 'react-redux' import { connect } from 'react-redux'
@@ -12,6 +12,7 @@ interface Props {
} }
const unitMapping = { const unitMapping = {
ms: 'milliseconds',
s: 'seconds', s: 'seconds',
m: 'minutes', m: 'minutes',
h: 'hours', h: 'hours',
@@ -21,7 +22,7 @@ class DateFormatter extends React.PureComponent<Props, {}> {
private intervalSince(intervalSince: Date) { private intervalSince(intervalSince: Date) {
const interval = intervalSince.getTime() - this.props.date.getTime() const interval = intervalSince.getTime() - this.props.date.getTime()
const unit = this.unitForInterval(interval) 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() { private legacyDate() {
@@ -31,10 +32,11 @@ class DateFormatter extends React.PureComponent<Props, {}> {
private localizedDate(locale: string) { private localizedDate(locale: string) {
return moment(this.props.date) return moment(this.props.date)
.locale(locale) .locale(locale)
.format(this.props.timeFirst ? 'LTS L' : 'L LTS') .format(this.props.timeFirst ? 'LTS.SSS L' : 'L LTS.SSS')
} }
private unitForInterval(milliseconds: number) { private unitForInterval(milliseconds: number) {
const oneSecond = 1000 * 1
const oneMinute = 1000 * 60 const oneMinute = 1000 * 60
const oneHour = oneMinute * 60 const oneHour = oneMinute * 60
@@ -46,9 +48,13 @@ class DateFormatter extends React.PureComponent<Props, {}> {
return 'm' return 'm'
} }
if (milliseconds > oneSecond * 0.5) {
return 's' return 's'
} }
return 'ms'
}
public render() { public render() {
const locale = this.props.overrideLocale || this.props.locale const locale = this.props.overrideLocale || this.props.locale
if (this.props.intervalSince) { 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 { ConnectionOptions, createEmptyConnection } from './ConnectionOptions'
import { v4 } from 'uuid'
interface LegacyConnectionSettings { interface LegacyConnectionSettings {
host: string host: string

View File

@@ -1,19 +1,77 @@
import * as q from '../../../backend/src/Model'
import { Destroyable } from '../../../backend/src/Model/Destroyable' import { Destroyable } from '../../../backend/src/Model/Destroyable'
import { MessageDecoder, decoders } from '../decoders'
import { EventDispatcher } from '../../../events' 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 { export class TopicViewModel implements Destroyable {
private selected: boolean private selected: boolean
private expanded: 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 selectionChange = new EventDispatcher<void>()
public expandedChange = 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.selected = false
this.expanded = false this.expanded = false
} }
public retain() {
this.referenceCounter += 1
}
public release() {
this.referenceCounter -= 1
if (this.referenceCounter <= 0) {
this.destroy()
}
}
public destroy() { public destroy() {
console.log('destroy', this.referenceCounter)
if (this.owner) {
this.owner.viewModel = undefined
this.owner = undefined
}
this.selectionChange.removeAllListeners() this.selectionChange.removeAllListeners()
this.onDecoderChange.removeAllListeners()
this.expandedChange.removeAllListeners()
} }
public isSelected() { public isSelected() {

View File

@@ -4,35 +4,23 @@
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true, "strictNullChecks": true,
"strict": true, "strict": true,
"lib": [ "lib": ["es2019", "dom"],
"es2017",
"dom"
],
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./build/", "outDir": "./build/",
"sourceMap": true, "sourceMap": true,
"module": "esnext", "module": "esnext",
"target": "es2017", "target": "ES2017",
"jsx": "react", "jsx": "react",
"paths": { "paths": {
"react": [ "react": ["./node_modules/@types/react"]
"./node_modules/@types/react"
]
}, },
"types": [ "types": ["react"],
"react"
],
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true "skipLibCheck": true,
"esModuleInterop": true
}, },
"include": [ "include": ["./src/**/*"],
"./src/**/*" "exclude": ["**/*.d.ts", ".src/**/*.png", "./node_modules"],
],
"exclude": [
"**/*.d.ts",
".src/**/*.png",
"./node_modules"
],
"awesomeTypescriptLoaderOptions": { "awesomeTypescriptLoaderOptions": {
"useCache": true, "useCache": true,
"transpileModule": 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'. // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: [
{
loader: 'ts-loader', loader: 'ts-loader',
// options: {
// configFile: './tsconfig.json',
// },
},
],
exclude: /node_modules/,
}, },
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
@@ -96,4 +104,7 @@ module.exports = {
// "react": "React", // "react": "React",
// "react-dom": "ReactDOM" // "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 FileAsync from 'lowdb/adapters/FileAsync'
import * as fs from 'fs-extra' import fs from 'fs-extra'
import * as lowdb from 'lowdb' import lowdb from 'lowdb'
import * as path from 'path' import path from 'path'
import { backendRpc } from '../../events' import { backendRpc } from '../../events'
import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents' import { storageClearEvent, storageLoadEvent, storageStoreEvent } from '../../events/StorageEvents'

View File

@@ -98,7 +98,7 @@ export class MqttSource implements DataSource<MqttOptions> {
public publish(msg: MqttMessage) { public publish(msg: MqttMessage) {
if (this.client) { 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, qos: msg.qos,
retain: msg.retain, retain: msg.retain,
}) })

View File

@@ -1,31 +1,93 @@
import { Base64 } from 'js-base64' import { Base64 } from 'js-base64'
import { Decoder } from './Decoder' import { TopicDataType } from './TreeNode'
export type Base64MessageDTO = Pick<Base64Message, 'base64Message'>
export class Base64Message { export class Base64Message {
private base64Message: string public base64Message: string
private unicodeValue: string private _unicodeValue: string | undefined
public decoder: Decoder
public length: number
private constructor(base64Str: string) { // Todo: Rename to `encodedLength`
this.base64Message = base64Str public get length(): number {
this.unicodeValue = Base64.decode(base64Str) return this.base64Message.length
this.length = base64Str.length
this.decoder = Decoder.NONE
} }
public static toUnicodeString(message: Base64Message) { private get unicodeValue(): string {
return message.unicodeValue || '' 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) { public static fromBuffer(buffer: Buffer) {
return new Base64Message(buffer.toString('base64')) return new Base64Message(buffer.toString('base64'))
} }
public toBuffer(): Buffer {
return Buffer.from(this.base64Message, 'base64')
}
public static fromString(str: string) { public static fromString(str: string) {
return new Base64Message(Base64.encode(str)) 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) { public static toDataUri(message: Base64Message, mimeType: string) {
return `data:${mimeType};base64,${message.base64Message}` return `data:${mimeType};base64,${message.base64Message}`
} }

View File

@@ -15,7 +15,7 @@ export class ChangeBuffer {
public push(val: MqttMessage) { public push(val: MqttMessage) {
if (!this.isFull()) { if (!this.isFull()) {
this.buffer.push({ message: val, received: new Date() }) 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 this.length += 1
} }
} }

View File

@@ -1,7 +1,8 @@
import { Base64Message } from './Base64Message' import { Base64Message } from './Base64Message'
import { QoS } from '../DataSource/MqttSource' import { QoS } from '../DataSource/MqttSource'
import { MemoryConsumptionExpressedByLength } from './RingBuffer'
export interface Message { export interface Message extends MemoryConsumptionExpressedByLength {
// mqtt based info // mqtt based info
payload: Base64Message | null payload: Base64Message | null
messageId?: number messageId?: number

View File

@@ -2,6 +2,8 @@ import { Destroyable } from './Destroyable'
import { Edge, Message, RingBuffer, MessageHistory } from './' import { Edge, Message, RingBuffer, MessageHistory } from './'
import { EventDispatcher } from '../../../events' import { EventDispatcher } from '../../../events'
export type TopicDataType = 'string' | 'json' | 'hex'
export class TreeNode<ViewModel extends Destroyable> { export class TreeNode<ViewModel extends Destroyable> {
public sourceEdge?: Edge<ViewModel> public sourceEdge?: Edge<ViewModel>
public message?: Message public message?: Message
@@ -17,6 +19,7 @@ export class TreeNode<ViewModel extends Destroyable> {
public onMessage = new EventDispatcher<Message>() public onMessage = new EventDispatcher<Message>()
public onDestroy = new EventDispatcher<TreeNode<ViewModel>>() public onDestroy = new EventDispatcher<TreeNode<ViewModel>>()
public isTree = false public isTree = false
public type: TopicDataType = 'json'
private cachedPath?: string private cachedPath?: string
private cachedChildTopics?: Array<TreeNode<ViewModel>> private cachedChildTopics?: Array<TreeNode<ViewModel>>
@@ -153,7 +156,7 @@ export class TreeNode<ViewModel extends Destroyable> {
public path(): string { public path(): string {
if (!this.cachedPath) { if (!this.cachedPath) {
return this.branch() this.cachedPath = this.branch()
.map(node => node.sourceEdge && node.sourceEdge.name) .map(node => node.sourceEdge && node.sourceEdge.name)
.filter(name => name !== undefined) .filter(name => name !== undefined)
.join('/') .join('/')

View File

@@ -1,6 +1,7 @@
import { Destroyable } from './Destroyable' import { Destroyable } from './Destroyable'
import { Edge, Tree, TreeNode } from './' import { Edge, Tree, TreeNode } from './'
import { MqttMessage } from '../../../events' import { MqttMessage } from '../../../events'
import { Base64Message } from './Base64Message'
export abstract class TreeNodeFactory { export abstract class TreeNodeFactory {
private static messageCounter = 0 private static messageCounter = 0
@@ -30,7 +31,8 @@ export abstract class TreeNodeFactory {
mqttMessage.retain mqttMessage.retain
node.setMessage({ node.setMessage({
...mqttMessage, ...mqttMessage,
length: mqttMessage.payload?.length ?? 0, payload: mqttMessage.payload && new Base64Message(mqttMessage.payload?.base64Message),
length: mqttMessage.payload?.base64Message.length ?? 0,
received: receiveDate, received: receiveDate,
messageNumber: this.messageCounter, messageNumber: this.messageCounter,
}) })

View File

@@ -1,5 +1,5 @@
export { Edge } from './Edge' export { Edge } from './Edge'
export { TreeNode } from './TreeNode' export { TreeNode, TopicDataType } from './TreeNode'
export { Message } from './Message' export { Message } from './Message'
export { TreeNodeFactory } from './TreeNodeFactory' export { TreeNodeFactory } from './TreeNodeFactory'
export { Tree } from './Tree' 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 'mocha'
import { expect } from 'chai' import { expect } from 'chai'
import { Base64Message } from '../Base64Message'
import { makeTreeNode } from './makeTreeNode' import { makeTreeNode } from './makeTreeNode'
describe('TreeNode', () => { describe('TreeNode', () => {
@@ -14,7 +13,7 @@ describe('TreeNode', () => {
it('updateWithNode should update value', () => { it('updateWithNode should update value', () => {
const topics = 'foo/bar'.split('/') const topics = 'foo/bar'.split('/')
const leaf = makeTreeNode('foo/bar', '3') 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') const updateLeave = makeTreeNode('foo/bar', '5')
@@ -22,13 +21,13 @@ describe('TreeNode', () => {
root.updateWithNode(updateLeave.firstNode()) root.updateWithNode(updateLeave.firstNode())
expect(root.sourceEdge).to.eq(undefined) 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', () => { it('updateWithNode should update intermediate nodes', () => {
const topics1 = 'foo/bar/baz'.split('/') const topics1 = 'foo/bar/baz'.split('/')
const leaf = makeTreeNode('foo/bar/baz', '3') 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 topics2 = 'foo/bar'.split('/')
const updateLeave = makeTreeNode('foo/bar', '5') const updateLeave = makeTreeNode('foo/bar', '5')
@@ -37,10 +36,10 @@ describe('TreeNode', () => {
const barNode = leaf.firstNode().findNode('foo/bar') const barNode = leaf.firstNode().findNode('foo/bar')
expect(barNode && barNode.sourceEdge && barNode.sourceEdge.name).to.eq('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(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', () => { it('updateWithNode should add nodes to the tree', () => {

View File

@@ -1,6 +1,5 @@
import 'mocha' import 'mocha'
import { expect } from 'chai' import { expect } from 'chai'
import { Base64Message } from '../Base64Message'
import { makeTreeNode } from './makeTreeNode' import { makeTreeNode } from './makeTreeNode'
describe('TreeNodeFactory', () => { describe('TreeNodeFactory', () => {
@@ -20,7 +19,7 @@ describe('TreeNodeFactory', () => {
expect(node).to.not.eq(undefined) expect(node).to.not.eq(undefined)
expect(node.sourceEdge.name).to.eq('bar') 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') const foo = node.firstNode().findNode('foo')
expect(foo && foo.sourceEdge && foo.sourceEdge.name).to.eq('foo') expect(foo && foo.sourceEdge && foo.sourceEdge.name).to.eq('foo')
@@ -34,7 +33,7 @@ describe('TreeNodeFactory', () => {
return 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') expect(node.sourceEdge.name).to.eq('baz')
const barNode = node.sourceEdge.source const barNode = node.sourceEdge.source

View File

@@ -10,7 +10,6 @@ import {
makePublishEvent, makePublishEvent,
removeConnection, removeConnection,
} from '../../events' } from '../../events'
import { SparkplugDecoder } from './Model/sparkplugb'
export class ConnectionManager { export class ConnectionManager {
private connections: { [s: string]: DataSource<any> } = {} private connections: { [s: string]: DataSource<any> } = {}
@@ -48,12 +47,7 @@ export class ConnectionManager {
} }
let decoded_payload = null 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) decoded_payload = Base64Message.fromBuffer(buffer)
}
backendEvents.emit(messageEvent, { backendEvents.emit(messageEvent, {
topic, 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 { DataSourceState, MqttOptions } from '../backend/src/DataSource'
import { UpdateInfo } from 'builder-util-runtime' import { UpdateInfo } from 'builder-util-runtime'
import { RpcEvent } from './EventSystem/Rpc' import { RpcEvent } from './EventSystem/Rpc'
@@ -32,7 +32,7 @@ export const updateAvailable: Event<UpdateInfo> = {
export interface MqttMessage { export interface MqttMessage {
topic: string topic: string
payload: Base64Message | null payload: Base64MessageDTO | null
qos: 0 | 1 | 2 qos: 0 | 1 | 2
retain: boolean retain: boolean
// Set if QoS is > 0 on received messages // Set if QoS is > 0 on received messages

View File

@@ -19,7 +19,8 @@ registerCrashReporter()
// const electronTelemetry = electronTelemetryFactory('9b0c8ca04a361eb8160d98c5', buildOptions) // 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(() => { app.whenReady().then(() => {
backendRpc.on(makeOpenDialogRpc(), async request => { backendRpc.on(makeOpenDialogRpc(), async request => {
return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], 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. // Load the index.html of the app.
if (isDev()) { if (isDev()) {
mainWindow.loadURL('http://localhost:8080') mainWindow.loadURL('http://localhost:8080')

View File

@@ -9,15 +9,11 @@
"moduleResolution": "node", "moduleResolution": "node",
"sourceRoot": "src/", "sourceRoot": "src/",
"target": "ES2017", "target": "ES2017",
"lib": [ "lib": ["ES2017", "dom"],
"es2017",
"dom"
],
"sourceMap": true, "sourceMap": true,
"types": [ "types": ["node"],
"node" "skipLibCheck": true,
], "esModuleInterop": true
"skipLibCheck": true
}, },
"include": [ "include": [
"src/electron.ts", "src/electron.ts",
@@ -26,7 +22,5 @@
"src/spec/leakTest.ts", "src/spec/leakTest.ts",
"scripts/*.ts" "scripts/*.ts"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }

260
yarn.lock
View File

@@ -659,6 +659,38 @@
minimatch "^3.0.4" minimatch "^3.0.4"
plist "^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": "@fastify/busboy@^2.0.0":
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
@@ -683,6 +715,30 @@
reflect-metadata "^0.1.12" reflect-metadata "^0.1.12"
tslib "^1.8.1" 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": "@isaacs/cliui@^8.0.2":
version "8.0.2" version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" 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" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 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" version "1.2.8"
resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 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" resolved "https://registry.npmjs.org/about-window/-/about-window-1.15.2.tgz"
integrity sha512-31mDAnLUfKm4uShfMzeEoS6a3nEto2tUt4zZn7qyAKedaTV4p0dGiW1n+YG8vtRh78mZiewghWJmoxDY+lHyYg== 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: acorn-walk@^8.1.1:
version "8.3.2" version "8.3.2"
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz"
integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==
acorn@^8.4.1: acorn@^8.11.3, acorn@^8.4.1:
version "8.11.3" version "8.11.3"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== 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" resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== 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" version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -2475,7 +2536,7 @@ cross-spawn@^6.0.5:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" 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" version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 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" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== 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: default-require-extensions@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz" 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" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== 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: esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 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: esutils@^1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz" resolved "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz"
integrity sha512-RG1ZkUT7iFJG9LSHr7KDuuMSlujfeTtMNIcInURxKAxhMtwQhI3NrQhz26gZQYlsYZQKzsnwtpKrFKj9K9Qu1A== 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: event-stream@=3.3.4:
version "3.3.4" version "3.3.4"
resolved "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz" 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" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== 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" version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 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" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 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: fastest-levenshtein@^1.0.16:
version "1.0.16" version "1.0.16"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" 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" resolved "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz"
integrity sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw== integrity sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==
find-up@5.0.0: find-up@5.0.0, find-up@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
@@ -3557,6 +3724,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
dependencies: dependencies:
is-glob "^4.0.1" 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: glob@8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" 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" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== 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: globalthis@^1.0.1, globalthis@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz" resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz"
@@ -3897,12 +4076,12 @@ ignore-walk@^6.0.4:
dependencies: dependencies:
minimatch "^9.0.0" minimatch "^9.0.0"
ignore@^5.2.4: ignore@^5.2.0, ignore@^5.2.4:
version "5.3.1" version "5.3.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== 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" version "3.3.0"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 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" resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 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" version "4.0.3"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 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" resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz"
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== 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: is-plain-obj@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" 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" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 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: json-stringify-nice@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" 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" resolved "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz"
integrity sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA== 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: libnpmaccess@^8.0.1:
version "8.0.5" version "8.0.5"
resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-8.0.5.tgz#ef14fecab8385669e91d6be27971ae448064211f" 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" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== 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: lodash.uniqby@^4.7.0:
version "4.7.0" version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
@@ -4924,7 +5126,7 @@ minimatch@5.0.1:
dependencies: dependencies:
brace-expansion "^2.0.1" 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" version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -5126,6 +5328,11 @@ mz@^2.4.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" 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: negotiator@^0.6.3:
version "0.6.3" version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
@@ -5499,6 +5706,18 @@ onetime@^6.0.0:
dependencies: dependencies:
mimic-fn "^4.0.0" 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: p-cancelable@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz" 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" cssesc "^3.0.0"
util-deprecate "^1.0.2" 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: prettier@^3.2.5:
version "3.2.5" version "3.2.5"
resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz" 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" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw== 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" version "3.1.1"
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 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" resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.4.0.tgz#a1cfcc50cf34da41bfd047cc744f804d1680ea34"
integrity sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g== 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" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== 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" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== 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: type-detect@^4.0.0, type-detect@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz"
@@ -7534,6 +7765,11 @@ which@^4.0.0:
dependencies: dependencies:
isexe "^3.1.1" 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: wordwrap@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"