Re-engineer UI in TypeScript (#89)
* Re-engineer UI in TypeScript * Switch to named imports where possible * Restructure file system layout * Update depencencies * Update README.md * Change explicit colors for better support for dark theme
This commit is contained in:
30
interface/src/system/OTASettingsController.tsx
Normal file
30
interface/src/system/OTASettingsController.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import { OTA_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import OTASettingsForm from './OTASettingsForm';
|
||||
import { OTASettings } from './types';
|
||||
|
||||
type OTASettingsControllerProps = RestControllerProps<OTASettings>;
|
||||
|
||||
class OTASettingsController extends Component<OTASettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="OTA Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <OTASettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(OTA_SETTINGS_ENDPOINT, OTASettingsController);
|
69
interface/src/system/OTASettingsForm.tsx
Normal file
69
interface/src/system/OTASettingsForm.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import { Checkbox } from '@material-ui/core';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { RestFormProps, BlockFormControlLabel, PasswordValidator, FormButton, FormActions } from '../components';
|
||||
import {isIP,isHostname,or} from '../validators';
|
||||
|
||||
import { OTASettings } from './types';
|
||||
|
||||
type OTASettingsFormProps = RestFormProps<OTASettings>;
|
||||
|
||||
class OTASettingsForm extends React.Component<OTASettingsFormProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, handleValueChange, handleCheckboxChange, saveData, loadData } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData}>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.enabled}
|
||||
onChange={handleCheckboxChange("enabled")}
|
||||
/>
|
||||
}
|
||||
label="Enable OTA Updates?"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']}
|
||||
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]}
|
||||
name="port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.port}
|
||||
type="number"
|
||||
onChange={handleValueChange('port')}
|
||||
margin="normal"
|
||||
/>
|
||||
<PasswordValidator
|
||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||
errorMessages={['OTA Password is required', 'OTA Point Password must be 64 characters or less']}
|
||||
name="password"
|
||||
label="Password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={handleValueChange('password')}
|
||||
margin="normal"
|
||||
/>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
Save
|
||||
</FormButton>
|
||||
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||
Reset
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OTASettingsForm;
|
38
interface/src/system/System.tsx
Normal file
38
interface/src/system/System.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import SystemStatusController from './SystemStatusController';
|
||||
import OTASettingsController from './OTASettingsController';
|
||||
|
||||
type SystemProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class System extends Component<SystemProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<MenuAppBar sectionTitle="System">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value="/system/status" label="System Status" />
|
||||
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact={true} path="/system/status" component={SystemStatusController} />
|
||||
<AuthenticatedRoute exact={true} path="/system/ota" component={OTASettingsController} />
|
||||
<Redirect to="/system/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(System);
|
30
interface/src/system/SystemStatusController.tsx
Normal file
30
interface/src/system/SystemStatusController.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import { SYSTEM_STATUS_ENDPOINT } from '../api';
|
||||
|
||||
import SystemStatusForm from './SystemStatusForm';
|
||||
import { SystemStatus } from './types';
|
||||
|
||||
type SystemStatusControllerProps = RestControllerProps<SystemStatus>;
|
||||
|
||||
class SystemStatusController extends Component<SystemStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="System Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <SystemStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(SYSTEM_STATUS_ENDPOINT, SystemStatusController);
|
155
interface/src/system/SystemStatusForm.tsx
Normal file
155
interface/src/system/SystemStatusForm.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { Avatar, Button, Divider, Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core';
|
||||
import { List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||
|
||||
import DevicesIcon from '@material-ui/icons/Devices';
|
||||
import MemoryIcon from '@material-ui/icons/Memory';
|
||||
import ShowChartIcon from '@material-ui/icons/ShowChart';
|
||||
import SdStorageIcon from '@material-ui/icons/SdStorage';
|
||||
import DataUsageIcon from '@material-ui/icons/DataUsage';
|
||||
import AutorenewIcon from '@material-ui/icons/Autorenew';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
import { RestFormProps, FormButton, FormActions } from '../components';
|
||||
import { RESTART_ENDPOINT } from '../api';
|
||||
|
||||
import { SystemStatus } from './types';
|
||||
|
||||
interface SystemStatusFormState {
|
||||
confirmRestart: boolean;
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
type SystemStatusFormProps = RestFormProps<SystemStatus>;
|
||||
|
||||
class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusFormState> {
|
||||
|
||||
state: SystemStatusFormState = {
|
||||
confirmRestart: false,
|
||||
processing: false
|
||||
}
|
||||
|
||||
createListItems() {
|
||||
const { data } = this.props
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem >
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DevicesIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Platform" secondary={data.esp_platform} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem >
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<ShowChartIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="CPU Frequency" secondary={data.cpu_freq_mhz + ' MHz'} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem >
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<MemoryIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Free Heap" secondary={data.free_heap + ' bytes'} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem >
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DataUsageIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Sketch Size (used/max)" secondary={data.sketch_size + ' / ' + data.free_sketch_space + ' bytes'} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem >
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<SdStorageIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Flash Chip Size" secondary={data.flash_chip_size + ' bytes'} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderRestartDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.confirmRestart}
|
||||
onClose={this.onRestartRejected}
|
||||
>
|
||||
<DialogTitle>Confirm Restart</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
Are you sure you want to restart the device?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<AutorenewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
|
||||
Restart
|
||||
</Button>
|
||||
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
onRestart = () => {
|
||||
this.setState({ confirmRestart: true });
|
||||
}
|
||||
|
||||
onRestartRejected = () => {
|
||||
this.setState({ confirmRestart: false });
|
||||
}
|
||||
|
||||
onRestartConfirmed = () => {
|
||||
this.setState({ processing: true });
|
||||
redirectingAuthorizedFetch(RESTART_ENDPOINT, { method: 'POST' })
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar("Device is restarting", { variant: 'info' });
|
||||
this.setState({ processing: false, confirmRestart: false });
|
||||
} else {
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.props.enqueueSnackbar(error.message || "Problem restarting device", { variant: 'error' });
|
||||
this.setState({ processing: false, confirmRestart: false });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
Refresh
|
||||
</FormButton>
|
||||
<FormButton startIcon={<AutorenewIcon />} variant="contained" color="primary" onClick={this.onRestart}>
|
||||
Restart
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
{this.renderRestartDialog()}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SystemStatusForm;
|
14
interface/src/system/types.ts
Normal file
14
interface/src/system/types.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export interface SystemStatus {
|
||||
esp_platform: string;
|
||||
cpu_freq_mhz: number;
|
||||
free_heap: number;
|
||||
sketch_size: number;
|
||||
free_sketch_space: number;
|
||||
flash_chip_size: number;
|
||||
}
|
||||
|
||||
export interface OTASettings {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
password: string;
|
||||
}
|
Reference in New Issue
Block a user