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 Delete from '@material-ui/icons/Delete'
|
||||||
import Settings from '@material-ui/icons/Settings'
|
import React, { useCallback, useState } from 'react'
|
||||||
import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew'
|
|
||||||
import Save from '@material-ui/icons/Save'
|
import Save from '@material-ui/icons/Save'
|
||||||
|
import Settings from '@material-ui/icons/Settings'
|
||||||
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 { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
@@ -10,23 +10,21 @@ import { bindActionCreators } from 'redux'
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { connectionActions, connectionManagerActions } from '../../actions'
|
import { connectionActions, connectionManagerActions } from '../../actions'
|
||||||
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
|
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
|
||||||
|
import { KeyCodes } from '../../utils/KeyCodes'
|
||||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||||
|
import { ToggleSwitch } from './ToggleSwitch'
|
||||||
|
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Switch,
|
|
||||||
TextField,
|
TextField,
|
||||||
} from '@material-ui/core'
|
} from '@material-ui/core'
|
||||||
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
|
|
||||||
import { KeyCodes } from '../../utils/KeyCodes'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connection: ConnectionOptions
|
connection: ConnectionOptions
|
||||||
@@ -39,57 +37,66 @@ interface Props {
|
|||||||
|
|
||||||
const protocols = ['mqtt', 'ws']
|
const protocols = ['mqtt', 'ws']
|
||||||
|
|
||||||
interface State {
|
function ConnectionSettings(props: Props) {
|
||||||
showPassword: boolean
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
}
|
|
||||||
|
|
||||||
class ConnectionSettings extends React.Component<Props, State> {
|
const toggleConnect = useCallback(() => {
|
||||||
constructor(props: any) {
|
if (props.connecting) {
|
||||||
super(props)
|
props.actions.disconnect()
|
||||||
|
return
|
||||||
this.state = {
|
|
||||||
showPassword: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
function renderBasePathInput() {
|
||||||
this.setState({ showPassword: !this.state.showPassword })
|
|
||||||
}
|
|
||||||
|
|
||||||
private requiresBasePath() {
|
|
||||||
return this.props.connection.protocol !== 'mqtt'
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderBasePathInput() {
|
|
||||||
return (
|
return (
|
||||||
<Grid item={true} xs={4}>
|
<Grid item={true} xs={4}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Basepath"
|
label="Basepath"
|
||||||
className={this.props.classes.textField}
|
className={props.classes.textField}
|
||||||
value={this.props.connection.basePath}
|
value={props.connection.basePath}
|
||||||
onChange={this.handleChange('basePath')}
|
onChange={handleChange('basePath')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChange = (name: string) => (event: any) => {
|
const handleChange = (name: string) => (event: any) => {
|
||||||
if (!this.props.connection) {
|
if (!props.connection) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateConnection(name, event.target.value)
|
updateConnection(name, event.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateConnection(name: string, value: any) {
|
const updateConnection = (name: string, value: any) => {
|
||||||
this.props.managerActions.updateConnection(this.props.connection.id, {
|
props.managerActions.updateConnection(props.connection.id, {
|
||||||
[name]: value,
|
[name]: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderProtocols() {
|
const renderProtocols = () => {
|
||||||
const { classes, connection } = this.props
|
const { classes, connection } = props
|
||||||
|
|
||||||
const protocolItems = protocols.map((value: string) => (
|
const protocolItems = protocols.map((value: string) => (
|
||||||
<MenuItem key={value} value={value}>
|
<MenuItem key={value} value={value}>
|
||||||
@@ -103,7 +110,7 @@ class ConnectionSettings extends React.Component<Props, State> {
|
|||||||
label="Protocol"
|
label="Protocol"
|
||||||
className={classes.textField}
|
className={classes.textField}
|
||||||
value={connection.protocol}
|
value={connection.protocol}
|
||||||
onChange={this.updateProtocol}
|
onChange={updateProtocol}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
{protocolItems}
|
{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
|
const value = event.target.value
|
||||||
this.updateConnection('protocol', value)
|
updateConnection('protocol', value)
|
||||||
if (event.target.value === 'mqtt') {
|
if (event.target.value === 'mqtt') {
|
||||||
this.updateConnection('basePath', undefined)
|
updateConnection('basePath', undefined)
|
||||||
} else {
|
} else {
|
||||||
this.updateConnection('basePath', 'ws')
|
updateConnection('basePath', 'ws')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCertValidationSwitch() {
|
const toggleCertValidation = () => {
|
||||||
const { classes, connection } = this.props
|
props.managerActions.updateConnection(props.connection.id, {
|
||||||
|
certValidation: !props.connection.certValidation,
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTlsSwitch() {
|
const toggleTls = () => {
|
||||||
const { classes, connection } = this.props
|
props.managerActions.updateConnection(props.connection.id, {
|
||||||
|
encryption: !props.connection.encryption,
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderConnectButton() {
|
function PasswordVisibilityButton(props: { showPassword: boolean; toggle: () => void }) {
|
||||||
const { classes, actions } = this.props
|
|
||||||
|
|
||||||
if (this.props.connecting) {
|
|
||||||
return (
|
|
||||||
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
|
|
||||||
<ConnectionHealthIndicator />
|
|
||||||
Abort
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
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">
|
<InputAdornment position="end">
|
||||||
<IconButton aria-label="Toggle password visibility" onClick={this.handleClickShowPassword}>
|
<IconButton aria-label="Toggle password visibility" onClick={props.toggle}>
|
||||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
{props.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</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) => {
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
136
app/src/components/Layout/SearchBar.tsx
Normal file
136
app/src/components/Layout/SearchBar.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import React, { useCallback, useState, useRef } from 'react'
|
||||||
|
import ClearAdornment from '../helper/ClearAdornment'
|
||||||
|
import Search from '@material-ui/icons/Search'
|
||||||
|
import { AppState } from '../../reducers'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import { InputBase } from '@material-ui/core'
|
||||||
|
import { settingsActions } from '../../actions'
|
||||||
|
import { fade, Theme, withStyles } from '@material-ui/core/styles'
|
||||||
|
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
|
||||||
|
|
||||||
|
function SearchBar(props: {
|
||||||
|
classes: any
|
||||||
|
topicFilter?: string
|
||||||
|
hasConnection: boolean
|
||||||
|
actions: {
|
||||||
|
settings: typeof settingsActions
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const { actions, classes, hasConnection, topicFilter } = props
|
||||||
|
|
||||||
|
const [hasFocus, setHasFocus] = useState(false)
|
||||||
|
const inputRef = useRef<HTMLInputElement>()
|
||||||
|
const onFocus = useCallback(() => setHasFocus(true), [])
|
||||||
|
const onBlur = useCallback(() => setHasFocus(false), [])
|
||||||
|
|
||||||
|
const clearFilter = useCallback(() => {
|
||||||
|
actions.settings.filterTopics('')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onFilterChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
actions.settings.filterTopics(event.target.value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useGlobalKeyEventHandler(undefined, event => {
|
||||||
|
const isCharacter = event.key.length === 1
|
||||||
|
const isBackspace = event.key === 'Backspace'
|
||||||
|
const isBodyActiveElement = document.activeElement && document.activeElement.tagName === 'BODY'
|
||||||
|
|
||||||
|
if ((isCharacter || isBackspace) && !hasFocus && isBodyActiveElement && hasConnection) {
|
||||||
|
// Focus input field, no preventDefault the event will reach the input element after it has been focussed
|
||||||
|
inputRef.current && inputRef.current.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.search}>
|
||||||
|
<div className={classes.searchIcon}>
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
<InputBase
|
||||||
|
value={topicFilter}
|
||||||
|
inputProps={{
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
ref: inputRef,
|
||||||
|
}}
|
||||||
|
onChange={onFilterChange}
|
||||||
|
placeholder="Search…"
|
||||||
|
endAdornment={
|
||||||
|
<div style={{ width: '24px', paddingRight: '8px' }}>
|
||||||
|
<ClearAdornment variant="primary" action={clearFilter} value={topicFilter} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
classes={{ root: classes.inputRoot, input: classes.inputInput }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
return {
|
||||||
|
topicFilter: state.settings.get('topicFilter'),
|
||||||
|
hasConnection: Boolean(state.connection.connectionId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => {
|
||||||
|
return {
|
||||||
|
actions: {
|
||||||
|
settings: bindActionCreators(settingsActions, dispatch),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = (theme: Theme) => ({
|
||||||
|
search: {
|
||||||
|
position: 'relative' as 'relative',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: fade(theme.palette.common.white, 0.15),
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: fade(theme.palette.common.white, 0.25),
|
||||||
|
},
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginLeft: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
maxWidth: '60%',
|
||||||
|
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
maxWidth: '30%',
|
||||||
|
|
||||||
|
marginLeft: theme.spacing(4),
|
||||||
|
width: 'auto' as 'auto',
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up(750)]: {
|
||||||
|
marginLeft: theme.spacing(4),
|
||||||
|
width: 'auto' as 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchIcon: {
|
||||||
|
width: theme.spacing(6),
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute' as 'absolute',
|
||||||
|
pointerEvents: 'none' as 'none',
|
||||||
|
display: 'flex' as 'flex',
|
||||||
|
alignItems: 'center' as 'center',
|
||||||
|
justifyContent: 'center' as 'center',
|
||||||
|
},
|
||||||
|
inputRoot: {
|
||||||
|
color: 'inherit' as 'inherit',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
inputInput: {
|
||||||
|
paddingTop: theme.spacing(1),
|
||||||
|
paddingRight: theme.spacing(1),
|
||||||
|
paddingBottom: theme.spacing(1),
|
||||||
|
paddingLeft: theme.spacing(6),
|
||||||
|
transition: theme.transitions.create('width'),
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(withStyles(styles)(SearchBar))
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ClearAdornment from '../helper/ClearAdornment'
|
|
||||||
import CloudOff from '@material-ui/icons/CloudOff'
|
import CloudOff from '@material-ui/icons/CloudOff'
|
||||||
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
|
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
|
||||||
import Menu from '@material-ui/icons/Menu'
|
import Menu from '@material-ui/icons/Menu'
|
||||||
import PauseButton from './PauseButton'
|
import PauseButton from './PauseButton'
|
||||||
import Search from '@material-ui/icons/Search'
|
import SearchBar from './SearchBar'
|
||||||
|
import { AppBar, Button, IconButton, Toolbar, Typography } from '@material-ui/core'
|
||||||
import { AppState } from '../../reducers'
|
import { AppState } from '../../reducers'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { connectionActions, globalActions, settingsActions } from '../../actions'
|
import { connectionActions, globalActions, settingsActions } from '../../actions'
|
||||||
import { fade } from '@material-ui/core/styles/colorManipulator'
|
|
||||||
import { Theme, withStyles } from '@material-ui/core/styles'
|
import { Theme, withStyles } from '@material-ui/core/styles'
|
||||||
import { AppBar, Button, IconButton, InputBase, Toolbar, Typography } from '@material-ui/core'
|
|
||||||
|
|
||||||
const styles = (theme: Theme) => ({
|
const styles = (theme: Theme) => ({
|
||||||
title: {
|
title: {
|
||||||
@@ -21,29 +19,6 @@ const styles = (theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
whiteSpace: 'nowrap' as 'nowrap',
|
whiteSpace: 'nowrap' as 'nowrap',
|
||||||
},
|
},
|
||||||
search: {
|
|
||||||
position: 'relative' as 'relative',
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.15),
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.25),
|
|
||||||
},
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginLeft: 0,
|
|
||||||
flexGrow: 1,
|
|
||||||
maxWidth: '60%',
|
|
||||||
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
maxWidth: '30%',
|
|
||||||
|
|
||||||
marginLeft: theme.spacing(4),
|
|
||||||
width: 'auto' as 'auto',
|
|
||||||
},
|
|
||||||
[theme.breakpoints.up(750)]: {
|
|
||||||
marginLeft: theme.spacing(4),
|
|
||||||
width: 'auto' as 'auto',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
disconnectIcon: {
|
disconnectIcon: {
|
||||||
[theme.breakpoints.down('xs')]: {
|
[theme.breakpoints.down('xs')]: {
|
||||||
display: 'none' as 'none',
|
display: 'none' as 'none',
|
||||||
@@ -51,27 +26,6 @@ const styles = (theme: Theme) => ({
|
|||||||
marginRight: '8px',
|
marginRight: '8px',
|
||||||
paddingLeft: '8px',
|
paddingLeft: '8px',
|
||||||
},
|
},
|
||||||
searchIcon: {
|
|
||||||
width: theme.spacing(6),
|
|
||||||
height: '100%',
|
|
||||||
position: 'absolute' as 'absolute',
|
|
||||||
pointerEvents: 'none' as 'none',
|
|
||||||
display: 'flex' as 'flex',
|
|
||||||
alignItems: 'center' as 'center',
|
|
||||||
justifyContent: 'center' as 'center',
|
|
||||||
},
|
|
||||||
inputRoot: {
|
|
||||||
color: 'inherit' as 'inherit',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
inputInput: {
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
paddingRight: theme.spacing(1),
|
|
||||||
paddingBottom: theme.spacing(1),
|
|
||||||
paddingLeft: theme.spacing(6),
|
|
||||||
transition: theme.transitions.create('width'),
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
menuButton: {
|
menuButton: {
|
||||||
marginLeft: -12,
|
marginLeft: -12,
|
||||||
marginRight: 20,
|
marginRight: 20,
|
||||||
@@ -98,37 +52,6 @@ class TitleBar extends React.Component<Props, {}> {
|
|||||||
this.state = {}
|
this.state = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSearch() {
|
|
||||||
const { classes, topicFilter } = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.search}>
|
|
||||||
<div className={classes.searchIcon}>
|
|
||||||
<Search />
|
|
||||||
</div>
|
|
||||||
<InputBase
|
|
||||||
value={topicFilter}
|
|
||||||
onChange={this.onFilterChange}
|
|
||||||
placeholder="Search…"
|
|
||||||
endAdornment={
|
|
||||||
<div style={{ width: '24px', paddingRight: '8px' }}>
|
|
||||||
<ClearAdornment variant="primary" action={this.clearFilter} value={topicFilter} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
classes={{ root: classes.inputRoot, input: classes.inputInput }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearFilter = () => {
|
|
||||||
this.props.actions.settings.filterTopics('')
|
|
||||||
}
|
|
||||||
|
|
||||||
private onFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
this.props.actions.settings.filterTopics(event.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { actions, classes } = this.props
|
const { actions, classes } = this.props
|
||||||
|
|
||||||
@@ -146,7 +69,7 @@ class TitleBar extends React.Component<Props, {}> {
|
|||||||
<Typography className={classes.title} variant="h6" color="inherit">
|
<Typography className={classes.title} variant="h6" color="inherit">
|
||||||
MQTT Explorer
|
MQTT Explorer
|
||||||
</Typography>
|
</Typography>
|
||||||
{this.renderSearch()}
|
<SearchBar />
|
||||||
<PauseButton />
|
<PauseButton />
|
||||||
<Button className={classes.disconnect} onClick={actions.connection.disconnect}>
|
<Button className={classes.disconnect} onClick={actions.connection.disconnect}>
|
||||||
Disconnect <CloudOff className={classes.disconnectIcon} />
|
Disconnect <CloudOff className={classes.disconnectIcon} />
|
||||||
|
|||||||
24
app/src/effects/useGlobalKeyEventHandler.tsx
Normal file
24
app/src/effects/useGlobalKeyEventHandler.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { KeyCodes } from '../utils/KeyCodes'
|
||||||
|
|
||||||
|
export function useGlobalKeyEventHandler(
|
||||||
|
key: KeyCodes | undefined,
|
||||||
|
callback: (event: KeyboardEvent) => void,
|
||||||
|
dependencies?: Array<any>
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
function handleKeyEvent(event: KeyboardEvent) {
|
||||||
|
if (key === undefined) {
|
||||||
|
callback(event)
|
||||||
|
} else if (event.keyCode === key) {
|
||||||
|
callback(event)
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyEvent, false)
|
||||||
|
return function cleanup() {
|
||||||
|
document.removeEventListener('keydown', handleKeyEvent, false)
|
||||||
|
}
|
||||||
|
}, dependencies)
|
||||||
|
}
|
||||||
31
app/src/effects/useKeyEventHandler.tsx
Normal file
31
app/src/effects/useKeyEventHandler.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { KeyCodes } from '../utils/KeyCodes'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
export function useKeyEventHandler(key: KeyCodes, callback: () => void, dependencies: Array<any> = []) {
|
||||||
|
return useKeyEventHandlers([{ key, callback }], dependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeyEventHandlers(
|
||||||
|
actions: Array<{
|
||||||
|
key: KeyCodes
|
||||||
|
callback: (event: KeyboardEvent) => void
|
||||||
|
preventDefault?: boolean
|
||||||
|
stopPropagation?: boolean
|
||||||
|
}>,
|
||||||
|
dependencies: Array<any> = []
|
||||||
|
) {
|
||||||
|
return useCallback(() => {
|
||||||
|
return function handleKeyEvent(event: KeyboardEvent) {
|
||||||
|
const action = actions.find(a => a.key === event.keyCode)
|
||||||
|
if (action) {
|
||||||
|
action.callback(event)
|
||||||
|
if (action.preventDefault !== false) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
if (action.stopPropagation !== false) {
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, dependencies)
|
||||||
|
}
|
||||||
@@ -10,14 +10,12 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"jsx": "react"
|
"jsx": "react",
|
||||||
|
"types": ["react"],
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./src/**/*"],
|
||||||
"./src/**/*"
|
"exclude": ["**/*.d.ts"],
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"**/*.d.ts"
|
|
||||||
],
|
|
||||||
"awesomeTypescriptLoaderOptions": {
|
"awesomeTypescriptLoaderOptions": {
|
||||||
"useCache": true,
|
"useCache": true,
|
||||||
"transpileModule": true,
|
"transpileModule": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user