Add support for keyboard events
This commit is contained in:
25
app/src/components/ConnectionSetup/ConnectButton.tsx
Normal file
25
app/src/components/ConnectionSetup/ConnectButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
|
||||
import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew'
|
||||
import React from 'react'
|
||||
import { Button } from '@material-ui/core'
|
||||
|
||||
function ConnectButton(props: { connecting: boolean; classes: any; toggle: () => void }) {
|
||||
const { classes, toggle, connecting } = props
|
||||
|
||||
if (connecting) {
|
||||
return (
|
||||
<Button variant="contained" color="primary" className={classes.button} onClick={toggle}>
|
||||
<ConnectionHealthIndicator />
|
||||
Abort
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="contained" color="primary" className={classes.button} onClick={toggle}>
|
||||
<PowerSettingsNew /> Connect
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConnectButton
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import ConnectButton from './ConnectButton'
|
||||
import Delete from '@material-ui/icons/Delete'
|
||||
import Settings from '@material-ui/icons/Settings'
|
||||
import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Save from '@material-ui/icons/Save'
|
||||
import Settings from '@material-ui/icons/Settings'
|
||||
import Visibility from '@material-ui/icons/Visibility'
|
||||
import VisibilityOff from '@material-ui/icons/VisibilityOff'
|
||||
import { AppState } from '../../reducers'
|
||||
@@ -10,23 +10,21 @@ import { bindActionCreators } from 'redux'
|
||||
import { connect } from 'react-redux'
|
||||
import { connectionActions, connectionManagerActions } from '../../actions'
|
||||
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
|
||||
import { KeyCodes } from '../../utils/KeyCodes'
|
||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||
|
||||
import { ToggleSwitch } from './ToggleSwitch'
|
||||
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
IconButton,
|
||||
Input,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Switch,
|
||||
TextField,
|
||||
} from '@material-ui/core'
|
||||
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
|
||||
import { KeyCodes } from '../../utils/KeyCodes'
|
||||
|
||||
interface Props {
|
||||
connection: ConnectionOptions
|
||||
@@ -39,57 +37,66 @@ interface Props {
|
||||
|
||||
const protocols = ['mqtt', 'ws']
|
||||
|
||||
interface State {
|
||||
showPassword: boolean
|
||||
}
|
||||
function ConnectionSettings(props: Props) {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
class ConnectionSettings extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
showPassword: false,
|
||||
const toggleConnect = useCallback(() => {
|
||||
if (props.connecting) {
|
||||
props.actions.disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
if (!props.connection) {
|
||||
return
|
||||
}
|
||||
|
||||
const mqttOptions = toMqttConnection(props.connection)
|
||||
if (mqttOptions) {
|
||||
props.actions.connect(mqttOptions, props.connection.id)
|
||||
}
|
||||
}, [props.connection, props.connecting])
|
||||
|
||||
useGlobalKeyEventHandler(KeyCodes.escape, props.actions.disconnect)
|
||||
useGlobalKeyEventHandler(KeyCodes.enter, toggleConnect, [props.connecting])
|
||||
|
||||
const handleClickShowPassword = useCallback(() => {
|
||||
setShowPassword(!showPassword)
|
||||
}, [showPassword])
|
||||
|
||||
function requiresBasePath() {
|
||||
return props.connection.protocol !== 'mqtt'
|
||||
}
|
||||
|
||||
private handleClickShowPassword = () => {
|
||||
this.setState({ showPassword: !this.state.showPassword })
|
||||
}
|
||||
|
||||
private requiresBasePath() {
|
||||
return this.props.connection.protocol !== 'mqtt'
|
||||
}
|
||||
|
||||
private renderBasePathInput() {
|
||||
function renderBasePathInput() {
|
||||
return (
|
||||
<Grid item={true} xs={4}>
|
||||
<TextField
|
||||
label="Basepath"
|
||||
className={this.props.classes.textField}
|
||||
value={this.props.connection.basePath}
|
||||
onChange={this.handleChange('basePath')}
|
||||
className={props.classes.textField}
|
||||
value={props.connection.basePath}
|
||||
onChange={handleChange('basePath')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
private handleChange = (name: string) => (event: any) => {
|
||||
if (!this.props.connection) {
|
||||
const handleChange = (name: string) => (event: any) => {
|
||||
if (!props.connection) {
|
||||
return
|
||||
}
|
||||
|
||||
this.updateConnection(name, event.target.value)
|
||||
updateConnection(name, event.target.value)
|
||||
}
|
||||
|
||||
private updateConnection(name: string, value: any) {
|
||||
this.props.managerActions.updateConnection(this.props.connection.id, {
|
||||
const updateConnection = (name: string, value: any) => {
|
||||
props.managerActions.updateConnection(props.connection.id, {
|
||||
[name]: value,
|
||||
})
|
||||
}
|
||||
|
||||
private renderProtocols() {
|
||||
const { classes, connection } = this.props
|
||||
const renderProtocols = () => {
|
||||
const { classes, connection } = props
|
||||
|
||||
const protocolItems = protocols.map((value: string) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
@@ -103,7 +110,7 @@ class ConnectionSettings extends React.Component<Props, State> {
|
||||
label="Protocol"
|
||||
className={classes.textField}
|
||||
value={connection.protocol}
|
||||
onChange={this.updateProtocol}
|
||||
onChange={updateProtocol}
|
||||
margin="normal"
|
||||
>
|
||||
{protocolItems}
|
||||
@@ -111,213 +118,141 @@ class ConnectionSettings extends React.Component<Props, State> {
|
||||
)
|
||||
}
|
||||
|
||||
private updateProtocol = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updateProtocol = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value
|
||||
this.updateConnection('protocol', value)
|
||||
updateConnection('protocol', value)
|
||||
if (event.target.value === 'mqtt') {
|
||||
this.updateConnection('basePath', undefined)
|
||||
updateConnection('basePath', undefined)
|
||||
} else {
|
||||
this.updateConnection('basePath', 'ws')
|
||||
updateConnection('basePath', 'ws')
|
||||
}
|
||||
}
|
||||
|
||||
private renderCertValidationSwitch() {
|
||||
const { classes, connection } = this.props
|
||||
|
||||
const certSwitch = (
|
||||
<Switch checked={connection.certValidation} onChange={this.toggleCertValidation} color="primary" />
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classes.switch}>
|
||||
<FormControlLabel control={certSwitch} label="Validate certificate" labelPlacement="bottom" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private toggleCertValidation = () => {
|
||||
this.props.managerActions.updateConnection(this.props.connection.id, {
|
||||
certValidation: !this.props.connection.certValidation,
|
||||
const toggleCertValidation = () => {
|
||||
props.managerActions.updateConnection(props.connection.id, {
|
||||
certValidation: !props.connection.certValidation,
|
||||
})
|
||||
}
|
||||
|
||||
private renderTlsSwitch() {
|
||||
const { classes, connection } = this.props
|
||||
|
||||
const tlsSwitch = <Switch checked={connection.encryption} onChange={this.toggleTls} color="primary" />
|
||||
|
||||
return (
|
||||
<div className={classes.switch}>
|
||||
<FormControlLabel control={tlsSwitch} label="Encryption (tls)" labelPlacement="bottom" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private toggleTls = () => {
|
||||
this.props.managerActions.updateConnection(this.props.connection.id, {
|
||||
encryption: !this.props.connection.encryption,
|
||||
const toggleTls = () => {
|
||||
props.managerActions.updateConnection(props.connection.id, {
|
||||
encryption: !props.connection.encryption,
|
||||
})
|
||||
}
|
||||
|
||||
private renderConnectButton() {
|
||||
const { classes, actions } = this.props
|
||||
|
||||
if (this.props.connecting) {
|
||||
return (
|
||||
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
|
||||
<ConnectionHealthIndicator />
|
||||
Abort
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
function PasswordVisibilityButton(props: { showPassword: boolean; toggle: () => void }) {
|
||||
return (
|
||||
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
|
||||
<PowerSettingsNew /> Connect
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
private connect() {
|
||||
if (!this.props.connection) {
|
||||
return
|
||||
}
|
||||
|
||||
const mqttOptions = toMqttConnection(this.props.connection)
|
||||
if (mqttOptions) {
|
||||
this.props.actions.connect(mqttOptions, this.props.connection.id)
|
||||
}
|
||||
}
|
||||
|
||||
private onClickConnect = () => {
|
||||
this.connect()
|
||||
}
|
||||
|
||||
private handleKeyEvent = (event: KeyboardEvent) => {
|
||||
if (event.keyCode === KeyCodes.enter) {
|
||||
this.connect()
|
||||
event.preventDefault()
|
||||
} else if (event.keyCode === KeyCodes.escape) {
|
||||
this.props.actions.disconnect()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener('keydown', this.handleKeyEvent, false)
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.handleKeyEvent, false)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { classes, connection } = this.props
|
||||
|
||||
const passwordVisibilityButton = (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label="Toggle password visibility" onClick={this.handleClickShowPassword}>
|
||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
<IconButton aria-label="Toggle password visibility" onClick={props.toggle}>
|
||||
{props.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className={classes.container} noValidate={true} autoComplete="off">
|
||||
<Grid container={true} spacing={3}>
|
||||
<Grid item={true} xs={5}>
|
||||
<TextField
|
||||
label="Name"
|
||||
className={classes.textField}
|
||||
value={connection.name}
|
||||
onChange={this.handleChange('name')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={4}>
|
||||
{this.renderCertValidationSwitch()}
|
||||
</Grid>
|
||||
<Grid item={true} xs={3}>
|
||||
{this.renderTlsSwitch()}
|
||||
</Grid>
|
||||
<Grid item={true} xs={2}>
|
||||
{this.renderProtocols()}
|
||||
</Grid>
|
||||
<Grid item={true} xs={7}>
|
||||
<TextField
|
||||
label="Host"
|
||||
className={classes.textField}
|
||||
value={connection.host}
|
||||
onChange={this.handleChange('host')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={3}>
|
||||
<TextField
|
||||
label="Port"
|
||||
className={classes.textField}
|
||||
value={connection.port}
|
||||
onChange={this.handleChange('port')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
{this.requiresBasePath() ? this.renderBasePathInput() : null}
|
||||
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
|
||||
<TextField
|
||||
label="Username"
|
||||
className={classes.textField}
|
||||
value={connection.username}
|
||||
onChange={this.handleChange('username')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={this.requiresBasePath() ? 4 : 6}>
|
||||
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
|
||||
<InputLabel htmlFor="adornment-password">Password</InputLabel>
|
||||
<Input
|
||||
id="adornment-password"
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
value={connection.password}
|
||||
onChange={this.handleChange('password')}
|
||||
endAdornment={passwordVisibilityButton}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
<div>
|
||||
<div style={{ float: 'left' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={() => this.props.managerActions.deleteConnection(this.props.connection.id)}
|
||||
>
|
||||
Delete <Delete />
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={this.props.managerActions.toggleAdvancedSettings}
|
||||
>
|
||||
<Settings /> Advanced
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ float: 'right' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={this.props.managerActions.saveConnectionSettings}
|
||||
>
|
||||
<Save /> Save
|
||||
</Button>
|
||||
{this.renderConnectButton()}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { classes, connection } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className={classes.container} noValidate={true} autoComplete="off">
|
||||
<Grid container={true} spacing={3}>
|
||||
<Grid item={true} xs={5}>
|
||||
<TextField
|
||||
label="Name"
|
||||
className={classes.textField}
|
||||
value={connection.name}
|
||||
onChange={handleChange('name')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={4}>
|
||||
<ToggleSwitch
|
||||
label="Validate certificate"
|
||||
classes={classes}
|
||||
value={connection.certValidation}
|
||||
toggle={toggleCertValidation}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={3}>
|
||||
<ToggleSwitch label="Encryption (tls)" classes={classes} value={connection.encryption} toggle={toggleTls} />
|
||||
</Grid>
|
||||
<Grid item={true} xs={2}>
|
||||
{renderProtocols()}
|
||||
</Grid>
|
||||
<Grid item={true} xs={7}>
|
||||
<TextField
|
||||
label="Host"
|
||||
className={classes.textField}
|
||||
value={connection.host}
|
||||
onChange={handleChange('host')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={3}>
|
||||
<TextField
|
||||
label="Port"
|
||||
className={classes.textField}
|
||||
value={connection.port}
|
||||
onChange={handleChange('port')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
{requiresBasePath() ? renderBasePathInput() : null}
|
||||
<Grid item={true} xs={requiresBasePath() ? 4 : 6}>
|
||||
<TextField
|
||||
label="Username"
|
||||
className={classes.textField}
|
||||
value={connection.username}
|
||||
onChange={handleChange('username')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item={true} xs={requiresBasePath() ? 4 : 6}>
|
||||
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
|
||||
<InputLabel htmlFor="adornment-password">Password</InputLabel>
|
||||
<Input
|
||||
id="adornment-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={connection.password}
|
||||
onChange={handleChange('password')}
|
||||
endAdornment={<PasswordVisibilityButton showPassword={showPassword} toggle={handleClickShowPassword} />}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
<div>
|
||||
<div style={{ float: 'left' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={() => props.managerActions.deleteConnection(props.connection.id)}
|
||||
>
|
||||
Delete <Delete />
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
onClick={props.managerActions.toggleAdvancedSettings}
|
||||
>
|
||||
<Settings /> Advanced
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ float: 'right' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={props.managerActions.saveConnectionSettings}
|
||||
>
|
||||
<Save /> Save
|
||||
</Button>
|
||||
<ConnectButton toggle={toggleConnect} connecting={props.connecting} classes={classes} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
|
||||
12
app/src/components/ConnectionSetup/ToggleSwitch.tsx
Normal file
12
app/src/components/ConnectionSetup/ToggleSwitch.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import { FormControlLabel, Switch } from '@material-ui/core'
|
||||
|
||||
export function ToggleSwitch(props: { value: boolean; classes: any; toggle: () => void; label: string }) {
|
||||
const { classes, value, toggle, label } = props
|
||||
const toggleSwitch = <Switch checked={value} onChange={toggle} color="primary" />
|
||||
return (
|
||||
<div className={classes.switch}>
|
||||
<FormControlLabel control={toggleSwitch} label={label} labelPlacement="bottom" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user