Compare commits

...
7 changed files with 316 additions and 10 deletions
+6
View File
@@ -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)
+18 -1
View File
@@ -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>
)
+4
View File
@@ -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,
},
}
}
+46
View File
@@ -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)
})
})