Refactor connection

This commit is contained in:
Thomas Nordquist
2019-01-19 14:54:37 +01:00
parent bfcee49e74
commit f3a686b23f
13 changed files with 332 additions and 180 deletions

View File

@@ -30,7 +30,6 @@
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8); background-color: rgba(140,140,140,0.8);
#-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
} }
</style> </style>
<style> <style>

View File

@@ -21,6 +21,7 @@ interface State {
interface Props { interface Props {
name: string name: string
connectionId: string
theme: Theme theme: Theme
settingsVisible: boolean settingsVisible: boolean
} }
@@ -93,17 +94,17 @@ class App extends React.Component<Props, State> {
<TitleBar /> <TitleBar />
<div style={centerContent}> <div style={centerContent}>
<div style={this.getStyles().left}> <div style={this.getStyles().left}>
<Tree connectionId={this.state.connectionId} didSelectNode={(node: q.TreeNode) => { <Tree connectionId={this.props.connectionId} didSelectNode={(node: q.TreeNode) => {
this.setState({ selectedNode: node }) this.setState({ selectedNode: node })
}} /> }} />
</div> </div>
<div style={this.getStyles().right}> <div style={this.getStyles().right}>
<Sidebar connectionId={this.state.connectionId} /> <Sidebar connectionId={this.props.connectionId} />
</div> </div>
</div> </div>
</div> </div>
<UpdateNotifier /> <UpdateNotifier />
<Connection onConnection={(connectionId: string) => this.setState({ connectionId })}/> <Connection />
</ErrorBoundary> </ErrorBoundary>
</div > </div >
) )
@@ -113,6 +114,7 @@ class App extends React.Component<Props, State> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
settingsVisible: state.settings.visible, settingsVisible: state.settings.visible,
connectionId: state.connectionId,
} }
} }

View File

