diff --git a/interface/src/api/system.ts b/interface/src/api/system.ts index a9749e7e9..0b550c307 100644 --- a/interface/src/api/system.ts +++ b/interface/src/api/system.ts @@ -10,12 +10,6 @@ export const readHardwareStatus = () => export const readSystemStatus = () => alovaInstance.Get('/rest/systemStatus'); -// commands -export const restart = () => alovaInstance.Post('/rest/restart'); -export const partition = () => alovaInstance.Post('/rest/partition'); -export const factoryPartition = () => alovaInstance.Post('/rest/factoryPartition'); -export const factoryReset = () => alovaInstance.Post('/rest/factoryReset'); - // SystemLog export const readLogSettings = () => alovaInstance.Get(`/rest/logSettings`); diff --git a/interface/src/app/main/Customizations.tsx b/interface/src/app/main/Customizations.tsx index 4f74dd0a9..efebe049a 100644 --- a/interface/src/app/main/Customizations.tsx +++ b/interface/src/app/main/Customizations.tsx @@ -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(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" diff --git a/interface/src/app/main/Help.tsx b/interface/src/app/main/Help.tsx index fb9c1eaed..369619319 100644 --- a/interface/src/app/main/Help.tsx +++ b/interface/src/app/main/Help.tsx @@ -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()} diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index e386951c1..093b55a9d 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -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; diff --git a/interface/src/app/settings/ApplicationSettings.tsx b/interface/src/app/settings/ApplicationSettings.tsx index 16ac0e31b..9ddc4dd50 100644 --- a/interface/src/app/settings/ApplicationSettings.tsx +++ b/interface/src/app/settings/ApplicationSettings.tsx @@ -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(); + 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 && ( - + { label={LL.ENABLE_SYSLOG()} /> {data.syslog_enabled && ( - + { {LL.FORMATTING_OPTIONS()} - + { {data.board_profile === 'CUSTOM' && ( <> - + { {data.phy_type !== 0 && ( - + { )} )} - + { /> )} - + { disabled={!data.shower_timer} /> - + {data.shower_timer && ( { 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={} variant="outlined" color="primary" - onClick={() => callAPI('system', 'info')} + onClick={() => callAPIandSave('system', 'info')} > {LL.SUPPORT_INFORMATION(0)} @@ -300,7 +301,7 @@ const DownloadUpload = () => { startIcon={} variant="outlined" color="primary" - onClick={() => callAPI('system', 'allvalues')} + onClick={() => callAPIandSave('system', 'allvalues')} > {LL.ALLVALUES()} @@ -420,15 +421,13 @@ const DownloadUpload = () => { {LL.UPLOAD_TEXT()} - + ); }; return ( - - {restarting ? : content()} - + {restarting ? : content()} ); }; diff --git a/interface/src/app/settings/Settings.tsx b/interface/src/app/settings/Settings.tsx index 06790ada4..f7ef83077 100644 --- a/interface/src/app/settings/Settings.tsx +++ b/interface/src/app/settings/Settings.tsx @@ -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(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 = () => { diff --git a/interface/src/app/status/RestartMonitor.tsx b/interface/src/app/status/RestartMonitor.tsx index 4b1f32daf..e652fe757 100644 --- a/interface/src/app/status/RestartMonitor.tsx +++ b/interface/src/app/status/RestartMonitor.tsx @@ -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 = ({ message }) => { - const [failed, setFailed] = useState(false); - const [timeoutId, setTimeoutId] = useState(); +const RestartMonitor = () => { + const [errorMessage, setErrorMessage] = useState(); 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 ( - + + + + + {data?.status === 'uploading' + ? LL.WAIT_FIRMWARE() + : data?.status === 'restarting' + ? LL.APPLICATION_RESTARTING() + : data?.status === 'ready' + ? 'Reloading' + : 'Preparing'} + + + {LL.PLEASE_WAIT()}… + + + {errorMessage ? ( + + ) : ( + + + + )} + + + ); }; diff --git a/interface/src/app/status/Status.tsx b/interface/src/app/status/Status.tsx index 95b880fa0..e14ee3948 100644 --- a/interface/src/app/status/Status.tsx +++ b/interface/src/app/status/Status.tsx @@ -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(false); const [restarting, setRestarting] = useState(); - 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 = () => {