fix: repair types

This commit is contained in:
Thomas Nordquist
2024-05-23 16:29:58 +02:00
parent c88978f0dd
commit a2c4388c78
19 changed files with 180 additions and 110 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,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'

View File

@@ -65,7 +65,7 @@ export const MessageHistory: React.FC<Props> = props => {
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 = node.message ? decodeMessage(node.message)?.format()[0] ?? null : null const value = node.message ? decodeMessage(message)?.message?.format()[0] ?? null : null
const element = { const element = {
value: value ?? '', value: value ?? '',
@@ -96,7 +96,7 @@ export const MessageHistory: React.FC<Props> = props => {
return element return element
}) })
const value = node.message ? decodeMessage(node.message)?.format()[0] ?? null : null const value = node.message ? decodeMessage(node.message)?.message?.format()[0] ?? null : null
const isMessagePlottable = isPlottable(value) const isMessagePlottable = isPlottable(value)
return ( return (

View File

@@ -56,7 +56,7 @@ function ValuePanel(props: Props) {
} }
const getDecodedValue = useCallback(() => { const getDecodedValue = useCallback(() => {
return node?.message && decodeMessage(node.message)?.toUnicodeString() return node?.message && decodeMessage(node.message)?.message?.toUnicodeString()
}, [node, decodeMessage]) }, [node, decodeMessage])
function messageMetaInfo() { function messageMetaInfo() {

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 { 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 { DecoderFunction, useDecoder } from '../../hooks/useDecoder' import { useDecoder } from '../../hooks/useDecoder'
import { TopicViewModel } from '../../../model/TopicViewModel'
interface Props { interface Props {
message: q.Message message: q.Message
@@ -15,86 +16,112 @@ interface Props {
renderMode: ValueRendererDisplayMode renderMode: ValueRendererDisplayMode
} }
export const ValueRenderer: React.FC<Props> = props => { type Language = 'json'
const decodeMessage = useDecoder(props.treeNode)
function renderDiff(current: string = '', previous: string = '', title?: string, language?: 'json') { function renderDiff(
return ( treeNode: q.TreeNode<TopicViewModel>,
<CodeDiff compareWithPreviousMessage: boolean,
treeNode={props.treeNode} current: string = '',
previous={previous} previous: string = '',
current={current} title?: string,
title={title} language?: Language
language={language} ) {
nameOfCompareMessage={props.compareWith ? 'selected' : 'previous'} return (
/> <CodeDiff
) treeNode={treeNode}
} previous={previous}
current={current}
title={title}
language={language}
nameOfCompareMessage={compareWithPreviousMessage ? 'selected' : 'previous'}
/>
)
}
function renderDiffMode( function renderDiffMode(
decodeMessage: DecoderFunction, treeNode: q.TreeNode<TopicViewModel>,
message: q.Message, currentStr: string | undefined,
treeNode: q.TreeNode<any>, compareStr: string | undefined,
compare?: q.Message currentType: Language | undefined,
compareType: Language | undefined,
compareWithPreviousMessage: boolean
) {
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined
return <div>{renderDiff(treeNode, compareWithPreviousMessage, currentStr, compareStr, undefined, language)}</div>
}
function renderRawMode(
treeNode: q.TreeNode<TopicViewModel>,
currentStr: string | undefined,
compareStr: string | undefined,
currentType: Language | undefined,
compareType: Language | undefined,
compareWithPreviousMessage: boolean
) {
return (
<div>
{renderDiff(treeNode, compareWithPreviousMessage, currentStr, currentStr, undefined, currentType)}
<Fade in={Boolean(compareStr)} timeout={400}>
<div>
{Boolean(compareStr)
? renderDiff(treeNode, compareWithPreviousMessage, compareStr, compareStr, 'selected', compareType)
: null}
</div>
</Fade>
</div>
)
}
export const ValueRenderer: React.FC<Props> = ({ treeNode, compareWith: compare, message, renderMode }) => {
const decodeMessage = useDecoder(treeNode)
const decodedMessage = useMemo(() => decodeMessage(message), [decodeMessage, message])
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compare || previousMessage || message
const compareWithPreviousMessage = !!compare
const [currentStr, currentType] = useMemo(
() => decodedMessage?.message?.format(treeNode.type) ?? [],
[decodedMessage, treeNode.type]
)
const [compareStr, compareType] = useMemo(
() => decodeMessage(compareMessage)?.message?.format(treeNode.type) ?? [],
[compareMessage, decodeMessage, treeNode.type]
)
function renderValue(
treeNode: q.TreeNode<TopicViewModel>,
currentStr: string | undefined,
compareStr: string | undefined,
currentType: Language | undefined,
compareType: Language | undefined,
renderMode: string,
compareWithPreviousMessage: boolean
) { ) {
if (!message.payload) { if (!decodedMessage) {
return
}
const previousMessages = treeNode.messageHistory.toArray()
const previousMessage = previousMessages[previousMessages.length - 2]
const compareMessage = compare || previousMessage || message
const [currentStr, currentType] = decodeMessage(message)?.format(treeNode.type) ?? []
const [compareStr, compareType] = decodeMessage(compareMessage)?.format(treeNode.type) ?? []
const language = currentType === compareType && compareType === 'json' ? 'json' : undefined
return <div>{renderDiff(currentStr, compareStr, undefined, language)}</div>
}
function renderRawMode(
decodeMessage: DecoderFunction,
message: q.Message,
treeNode: q.TreeNode<any>,
compare?: q.Message
) {
if (!message.payload) {
return
}
const [currentStr, currentType] = decodeMessage(message)?.format(treeNode.type) ?? []
const [compareStr, compareType] =
compare && compare.payload ? decodeMessage(compare)?.format(treeNode.type) ?? [] : []
return (
<div>
{renderDiff(currentStr, currentStr, undefined, currentType)}
<Fade in={Boolean(compareStr)} timeout={400}>
<div>{Boolean(compareStr) ? renderDiff(compareStr, compareStr, 'selected', compareType) : null}</div>
</Fade>
</div>
)
}
function renderValue(decodeMessage: DecoderFunction) {
const { message, treeNode, compareWith, renderMode } = props
if (!message.payload) {
return null return null
} }
switch (renderMode) { switch (renderMode) {
case 'diff': case 'diff':
return renderDiffMode(decodeMessage, message, treeNode, compareWith) return renderDiffMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
default: default:
return renderRawMode(decodeMessage, message, treeNode, compareWith) return renderRawMode(treeNode, currentStr, compareStr, currentType, compareType, compareWithPreviousMessage)
} }
} }
const renderedValue = useMemo(
() =>
renderValue(treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage),
[treeNode, currentStr, compareStr, currentType, compareType, renderMode, compareWithPreviousMessage]
)
return ( return (
<div style={{ padding: '0px 0px 8px 0px', width: '100%' }}> <div style={{ padding: '0px 0px 8px 0px', width: '100%' }}>
{props.message?.payload?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'} {decodedMessage?.decoder === Decoder.SPARKPLUG && 'Decoded SparkplugB'}
{renderValue(decodeMessage)} {renderedValue}
</div> </div>
) )
} }

View File

@@ -30,7 +30,7 @@ function filterUsingTimeRange(startTime: number | undefined, data: Array<q.Messa
function nodeToHistory(decodeMessage: DecoderFunction, 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 decoded = decodeMessage(message)?.toUnicodeString() const decoded = decodeMessage(message)?.message?.toUnicodeString()
return { x: message.received.getTime(), y: toPlottableValue(decoded) } 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
@@ -46,7 +46,7 @@ function nodeDotPathToHistory(
.map((message: q.Message) => { .map((message: q.Message) => {
let json: any = {} let json: any = {}
try { try {
const decoded = decodeMessage(message) const decoded = decodeMessage(message)?.message
json = decoded ? JSON.parse(decoded.toUnicodeString()) : {} json = decoded ? JSON.parse(decoded.toUnicodeString()) : {}
} catch (ignore) {} } catch (ignore) {}

View File

@@ -32,7 +32,7 @@ export const TreeNodeTitle = (props: TreeNodeProps) => {
if (!props.treeNode.message || !props.treeNode.message.payload) { if (!props.treeNode.message || !props.treeNode.message.payload) {
return '' return ''
} }
const [value = ''] = decodeMessage(props.treeNode.message)?.format(props.treeNode.type) ?? [] const [value = ''] = decodeMessage(props.treeNode.message)?.message?.format(props.treeNode.type) ?? []
return value.length > limit ? `${value.slice(0, limit)}` : value return value.length > limit ? `${value.slice(0, limit)}` : value
} }

View File

@@ -1,11 +1,12 @@
import * as q from '../../../../backend/src/Model' import * as q from '../../../../backend/src/Model'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { TopicViewModel } from '../../model/TopicViewModel' import { TopicViewModel } from '../../model/TopicViewModel'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import { useSubscription } from './useSubscription' import { useSubscription } from './useSubscription'
import { useViewModel } from '../Tree/TreeNode/effects/useViewModel' 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) => Base64Message | null export type DecoderFunction = (message: q.Message) => DecoderEnvelope | undefined
/** /**
* Provides the latest decoder for a topic * Provides the latest decoder for a topic
@@ -21,7 +22,9 @@ export function useDecoder(treeNode: q.TreeNode<TopicViewModel> | undefined): De
return useCallback( return useCallback(
message => { message => {
return decoder && message.payload ? decoder.decoder.decode(message.payload, decoder.format) : message.payload return decoder && message.payload
? decoder.decoder.decode(message.payload, decoder.format)
: { message: message.payload ?? undefined, decoder: Decoder.NONE }
}, },
[decoder] [decoder]
) )

View File

@@ -1,4 +1,6 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Decoder } from '../../../backend/src/Model/Decoder'
import { DecoderEnvelope } from './DecoderEnvelope'
import { MessageDecoder } from './MessageDecoder' import { MessageDecoder } from './MessageDecoder'
type BinaryFormats = type BinaryFormats =
@@ -18,7 +20,7 @@ type BinaryFormats =
*/ */
export const BinaryDecoder: MessageDecoder<BinaryFormats> = { export const BinaryDecoder: MessageDecoder<BinaryFormats> = {
formats: ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float', 'double'], formats: ['int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float', 'double'],
decode(input: Base64Message, format: BinaryFormats): Base64Message { decode(input: Base64Message, format: BinaryFormats): DecoderEnvelope {
const decodingOption = { const decodingOption = {
int8: [Buffer.prototype.readInt8, 1], int8: [Buffer.prototype.readInt8, 1],
int16: [Buffer.prototype.readInt16LE, 2], int16: [Buffer.prototype.readInt16LE, 2],
@@ -37,12 +39,18 @@ export const BinaryDecoder: MessageDecoder<BinaryFormats> = {
const buf = input.toBuffer() const buf = input.toBuffer()
let str: String[] = [] let str: String[] = []
if (buf.length % bytesToRead !== 0) { if (buf.length % bytesToRead !== 0) {
return new Base64Message(undefined, 'Data type does not align with message') return {
error: 'Data type does not align with message',
decoder: Decoder.NONE,
}
} }
for (let index = 0; index < buf.length; index += bytesToRead) { for (let index = 0; index < buf.length; index += bytesToRead) {
str.push((readNumber as any).apply(buf, [index]).toString()) str.push((readNumber as any).apply(buf, [index]).toString())
} }
return Base64Message.fromString(JSON.stringify(str.length === 1 ? str[0] : str)) 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

@@ -1,4 +1,5 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { DecoderEnvelope } from './DecoderEnvelope'
export interface MessageDecoder<T = string> { export interface MessageDecoder<T = string> {
/** /**
@@ -8,5 +9,5 @@ export interface MessageDecoder<T = string> {
formats: T[] formats: T[]
canDecodeTopic?(topic: string): boolean canDecodeTopic?(topic: string): boolean
canDecodeData?(data: Base64Message): boolean canDecodeData?(data: Base64Message): boolean
decode(input: Base64Message, format: T | string | undefined): Base64Message decode(input: Base64Message, format: T | string | undefined): DecoderEnvelope
} }

View File

@@ -9,7 +9,7 @@ export const SparkplugDecoder: MessageDecoder = {
canDecodeTopic(topic: string) { canDecodeTopic(topic: string) {
return !!topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u) return !!topic.match(/^spBv1\.0\/[^/]+\/[ND](DATA|CMD|DEATH|BIRTH)\/[^/]+(\/[^/]+)?$/u)
}, },
decode(input: Base64Message): Base64Message { decode(input) {
try { try {
const message = Base64Message.fromString( const message = Base64Message.fromString(
JSON.stringify( JSON.stringify(
@@ -17,12 +17,12 @@ export const SparkplugDecoder: MessageDecoder = {
sparkplug.decodePayload(new Uint8Array(input.toBuffer())) sparkplug.decodePayload(new Uint8Array(input.toBuffer()))
) )
) )
message.decoder = Decoder.SPARKPLUG return { message, decoder: Decoder.SPARKPLUG }
return message
} catch { } catch {
const message = new Base64Message(undefined, 'Failed to decode sparkplugb payload') return {
message.decoder = Decoder.NONE error: 'Failed to decode sparkplugb payload',
return message decoder: Decoder.NONE,
}
} }
}, },
} }

View File

@@ -1,9 +1,10 @@
import { Base64Message } from '../../../backend/src/Model/Base64Message' import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Decoder } from '../../../backend/src/Model/Decoder'
import { MessageDecoder } from './MessageDecoder' import { MessageDecoder } from './MessageDecoder'
export const StringDecoder: MessageDecoder = { export const StringDecoder: MessageDecoder = {
formats: ['string'], formats: ['string'],
decode(input: Base64Message): Base64Message { decode(input: Base64Message) {
return input return { message: input, decoder: Decoder.NONE }
}, },
} }

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?.toBuffer() ?? '', { 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,20 +1,42 @@
import { Base64 } from 'js-base64' import { Base64 } from 'js-base64'
import { Decoder } from './Decoder'
import { TopicDataType } from './TreeNode' import { TopicDataType } from './TreeNode'
export type Base64MessageDTO = Pick<Base64Message, 'base64Message'>
export class Base64Message { export class Base64Message {
public base64Message: string public base64Message: string
private unicodeValue: string private _unicodeValue: string | undefined
public error?: string
public decoder: Decoder
public length: number
constructor(base64Str?: string, error?: string) { // Todo: Rename to `encodedLength`
this.base64Message = base64Str ?? '' public get length(): number {
this.error = error return this.base64Message.length
this.unicodeValue = Base64.decode(base64Str ?? '') }
this.length = base64Str?.length ?? 0
this.decoder = Decoder.NONE private get unicodeValue(): string {
if (!this._unicodeValue) {
this._unicodeValue = Base64.decode(this.base64Message ?? '')
}
return this._unicodeValue
}
constructor(base64Str?: string | Base64MessageDTO, error?: string) {
if (typeof base64Str === 'string' || typeof base64Str === 'undefined') {
this.base64Message = base64Str ?? ''
} else {
if (typeof base64Str.base64Message !== 'string') {
throw new Error('Received unexpected type in copy constructor')
}
this.base64Message = base64Str.base64Message
}
}
/**
* Override default JSON serialization behavior to only return the DTO
* @returns
*/
public toJSON(): Base64MessageDTO {
return { base64Message: this.base64Message }
} }
public toUnicodeString() { public toUnicodeString() {

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

@@ -32,7 +32,7 @@ export abstract class TreeNodeFactory {
node.setMessage({ node.setMessage({
...mqttMessage, ...mqttMessage,
payload: mqttMessage.payload && new Base64Message(mqttMessage.payload?.base64Message), payload: mqttMessage.payload && new Base64Message(mqttMessage.payload?.base64Message),
length: mqttMessage.payload?.length ?? 0, length: mqttMessage.payload?.base64Message.length ?? 0,
received: receiveDate, received: receiveDate,
messageNumber: this.messageCounter, messageNumber: this.messageCounter,
}) })

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