mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-12 17:43:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be83c3f2cc | ||
|
|
1a8a9224b4 | ||
|
|
651246ff83 | ||
|
|
73b8e6ff9f |
@@ -147,6 +147,12 @@ export const toggleCertificateSettings = (): Action => ({
|
||||
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS,
|
||||
})
|
||||
|
||||
export const moveConnection = (connectionId: string, direction: 'up' | 'down'): Action => ({
|
||||
connectionId,
|
||||
direction,
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
})
|
||||
|
||||
export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
const connectionIds = Object.keys(getState().connectionManager.connections)
|
||||
const connectionIdLocation = connectionIds.indexOf(connectionId)
|
||||
|
||||
@@ -68,6 +68,17 @@ let migrations: Migration[] = [
|
||||
}
|
||||
},
|
||||
},
|
||||
// Add order field for connection ordering
|
||||
{
|
||||
from: 1,
|
||||
apply: (connection: ConnectionOptions): ConnectionOptions => {
|
||||
if (connection.order !== undefined) {
|
||||
return connection
|
||||
}
|
||||
// Order will be assigned during migration based on current position
|
||||
return connection
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const connectionMigrator = new ConfigMigrator(migrations)
|
||||
@@ -80,8 +91,14 @@ function isMigrationNecessary(connections: ConnectionDictionary): boolean {
|
||||
|
||||
function applyMigrations(connections: ConnectionDictionary): ConnectionDictionary {
|
||||
let newConnectionDictionary: ConnectionDictionary = {}
|
||||
Object.keys(connections).forEach(key => {
|
||||
const connectionKeys = Object.keys(connections)
|
||||
|
||||
connectionKeys.forEach((key, index) => {
|
||||
let newConnection = connectionMigrator.applyMigrations(connections[key]) as any
|
||||
// If the migration didn't assign an order, assign one based on current position
|
||||
if (newConnection.order === undefined) {
|
||||
newConnection.order = index
|
||||
}
|
||||
newConnectionDictionary[newConnection.id] = newConnection
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { ListItem, Typography } from '@mui/material'
|
||||
import { ListItem, Typography, Box } from '@mui/material'
|
||||
import { DragIndicator } from '@mui/icons-material'
|
||||
import { toMqttConnection, ConnectionOptions } from '../../../model/ConnectionOptions'
|
||||
import { withStyles } from '@mui/styles'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
@@ -15,6 +16,9 @@ export interface Props {
|
||||
}
|
||||
selected: boolean
|
||||
classes: any
|
||||
onDragStart: (connectionId: string) => void
|
||||
onDragOver: (e: React.DragEvent) => void
|
||||
onDrop: (connectionId: string) => void
|
||||
}
|
||||
|
||||
const ConnectionItem = (props: Props) => {
|
||||
@@ -25,20 +29,48 @@ const ConnectionItem = (props: Props) => {
|
||||
}
|
||||
}, [props.connection, props])
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
e.stopPropagation()
|
||||
props.onDragStart(props.connection.id)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
props.onDragOver(e)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
props.onDrop(props.connection.id)
|
||||
}
|
||||
|
||||
const connection = props.connection.host && toMqttConnection(props.connection)
|
||||
return (
|
||||
<ListItem
|
||||
button={true}
|
||||
selected={props.selected}
|
||||
style={{ display: 'block' }}
|
||||
className={props.classes.itemContainer}
|
||||
onClick={() => props.actions.connectionManager.selectConnection(props.connection.id)}
|
||||
onDoubleClick={() => {
|
||||
props.actions.connectionManager.selectConnection(props.connection.id)
|
||||
connect()
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
|
||||
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
|
||||
<Box
|
||||
className={props.classes.dragHandle}
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
>
|
||||
<DragIndicator fontSize="small" />
|
||||
</Box>
|
||||
<Box className={props.classes.textContainer}>
|
||||
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
|
||||
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
|
||||
</Box>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
@@ -66,6 +98,25 @@ export const connectionItemStyle = (theme: Theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: '0.7em',
|
||||
},
|
||||
itemContainer: {
|
||||
display: 'flex' as 'flex',
|
||||
alignItems: 'center' as 'center',
|
||||
padding: '8px 8px 8px 8px',
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
overflow: 'hidden' as 'hidden',
|
||||
},
|
||||
dragHandle: {
|
||||
display: 'flex' as 'flex',
|
||||
alignItems: 'center' as 'center',
|
||||
marginRight: '8px',
|
||||
cursor: 'grab' as 'grab',
|
||||
color: theme.palette.text.secondary,
|
||||
'&:active': {
|
||||
cursor: 'grabbing' as 'grabbing',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem) as any)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ConnectionItem from './ConnectionItem'
|
||||
const ConnectionItemAny = ConnectionItem as any
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { AddButton } from './AddButton'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
@@ -22,13 +22,14 @@ interface Props {
|
||||
|
||||
function ProfileList(props: Props) {
|
||||
const { actions, classes, connections, selected } = props
|
||||
const [draggedConnectionId, setDraggedConnectionId] = useState<string | null>(null)
|
||||
|
||||
const selectConnection = (dir: 'next' | 'previous') => (event: KeyboardEvent) => {
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
const indexDirection = dir === 'next' ? 1 : -1
|
||||
const connectionArray = Object.values(connections)
|
||||
const connectionArray = Object.values(connections).sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
const selectedIndex = connectionArray.map(connection => connection.id).indexOf(selected)
|
||||
const nextConnection = connectionArray[selectedIndex + indexDirection]
|
||||
if (nextConnection) {
|
||||
@@ -40,6 +41,39 @@ function ProfileList(props: Props) {
|
||||
useGlobalKeyEventHandler(KeyCodes.arrow_down, selectConnection('next'))
|
||||
useGlobalKeyEventHandler(KeyCodes.arrow_up, selectConnection('previous'))
|
||||
|
||||
const handleDragStart = (connectionId: string) => {
|
||||
setDraggedConnectionId(connectionId)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleDrop = (targetConnectionId: string) => {
|
||||
if (!draggedConnectionId || draggedConnectionId === targetConnectionId) {
|
||||
setDraggedConnectionId(null)
|
||||
return
|
||||
}
|
||||
|
||||
const sortedConnections = Object.values(connections).sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
const draggedIndex = sortedConnections.findIndex(c => c.id === draggedConnectionId)
|
||||
const targetIndex = sortedConnections.findIndex(c => c.id === targetConnectionId)
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) {
|
||||
setDraggedConnectionId(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Swap order values
|
||||
const draggedConnection = sortedConnections[draggedIndex]
|
||||
const targetConnection = sortedConnections[targetIndex]
|
||||
|
||||
actions.updateConnection(draggedConnection.id, { order: targetConnection.order })
|
||||
actions.updateConnection(targetConnection.id, { order: draggedConnection.order })
|
||||
|
||||
setDraggedConnectionId(null)
|
||||
}
|
||||
|
||||
const createConnectionButton = (
|
||||
<div style={{ padding: '8px 16px' }}>
|
||||
<AddButton action={actions.createConnection} />
|
||||
@@ -50,9 +84,18 @@ function ProfileList(props: Props) {
|
||||
return (
|
||||
<List style={{ height: '100%' }} component="nav" subheader={createConnectionButton}>
|
||||
<div className={classes.list}>
|
||||
{Object.values(connections).map(connection => (
|
||||
<ConnectionItemAny connection={connection} key={connection.id} selected={selected === connection.id} />
|
||||
))}
|
||||
{Object.values(connections)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map(connection => (
|
||||
<ConnectionItemAny
|
||||
connection={connection}
|
||||
key={connection.id}
|
||||
selected={selected === connection.id}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</List>
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface ConnectionOptions {
|
||||
clientKey?: CertificateParameters
|
||||
clientId?: string
|
||||
subscriptions: Array<Subscription>
|
||||
order?: number
|
||||
}
|
||||
|
||||
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
|
||||
@@ -71,6 +72,7 @@ export function createEmptyConnection(): ConnectionOptions {
|
||||
host: '',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
order: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +84,14 @@ export function makeDefaultConnections() {
|
||||
id: 'mqtt.eclipseprojects.io',
|
||||
name: 'mqtt.eclipseprojects.io',
|
||||
host: 'mqtt.eclipseprojects.io',
|
||||
order: 0,
|
||||
},
|
||||
'test.mosquitto.org': {
|
||||
...createEmptyConnection(),
|
||||
id: 'test.mosquitto.org',
|
||||
name: 'test.mosquitto.org',
|
||||
host: 'test.mosquitto.org',
|
||||
order: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export type Action =
|
||||
| ToggleCertificateSettings
|
||||
| DeleteSubscription
|
||||
| AddSubscription
|
||||
| MoveConnection
|
||||
|
||||
export enum ActionTypes {
|
||||
CONNECTION_MANAGER_SET_CONNECTIONS = 'CONNECTION_MANAGER_SET_CONNECTIONS',
|
||||
@@ -37,6 +38,7 @@ export enum ActionTypes {
|
||||
CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS = 'CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS',
|
||||
CONNECTION_MANAGER_ADD_SUBSCRIPTION = 'CONNECTION_MANAGER_ADD_SUBSCRIPTION',
|
||||
CONNECTION_MANAGER_DELETE_SUBSCRIPTION = 'CONNECTION_MANAGER_DELETE_SUBSCRIPTION',
|
||||
CONNECTION_MANAGER_MOVE_CONNECTION = 'CONNECTION_MANAGER_MOVE_CONNECTION',
|
||||
}
|
||||
|
||||
export interface SetConnections {
|
||||
@@ -85,6 +87,12 @@ export interface ToggleCertificateSettings {
|
||||
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS
|
||||
}
|
||||
|
||||
export interface MoveConnection {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION
|
||||
connectionId: string
|
||||
direction: 'up' | 'down'
|
||||
}
|
||||
|
||||
export const connectionManagerReducer = createReducer(initialState, {
|
||||
CONNECTION_MANAGER_SET_CONNECTIONS: setConnections,
|
||||
CONNECTION_MANAGER_SELECT_CONNECTION: selectConnection,
|
||||
@@ -95,6 +103,7 @@ export const connectionManagerReducer = createReducer(initialState, {
|
||||
CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS: toggleCertificateSettings,
|
||||
CONNECTION_MANAGER_DELETE_SUBSCRIPTION: deleteSubscription,
|
||||
CONNECTION_MANAGER_ADD_SUBSCRIPTION: addSubscription,
|
||||
CONNECTION_MANAGER_MOVE_CONNECTION: moveConnection,
|
||||
})
|
||||
|
||||
function setConnections(state: ConnectionManagerState, action: SetConnections): ConnectionManagerState {
|
||||
@@ -222,3 +231,40 @@ function updateConnection(state: ConnectionManagerState, action: UpdateConnectio
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function moveConnection(state: ConnectionManagerState, action: MoveConnection): ConnectionManagerState {
|
||||
const connections = Object.values(state.connections).sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
const currentIndex = connections.findIndex(c => c.id === action.connectionId)
|
||||
|
||||
if (currentIndex === -1) {
|
||||
return state
|
||||
}
|
||||
|
||||
const targetIndex = action.direction === 'up' ? currentIndex - 1 : currentIndex + 1
|
||||
|
||||
// Can't move beyond bounds
|
||||
if (targetIndex < 0 || targetIndex >= connections.length) {
|
||||
return state
|
||||
}
|
||||
|
||||
// Swap order values
|
||||
const currentConnection = connections[currentIndex]
|
||||
const targetConnection = connections[targetIndex]
|
||||
const currentOrder = currentConnection.order || 0
|
||||
const targetOrder = targetConnection.order || 0
|
||||
|
||||
return {
|
||||
...state,
|
||||
connections: {
|
||||
...state.connections,
|
||||
[currentConnection.id]: {
|
||||
...currentConnection,
|
||||
order: targetOrder,
|
||||
},
|
||||
[targetConnection.id]: {
|
||||
...targetConnection,
|
||||
order: currentOrder,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { connectionManagerReducer, ConnectionManagerState, ActionTypes } from '../ConnectionManager'
|
||||
import { createEmptyConnection } from '../../model/ConnectionOptions'
|
||||
|
||||
describe('ConnectionManager - moveConnection', () => {
|
||||
it('should move connection up', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
const connection3 = { ...createEmptyConnection(), id: 'conn3', name: 'Connection 3', order: 2 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
conn3: connection3,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn2',
|
||||
direction: 'up' as 'up',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(1)
|
||||
expect(newState.connections.conn2.order).to.equal(0)
|
||||
expect(newState.connections.conn3.order).to.equal(2)
|
||||
})
|
||||
|
||||
it('should move connection down', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
const connection3 = { ...createEmptyConnection(), id: 'conn3', name: 'Connection 3', order: 2 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
conn3: connection3,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn2',
|
||||
direction: 'down' as 'down',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
expect(newState.connections.conn2.order).to.equal(2)
|
||||
expect(newState.connections.conn3.order).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not move connection up when already at top', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn1',
|
||||
direction: 'up' as 'up',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
expect(newState.connections.conn2.order).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not move connection down when already at bottom', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn2',
|
||||
direction: 'down' as 'down',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
expect(newState.connections.conn2.order).to.equal(1)
|
||||
})
|
||||
|
||||
it('should handle non-existent connection', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'nonexistent',
|
||||
direction: 'up' as 'up',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user