@@ -0,0 +1,40 @@
import { ActionTypes, NodeOrder, CustomAction, AppState } from '../reducers'
import { MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
rendererEvents.subscribe(event, (dataSourceState) => {
if (dataSourceState.connected) {
dispatch(connected())
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
})
}
export const connected: () => CustomAction = () => ({
type: ActionTypes.connected,
})
export const connecting: (connectionId: string) => CustomAction = (connectionId: string) => ({
connectionId,
type: ActionTypes.connecting,
})
export const showError = (error?: string) => ({
error,
type: ActionTypes.showError,
})
export const disconnect = () => (dispatch: Dispatch<CustomAction>, getState: () => AppState) => {
rendererEvents.emit(removeConnection, getState().connectionId)
dispatch({
type: ActionTypes.disconnect,
})
}

View File

@@ -2,5 +2,6 @@ import * as settingsActions from './Settings'
import * as sidebarActions from './Sidebar' import * as sidebarActions from './Sidebar'
import * as treeActions from './Tree' import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier' import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection'
export { settingsActions, treeActions, sidebarActions, updateNotifierActions } export { settingsActions, treeActions, sidebarActions, updateNotifierActions, connectionActions }

3
app/src/bugtracking.ts Normal file
View File

@@ -0,0 +1,3 @@
import { electronRendererTelementry } from 'electron-telemetry'
const spareMeFromGc = electronRendererTelementry

View File

@@ -18,21 +18,27 @@ import {
Toolbar, Toolbar,
Typography, Typography,
} from '@material-ui/core' } from '@material-ui/core'
import { DataSourceState, MqttOptions } from '../../../../backend/src/DataSource' import { connect } from 'react-redux'
import { MqttOptions } from '../../../../backend/src/DataSource'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles' import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../../events'
import Notification from './Notification' import Notification from './Notification'
import Visibility from '@material-ui/icons/Visibility' import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff' import VisibilityOff from '@material-ui/icons/VisibilityOff'
import sha1 = require('sha1') import sha1 = require('sha1')
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connectionActions } from '../../actions'
interface Props { interface Props {
classes: {[s: string]: string} classes: {[s: string]: string}
theme: Theme theme: Theme
onAbort: () => void, actions: typeof connectionActions,
onConnection: (connectionId: string) => void visible: boolean
connected: boolean
connecting: boolean
error?: string
} }
const protocols = [ const protocols = [
@@ -41,39 +47,27 @@ const protocols = [
] ]
interface State { interface State {
connecting: boolean showPassword: boolean
connectionId?: string connectionSettings: ConnectionSettings
error?: string }
visible: boolean
interface ConnectionSettings {
host: string host: string
protocol: string protocol: string
port: number port: number
tls: boolean tls: boolean
certValidation: boolean certValidation: boolean
clientId: string clientId: string
connectionId?: string
username: string username: string
password: string password: string
showPassword: boolean
} }
declare var window: any declare var window: any
class Connection extends React.Component<Props, State> { class Connection extends React.Component<Props, State> {
private randomClientId: tring private randomClientId: string
constructor(props: any) { private defaultConnectionSettings: ConnectionSettings = {
super(props)
const storedSettingsString = window.localStorage.getItem('connectionSettings')
let storedSettings
try {
storedSettings = storedSettingsString ? JSON.parse(storedSettingsString) : undefined
} catch {
window.localStorage.setItem('connectionSettings', undefined)
}
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
this.randomClientId = `mqtt-explorer-${clientIdSha}`
const defaultState = {
visible: true,
host: 'iot.eclipse.org', host: 'iot.eclipse.org',
protocol: protocols[0], protocol: protocols[0],
port: 1883, port: 1883,
@@ -82,16 +76,35 @@ class Connection extends React.Component<Props, State> {
clientId: '', clientId: '',
username: '', username: '',
password: '', password: '',
connecting: false,
connectionId: undefined, connectionId: undefined,
showPassword: false,
} }
this.state = Object.assign({}, defaultState, storedSettings) constructor(props: any) {
super(props)
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
this.randomClientId = `mqtt-explorer-${clientIdSha}`
this.state = {
connectionSettings: this.loadConnectionSettings(),
showPassword: false,
}
}
private loadConnectionSettings(): ConnectionSettings {
let storedSettings: ConnectionSettings | undefined
const storedSettingsString = window.localStorage.getItem('connectionSettings')
try {
storedSettings = storedSettingsString ? JSON.parse(storedSettingsString) : undefined
} catch {
window.localStorage.setItem('connectionSettings', undefined)
}
return storedSettings || this.defaultConnectionSettings
} }
private saveConnectionSettings() { private saveConnectionSettings() {
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state)) window.localStorage.setItem('connectionSettings', JSON.stringify(this.state.connectionSettings))
} }
private handleClickShowPassword = () => { private handleClickShowPassword = () => {
@@ -99,49 +112,19 @@ class Connection extends React.Component<Props, State> {
} }
private optionsFromState(): MqttOptions { private optionsFromState(): MqttOptions {
const protocol = this.state.protocol === 'tcp://' ? 'mqtt://' : this.state.protocol const protocol = this.state.connectionSettings.protocol === 'tcp://' ? 'mqtt://' : this.state.connectionSettings.protocol
const url = `${protocol}${this.state.host}:${this.state.port}` const url = `${protocol}${this.state.connectionSettings.host}:${this.state.connectionSettings.port}`
return { return {
url, url,
username: this.state.username || undefined, username: this.state.connectionSettings.username || undefined,
password: this.state.password || undefined, password: this.state.connectionSettings.password || undefined,
clientId: this.state.clientId || this.randomClientId, clientId: this.state.connectionSettings.clientId || this.randomClientId,
tls: this.state.tls, tls: this.state.connectionSettings.tls,
certValidation: this.state.certValidation, certValidation: this.state.connectionSettings.certValidation,
} }
} }
private connect() {
this.setState({
connecting: true,
})
const options = this.optionsFromState()
const connectionId = (sha1(Math.random() + JSON.stringify(options)).slice(0, 8)) as string
this.setState({ connectionId })
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
rendererEvents.subscribe(event, (state: DataSourceState) => {
console.log(state)
if (state.connected) {
this.props.onConnection(connectionId)
this.setState({ visible: false })
} else if (state.error) {
this.setState({ error: state.error })
this.disconnect()
}
})
}
private disconnect() {
this.setState({
connecting: false,
})
rendererEvents.emit(removeConnection, this.state.connectionId)
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => { public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return { return {
root: { root: {
@@ -176,10 +159,12 @@ class Connection extends React.Component<Props, State> {
} }
private handleChange = (name: string) => (event: any) => { private handleChange = (name: string) => (event: any) => {
const state: any = { this.setState({
connectionSettings: {
...this.state.connectionSettings,
[name]: event.target.value, [name]: event.target.value,
} },
this.setState(state) })
} }
public render() { public render() {
@@ -197,12 +182,11 @@ class Connection extends React.Component<Props, State> {
) )
let renderError = null let renderError = null
if (this.state.error) { if (this.props.error) {
renderError = ( renderError = (
<Notification <Notification
message={this.state.error} message={this.props.error}
type="error" onClose={() => { this.props.actions.showError(undefined) }}
onClose={() => { this.setState({ error: undefined }) }}
/> />
) )
} }
@@ -210,7 +194,7 @@ class Connection extends React.Component<Props, State> {
return ( return (
<div> <div>
{renderError} {renderError}
<Modal open={this.state.visible} disableAutoFocus={true}> <Modal open={this.props.visible} disableAutoFocus={true}>
<Paper className={classes.root}> <Paper className={classes.root}>
<Toolbar> <Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography> <Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
@@ -218,26 +202,13 @@ class Connection extends React.Component<Props, State> {
<form className={classes.container} noValidate={true} autoComplete="off"> <form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={24}> <Grid container={true} spacing={24}>
<Grid item={true} xs={2}> <Grid item={true} xs={2}>
<TextField {this.renderProtocols()}
select={true}
label="Protocol"
className={classes.textField}
value={this.state.protocol}
onChange={this.handleChange('protocol')}
margin="normal"
>
{protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))}
</TextField>
</Grid> </Grid>
<Grid item xs={7}> <Grid item={true} xs={7}>
<TextField <TextField
label="Host" label="Host"
className={classes.textField} className={classes.textField}
value={this.state.host} value={this.state.connectionSettings.host}
onChange={this.handleChange('host')} onChange={this.handleChange('host')}
margin="normal" margin="normal"
/> />
@@ -246,7 +217,7 @@ class Connection extends React.Component<Props, State> {
<TextField <TextField
label="Port" label="Port"
className={classes.textField} className={classes.textField}
value={this.state.port} value={this.state.connectionSettings.port}
onChange={this.handleChange('port')} onChange={this.handleChange('port')}
margin="normal" margin="normal"
/> />
@@ -255,7 +226,7 @@ class Connection extends React.Component<Props, State> {
<TextField <TextField
label="Username" label="Username"
className={classes.textField} className={classes.textField}
value={this.state.username} value={this.state.connectionSettings.username}
onChange={this.handleChange('username')} onChange={this.handleChange('username')}
margin="normal" margin="normal"
/> />
@@ -266,7 +237,7 @@ class Connection extends React.Component<Props, State> {
<Input <Input
id="adornment-password" id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'} type={this.state.showPassword ? 'text' : 'password'}
value={this.state.password} value={this.state.connectionSettings.password}
onChange={this.handleChange('password')} onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton} endAdornment={passwordVisibilityButton}
/> />
@@ -278,41 +249,17 @@ class Connection extends React.Component<Props, State> {
<Input <Input
placeholder={this.randomClientId} placeholder={this.randomClientId}
className={classes.textField} className={classes.textField}
value={this.state.clientId || ''} value={this.state.connectionSettings.clientId || ''}
onChange={this.handleChange('clientId')} onChange={this.handleChange('clientId')}
startAdornment={<span />} startAdornment={<span />}
/> />
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item={true} xs={4}> <Grid item={true} xs={4}>
<div className={classes.switch}> {this.renderCertValidationSwitch()}
<FormControlLabel
control={(
<Switch
checked={this.state.certValidation}
onChange={() => this.setState({ certValidation: !this.state.certValidation })}
color="primary"
/>
)}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
</Grid> </Grid>
<Grid item={true} xs={3}> <Grid item={true} xs={3}>
<div className={classes.switch}> {this.renderTlsSwitch()}
<FormControlLabel
control={(
<Switch
checked={this.state.tls}
onChange={() => this.setState({ tls: !this.state.tls })}
color="primary"
/>
)}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
</Grid> </Grid>
</Grid> </Grid>
<br /> <br />
@@ -329,12 +276,90 @@ class Connection extends React.Component<Props, State> {
) )
} }
private renderConnectButton() { private renderProtocols() {
const { classes } = this.props const { classes } = this.props
const protocolItems = protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))
if (this.state.connecting) {
return ( return (
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickAbort}> <TextField
select={true}
label="Protocol"
className={classes.textField}
value={this.state.connectionSettings.protocol}
onChange={this.handleChange('protocol')}
margin="normal"
>
{protocolItems}
</TextField>
)
}
private renderCertValidationSwitch() {
const { classes } = this.props
const certSwitch = (
<Switch
checked={this.state.connectionSettings.certValidation}
onChange={this.toggleCertValidation}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={certSwitch}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
)
}
private toggleCertValidation = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
certValidation: !this.state.connectionSettings.certValidation,
},
})
private renderTlsSwitch() {
const { classes } = this.props
const tlsSwitch = (
<Switch
checked={this.state.connectionSettings.tls}
onChange={this.toggleTls}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={tlsSwitch}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
)
}
private toggleTls = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
tls: !this.state.connectionSettings.tls,
},
})
private renderConnectButton() {
const { classes, actions } = this.props
if (this.props.connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
<CircularProgress size={22} style={{ marginRight: '10px' }} color="secondary" /> Abort <CircularProgress size={22} style={{ marginRight: '10px' }} color="secondary" /> Abort
</Button> </Button>
) )
@@ -347,12 +372,25 @@ class Connection extends React.Component<Props, State> {
} }
private onClickConnect = () => { private onClickConnect = () => {
this.connect() const connectionId = String(sha1(String(Math.random())).slice(0, 8))
} const options = this.optionsFromState()
this.props.actions.connect(options, connectionId)
private onClickAbort = () => {
this.disconnect()
} }
} }
export default withStyles(Connection.styles, { withTheme: true })(Connection) const mapStateToProps = (state: AppState) => {
return {
visible: !state.connected,
connected: state.connected,
connecting: state.connecting,
error: state.error,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(Connection.styles, { withTheme: true })(Connection))

View File

@@ -4,14 +4,8 @@ import { Snackbar, SnackbarContent } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
import { green, red } from '@material-ui/core/colors' import { green, red } from '@material-ui/core/colors'
enum MessageType {
success = 'success',
error = 'error',
}
interface Props { interface Props {
message?: string message?: string
type: MessageType
onClose: () => void onClose: () => void
classes: any classes: any
} }
@@ -33,18 +27,20 @@ class Notification extends React.Component<Props, {}> {
}) })
public render() { public render() {
const snackbarAnchor = {
vertical: 'bottom' as 'bottom',
horizontal: 'left' as 'left',
}
return ( return (
<Snackbar <Snackbar
anchorOrigin={{ anchorOrigin={snackbarAnchor}
vertical: 'bottom',
horizontal: 'left',
}}
open={Boolean(this.props.message)} open={Boolean(this.props.message)}
autoHideDuration={10000} autoHideDuration={10000}
onClose={this.props.onClose} onClose={this.props.onClose}
> >
<SnackbarContent <SnackbarContent
className={this.props.classes[this.props.type]} className={this.props.classes.error}
message={this.props.message} message={this.props.message}
/> />
</Snackbar> </Snackbar>

View File

@@ -1,15 +1,16 @@
import * as React from 'react' import * as React from 'react'
import * as q from '../../../backend/src/Model' import * as q from '../../../backend/src/Model'
import { AppBar, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core' import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles' import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import CloudOff from '@material-ui/icons/CloudOff'
import Menu from '@material-ui/icons/Menu' import Menu from '@material-ui/icons/Menu'
import Search from '@material-ui/icons/Search' import Search from '@material-ui/icons/Search'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { fade } from '@material-ui/core/styles/colorManipulator' import { fade } from '@material-ui/core/styles/colorManipulator'
import { settingsActions } from '../actions' import { settingsActions, connectionActions } from '../actions'
const styles: StyleRulesCallback = theme => ({ const styles: StyleRulesCallback = theme => ({
title: { title: {
@@ -85,7 +86,7 @@ class TitleBar extends React.Component<Props, State> {
} }
public render() { public render() {
const { classes } = this.props const { actions, classes } = this.props
return ( return (
<AppBar position="static"> <AppBar position="static">
@@ -94,6 +95,9 @@ class TitleBar extends React.Component<Props, State> {
<Menu /> <Menu />
</IconButton> </IconButton>
<Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography> <Typography className={classes.title} variant="h6" color="inherit">MQTT-Explorer</Typography>
<Button style={{ margin: 'auto 8px auto auto' }} onClick={actions.disconnect}>
Disconnect <CloudOff style={{ marginRight: '8px', paddingLeft: '8px' }}/>
</Button>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
) )
@@ -119,8 +123,8 @@ class TitleBar extends React.Component<Props, State> {
const mapDispatchToProps = (dispatch: any) => { const mapDispatchToProps = (dispatch: any) => {
return { return {
actions: bindActionCreators(settingsActions, dispatch), actions: { ...bindActionCreators(connectionActions, dispatch), ...bindActionCreators(settingsActions, dispatch) },
} }
} }
export default withStyles(styles)(connect(null, mapDispatchToProps)(TitleBar)) export default connect(undefined, mapDispatchToProps)(withStyles(styles)(TitleBar))

View File

@@ -18,6 +18,7 @@ interface Props {
autoExpandLimit: number autoExpandLimit: number
didSelectNode?: (node: q.TreeNode) => void didSelectNode?: (node: q.TreeNode) => void
connectionId?: string connectionId?: string
connected: boolean
} }
interface TreeState { interface TreeState {
@@ -32,8 +33,7 @@ class Tree extends React.Component<Props, TreeState> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
const tree = new q.Tree() this.state = { tree: new q.Tree(), msg: {} }
this.state = { tree, msg: {} }
} }
public time(): number { public time(): number {
@@ -67,20 +67,18 @@ class Tree extends React.Component<Props, TreeState> {
} }
public componentWillReceiveProps(nextProps: Props) { public componentWillReceiveProps(nextProps: Props) {
if (this.props.connectionId) { this.registerAndUnregisterEventSubscriptionsForNewProps(nextProps)
const event = makeConnectionMessageEvent(this.props.connectionId)
rendererEvents.unsubscribeAll(event)
}
if (nextProps.connectionId) {
const event = makeConnectionMessageEvent(nextProps.connectionId)
rendererEvents.subscribe(event, this.handleNewData)
}
} }
public componentDidMount() { private registerAndUnregisterEventSubscriptionsForNewProps(nextProps: Props) {
if (this.props.connectionId !== nextProps.connectionId) {
if (this.props.connectionId) { if (this.props.connectionId) {
const event = makeConnectionMessageEvent(this.props.connectionId) this.setState({ tree: new q.Tree() })
rendererEvents.subscribe(event, this.handleNewData) rendererEvents.unsubscribeAll(makeConnectionMessageEvent(this.props.connectionId))
}
if (nextProps.connectionId) {
rendererEvents.subscribe(makeConnectionMessageEvent(nextProps.connectionId), this.handleNewData)
}
} }
} }
@@ -92,6 +90,7 @@ class Tree extends React.Component<Props, TreeState> {
} }
private handleNewData = (msg: any) => { private handleNewData = (msg: any) => {
console.log('new data')
const edges = msg.topic.split('/') const edges = msg.topic.split('/')
const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString()) const node = q.TreeNodeFactory.fromEdgesAndValue(edges, Buffer.from(msg.payload, 'base64').toString())
this.state.tree.updateWithNode(node.firstNode()) this.state.tree.updateWithNode(node.firstNode())
@@ -100,7 +99,9 @@ class Tree extends React.Component<Props, TreeState> {
} }
public render() { public render() {
console.log('render called') if (!this.props.connected) {
return null
}
const style: React.CSSProperties = { const style: React.CSSProperties = {
lineHeight: '1.1', lineHeight: '1.1',
@@ -129,6 +130,7 @@ class Tree extends React.Component<Props, TreeState> {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
return { return {
autoExpandLimit: state.settings.autoExpandLimit, autoExpandLimit: state.settings.autoExpandLimit,
connected: state.connected,
} }
} }

View File

@@ -2,25 +2,24 @@ import './tracking'
import * as React from 'react' import * as React from 'react'
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom'
import reduxThunk from 'redux-thunk'
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles' import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'
import reducers, { AppState, NodeOrder } from './reducers' import reducers, { AppState, NodeOrder } from './reducers'
import App from './App' import App from './App'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { createStore } from 'redux' import { createStore, applyMiddleware, compose } from 'redux'
const initialAppState: AppState = { const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
settings: { const store = createStore(
autoExpandLimit: 0, reducers,
nodeOrder: NodeOrder.none, composeEnhancers(
visible: false, applyMiddleware(
}, reduxThunk,
sidebar: {}, ),
selectedTopic: undefined, ),
showUpdateDetails: false, )
}
const store = createStore(reducers, initialAppState)
const theme = createMuiTheme({ const theme = createMuiTheme({
palette: { palette: {

View File

@@ -3,8 +3,12 @@ import * as q from '../../../backend/src/Model'
import { Action, Reducer } from 'redux' import { Action, Reducer } from 'redux'
import { trackEvent } from '../tracking' import { trackEvent } from '../tracking'
import { MqttOptions, DataSourceStateMachine } from '../../../backend/src/DataSource'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent } from '../../../events'
export enum ActionTypes { export enum ActionTypes {
disconnect = 'DISCONNECT',
showError = 'SHOW_ERROR',
setAutoExpandLimit = 'SET_AUTO_EXPAND_LIMIT', setAutoExpandLimit = 'SET_AUTO_EXPAND_LIMIT',
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY', toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY',
setNodeOrder = 'SET_NODE_ORDER', setNodeOrder = 'SET_NODE_ORDER',
@@ -13,6 +17,8 @@ export enum ActionTypes {
setPublishPayload = 'SET_PUBLISH_PAYLOAD', setPublishPayload = 'SET_PUBLISH_PAYLOAD',
showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION', showUpdateNotification = 'SHOW_UPDATE_NOTIFICATION',
showUpdateDetails = 'SHOW_UPDATE_DETAILS', showUpdateDetails = 'SHOW_UPDATE_DETAILS',
connecting = 'CONNECTING',
connected = 'CONNECTED',
} }
export interface CustomAction extends Action { export interface CustomAction extends Action {
@@ -24,6 +30,9 @@ export interface CustomAction extends Action {
publishPayload?: string publishPayload?: string
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails?: boolean showUpdateDetails?: boolean
connectionOptions?: MqttOptions
connectionId?: string
error?: string
} }
export interface SidebarState { export interface SidebarState {
@@ -37,6 +46,10 @@ export interface AppState {
sidebar: SidebarState sidebar: SidebarState
showUpdateNotification?: boolean showUpdateNotification?: boolean
showUpdateDetails: boolean showUpdateDetails: boolean
connecting: boolean
connected: boolean
error?: string
connectionId?: string
} }
export interface SettingsState { export interface SettingsState {
@@ -52,7 +65,21 @@ export enum NodeOrder {
topics = '#topics', topics = '#topics',
} }
const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) => { const initialAppState: AppState = {
settings: {
autoExpandLimit: 0,
nodeOrder: NodeOrder.none,
visible: false,
},
sidebar: {},
selectedTopic: undefined,
showUpdateDetails: false,
connected: false,
connecting: false,
error: undefined,
}
const reducer: Reducer<AppState | undefined, CustomAction> = (state = initialAppState, action) => {
if (!state) { if (!state) {
throw Error('No initial state') throw Error('No initial state')
} }
@@ -117,6 +144,40 @@ const reducer: Reducer<AppState | undefined, CustomAction> = (state, action) =>
...state, ...state,
showUpdateDetails: action.showUpdateDetails, showUpdateDetails: action.showUpdateDetails,
} }
case ActionTypes.connecting:
if (!action.connectionId) {
return state
}
return {
...state,
connected: false,
connecting: true,
connectionId: action.connectionId,
}
case ActionTypes.disconnect:
return {
...state,
connected: false,
connecting: false,
connectionId: undefined,
}
case ActionTypes.connected:
return {
...state,
connected: true,
connecting: false,
}
case ActionTypes.showError:
return {
...state,
error: action.error,
}
default: default:
return state return state
} }

6
package-lock.json generated
View File

@@ -4227,6 +4227,12 @@
"strip-indent": "^1.0.1" "strip-indent": "^1.0.1"
} }
}, },
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==",
"dev": true
},
"reflect-metadata": { "reflect-metadata": {
"version": "0.1.12", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",

View File

@@ -55,6 +55,7 @@
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mustache": "^3.0.1", "mustache": "^3.0.1",
"nyc": "^13.1.0", "nyc": "^13.1.0",
"redux-thunk": "^2.3.0",
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
"ts-node": "^7.0.1", "ts-node": "^7.0.1",
"tslint": "^5.12.0", "tslint": "^5.12.0",