Merge remote-tracking branch 'origin/v3.4' into dev

This commit is contained in:
proddy
2022-01-23 17:56:52 +01:00
parent 02e2b51814
commit 77e1898512
538 changed files with 32282 additions and 38655 deletions

View File

@@ -0,0 +1,32 @@
import { AxiosPromise } from 'axios';
import { FC } from 'react';
import { FileUploadConfig } from '../../api/endpoints';
import { MessageBox, SingleUpload, useFileUpload } from '../../components';
interface UploadFirmwareProps {
uploadFirmware: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
}
const FirmwareFileUpload: FC<UploadFirmwareProps> = ({ uploadFirmware }) => {
const [uploadFile, cancelUpload, uploading, uploadProgress] = useFileUpload({ upload: uploadFirmware });
return (
<>
<MessageBox
message="Upload a new firmware (.bin) file below to replace the existing firmware"
level="warning"
my={2}
/>
<SingleUpload
accept="application/octet-stream"
onDrop={uploadFile}
onCancel={cancelUpload}
uploading={uploading}
progress={uploadProgress}
/>
</>
);
};
export default FirmwareFileUpload;

View File

@@ -0,0 +1,43 @@
import { useEffect } from 'react';
import { FC, useRef, useState } from 'react';
import * as SystemApi from '../../api/system';
import { FormLoader } from '../../components';
const RESTART_TIMEOUT = 2 * 60 * 1000;
const POLL_TIMEOUT = 2000;
const POLL_INTERVAL = 5000;
const FirmwareRestartMonitor: FC = () => {
const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
const poll = useRef(async () => {
try {
await SystemApi.readSystemStatus(POLL_TIMEOUT);
document.location.href = '/firmwareUpdated';
} catch (error: any) {
if (new Date().getTime() < timeoutAt.current) {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
} else {
setFailed(true);
}
}
});
useEffect(() => {
poll.current();
}, []);
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
return (
<FormLoader
message="EMS-ESP is restarting, please wait&hellip;"
errorMessage={failed ? 'Timed out waiting for device to restart.' : undefined}
/>
);
};
export default FirmwareRestartMonitor;

View File

@@ -0,0 +1,97 @@
import { FC, useState } from 'react';
import { Button, Checkbox } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import * as SystemApi from '../../api/system';
import {
BlockFormControlLabel,
ButtonRow,
FormLoader,
SectionContent,
ValidatedPasswordField,
ValidatedTextField
} from '../../components';
import { OTASettings } from '../../types';
import { numberValue, updateValue, useRest } from '../../utils';
import { ValidateFieldsError } from 'async-validator';
import { validate } from '../../validators';
import { OTA_SETTINGS_VALIDATOR } from '../../validators/system';
const OTASettingsForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<OTASettings>({
read: SystemApi.readOTASettings,
update: SystemApi.updateOTASettings
});
const updateFormValue = updateValue(setData);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(OTA_SETTINGS_VALIDATOR, data);
saveData();
} catch (errors: any) {
setFieldErrors(errors);
}
};
return (
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
label="Enable OTA Updates"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Password"
fullWidth
variant="outlined"
value={data.password}
onChange={updateFormValue}
margin="normal"
/>
<ButtonRow>
<Button
startIcon={<SaveIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={validateAndSubmit}
>
Save
</Button>
</ButtonRow>
</>
);
};
return (
<SectionContent title="OTA Settings" titleGutter>
{content()}
</SectionContent>
);
};
export default OTASettingsForm;

View File

@@ -0,0 +1,60 @@
import React, { FC, useContext } from 'react';
import { Navigate, Routes, Route } from 'react-router-dom';
import { Tab } from '@mui/material';
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from '../../components';
import { AuthenticatedContext } from '../../contexts/authentication';
import { FeaturesContext } from '../../contexts/features';
import UploadFirmwareForm from './UploadFirmwareForm';
import SystemStatusForm from './SystemStatusForm';
import OTASettingsForm from './OTASettingsForm';
import SystemLog from './SystemLog';
const System: FC = () => {
useLayoutTitle('System');
const { me } = useContext(AuthenticatedContext);
const { features } = useContext(FeaturesContext);
const { routerTab } = useRouterTab();
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="System Status" />
<Tab value="log" label="System Log" />
{features.ota && <Tab value="ota" label="OTA Settings" disabled={!me.admin} />}
{features.upload_firmware && <Tab value="upload" label="Upload Firmware" disabled={!me.admin} />}
</RouterTabs>
<Routes>
<Route path="status" element={<SystemStatusForm />} />
<Route path="log" element={<SystemLog />} />
{features.ota && (
<Route
path="ota"
element={
<RequireAdmin>
<OTASettingsForm />
</RequireAdmin>
}
/>
)}
{features.upload_firmware && (
<Route
path="upload"
element={
<RequireAdmin>
<UploadFirmwareForm />
</RequireAdmin>
}
/>
)}
<Route path="/*" element={<Navigate replace to="status" />} />
</Routes>
</>
);
};
export default System;

