initial commit

This commit is contained in:
proddy
2020-07-05 18:29:08 +02:00
parent 26b201ea2f
commit c5933e8c14
739 changed files with 86566 additions and 20952 deletions

View 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);

View 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, saveData, loadData } = this.props;
return (
<ValidatorForm onSubmit={saveData}>
<BlockFormControlLabel
control={
<Checkbox
checked={data.enabled}
onChange={handleValueChange("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;

View File

@@ -0,0 +1,51 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Tabs, Tab } from '@material-ui/core';
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
import { MenuAppBar } from '../components';
import SystemStatusController from './SystemStatusController';
import OTASettingsController from './OTASettingsController';
import UploadFirmwareController from './UploadFirmwareController';
type SystemProps = AuthenticatedContextProps & RouteComponentProps & WithFeaturesProps;
class System extends Component<SystemProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
this.props.history.push(path);
};
render() {
const { authenticatedContext, features } = this.props;
return (
<MenuAppBar sectionTitle="System">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tab value="/system/status" label="System Status" />
{features.ota && (
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
)}
{features.upload_firmware && (
<Tab value="/system/upload" label="Upload Firmware" disabled={!authenticatedContext.me.admin} />
)}
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/system/status" component={SystemStatusController} />
{features.ota && (
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
)}
{features.upload_firmware && (
<AuthenticatedRoute exact path="/system/upload" component={UploadFirmwareController} />
)}
<Redirect to="/system/status" />
</Switch>
</MenuAppBar>
)
}
}
export default withFeatures(withAuthenticatedContext(System));

View 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);

View File

@@ -0,0 +1,245 @@
import React, { Component, Fragment } from 'react';
import { Avatar, Button, Divider, Dialog, DialogTitle, DialogContent, DialogActions, Box } 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 FolderIcon from '@material-ui/icons/Folder';
import DataUsageIcon from '@material-ui/icons/DataUsage';
import AppsIcon from '@material-ui/icons/Apps';
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew';
import RefreshIcon from '@material-ui/icons/Refresh';
import SettingsBackupRestoreIcon from '@material-ui/icons/SettingsBackupRestore';
import { redirectingAuthorizedFetch, AuthenticatedContextProps, withAuthenticatedContext } from '../authentication';
import { RestFormProps, FormButton, ErrorButton } from '../components';
import { FACTORY_RESET_ENDPOINT, RESTART_ENDPOINT } from '../api';
import { SystemStatus, EspPlatform } from './types';
interface SystemStatusFormState {
confirmRestart: boolean;
confirmFactoryReset: boolean;
processing: boolean;
}
type SystemStatusFormProps = AuthenticatedContextProps & RestFormProps<SystemStatus>;
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusFormState> {
state: SystemStatusFormState = {
confirmRestart: false,
confirmFactoryReset: false,
processing: false
}
createListItems() {
const { data } = this.props
return (
<Fragment>
<ListItem >
<ListItemAvatar>
<Avatar>
<DevicesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Device (Platform / SDK)" secondary={data.esp_platform + ' / ' + data.sdk_version} />
</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="Heap (Free / Max Alloc)" secondary={formatNumber(data.free_heap) + ' / ' + formatNumber(data.max_alloc_heap) + ' bytes ' + (data.esp_platform === EspPlatform.ESP8266 ? '(' + data.heap_fragmentation + '% fragmentation)' : '')} />
</ListItem>
{
(data.esp_platform === EspPlatform.ESP32 && data.psram_size > 0) && (
<Fragment>
<Divider variant="inset" component="li" />
<ListItem >
<ListItemAvatar>
<Avatar>
<AppsIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="PSRAM (Size / Free)" secondary={formatNumber(data.psram_size) + ' / ' + formatNumber(data.free_psram) + ' bytes'} />
</ListItem>
</Fragment>)
}
<Divider variant="inset" component="li" />
<ListItem >
<ListItemAvatar>
<Avatar>
<DataUsageIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Sketch (Size / Free)" secondary={formatNumber(data.sketch_size) + ' / ' + formatNumber(data.free_sketch_space) + ' bytes'} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem >
<ListItemAvatar>
<Avatar>
<SdStorageIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Flash Chip (Size / Speed)" secondary={formatNumber(data.flash_chip_size) + ' bytes / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem >
<ListItemAvatar>
<Avatar>
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="File System (Used / Total)" secondary={formatNumber(data.fs_used) + ' / ' + formatNumber(data.fs_total) + ' bytes (' + formatNumber(data.fs_total - data.fs_used) + '\xa0bytes free)'} />
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
);
}
renderRestartDialog() {
return (
<Dialog
open={this.state.confirmRestart}
onClose={this.onRestartRejected}
>
<DialogTitle>Confirm Restart</DialogTitle>
<DialogContent dividers>
Are you sure you want to restart the device?
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
Cancel
</Button>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
Restart
</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 });
});
}
renderFactoryResetDialog() {
return (
<Dialog
open={this.state.confirmFactoryReset}
onClose={this.onFactoryResetRejected}
>
<DialogTitle>Confirm Factory Reset</DialogTitle>
<DialogContent dividers>
Are you sure you want to reset the device to its factory defaults?
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={this.onFactoryResetRejected} color="secondary">
Cancel
</Button>
<ErrorButton startIcon={<SettingsBackupRestoreIcon />} variant="contained" onClick={this.onFactoryResetConfirmed} disabled={this.state.processing} autoFocus>
Factory Reset
</ErrorButton>
</DialogActions>
</Dialog>
)
}
onFactoryReset = () => {
this.setState({ confirmFactoryReset: true });
}
onFactoryResetRejected = () => {
this.setState({ confirmFactoryReset: false });
}
onFactoryResetConfirmed = () => {
this.setState({ processing: true });
redirectingAuthorizedFetch(FACTORY_RESET_ENDPOINT, { method: 'POST' })
.then(response => {
if (response.status === 200) {
this.props.enqueueSnackbar("Factory reset in progress.", { variant: 'error' });
this.setState({ processing: false, confirmFactoryReset: false });
} else {
throw Error("Invalid status code: " + response.status);
}
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem factory resetting device", { variant: 'error' });
this.setState({ processing: false, confirmRestart: false });
});
}
render() {
const me = this.props.authenticatedContext.me;
return (
<Fragment>
<List>
{this.createListItems()}
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} padding={1}>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
Refresh
</FormButton>
</Box>
{me.admin &&
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<FormButton startIcon={<PowerSettingsNewIcon />} variant="contained" color="primary" onClick={this.onRestart}>
Restart
</FormButton>
<ErrorButton startIcon={<SettingsBackupRestoreIcon />} variant="contained" onClick={this.onFactoryReset}>
Factory reset
</ErrorButton>
</Box>
}
</Box>
{this.renderRestartDialog()}
{this.renderFactoryResetDialog()}
</Fragment>
);
}
}
export default withAuthenticatedContext(SystemStatusForm);

