397 lines
11 KiB
TypeScript
397 lines
11 KiB
TypeScript
import * as React from 'react'
|
||
|
||
import {
|
||
Button,
|
||
CircularProgress,
|
||
FormControl,
|
||
FormControlLabel,
|
||
Grid,
|
||
IconButton,
|
||
Input,
|
||
InputAdornment,
|
||
InputLabel,
|
||
MenuItem,
|
||
Modal,
|
||
Paper,
|
||
Switch,
|
||
TextField,
|
||
Toolbar,
|
||
Typography,
|
||
} from '@material-ui/core'
|
||
import { connect } from 'react-redux'
|
||
import { MqttOptions } from '../../../../backend/src/DataSource'
|
||
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
|
||
|
||
import Notification from './Notification'
|
||
import Visibility from '@material-ui/icons/Visibility'
|
||
import VisibilityOff from '@material-ui/icons/VisibilityOff'
|
||
|
||
import sha1 = require('sha1')
|
||
import { AppState } from '../../reducers'
|
||
import { bindActionCreators } from 'redux'
|
||
import { connectionActions } from '../../actions'
|
||
|
||
interface Props {
|
||
classes: {[s: string]: string}
|
||
theme: Theme
|
||
actions: typeof connectionActions,
|
||
visible: boolean
|
||
connected: boolean
|
||
connecting: boolean
|
||
error?: string
|
||
}
|
||
|
||
const protocols = [
|
||
'mqtt://',
|
||
'ws://',
|
||
]
|
||
|
||
interface State {
|
||
showPassword: boolean
|
||
connectionSettings: ConnectionSettings
|
||
}
|
||
|
||
interface ConnectionSettings {
|
||
host: string
|
||
protocol: string
|
||
port: number
|
||
tls: boolean
|
||
certValidation: boolean
|
||
clientId: string
|
||
connectionId?: string
|
||
username: string
|
||
password: string
|
||
}
|
||
|
||
declare var window: any
|
||
|
||
class Connection extends React.Component<Props, State> {
|
||
private randomClientId: string
|
||
private defaultConnectionSettings: ConnectionSettings = {
|
||
host: 'iot.eclipse.org',
|
||
protocol: protocols[0],
|
||
port: 1883,
|
||
tls: false,
|
||
certValidation: true,
|
||
clientId: '',
|
||
username: '',
|
||
password: '',
|
||
connectionId: undefined,
|
||
}
|
||
|
||
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() {
|
||
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state.connectionSettings))
|
||
}
|
||
|
||
private handleClickShowPassword = () => {
|
||
this.setState({ showPassword: !this.state.showPassword })
|
||
}
|
||
|
||
private optionsFromState(): MqttOptions {
|
||
const protocol = this.state.connectionSettings.protocol === 'tcp://' ? 'mqtt://' : this.state.connectionSettings.protocol
|
||
const url = `${protocol}${this.state.connectionSettings.host}:${this.state.connectionSettings.port}`
|
||
|
||
return {
|
||
url,
|
||
username: this.state.connectionSettings.username || undefined,
|
||
password: this.state.connectionSettings.password || undefined,
|
||
clientId: this.state.connectionSettings.clientId || this.randomClientId,
|
||
tls: this.state.connectionSettings.tls,
|
||
certValidation: this.state.connectionSettings.certValidation,
|
||
}
|
||
}
|
||
|
||
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
|
||
return {
|
||
root: {
|
||
minWidth: 550,
|
||
maxWidth: 650,
|
||
backgroundColor: theme.palette.background.default,
|
||
margin: '14vh auto auto auto',
|
||
padding: `${2 * theme.spacing.unit}px`,
|
||
outline: 'none',
|
||
},
|
||
title: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
paper: {
|
||
padding: theme.spacing.unit * 2,
|
||
textAlign: 'center',
|
||
color: theme.palette.text.secondary,
|
||
},
|
||
textField: {
|
||
width: '100%',
|
||
},
|
||
switch: {
|
||
marginTop: `${1 * theme.spacing.unit}px`,
|
||
},
|
||
button: {
|
||
margin: theme.spacing.unit,
|
||
},
|
||
inputFormControl: {
|
||
marginTop: '16px',
|
||
},
|
||
}
|
||
}
|
||
|
||
private handleChange = (name: string) => (event: any) => {
|
||
this.setState({
|
||
connectionSettings: {
|
||
...this.state.connectionSettings,
|
||
[name]: event.target.value,
|
||
},
|
||
})
|
||
}
|
||
|
||
public render() {
|
||
const { classes } = this.props
|
||
|
||
const passwordVisibilityButton = (
|
||
<InputAdornment position="end">
|
||
<IconButton
|
||
aria-label="Toggle password visibility"
|
||
onClick={this.handleClickShowPassword}
|
||
>
|
||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||
</IconButton>
|
||
</InputAdornment>
|
||
)
|
||
|
||
let renderError = null
|
||
if (this.props.error) {
|
||
renderError = (
|
||
<Notification
|
||
message={this.props.error}
|
||
onClose={() => { this.props.actions.showError(undefined) }}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{renderError}
|
||
<Modal open={this.props.visible} disableAutoFocus={true}>
|
||
<Paper className={classes.root}>
|
||
<Toolbar>
|
||
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
|
||
</Toolbar>
|
||
<form className={classes.container} noValidate={true} autoComplete="off">
|
||
<Grid container={true} spacing={24}>
|
||
<Grid item={true} xs={2}>
|
||
{this.renderProtocols()}
|
||
</Grid>
|
||
<Grid item={true} xs={7}>
|
||
<TextField
|
||
label="Host"
|
||
className={classes.textField}
|
||
value={this.state.connectionSettings.host}
|
||
onChange={this.handleChange('host')}
|
||
margin="normal"
|
||
/>
|
||
</Grid>
|
||
<Grid item={true} xs={3}>
|
||
<TextField
|
||
label="Port"
|
||
className={classes.textField}
|
||
value={this.state.connectionSettings.port}
|
||
onChange={this.handleChange('port')}
|
||
margin="normal"
|
||
/>
|
||
</Grid>
|
||
<Grid item={true} xs={5}>
|
||
<TextField
|
||
label="Username"
|
||
className={classes.textField}
|
||
value={this.state.connectionSettings.username}
|
||
onChange={this.handleChange('username')}
|
||
margin="normal"
|
||
/>
|
||
</Grid>
|
||
<Grid item={true} xs={5}>
|
||
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
|
||
<InputLabel htmlFor="adornment-password">Password</InputLabel>
|
||
<Input
|
||
id="adornment-password"
|
||
type={this.state.showPassword ? 'text' : 'password'}
|
||
value={this.state.connectionSettings.password}
|
||
onChange={this.handleChange('password')}
|
||
endAdornment={passwordVisibilityButton}
|
||
/>
|
||
</FormControl>
|
||
</Grid>
|
||
<Grid item={true} xs={5}>
|
||
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
|
||
<InputLabel htmlFor="client-id">Client ID</InputLabel>
|
||
<Input
|
||
placeholder={this.randomClientId}
|
||
className={classes.textField}
|
||
value={this.state.connectionSettings.clientId || ''}
|
||
onChange={this.handleChange('clientId')}
|
||
startAdornment={<span />}
|
||
/>
|
||
</FormControl>
|
||
</Grid>
|
||
<Grid item={true} xs={4}>
|
||
{this.renderCertValidationSwitch()}
|
||
</Grid>
|
||
<Grid item={true} xs={3}>
|
||
{this.renderTlsSwitch()}
|
||
</Grid>
|
||
</Grid>
|
||
<br />
|
||
<div style={{ textAlign: 'right' }}>
|
||
<Button variant="contained" color="secondary" className={classes.button} onClick={() => this.saveConnectionSettings()}>
|
||
Save
|
||
</Button>
|
||
{this.renderConnectButton()}
|
||
</div>
|
||
</form>
|
||
</Paper>
|
||
</Modal>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
private renderProtocols() {
|
||
const { classes } = this.props
|
||
const protocolItems = protocols.map((value: string) => (
|
||
<MenuItem key={value} value={value}>
|
||
{value}
|
||
</MenuItem>
|
||
))
|
||
|
||
return (
|
||
<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
|
||
</Button>
|
||
)
|
||
}
|
||
return (
|
||
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
|
||
Connect
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
private onClickConnect = () => {
|
||
const connectionId = String(sha1(String(Math.random())).slice(0, 8))
|
||
const options = this.optionsFromState()
|
||
this.props.actions.connect(options, connectionId)
|
||
}
|
||
}
|
||
|
||
const mapStateToProps = (state: AppState) => {
|
||
return {
|
||
visible: !state.tooBigReducer.connected,
|
||
connected: state.tooBigReducer.connected,
|
||
connecting: state.tooBigReducer.connecting,
|
||
error: state.tooBigReducer.error,
|
||
}
|
||
}
|
||
|
||
const mapDispatchToProps = (dispatch: any) => {
|
||
return {
|
||
actions: bindActionCreators(connectionActions, dispatch),
|
||
}
|
||
}
|
||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(Connection.styles, { withTheme: true })(Connection))
|