refactored restart and format services to be non-blocking

This commit is contained in:
proddy
2024-08-31 16:12:30 +02:00
parent 382c46622d
commit 931827c526
19 changed files with 243 additions and 202 deletions

View File

@@ -26,8 +26,6 @@ import {
} from '@mui/material';
import Grid from '@mui/material/Grid2';
import { restart } from 'api/system';
import {
Body,
Cell,
@@ -51,6 +49,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react';
import {
API,
readDeviceEntities,
readDevices,
resetCustomizations,
@@ -61,7 +60,7 @@ import SettingsCustomizationsDialog from './CustomizationsDialog';
import EntityMaskToggle from './EntityMaskToggle';
import OptionIcon from './OptionIcon';
import { DeviceEntityMask } from './types';
import type { DeviceEntity, DeviceShort } from './types';
import type { APIcall, DeviceEntity, DeviceShort } from './types';
export const APIURL = window.location.origin + '/api/';
@@ -85,6 +84,10 @@ const Customizations = () => {
// fetch devices first
const { data: devices, send: fetchDevices } = useRequest(readDevices);
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const [selectedDevice, setSelectedDevice] = useState<number>(
Number(useLocation().state) || -1
);
@@ -132,9 +135,14 @@ const Customizations = () => {
);
};
const { send: sendRestart } = useRequest(restart(), {
immediate: false
});
const doRestart = async () => {
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const entities_theme = useTheme({
Table: `
@@ -247,13 +255,6 @@ const Customizations = () => {
}
}, [devices, selectedDevice]);
const doRestart = async () => {
await sendRestart().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
function formatValue(value: unknown) {
if (typeof value === 'number') {
return new Intl.NumberFormat().format(value);
@@ -509,7 +510,7 @@ const Customizations = () => {
container
mb={1}
mt={0}
spacing={1}
spacing={2}
direction="row"
justifyContent="flex-start"
alignItems="center"

View File

@@ -28,7 +28,7 @@ const Help = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.HELP());
const { send: getAPI } = useRequest((data: APIcall) => API(data), {
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
}).onSuccess((event) => {
const anchor = document.createElement('a');
@@ -45,8 +45,8 @@ const Help = () => {
toast.info(LL.DOWNLOAD_SUCCESSFUL());
});
const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
const callAPI = async (device: string, cmd: string) => {
await sendAPI({ device, cmd, id: 0 }).catch((error: Error) => {
toast.error(error.message);
});
};
@@ -113,7 +113,7 @@ const Help = () => {
color="primary"
onClick={() => callAPI('system', 'allvalues')}
>
{LL.ALLVALUES(0)}
{LL.ALLVALUES()}
</Button>
<Box border={1} p={1} mt={4}>

View File

@@ -272,8 +272,8 @@ export interface BoardProfile {
export interface APIcall {
device: string;
entity: string;
id: unknown;
cmd: string;
id: number;
}
export interface WriteAnalogSensor {
id: number;

View File

@@ -16,7 +16,7 @@ import {
} from '@mui/material';
import Grid from '@mui/material/Grid2';
import { readHardwareStatus, restart } from 'api/system';
import { readHardwareStatus } from 'api/system';
import { useRequest } from 'alova/client';
import RestartMonitor from 'app/status/RestartMonitor';
@@ -35,9 +35,9 @@ import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { getBoardProfile, readSettings, writeSettings } from '../../api/app';
import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
import { BOARD_PROFILES } from '../main/types';
import type { Settings } from '../main/types';
import type { APIcall, Settings } from '../main/types';
import { createSettingsValidator } from '../main/validators';
export function boardProfileSelectItems() {
@@ -80,6 +80,10 @@ const ApplicationSettings = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const { loading: processingBoard, send: readBoardProfile } = useRequest(
(boardProfile: string) => getBoardProfile(boardProfile),
{
@@ -102,9 +106,14 @@ const ApplicationSettings = () => {
});
});
const { send: restartCommand } = useRequest(restart(), {
immediate: false
});
const doRestart = async () => {
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error: Error) => {
@@ -158,10 +167,7 @@ const ApplicationSettings = () => {
const restart = async () => {
await validateAndSubmit();
await restartCommand().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
await doRestart();
};
return (
@@ -204,7 +210,7 @@ const ApplicationSettings = () => {
label={LL.ENABLE_MODBUS()}
/>
{data.modbus_enabled && (
<Grid container spacing={1} rowSpacing={0}>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -258,7 +264,7 @@ const ApplicationSettings = () => {
label={LL.ENABLE_SYSLOG()}
/>
{data.syslog_enabled && (
<Grid container spacing={1} rowSpacing={0}>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -351,7 +357,7 @@ const ApplicationSettings = () => {
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()}
</Typography>
<Grid container spacing={1}>
<Grid container spacing={2}>
<Grid size={3}>
<TextField
name="locale"
@@ -469,7 +475,7 @@ const ApplicationSettings = () => {
</TextField>
{data.board_profile === 'CUSTOM' && (
<>
<Grid container spacing={1} rowSpacing={0}>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -555,7 +561,7 @@ const ApplicationSettings = () => {
</Grid>
</Grid>
{data.phy_type !== 0 && (
<Grid container spacing={1} rowSpacing={0}>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<TextField
name="eth_power"
@@ -601,7 +607,7 @@ const ApplicationSettings = () => {
)}
</>
)}
<Grid container spacing={1} rowSpacing={0}>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<TextField
name="tx_mode"
@@ -717,7 +723,7 @@ const ApplicationSettings = () => {
/>
</Box>
)}
<Grid container spacing={1} rowSpacing={0}>
<Grid container spacing={2} rowSpacing={0}>
<BlockFormControlLabel
control={
<Checkbox
@@ -740,7 +746,7 @@ const ApplicationSettings = () => {
disabled={!data.shower_timer}
/>
</Grid>
<Grid container spacing={1} rowSpacing={2} sx={{ pt: 2 }}>
<Grid container spacing={2} sx={{ pt: 2 }}>
{data.shower_timer && (
<Grid>
<ValidatedTextField

View File

@@ -28,7 +28,6 @@ import {
checkUpgrade,
getDevVersion,
getStableVersion,
restart,
uploadURL
} from 'api/system';
@@ -76,7 +75,11 @@ const DownloadUpload = () => {
saveFile(event.data, 'schedule.json');
});
const { send: getAPI } = useRequest((data: APIcall) => API(data), {
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const { send: sendAPIandSave } = useRequest((data: APIcall) => API(data), {
immediate: false
}).onSuccess((event) => {
saveFile(
@@ -98,15 +101,13 @@ const DownloadUpload = () => {
}
);
const { send: restartCommand } = useRequest(restart(), {
immediate: false
});
const callRestart = async () => {
const doRestart = async () => {
setRestarting(true);
await restartCommand().catch((error: Error) => {
toast.error(error.message);
});
await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const { send: sendCheckUpgrade } = useRequest(checkUpgrade, {
@@ -200,8 +201,8 @@ const DownloadUpload = () => {
});
};
const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
const callAPIandSave = async (device: string, cmd: string) => {
await sendAPIandSave({ device, cmd, id: 0 }).catch((error: Error) => {
toast.error(error.message);
});
};
@@ -291,7 +292,7 @@ const DownloadUpload = () => {
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'info')}
onClick={() => callAPIandSave('system', 'info')}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
@@ -300,7 +301,7 @@ const DownloadUpload = () => {
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'allvalues')}
onClick={() => callAPIandSave('system', 'allvalues')}
>
{LL.ALLVALUES()}
</Button>
@@ -420,15 +421,13 @@ const DownloadUpload = () => {
<Typography variant="body2">{LL.UPLOAD_TEXT()}</Typography>
</Box>
<SingleUpload callRestart={callRestart} />
<SingleUpload doRestart={doRestart} />
</>
);
};
return (
<SectionContent>
{restarting ? <RestartMonitor message={LL.WAIT_FIRMWARE()} /> : content()}
</SectionContent>
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
};

View File

@@ -21,9 +21,11 @@ import {
} from '@mui/material';
import * as SystemApi from 'api/system';
import { API } from 'api/app';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { useI18nContext } from 'i18n/i18n-react';
@@ -34,13 +36,14 @@ const Settings = () => {
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const factoryReset = async () => {
await factoryResetCommand();
setConfirmFactoryReset(false);
const doFormat = async () => {
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
setConfirmFactoryReset(false);
});
};
const renderFactoryResetDialog = () => (
@@ -63,7 +66,7 @@ const Settings = () => {
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={factoryReset}
onClick={doFormat}
color="error"
>
{LL.FACTORY_RESET()}

View File

@@ -22,9 +22,10 @@ import {
} from '@mui/material';
import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system';
import { API } from 'api/app';
import { updateState, useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
@@ -71,7 +72,7 @@ const NetworkSettings = () => {
update: NetworkApi.updateNetworkSettings
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
@@ -131,11 +132,13 @@ const NetworkSettings = () => {
await loadData();
};
const restart = async () => {
await restartCommand().catch((error: Error) => {
toast.error(error.message);
});
const doRestart = async () => {
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
return (
@@ -358,7 +361,7 @@ const NetworkSettings = () => {
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
onClick={doRestart}
>
{LL.RESTART()}
</Button>

View File

@@ -1,52 +1,80 @@
import { type FC, useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import {
Box,
CircularProgress,
Dialog,
DialogContent,
Typography
} from '@mui/material';
import { readHardwareStatus } from 'api/system';
import { useRequest } from 'alova/client';
import { FormLoader } from 'components';
import { dialogStyle } from 'CustomTheme';
import { useAutoRequest } from 'alova/client';
import MessageBox from 'components/MessageBox';
import { useI18nContext } from 'i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000; // 2 minutes
const POLL_INTERVAL = 2000; // every 2 seconds
export interface RestartMonitorProps {
message?: string;
}
const RestartMonitor: FC<RestartMonitorProps> = ({ message }) => {
const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const RestartMonitor = () => {
const [errorMessage, setErrorMessage] = useState<string>();
const { LL } = useI18nContext();
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
let count = 0;
const { send } = useRequest(readHardwareStatus, { immediate: false });
const poll = useRef(async () => {
try {
await send();
document.location.href = '/';
} catch {
if (new Date().getTime() < timeoutAt.current) {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
} else {
setFailed(true);
const { data } = useAutoRequest(readHardwareStatus, {
pollingTime: 1000,
force: true,
initialData: { status: 'Getting ready...' },
async middleware(_, next) {
if (count++ >= 1) {
// skip first request (1 seconds) to allow AsyncWS to send its response
await next();
}
}
});
useEffect(() => {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
}, []);
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
})
.onSuccess((event) => {
console.log(event.data.status); // TODO remove
if (event.data.status === 'ready' || event.data.status === undefined) {
document.location.href = '/';
}
})
.onError((error, _method) => {
setErrorMessage(error.message);
});
return (
<FormLoader
message={message ? message : LL.APPLICATION_RESTARTING() + '...'}
errorMessage={failed ? 'Timed out' : undefined}
/>
<Dialog fullWidth={true} sx={dialogStyle} open={true}>
<DialogContent dividers>
<Box m={2} py={2} display="flex" alignItems="center" flexDirection="column">
<Typography
color="secondary"
variant="h6"
fontWeight={400}
textAlign="center"
>
{data?.status === 'uploading'
? LL.WAIT_FIRMWARE()
: data?.status === 'restarting'
? LL.APPLICATION_RESTARTING()
: data?.status === 'ready'
? 'Reloading'
: 'Preparing'}
</Typography>
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center">
{LL.PLEASE_WAIT()}&hellip;
</Typography>
{errorMessage ? (
<MessageBox my={2} level="error" message={errorMessage} />
) : (
<Box py={2}>
<CircularProgress size={48} />
</Box>
)}
</Box>
</DialogContent>
</Dialog>
);
};

View File

@@ -30,10 +30,11 @@ import {
} from '@mui/material';
import * as SystemApi from 'api/system';
import { API } from 'api/app';
import { dialogStyle } from 'CustomTheme';
import { useAutoRequest, useRequest } from 'alova/client';
import { busConnectionStatus } from 'app/main/types';
import { type APIcall, busConnectionStatus } from 'app/main/types';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
@@ -54,7 +55,7 @@ const SystemStatus = () => {
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [restarting, setRestarting] = useState<boolean>();
const { send: restartCommand } = useRequest(SystemApi.restart(), {
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
@@ -64,7 +65,12 @@ const SystemStatus = () => {
error
} = useAutoRequest(SystemApi.readSystemStatus, {
initialData: [],
pollingTime: 5000
pollingTime: 5000,
async middleware(_, next) {
if (!restarting) {
await next();
}
}
});
const theme = useTheme();
@@ -195,17 +201,14 @@ const SystemStatus = () => {
const activeHighlight = (value: boolean) =>
value ? theme.palette.success.main : theme.palette.info.main;
const restart = async () => {
await restartCommand()
.then(() => {
setRestarting(true);
})
.catch((error: Error) => {
const doRestart = async () => {
setConfirmRestart(false);
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: -1 }).catch(
(error: Error) => {
toast.error(error.message);
})
.finally(() => {
setConfirmRestart(false);
});
}
);
};
const renderRestartDialog = () => (
@@ -228,7 +231,7 @@ const SystemStatus = () => {
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={restart}
onClick={doRestart}
color="error"
>
{LL.RESTART()}