View File

@@ -0,0 +1,71 @@
import React, { Component } from 'react';
import { SectionContent } from '../components';
import { UPLOAD_FIRMWARE_ENDPOINT } from '../api';
import UploadFirmwareForm from './UploadFirmwareForm';
import { redirectingAuthorizedUpload } from '../authentication';
import { withSnackbar, WithSnackbarProps } from 'notistack';
interface UploadFirmwareControllerState {
xhr?: XMLHttpRequest;
progress?: ProgressEvent;
}
class UploadFirmwareController extends Component<WithSnackbarProps, UploadFirmwareControllerState> {
state: UploadFirmwareControllerState = {
xhr: undefined,
progress: undefined
};
componentWillUnmount() {
this.state.xhr?.abort();
}
updateProgress = (progress: ProgressEvent) => {
this.setState({ progress });
}
uploadFile = (file: File) => {
if (this.state.xhr) {
return;
}
var xhr = new XMLHttpRequest();
this.setState({ xhr });
redirectingAuthorizedUpload(xhr, UPLOAD_FIRMWARE_ENDPOINT, file, this.updateProgress).then(() => {
if (xhr.status !== 200) {
throw Error("Invalid status code: " + xhr.status);
}
this.props.enqueueSnackbar("Activating new firmware", { variant: 'success' });
this.setState({ xhr: undefined, progress: undefined });
}).catch((error: Error) => {
if (error.name === 'AbortError') {
this.props.enqueueSnackbar("Upload cancelled by user", { variant: 'warning' });
} else {
const errorMessage = error.name === 'UploadError' ? "Error during upload" : (error.message || "Unknown error");
this.props.enqueueSnackbar("Problem uploading: " + errorMessage, { variant: 'error' });
this.setState({ xhr: undefined, progress: undefined });
}
});
}
cancelUpload = () => {
if (this.state.xhr) {
this.state.xhr.abort();
this.setState({ xhr: undefined, progress: undefined });
}
}
render() {
const { xhr, progress } = this.state;
return (
<SectionContent title="Upload Firmware">
<UploadFirmwareForm onFileSelected={this.uploadFile} onCancel={this.cancelUpload} uploading={!!xhr} progress={progress} />
</SectionContent>
);
}
}
export default withSnackbar(UploadFirmwareController);

View File

@@ -0,0 +1,35 @@
import React, { Fragment } from 'react';
import { SingleUpload } from '../components';
import { Box } from '@material-ui/core';
interface UploadFirmwareFormProps {
uploading: boolean;
progress?: ProgressEvent;
onFileSelected: (file: File) => void;
onCancel: () => void;
}
class UploadFirmwareForm extends React.Component<UploadFirmwareFormProps> {
handleDrop = (files: File[]) => {
const file = files[0];
if (file) {
this.props.onFileSelected(files[0]);
}
};
render() {
const { uploading, progress, onCancel } = this.props;
return (
<Fragment>
<Box py={2}>
Upload a new firmware (.bin) file below to replace the existing firmware.
</Box>
<SingleUpload accept="application/octet-stream" onDrop={this.handleDrop} uploading={uploading} progress={progress} onCancel={onCancel} />
</Fragment>
);
}
}
export default UploadFirmwareForm;

View File

@@ -0,0 +1,37 @@
export enum EspPlatform {
ESP8266 = "esp8266",
ESP32 = "esp32"
}
interface ESPSystemStatus {
esp_platform: EspPlatform;
max_alloc_heap: number;
cpu_freq_mhz: number;
free_heap: number;
sketch_size: number;
free_sketch_space: number;
sdk_version: string;
flash_chip_size: number;
flash_chip_speed: number;
fs_used: number;
fs_total: number;
}
export interface ESP32SystemStatus extends ESPSystemStatus {
esp_platform: EspPlatform.ESP32;
psram_size: number;
free_psram: number;
}
export interface ESP8266SystemStatus extends ESPSystemStatus {
esp_platform: EspPlatform.ESP8266;
heap_fragmentation: number;
}
export type SystemStatus = ESP8266SystemStatus | ESP32SystemStatus;
export interface OTASettings {
enabled: boolean;
port: number;
password: string;
}