Compare commits

...
Author SHA1 Message Date
Thomas Nordquist affa56f8a2 Merge branch 'master' into copilot/make-byte-limit-configurable 2025-12-21 13:46:34 +01:00
copilot-swe-agent[bot]andthomasnordquist f0533a25db Adopt EventsV2 structure for setMaxMessageSize event
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-20 02:52:13 +00:00
Thomas Nordquist e0f6f86773 Merge branch 'master' into copilot/make-byte-limit-configurable 2025-12-20 03:46:44 +01:00
copilot-swe-agent[bot]andthomasnordquist de53571b88 Simplify validation to accept any integer >= 20KB
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 21:17:36 +00:00
copilot-swe-agent[bot]andthomasnordquist c7021e19ca Explicitly set unlimited to default when persisting
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 21:09:11 +00:00
copilot-swe-agent[bot]andthomasnordquist a84d79ac3a Fix backend validation to match all frontend size options
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 21:08:12 +00:00
copilot-swe-agent[bot]andthomasnordquist a3bca962ae Change to predefined size options (20KB, 100KB, 1MB, 5MB, Unlimited)
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 21:06:37 +00:00
copilot-swe-agent[bot]andthomasnordquist 715c50127b Improve input validation to prevent partial numeric strings
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 20:59:12 +00:00
copilot-swe-agent[bot]andthomasnordquist 1ad2ed73ec Improve UX with local state for max message size input
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 20:57:41 +00:00
copilot-swe-agent[bot]andthomasnordquist e607d70374 Extract magic numbers to shared constants
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 20:55:53 +00:00
copilot-swe-agent[bot]andthomasnordquist 1b1558ff12 Improve validation for max message size setting
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 20:53:11 +00:00
copilot-swe-agent[bot]andthomasnordquist 96b64fcffd Add configurable max message size setting with UI
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-19 20:50:54 +00:00
copilot-swe-agent[bot] 1ebb813261 Initial plan 2025-12-19 20:43:20 +00:00
6 changed files with 130 additions and 4 deletions
+22 -1
View File
@@ -10,6 +10,12 @@ import { globalActions } from './'
import { showError } from './Global'
import { showTree } from './Tree'
import { TopicViewModel } from '../model/TopicViewModel'
import { backendEvents } from '../../../events'
import {
Events,
MAX_MESSAGE_SIZE_UNLIMITED,
MAX_MESSAGE_SIZE_DEFAULT,
} from '../../../events/EventsV2'
const settingsIdentifier: StorageIdentifier<Partial<SettingsStateModel>> = {
id: 'Settings',
@@ -22,6 +28,9 @@ export const loadSettings = () => async (dispatch: Dispatch<any>, getState: () =
settings: getState().settings.merge(settings),
type: ActionTypes.SETTINGS_DID_LOAD_SETTINGS,
})
// Emit the maxMessageSize to backend after loading settings
const maxMessageSize = getState().settings.get('maxMessageSize')
backendEvents.emit(Events.setMaxMessageSize, maxMessageSize)
} catch (error) {
dispatch(showError(error))
}
@@ -29,11 +38,14 @@ export const loadSettings = () => async (dispatch: Dispatch<any>, getState: () =
}
export const storeSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const currentSettings = getState().settings.toJS()
const settings = {
...getState().settings.toJS(),
...currentSettings,
autoExpandLimit: undefined,
topicFilter: undefined,
visible: undefined,
// Don't persist unlimited - reset to default
maxMessageSize: currentSettings.maxMessageSize === MAX_MESSAGE_SIZE_UNLIMITED ? MAX_MESSAGE_SIZE_DEFAULT : currentSettings.maxMessageSize,
}
try {
@@ -169,3 +181,12 @@ export const toggleTheme = () => (dispatch: Dispatch<any>, getState: () => AppSt
})
dispatch(storeSettings())
}
export const setMaxMessageSize = (maxMessageSize: number) => (dispatch: Dispatch<any>) => {
dispatch({
maxMessageSize,
type: ActionTypes.SETTINGS_SET_MAX_MESSAGE_SIZE,
})
dispatch(storeSettings())
backendEvents.emit(Events.setMaxMessageSize, maxMessageSize)
}
@@ -11,6 +11,13 @@ import { shell } from 'electron'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { TopicOrder } from '../../reducers/Settings'
import {
MAX_MESSAGE_SIZE_20KB,
MAX_MESSAGE_SIZE_100KB,
MAX_MESSAGE_SIZE_1MB,
MAX_MESSAGE_SIZE_5MB,
MAX_MESSAGE_SIZE_UNLIMITED,
} from '../../../../events/EventsV2'
import {
Divider,
@@ -89,6 +96,7 @@ interface Props {
topicOrder: TopicOrder
visible: boolean
theme: 'light' | 'dark'
maxMessageSize: number
}
class Settings extends React.PureComponent<Props, {}> {
@@ -204,6 +212,47 @@ class Settings extends React.PureComponent<Props, {}> {
this.props.actions.settings.setTopicOrder(e.target.value as TopicOrder)
}
private renderMaxMessageSize() {
const { classes, maxMessageSize } = this.props
const formatSize = (size: number) => {
if (size === MAX_MESSAGE_SIZE_UNLIMITED) {
return 'Unlimited'
} else if (size >= 1000000) {
return `${size / 1000000} MB`
} else if (size >= 1000) {
return `${size / 1000} KB`
}
return `${size} bytes`
}
return (
<div style={{ padding: '8px', display: 'flex' }}>
<InputLabel htmlFor="max-message-size" style={{ flex: '1', marginTop: '8px' }}>
Max Message Size
</InputLabel>
<Select
value={maxMessageSize}
onChange={this.onChangeMaxMessageSize}
input={<Input name="max-message-size" id="max-message-size-label-placeholder" />}
name="max-message-size"
className={classes.input}
style={{ flex: '1' }}
>
<MenuItem value={MAX_MESSAGE_SIZE_20KB}>{formatSize(MAX_MESSAGE_SIZE_20KB)}</MenuItem>
<MenuItem value={MAX_MESSAGE_SIZE_100KB}>{formatSize(MAX_MESSAGE_SIZE_100KB)}</MenuItem>
<MenuItem value={MAX_MESSAGE_SIZE_1MB}>{formatSize(MAX_MESSAGE_SIZE_1MB)}</MenuItem>
<MenuItem value={MAX_MESSAGE_SIZE_5MB}>{formatSize(MAX_MESSAGE_SIZE_5MB)}</MenuItem>
<MenuItem value={MAX_MESSAGE_SIZE_UNLIMITED}>{formatSize(MAX_MESSAGE_SIZE_UNLIMITED)}</MenuItem>
</Select>
</div>
)
}
private onChangeMaxMessageSize = (e: React.ChangeEvent<{ value: unknown }>) => {
this.props.actions.settings.setMaxMessageSize(parseInt(String(e.target.value), 10))
}
public render() {
const { classes, actions, visible } = this.props
return (
@@ -221,6 +270,7 @@ class Settings extends React.PureComponent<Props, {}> {
{this.renderAutoExpand()}
{this.renderNodeOrder()}
<TimeLocale />
{this.renderMaxMessageSize()}
{this.renderHighlightTopicUpdates()}
{this.selectTopicsOnMouseOver()}
{this.toggleTheme()}
@@ -244,6 +294,7 @@ const mapStateToProps = (state: AppState) => {
highlightTopicUpdates: state.settings.get('highlightTopicUpdates'),
selectTopicWithMouseOver: state.settings.get('selectTopicWithMouseOver'),
theme: state.settings.get('theme'),
maxMessageSize: state.settings.get('maxMessageSize'),
}
}
+16 -1
View File
@@ -1,5 +1,6 @@
import { createReducer } from './lib'
import { Record } from 'immutable'
import { MAX_MESSAGE_SIZE_DEFAULT } from '../../../events/EventsV2'
export enum TopicOrder {
none = 'none',
@@ -18,6 +19,7 @@ export interface SettingsStateModel {
valueRendererDisplayMode: ValueRendererDisplayMode
selectTopicWithMouseOver: boolean
theme: 'light' | 'dark'
maxMessageSize: number
}
export type SettingsState = Record<SettingsStateModel>
@@ -30,7 +32,8 @@ export type Actions = SetAutoExpandLimitAction &
SetValueRendererDisplayModeAction &
SetTheme &
SetSelectTopicWithMouseOverAction &
SetTimeLocale
SetTimeLocale &
SetMaxMessageSizeAction
export enum ActionTypes {
SETTINGS_SET_AUTO_EXPAND_LIMIT = 'SETTINGS_SET_AUTO_EXPAND_LIMIT',
@@ -43,6 +46,7 @@ export enum ActionTypes {
SETTINGS_SET_THEME_LIGHT = 'SETTINGS_SET_THEME_LIGHT',
SETTINGS_SET_THEME_DARK = 'SETTINGS_SET_THEME_DARK',
SETTINGS_SET_TIME_LOCALE = 'SETTINGS_SET_TIME_LOCALE',
SETTINGS_SET_MAX_MESSAGE_SIZE = 'SETTINGS_SET_MAX_MESSAGE_SIZE',
}
const initialState = Record<SettingsStateModel>({
@@ -54,6 +58,7 @@ const initialState = Record<SettingsStateModel>({
selectTopicWithMouseOver: false,
theme: 'light',
topicFilter: undefined,
maxMessageSize: MAX_MESSAGE_SIZE_DEFAULT,
})
const setTheme = (theme: 'light' | 'dark') => (state: SettingsState) => {
@@ -73,6 +78,7 @@ const reducerActions: {
SETTINGS_SET_THEME_LIGHT: setTheme('light'),
SETTINGS_SET_THEME_DARK: setTheme('dark'),
SETTINGS_SET_TIME_LOCALE: setTimeLocale,
SETTINGS_SET_MAX_MESSAGE_SIZE: setMaxMessageSize,
}
export const settingsReducer = createReducer(initialState(), reducerActions)
@@ -153,3 +159,12 @@ export interface FilterTopicsAction {
function filterTopics(state: SettingsState, action: FilterTopicsAction) {
return state.set('topicFilter', action.topicFilter)
}
export interface SetMaxMessageSizeAction {
type: ActionTypes.SETTINGS_SET_MAX_MESSAGE_SIZE
maxMessageSize: number
}
function setMaxMessageSize(state: SettingsState, action: SetMaxMessageSizeAction) {
return state.set('maxMessageSize', action.maxMessageSize)
}
+18 -2
View File
@@ -9,10 +9,17 @@ import {
makePublishEvent,
removeConnection,
} from '../../events'
import {
Events,
MAX_MESSAGE_SIZE_DEFAULT,
MAX_MESSAGE_SIZE_20KB,
MAX_MESSAGE_SIZE_UNLIMITED,
} from '../../events/EventsV2'
import { EventBusInterface } from '../../events/EventSystem/EventBusInterface'
export class ConnectionManager {
private connections: { [s: string]: DataSource<any> } = {}
private maxMessageSize: number = MAX_MESSAGE_SIZE_DEFAULT
private backendEvents: EventBusInterface
constructor(backendEvents: EventBusInterface) {
@@ -47,8 +54,9 @@ export class ConnectionManager {
const messageEvent = makeConnectionMessageEvent(connectionId)
connection.onMessage((topic: string, payload: Buffer, packet: any) => {
let buffer = payload
if (buffer.length > 20000) {
buffer = buffer.slice(0, 20000)
// Only apply limit if not unlimited
if (this.maxMessageSize !== MAX_MESSAGE_SIZE_UNLIMITED && buffer.length > this.maxMessageSize) {
buffer = buffer.slice(0, this.maxMessageSize)
}
let decoded_payload = null
@@ -69,6 +77,14 @@ export class ConnectionManager {
this.backendEvents.subscribe(removeConnection, (connectionId: string) => {
this.removeConnection(connectionId)
})
this.backendEvents.subscribe(Events.setMaxMessageSize, (maxMessageSize: number) => {
// Validate: must be an integer >= 20KB or unlimited (-1)
if (typeof maxMessageSize === 'number' && Number.isInteger(maxMessageSize)) {
if (maxMessageSize === MAX_MESSAGE_SIZE_UNLIMITED || maxMessageSize >= MAX_MESSAGE_SIZE_20KB) {
this.maxMessageSize = maxMessageSize
}
}
})
}
public removeConnection(connectionId: string) {
+12
View File
@@ -62,3 +62,15 @@ export const writeToFile: RpcEvent<{ filePath: string; data: string; encoding?:
export const readFromFile: RpcEvent<{ filePath: string; encoding?: string }, Buffer> = {
topic: 'readFromFile',
}
export const MAX_MESSAGE_SIZE_20KB = 20000
export const MAX_MESSAGE_SIZE_100KB = 100000
export const MAX_MESSAGE_SIZE_1MB = 1000000
export const MAX_MESSAGE_SIZE_5MB = 5000000
export const MAX_MESSAGE_SIZE_UNLIMITED = -1
export const MAX_MESSAGE_SIZE_DEFAULT = MAX_MESSAGE_SIZE_20KB
export const setMaxMessageSize: Event<number> = {
topic: 'settings/maxMessageSize',
}
+11
View File
@@ -22,6 +22,9 @@ export const Events = {
removeConnection: { topic: 'connection/remove' } as EventV2<string>,
updateAvailable: { topic: 'app/update/available' } as EventV2<UpdateInfo>,
// Settings
setMaxMessageSize: { topic: 'settings/maxMessageSize' } as EventV2<number>,
// Parameterized events (for connection-specific events)
connectionState: (connectionId: string) => ({ topic: `conn/state/${connectionId}` }) as EventV2<DataSourceState>,
connectionMessage: (connectionId: string) => ({ topic: `conn/${connectionId}` }) as EventV2<MqttMessageV2>,
@@ -62,6 +65,14 @@ export interface CertificateUploadResponse {
data: string // base64 encoded
}
// Message size constants
export const MAX_MESSAGE_SIZE_20KB = 20000
export const MAX_MESSAGE_SIZE_100KB = 100000
export const MAX_MESSAGE_SIZE_1MB = 1000000
export const MAX_MESSAGE_SIZE_5MB = 5000000
export const MAX_MESSAGE_SIZE_UNLIMITED = -1
export const MAX_MESSAGE_SIZE_DEFAULT = MAX_MESSAGE_SIZE_20KB
// Electron dialog types (re-exported for convenience)
import { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'