View File

@@ -0,0 +1,282 @@
import { FC, useState, useEffect, useCallback, useLayoutEffect } from 'react';
import { Box, styled, Button, Checkbox, MenuItem, Grid, Slider, FormLabel } from '@mui/material';
import * as SystemApi from '../../api/system';
import { addAccessTokenParameter } from '../../api/authentication';
import { SectionContent, FormLoader, BlockFormControlLabel, ValidatedTextField } from '../../components';
import { LogSettings, LogEntry, LogEntries, LogLevel } from '../../types';
import { updateValue, useRest, extractErrorMessage } from '../../utils';
import DownloadIcon from '@mui/icons-material/GetApp';
import { useSnackbar } from 'notistack';
import { EVENT_SOURCE_ROOT } from '../../api/endpoints';
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
const useWindowSize = () => {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
};
const LogEntryLine = styled('div')(({ theme }) => ({
color: '#bbbbbb',
fontFamily: 'monospace',
fontSize: '14px',
letterSpacing: 'normal',
whiteSpace: 'nowrap'
}));
const topOffset = () => document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
const leftOffset = () => document.getElementById('log-window')?.getBoundingClientRect().left || 0;
const levelLabel = (level: LogLevel) => {
switch (level) {
case LogLevel.ERROR:
return 'ERROR';
case LogLevel.WARNING:
return 'WARNING';
case LogLevel.NOTICE:
return 'NOTICE';
case LogLevel.INFO:
return 'INFO';
case LogLevel.DEBUG:
return 'DEBUG';
case LogLevel.TRACE:
return 'TRACE';
default:
return '';
}
};
const SystemLog: FC = () => {
useWindowSize();
const { loadData, data, setData } = useRest<LogSettings>({
read: SystemApi.readLogSettings
});
const [errorMessage, setErrorMessage] = useState<string>();
const [reconnectTimeout, setReconnectTimeout] = useState<NodeJS.Timeout>();
const [logEntries, setLogEntries] = useState<LogEntries>({ events: [] });
const [lastIndex, setLastIndex] = useState<number>(0);
const paddedLevelLabel = (level: LogLevel) => {
const label = levelLabel(level);
return data?.compact ? ' ' + label[0] : label.padStart(8, '\xa0');
};
const paddedNameLabel = (name: string) => {
const label = '[' + name + ']';
return data?.compact ? label : label.padEnd(12, '\xa0');
};
const paddedIDLabel = (id: number) => {
const label = id + ':';
return data?.compact ? label : label.padEnd(7, '\xa0');
};
const updateFormValue = updateValue(setData);
const { enqueueSnackbar } = useSnackbar();
const reloadPage = () => {
window.location.reload();
};
const sendSettings = async (new_max_messages: number, new_level: number) => {
if (data) {
try {
const response = await SystemApi.updateLogSettings({
level: new_level,
max_messages: new_max_messages,
compact: data.compact
});
if (response.status !== 200) {
enqueueSnackbar('Problem applying log settings', { variant: 'error' });
}
} catch (error: any) {
enqueueSnackbar(extractErrorMessage(error, 'Problem applying log settings'), { variant: 'error' });
}
}
};
const changeLevel = (event: React.ChangeEvent<HTMLInputElement>) => {
if (data) {
setData({
...data,
level: parseInt(event.target.value)
});
sendSettings(data.max_messages, parseInt(event.target.value));
}
};
const changeMaxMessages = (event: Event, value: number | number[]) => {
if (data) {
setData({
...data,
max_messages: value as number
});
}
};
const onDownload = () => {
let result = '';
for (let i of logEntries.events) {
result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
}
const a = document.createElement('a');
a.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(result));
a.setAttribute('download', 'log.txt');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const onMessage = (event: MessageEvent) => {
const rawData = event.data;
if (typeof rawData === 'string' || rawData instanceof String) {
const logentry = JSON.parse(rawData as string) as LogEntry;
if (logentry.i > lastIndex) {
setLastIndex(logentry.i);
setLogEntries((old) => ({ events: [...old.events, logentry] }));
}
}
};
const fetchLog = useCallback(async () => {
try {
setLogEntries((await SystemApi.readLogEntries()).data);
} catch (error: any) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch log'));
}
}, []);
useEffect(() => {
fetchLog();
}, [fetchLog]);
useEffect(() => {
const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL));
es.onmessage = onMessage;
es.onerror = () => {
if (reconnectTimeout) {
es.close();
setReconnectTimeout(setTimeout(reloadPage, 1000));
}
};
return () => {
es.close();
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
}
};
// eslint-disable-next-line
}, [reconnectTimeout]);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
return (
<>
<Grid container spacing={3} direction="row" justifyContent="flex-start" alignItems="center">
<Grid item xs={4}>
<ValidatedTextField
name="level"
label="Log Level"
value={data.level}
fullWidth
variant="outlined"
onChange={changeLevel}
margin="normal"
select
>
<MenuItem value={3}>ERROR</MenuItem>
<MenuItem value={4}>WARNING</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={9}>ALL</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={3}>
<FormLabel>Buffer size</FormLabel>
<Slider
value={data.max_messages}
valueLabelDisplay="auto"
name="max_messages"
marks={[
{ value: 25, label: '25' },
{ value: 50, label: '50' },
{ value: 75, label: '75' },
{ value: 100, label: '100' }
]}
step={25}
min={25}
max={100}
onChange={changeMaxMessages}
onChangeCommitted={() => sendSettings(data.max_messages, data.level)}
/>
</Grid>
<Grid item>
<BlockFormControlLabel
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />}
label="Compact"
/>
</Grid>
<Grid item>
<Button startIcon={<DownloadIcon />} variant="outlined" color="secondary" onClick={onDownload}>
Export
</Button>
</Grid>
</Grid>
<Box
sx={{
backgroundColor: 'black',
overflow: 'scroll',
position: 'absolute',
right: 18,
bottom: 18,
left: () => leftOffset(),
top: () => topOffset(),
p: 1
}}
>
{logEntries &&
logEntries.events.map((e) => (
<LogEntryLine key={e.i}>
<span>{e.t}</span>
{data.compact && <span>{paddedLevelLabel(e.l)} </span>}
{!data.compact && <span>{paddedLevelLabel(e.l)}&nbsp;</span>}
<span>{paddedIDLabel(e.i)} </span>
<span>{paddedNameLabel(e.n)} </span>
<span>{e.m}</span>
</LogEntryLine>
))}
</Box>
</>
);
};
return (
<SectionContent title="System Log" titleGutter id="log-window">
{content()}
</SectionContent>
);
};
export default SystemLog;

