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