View File

@@ -0,0 +1,379 @@
import { FC, useContext, useState, useEffect } from 'react';
import { useSnackbar } from 'notistack';
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
Link,
Typography
} from '@mui/material';
import DevicesIcon from '@mui/icons-material/Devices';
import ShowChartIcon from '@mui/icons-material/ShowChart';
import MemoryIcon from '@mui/icons-material/Memory';
import AppsIcon from '@mui/icons-material/Apps';
import SdStorageIcon from '@mui/icons-material/SdStorage';
import FolderIcon from '@mui/icons-material/Folder';
import RefreshIcon from '@mui/icons-material/Refresh';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import BuildIcon from '@mui/icons-material/Build';
import TimerIcon from '@mui/icons-material/Timer';
import CancelIcon from '@mui/icons-material/Cancel';
import { ButtonRow, FormLoader, SectionContent, MessageBox } from '../../components';
import { EspPlatform, SystemStatus, Version } from '../../types';
import * as SystemApi from '../../api/system';
import { extractErrorMessage, useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import axios from 'axios';
export const VERSIONCHECK_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/latest';
export const VERSIONCHECK_DEV_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/latest';
export const uploadURL = window.location.origin + '/system/upload';
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
const SystemStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<SystemStatus>({ read: SystemApi.readSystemStatus });
const { me } = useContext(AuthenticatedContext);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar();
const [showingVersion, setShowingVersion] = useState<boolean>(false);
const [latestVersion, setLatestVersion] = useState<Version>();
const [latestDevVersion, setLatestDevVersion] = useState<Version>();
useEffect(() => {
axios.get(VERSIONCHECK_ENDPOINT).then((response) => {
setLatestVersion({
version: response.data.name,
url: response.data.assets[1].browser_download_url,
changelog: response.data.html_url
});
});
axios.get(VERSIONCHECK_DEV_ENDPOINT).then((response) => {
setLatestDevVersion({
version: response.data.name.split(/\s+/).splice(-1),
url: response.data.assets[1].browser_download_url,
changelog: response.data.assets[0].browser_download_url
});
});
}, []);
const restart = async () => {
setProcessing(true);
try {
await SystemApi.restart();
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
} catch (error: any) {
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
} finally {
setConfirmRestart(false);
setProcessing(false);
}
};
const renderRestartDialog = () => (
<Dialog open={confirmRestart} onClose={() => setConfirmRestart(false)}>
<DialogTitle>Restart</DialogTitle>
<DialogContent dividers>Are you sure you want to restart EMS-ESP?</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmRestart(false)}
color="secondary"
>
Cancel
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={restart}
disabled={processing}
color="primary"
autoFocus
>
Restart
</Button>
</DialogActions>
</Dialog>
);
const renderVersionDialog = () => {
return (
<Dialog open={showingVersion} onClose={() => setShowingVersion(false)}>
<DialogTitle>Version Check</DialogTitle>
<DialogContent dividers>
<MessageBox
my={0}
level="info"
message={'You are currently running EMS-ESP version ' + data?.emsesp_version}
/>
{latestVersion && (
<Box mt={2} mb={2}>
The latest <u>official</u> version is <b>{latestVersion.version}</b>&nbsp;(
<Link target="_blank" href={latestVersion.changelog} color="primary">
{'release notes'}
</Link>
)&nbsp;(
<Link target="_blank" href={latestVersion.url} color="primary">
{'download'}
</Link>
)
</Box>
)}
{latestDevVersion && (
<Box mt={2} mb={2}>
The latest <u>development</u> version is&nbsp;<b>{latestDevVersion.version}</b>
&nbsp;(
<Link target="_blank" href={latestDevVersion.changelog} color="primary">
{'release notes'}
</Link>
)&nbsp;(
<Link target="_blank" href={latestDevVersion.url} color="primary">
{'download'}
</Link>
)
</Box>
)}
<Box color="warning.main" p={0} pl={0} pr={0} mt={4} mb={0}>
<Typography variant="body2">
Use&nbsp;
<Link target="_blank" href={uploadURL} color="primary">
{'UPLOAD FIRMWARE'}
</Link>
&nbsp;to apply the new firmware
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setShowingVersion(false)} color="secondary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
const factoryReset = async () => {
setProcessing(true);
try {
await SystemApi.factoryReset();
enqueueSnackbar('Device has been factory reset and will now restart', { variant: 'info' });
} catch (error: any) {
enqueueSnackbar(extractErrorMessage(error, 'Problem factory resetting the device'), { variant: 'error' });
} finally {
setConfirmFactoryReset(false);
setProcessing(false);
}
};
const renderFactoryResetDialog = () => (
<Dialog open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
<DialogTitle>Factory Reset</DialogTitle>
<DialogContent dividers>Are you sure you want to reset the device to its factory defaults?</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmFactoryReset(false)}
color="secondary"
>
Cancel
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={factoryReset}
disabled={processing}
autoFocus
color="error"
>
Factory Reset
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar>
<BuildIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="EMS-ESP Version" secondary={'v' + data.emsesp_version} />
{latestVersion && (
<Button color="primary" onClick={() => setShowingVersion(true)}>
Version Check
</Button>
)}
</ListItem>
<Divider variant="inset" component="li" />
<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>
<TimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="System Uptime" secondary={data.uptime} />
</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 && (
<>
<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>
</>
)}
<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" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
</Button>
</ButtonRow>
</Box>
{me.admin && (
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
color="primary"
onClick={() => setConfirmRestart(true)}
>
Restart
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={() => setConfirmFactoryReset(true)}
color="error"
>
Factory reset
</Button>
</ButtonRow>
</Box>
)}
</Box>
{renderVersionDialog()}
{renderRestartDialog()}
{renderFactoryResetDialog()}
</>
);
};
return (
<SectionContent title="System Status" titleGutter>
{content()}
</SectionContent>
);
};
export default SystemStatusForm;

View File

@@ -0,0 +1,26 @@
import { FC, useRef, useState } from 'react';
import * as SystemApi from '../../api/system';
import { SectionContent } from '../../components';
import { FileUploadConfig } from '../../api/endpoints';
import FirmwareFileUpload from './FirmwareFileUpload';
import FirmwareRestartMonitor from './FirmwareRestartMonitor';
const UploadFirmwareForm: FC = () => {
const [restarting, setRestarting] = useState<boolean>();
const uploadFirmware = useRef(async (file: File, config?: FileUploadConfig) => {
const response = await SystemApi.uploadFirmware(file, config);
setRestarting(true);
return response;
});
return (
<SectionContent title="Upload Firmware" titleGutter>
{restarting ? <FirmwareRestartMonitor /> : <FirmwareFileUpload uploadFirmware={uploadFirmware.current} />}
</SectionContent>
);
};
export default UploadFirmwareForm;