diff --git a/interface/src/AuthenticatedRouting.tsx b/interface/src/AuthenticatedRouting.tsx index 2c99ce3c3..b0fe722ff 100644 --- a/interface/src/AuthenticatedRouting.tsx +++ b/interface/src/AuthenticatedRouting.tsx @@ -21,22 +21,23 @@ const AuthenticatedRouting: FC = () => { const location = useLocation(); const navigate = useNavigate(); - const handleApiResponseError = useCallback( - (error: AxiosError) => { - if (error.response && error.response.status === 401) { - AuthenticationApi.storeLoginRedirect(location); - navigate('/unauthorized'); - } - return Promise.reject(error); - }, - [location, navigate] - ); + // TODO fix this - how to redirect on a 401 + // const handleApiResponseError = useCallback( + // (error: AxiosError) => { + // if (error.response && error.response.status === 401) { + // AuthenticationApi.storeLoginRedirect(location); + // navigate('/unauthorized'); + // } + // return Promise.reject(error); + // }, + // [location, navigate] + // ); - useEffect(() => { - // TODO replace AXIOS.interceptors.response.use ??? - const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError); - return () => AXIOS.interceptors.response.eject(axiosHandlerId); - }, [handleApiResponseError]); + // useEffect(() => { + // // TODO replace AXIOS.interceptors.response.use ??? + // const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError); + // return () => AXIOS.interceptors.response.eject(axiosHandlerId); + // }, [handleApiResponseError]); return ( diff --git a/interface/src/SignIn.tsx b/interface/src/SignIn.tsx index bcd618fd2..138427b55 100644 --- a/interface/src/SignIn.tsx +++ b/interface/src/SignIn.tsx @@ -1,5 +1,6 @@ import ForwardIcon from '@mui/icons-material/Forward'; import { Box, Fab, Paper, Typography, Button } from '@mui/material'; +import { useRequest } from 'alova'; import { useContext, useState } from 'react'; import { toast } from 'react-toastify'; import type { ValidateFieldsError } from 'async-validator'; @@ -23,7 +24,7 @@ import { ReactComponent as SVflag } from 'i18n/SV.svg'; import { ReactComponent as TRflag } from 'i18n/TR.svg'; import { I18nContext } from 'i18n/i18n-react'; import { loadLocaleAsync } from 'i18n/i18n-util.async'; -import { extractErrorMessage, onEnterCallback, updateValue } from 'utils'; +import { onEnterCallback, updateValue } from 'utils'; import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators'; const SignIn: FC = () => { @@ -38,23 +39,27 @@ const SignIn: FC = () => { const [processing, setProcessing] = useState(false); const [fieldErrors, setFieldErrors] = useState(); + const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), { + immediate: false + }); + + onSuccess((response) => { + if (response.data) { + authenticationContext.signIn(response.data.access_token); + } + }); + const updateLoginRequestValue = updateValue(setSignInRequest); const signIn = async () => { - try { - // TODO move to Alova - const { data: loginResponse } = await AuthenticationApi.signIn(signInRequest); - authenticationContext.signIn(loginResponse.access_token); - } catch (error) { - if (error.response) { - if (error.response?.status === 401) { - toast.warn(LL.INVALID_LOGIN()); - } + await callSignIn(signInRequest).catch((event) => { + if (event.message === 'Unauthorized') { + toast.warn(LL.INVALID_LOGIN()); } else { - toast.error(extractErrorMessage(error, LL.ERROR())); + toast.error(LL.ERROR() + ' ' + event.message); } setProcessing(false); - } + }); }; const validateAndSignIn = async () => { diff --git a/interface/src/api/authentication.ts b/interface/src/api/authentication.ts index 805596a3c..b4f3a9bd3 100644 --- a/interface/src/api/authentication.ts +++ b/interface/src/api/authentication.ts @@ -1,6 +1,5 @@ import jwtDecode from 'jwt-decode'; -import { ACCESS_TOKEN, AXIOS } from './endpoints'; -import type { AxiosPromise } from 'axios'; +import { ACCESS_TOKEN, alovaInstance } from './endpoints'; import type * as H from 'history'; import type { Path } from 'react-router-dom'; @@ -9,14 +8,8 @@ import type { Me, SignInRequest, SignInResponse } from 'types'; export const SIGN_IN_PATHNAME = 'loginPathname'; export const SIGN_IN_SEARCH = 'loginSearch'; -// TODO move verifyAuthorization to Alova -export function verifyAuthorization(): AxiosPromise { - return AXIOS.get('/verifyAuthorization'); -} -// TODO move signIn to Alova -export function signIn(request: SignInRequest): AxiosPromise { - return AXIOS.post('/signIn', request); -} +export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization'); +export const signIn = (request: SignInRequest) => alovaInstance.Post('/rest/signIn', request); export function getStorage() { return localStorage || sessionStorage; diff --git a/interface/src/api/endpoints.ts b/interface/src/api/endpoints.ts index ddb1514af..10463d74e 100644 --- a/interface/src/api/endpoints.ts +++ b/interface/src/api/endpoints.ts @@ -4,10 +4,7 @@ import ReactHook from 'alova/react'; import axios from 'axios'; import { unpack } from '../api/unpack'; -// TODO axios can be removed import type { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios'; -export const REST_BASE_URL = '/rest/'; -export const API_BASE_URL = '/api/'; export const ACCESS_TOKEN = 'access_token'; @@ -52,14 +49,26 @@ export const alovaInstance = createAlova({ return data; }, - onError: (error) => { + // TODO handle errors + // Interceptor for request failure + // This interceptor will be entered when the request is wrong. + // The second parameter is the method instance of the current request, you can use it to synchronize the configuration information before and after the request + onError: (error, method) => { + console.log('error:', error); // TODO fix me + console.log('method:', method); // TODO fix me alert(error.message); } } }); +export const alovaInstanceGH = createAlova({ + baseURL: 'https://api.github.com/repos/emsesp/EMS-ESP32', + statesHook: ReactHook, + requestAdapter: xhrRequestAdapter() +}); + export const AXIOS = axios.create({ - baseURL: REST_BASE_URL, + baseURL: '/rest/', headers: { 'Content-Type': 'application/json' }, @@ -78,49 +87,8 @@ export const AXIOS = axios.create({ ] }); -export const AXIOS_API = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'Content-Type': 'application/json' - }, - transformRequest: [ - (data, headers) => { - if (headers) { - if (localStorage.getItem(ACCESS_TOKEN)) { - headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN); - } - if (headers['Content-Type'] !== 'application/json') { - return data; - } - } - return JSON.stringify(data); - } - ] -}); - -export const AXIOS_BIN = axios.create({ - baseURL: REST_BASE_URL, - headers: { - 'Content-Type': 'application/json' - }, - responseType: 'arraybuffer', - transformRequest: [ - (data, headers) => { - if (headers) { - if (localStorage.getItem(ACCESS_TOKEN)) { - headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN); - } - if (headers['Content-Type'] !== 'application/json') { - return data; - } - } - return JSON.stringify(data); - } - ], - transformResponse: [(data) => unpack(data)] -}); - -// TODO replace fileupload with alova, see https://alova.js.org/next-step/download-upload-progress +// TODO fileupload move to alova +// see https://alova.js.org/next-step/download-upload-progress export interface FileUploadConfig { cancelToken?: CancelToken; onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; diff --git a/interface/src/api/security.ts b/interface/src/api/security.ts index ff4cb4e64..6cbcce6d9 100644 --- a/interface/src/api/security.ts +++ b/interface/src/api/security.ts @@ -1,17 +1,13 @@ -import { AXIOS } from './endpoints'; -import type { AxiosPromise } from 'axios'; +import { alovaInstance } from './endpoints'; import type { SecuritySettings, Token } from 'types'; -// TODO move these 3 to Alova -export function readSecuritySettings(): AxiosPromise { - return AXIOS.get('/securitySettings'); -} +export const readSecuritySettings = () => alovaInstance.Get('/rest/securitySettings'); -export function updateSecuritySettings(securitySettings: SecuritySettings): AxiosPromise { - return AXIOS.post('/securitySettings', securitySettings); -} +export const updateSecuritySettings = (securitySettings: SecuritySettings) => + alovaInstance.Post('/rest/securitySettings', securitySettings); -export function generateToken(username?: string): AxiosPromise { - return AXIOS.get('/generateToken', { params: { username } }); -} +export const generateToken = (username?: string) => + alovaInstance.Get('/rest/generateToken', { + params: { username } + }); diff --git a/interface/src/api/system.ts b/interface/src/api/system.ts index 41c4bb639..5e21a1292 100644 --- a/interface/src/api/system.ts +++ b/interface/src/api/system.ts @@ -1,8 +1,8 @@ -import { alovaInstance, startUploadFile } from './endpoints'; +import { alovaInstance, alovaInstanceGH, startUploadFile } from './endpoints'; import type { FileUploadConfig } from './endpoints'; import type { AxiosPromise } from 'axios'; -import type { OTASettings, SystemStatus, LogSettings } from 'types'; +import type { OTASettings, SystemStatus, LogSettings, Version } from 'types'; // SystemStatus - also used to ping in Restart monitor export const readSystemStatus = () => alovaInstance.Get('/rest/systemStatus'); @@ -21,6 +21,28 @@ export const readLogSettings = () => alovaInstance.Get(`/rest/logSe export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data); export const fetchLog = () => alovaInstance.Post('/rest/fetchLog'); -// TODO fileupload move to Alova +// Get versions from github +export const getStableVersion = () => + alovaInstanceGH.Get('releases/latest', { + transformData(reponse: any) { + return { + version: reponse.data.name, + url: reponse.data.assets[1].browser_download_url, + changelog: reponse.data.assets[0].browser_download_url + }; + } + }); +export const getDevVersion = () => + alovaInstanceGH.Get('releases/tags/latest', { + transformData(reponse: any) { + return { + version: reponse.data.name.split(/\s+/).splice(-1), + url: reponse.data.assets[1].browser_download_url, + changelog: reponse.data.assets[0].browser_download_url + }; + } + }); + +// TODO fileupload move to alova export const uploadFile = (file: File, config?: FileUploadConfig): AxiosPromise => startUploadFile('/uploadFile', file, config); diff --git a/interface/src/components/upload/useFileUpload.ts b/interface/src/components/upload/useFileUpload.ts index b3a62a39b..dcd92ebc5 100644 --- a/interface/src/components/upload/useFileUpload.ts +++ b/interface/src/components/upload/useFileUpload.ts @@ -6,7 +6,6 @@ import type { FileUploadConfig } from 'api/endpoints'; import type { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios'; import { useI18nContext } from 'i18n/i18n-react'; -import { extractErrorMessage } from 'utils'; interface MediaUploadOptions { // TODO fileupload move to alova @@ -40,6 +39,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => { [uploadCancelToken] ); + // TODO fileupload move to alova const uploadFile = async (images: File[]) => { try { const cancelToken = axios.CancelToken.source(); @@ -61,7 +61,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => { toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED()); } else { resetUploadingStates(); - toast.error(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED(0))); + toast.error(LL.UPLOAD() + ' ' + LL.FAILED(0)); } } }; diff --git a/interface/src/contexts/authentication/Authentication.tsx b/interface/src/contexts/authentication/Authentication.tsx index 1ff343b19..f9982bffd 100644 --- a/interface/src/contexts/authentication/Authentication.tsx +++ b/interface/src/contexts/authentication/Authentication.tsx @@ -1,3 +1,4 @@ +import { useRequest } from 'alova'; import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; @@ -19,6 +20,10 @@ const Authentication: FC = ({ children }) => { const [initialized, setInitialized] = useState(false); const [me, setMe] = useState(); + const { send: verifyAuthorization } = useRequest(AuthenticationApi.verifyAuthorization(), { + immediate: false + }); + const signIn = (accessToken: string) => { try { AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken); @@ -42,18 +47,17 @@ const Authentication: FC = ({ children }) => { const refresh = useCallback(async () => { const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN); if (accessToken) { - try { - await AuthenticationApi.verifyAuthorization(); - setMe(AuthenticationApi.decodeMeJWT(accessToken)); - setInitialized(true); - } catch (error) { - setMe(undefined); - setInitialized(true); - } - } else { - setMe(undefined); - setInitialized(true); + await verifyAuthorization() + .then(() => { + setMe(AuthenticationApi.decodeMeJWT(accessToken)); + setInitialized(true); + }) + .catch(() => { + setMe(undefined); + setInitialized(true); + }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { diff --git a/interface/src/framework/ap/APSettingsForm.tsx b/interface/src/framework/ap/APSettingsForm.tsx index b6e7cacf9..ade6dab56 100644 --- a/interface/src/framework/ap/APSettingsForm.tsx +++ b/interface/src/framework/ap/APSettingsForm.tsx @@ -20,7 +20,7 @@ import { import { useI18nContext } from 'i18n/i18n-react'; import { APProvisionMode } from 'types'; -import { numberValue, updateValueDirty, useRest2 } from 'utils'; +import { numberValue, updateValueDirty, useRest } from 'utils'; import { createAPSettingsValidator, validate } from 'validators'; @@ -39,7 +39,7 @@ const APSettingsForm: FC = () => { blocker, saveData, errorMessage - } = useRest2({ + } = useRest({ read: APApi.readAPSettings, update: APApi.updateAPSettings }); diff --git a/interface/src/framework/mqtt/MqttSettingsForm.tsx b/interface/src/framework/mqtt/MqttSettingsForm.tsx index eba8d018c..d6b825add 100644 --- a/interface/src/framework/mqtt/MqttSettingsForm.tsx +++ b/interface/src/framework/mqtt/MqttSettingsForm.tsx @@ -17,7 +17,7 @@ import { BlockNavigation } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; -import { numberValue, updateValueDirty, useRest2 } from 'utils'; +import { numberValue, updateValueDirty, useRest } from 'utils'; import { createMqttSettingsValidator, validate } from 'validators'; @@ -33,7 +33,7 @@ const MqttSettingsForm: FC = () => { blocker, saveData, errorMessage - } = useRest2({ + } = useRest({ read: MqttApi.readMqttSettings, update: MqttApi.updateMqttSettings }); diff --git a/interface/src/framework/network/NetworkSettingsForm.tsx b/interface/src/framework/network/NetworkSettingsForm.tsx index 3ca414935..f4fefbb2a 100644 --- a/interface/src/framework/network/NetworkSettingsForm.tsx +++ b/interface/src/framework/network/NetworkSettingsForm.tsx @@ -43,7 +43,7 @@ import { } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; -import { numberValue, updateValueDirty, useRest2 } from 'utils'; +import { numberValue, updateValueDirty, useRest } from 'utils'; import { validate } from 'validators'; import { createNetworkSettingsValidator } from 'validators/network'; @@ -68,7 +68,7 @@ const WiFiSettingsForm: FC = () => { saveData, errorMessage, restartNeeded - } = useRest2({ + } = useRest({ read: NetworkApi.readNetworkSettings, update: NetworkApi.updateNetworkSettings }); diff --git a/interface/src/framework/ntp/NTPSettingsForm.tsx b/interface/src/framework/ntp/NTPSettingsForm.tsx index b1da11b97..02ba28ef2 100644 --- a/interface/src/framework/ntp/NTPSettingsForm.tsx +++ b/interface/src/framework/ntp/NTPSettingsForm.tsx @@ -19,7 +19,7 @@ import { BlockNavigation } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; -import { updateValueDirty, useRest2 } from 'utils'; +import { updateValueDirty, useRest } from 'utils'; import { validate } from 'validators'; import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; @@ -35,7 +35,7 @@ const NTPSettingsForm: FC = () => { blocker, saveData, errorMessage - } = useRest2({ + } = useRest({ read: NTPApi.readNTPSettings, update: NTPApi.updateNTPSettings }); diff --git a/interface/src/framework/security/GenerateToken.tsx b/interface/src/framework/security/GenerateToken.tsx index d3937b109..0a2e8a0ef 100644 --- a/interface/src/framework/security/GenerateToken.tsx +++ b/interface/src/framework/security/GenerateToken.tsx @@ -10,16 +10,14 @@ import { TextField, Button } from '@mui/material'; -import { useCallback, useState, useEffect } from 'react'; +import { useRequest } from 'alova'; +import { useEffect } from 'react'; -import { toast } from 'react-toastify'; import type { FC } from 'react'; -import type { Token } from 'types'; import * as SecurityApi from 'api/security'; import { MessageBox } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; -import { extractErrorMessage } from 'utils'; interface GenerateTokenProps { username?: string; @@ -27,24 +25,18 @@ interface GenerateTokenProps { } const GenerateToken: FC = ({ username, onClose }) => { - const [token, setToken] = useState(); + const { LL } = useI18nContext(); const open = !!username; - const { LL } = useI18nContext(); - - const getToken = useCallback(async () => { - try { - setToken((await SecurityApi.generateToken(username)).data); - } catch (error) { - toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING())); - } - }, [username, LL]); + const { data: token, send: generateToken } = useRequest(SecurityApi.generateToken(username), { + immediate: false + }); useEffect(() => { if (open) { - void getToken(); + void generateToken(); } - }, [open, getToken]); + }, [open, generateToken]); return ( diff --git a/interface/src/framework/security/ManageUsersForm.tsx b/interface/src/framework/security/ManageUsersForm.tsx index 1c0f6a856..e4c14c91f 100644 --- a/interface/src/framework/security/ManageUsersForm.tsx +++ b/interface/src/framework/security/ManageUsersForm.tsx @@ -23,8 +23,7 @@ import { useRest } from 'utils'; import { createUserValidator } from 'validators'; const ManageUsersForm: FC = () => { - // TODO move to Alova - const { loadData, saving, data, setData, saveData, errorMessage } = useRest({ + const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest({ read: SecurityApi.readSecuritySettings, update: SecurityApi.updateSecuritySettings }); @@ -85,7 +84,7 @@ const ManageUsersForm: FC = () => { const removeUser = (toRemove: User) => { const users = data.users.filter((u) => u.username !== toRemove.username); - setData({ ...data, users }); + updateDataValue({ ...data, users }); }; const createUser = () => { @@ -109,7 +108,7 @@ const ManageUsersForm: FC = () => { const doneEditingUser = () => { if (user) { const users = [...data.users.filter((u) => u.username !== user.username), user]; - setData({ ...data, users }); + updateDataValue({ ...data, users }); setUser(undefined); } }; diff --git a/interface/src/framework/security/SecuritySettingsForm.tsx b/interface/src/framework/security/SecuritySettingsForm.tsx index 67839522d..3af9474c5 100644 --- a/interface/src/framework/security/SecuritySettingsForm.tsx +++ b/interface/src/framework/security/SecuritySettingsForm.tsx @@ -18,16 +18,25 @@ const SecuritySettingsForm: FC = () => { const { LL } = useI18nContext(); const [fieldErrors, setFieldErrors] = useState(); - // TODO move to Alova - const { loadData, saving, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, saveData, errorMessage } = - useRest({ - read: SecurityApi.readSecuritySettings, - update: SecurityApi.updateSecuritySettings - }); + const { + loadData, + saving, + data, + updateDataValue, + origData, + dirtyFlags, + setDirtyFlags, + blocker, + saveData, + errorMessage + } = useRest({ + read: SecurityApi.readSecuritySettings, + update: SecurityApi.updateSecuritySettings + }); const authenticatedContext = useContext(AuthenticatedContext); - const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); + const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const content = () => { if (!data) { diff --git a/interface/src/framework/system/OTASettingsForm.tsx b/interface/src/framework/system/OTASettingsForm.tsx index e6cbaf6a5..77ccb0f88 100644 --- a/interface/src/framework/system/OTASettingsForm.tsx +++ b/interface/src/framework/system/OTASettingsForm.tsx @@ -18,7 +18,7 @@ import { } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; -import { numberValue, updateValueDirty, useRest2 } from 'utils'; +import { numberValue, updateValueDirty, useRest } from 'utils'; import { validate } from 'validators'; import { OTA_SETTINGS_VALIDATOR } from 'validators/system'; @@ -35,7 +35,7 @@ const OTASettingsForm: FC = () => { setDirtyFlags, blocker, errorMessage - } = useRest2({ + } = useRest({ read: SystemApi.readOTASettings, update: SystemApi.updateOTASettings }); diff --git a/interface/src/framework/system/SystemLog.tsx b/interface/src/framework/system/SystemLog.tsx index 4cff7ebea..6f400efe9 100644 --- a/interface/src/framework/system/SystemLog.tsx +++ b/interface/src/framework/system/SystemLog.tsx @@ -16,7 +16,7 @@ import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } fr import { useI18nContext } from 'i18n/i18n-react'; import { LogLevel } from 'types'; -import { updateValueDirty, useRest2 } from 'utils'; +import { updateValueDirty, useRest } from 'utils'; export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log'; @@ -52,7 +52,7 @@ const SystemLog: FC = () => { const { LL } = useI18nContext(); const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = - useRest2({ + useRest({ read: SystemApi.readLogSettings, update: SystemApi.updateLogSettings }); diff --git a/interface/src/framework/system/SystemStatusForm.tsx b/interface/src/framework/system/SystemStatusForm.tsx index c90202624..333262a2a 100644 --- a/interface/src/framework/system/SystemStatusForm.tsx +++ b/interface/src/framework/system/SystemStatusForm.tsx @@ -57,8 +57,6 @@ const SystemStatusForm: FC = () => { const [confirmFactoryReset, setConfirmFactoryReset] = useState(false); const [processing, setProcessing] = useState(false); const [showingVersion, setShowingVersion] = useState(false); - const [latestVersion, setLatestVersion] = useState(); - const [latestDevVersion, setLatestDevVersion] = useState(); const [restarting, setRestarting] = useState(); const { send: restartCommand } = useRequest(SystemApi.restart(), { @@ -73,24 +71,10 @@ const SystemStatusForm: FC = () => { immediate: false }); - const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true }); + const { data: latestVersion } = useRequest(SystemApi.getStableVersion); + const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion); - useEffect(() => { - void axios.get(VERSIONCHECK_ENDPOINT).then((response) => { - setLatestVersion({ - version: response.data.name, - url: response.data.assets[1].browser_download_url, - changelog: response.data.assets[0].browser_download_url - }); - }); - void 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 { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true }); const restart = async () => { setProcessing(true); diff --git a/interface/src/project/SettingsApplication.tsx b/interface/src/project/SettingsApplication.tsx index 9a27e997f..8dc037f06 100644 --- a/interface/src/project/SettingsApplication.tsx +++ b/interface/src/project/SettingsApplication.tsx @@ -25,7 +25,7 @@ import { import RestartMonitor from 'framework/system/RestartMonitor'; import { useI18nContext } from 'i18n/i18n-react'; -import { numberValue, updateValueDirty, useRest2 } from 'utils'; +import { numberValue, updateValueDirty, useRest } from 'utils'; import { validate } from 'validators'; export function boardProfileSelectItems() { @@ -49,7 +49,7 @@ const SettingsApplication: FC = () => { blocker, errorMessage, restartNeeded - } = useRest2({ + } = useRest({ read: EMSESP.readSettings, update: EMSESP.writeSettings }); diff --git a/interface/src/utils/endpoints.ts b/interface/src/utils/endpoints.ts deleted file mode 100644 index d0de377fe..000000000 --- a/interface/src/utils/endpoints.ts +++ /dev/null @@ -1,9 +0,0 @@ -// TODO extractErrorMessage function can be removed! -export const extractErrorMessage = (error: any, defaultMessage: string) => { - if (error.request) { - return defaultMessage + ' (' + error.request.status + ': ' + error.request.statusText + ')'; - } else if (error instanceof Error) { - return defaultMessage + ' (' + error.message + ')'; - } - return defaultMessage; -}; diff --git a/interface/src/utils/index.ts b/interface/src/utils/index.ts index c17d6949b..7ee4414df 100644 --- a/interface/src/utils/index.ts +++ b/interface/src/utils/index.ts @@ -1,9 +1,6 @@ export * from './binding'; -export * from './endpoints'; export * from './route'; export * from './submit'; export * from './time'; export * from './useRest'; -// TODO remove useRest2 -export * from './useRest2'; export * from './props'; diff --git a/interface/src/utils/useRest.ts b/interface/src/utils/useRest.ts index 1d71f1a07..1a7fbcb21 100644 --- a/interface/src/utils/useRest.ts +++ b/interface/src/utils/useRest.ts @@ -1,85 +1,76 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useRequest, type Method } from 'alova'; +import { useState } from 'react'; import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { extractErrorMessage } from '.'; -import type { AxiosPromise } from 'axios'; - import { useI18nContext } from 'i18n/i18n-react'; -export interface RestRequestOptions { - read: () => AxiosPromise; - update?: (value: D) => AxiosPromise; +export interface RestRequestOptions2 { + read: () => Method; + update: (value: D) => Method; } -export const useRest = ({ read, update }: RestRequestOptions) => { +export const useRest = ({ read, update }: RestRequestOptions2) => { const { LL } = useI18nContext(); - const [data, setData] = useState(); - const [saving, setSaving] = useState(false); const [errorMessage, setErrorMessage] = useState(); const [restartNeeded, setRestartNeeded] = useState(false); - const [origData, setOrigData] = useState(); - const [dirtyFlags, setDirtyFlags] = useState(); - const blocker = useBlocker(dirtyFlags?.length !== 0); + const [dirtyFlags, setDirtyFlags] = useState([]); + const blocker = useBlocker(dirtyFlags.length !== 0); - const loadData = useCallback(async () => { - setData(undefined); + const { data: data, send: readData, update: updateData, onComplete: onReadComplete } = useRequest(read()); + + const { + loading: saving, + send: writeData, + onSuccess: onWriteSuccess + } = useRequest((newData: D) => update(newData), { immediate: false }); + + const updateDataValue = (new_data: D) => { + updateData({ data: new_data }); + }; + + onWriteSuccess(() => { + toast.success(LL.UPDATED_OF(LL.SETTINGS())); + setDirtyFlags([]); + }); + + onReadComplete((event) => { + setOrigData(event.data); + }); + + const loadData = async () => { setDirtyFlags([]); setErrorMessage(undefined); - try { - const fetch_data = (await read()).data; - setData(fetch_data); - setOrigData(fetch_data); - } catch (error) { - const message = extractErrorMessage(error, LL.PROBLEM_LOADING()); - toast.error(message); - setErrorMessage(message); + await readData().catch((error) => { + toast.error(error.message); + setErrorMessage(error.message); + }); + }; + + const saveData = async () => { + if (!data) { + return; } - }, [read, LL]); - - const save = useCallback( - async (toSave: D) => { - if (!update) { - return; + setRestartNeeded(false); + setErrorMessage(undefined); + await writeData(data).catch((error) => { + if (error.message === 'Reboot required') { + setRestartNeeded(true); + } else { + toast.error(error.message); + setErrorMessage(error.message); } - setSaving(true); - setRestartNeeded(false); - setErrorMessage(undefined); - try { - const response = await update(toSave); - setOrigData(response.data); - setData(response.data); - if (response.status === 205) { - setRestartNeeded(true); // reboot required - } else { - toast.success(LL.UPDATED_OF(LL.SETTINGS())); - } - } catch (error) { - const message = extractErrorMessage(error, LL.PROBLEM_UPDATING()); - toast.error(message); - setErrorMessage(message); - } finally { - setSaving(false); - setDirtyFlags([]); - } - }, - [update, LL] - ); - - const saveData = () => data && save(data); - - useEffect(() => { - void loadData(); - }, [loadData]); + }); + }; return { loadData, saveData, saving, - setData, + updateDataValue, data, origData, dirtyFlags, diff --git a/interface/src/utils/useRest2.ts b/interface/src/utils/useRest2.ts deleted file mode 100644 index b71b1c9cf..000000000 --- a/interface/src/utils/useRest2.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useRequest, type Method } from 'alova'; -import { useState } from 'react'; -import { unstable_useBlocker as useBlocker } from 'react-router-dom'; -import { toast } from 'react-toastify'; - -import { useI18nContext } from 'i18n/i18n-react'; - -export interface RestRequestOptions2 { - read: () => Method; - update: (value: D) => Method; -} - -// TODO rename back to useRest -export const useRest2 = ({ read, update }: RestRequestOptions2) => { - const { LL } = useI18nContext(); - - const [errorMessage, setErrorMessage] = useState(); - const [restartNeeded, setRestartNeeded] = useState(false); - const [origData, setOrigData] = useState(); - - const [dirtyFlags, setDirtyFlags] = useState([]); - const blocker = useBlocker(dirtyFlags.length !== 0); - - const { data: data, send: readData, update: updateData, onComplete: onReadComplete } = useRequest(read()); - - const { - loading: saving, - send: writeData, - onSuccess: onWriteSuccess - } = useRequest((newData: D) => update(newData), { immediate: false }); - - const updateDataValue = (new_data: D) => { - updateData({ data: new_data }); - }; - - onWriteSuccess(() => { - toast.success(LL.UPDATED_OF(LL.SETTINGS())); - setDirtyFlags([]); - }); - - onReadComplete((event) => { - setOrigData(event.data); - }); - - const loadData = async () => { - setDirtyFlags([]); - setErrorMessage(undefined); - await readData().catch((error) => { - toast.error(error.message); - setErrorMessage(error.message); - }); - }; - - const saveData = async () => { - if (!data) { - return; - } - setRestartNeeded(false); - setErrorMessage(undefined); - await writeData(data).catch((error) => { - if (error.message === 'Reboot required') { - setRestartNeeded(true); - } else { - toast.error(error.message); - setErrorMessage(error.message); - } - }); - }; - - return { - loadData, - saveData, - saving, - updateDataValue, - data, - origData, - dirtyFlags, - setDirtyFlags, - setOrigData, - blocker, - errorMessage, - restartNeeded - } as const; -}; diff --git a/mock-api/server.js b/mock-api/server.js index 045ab6695..cf2423da4 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -2024,11 +2024,11 @@ rest_server.post(NETWORK_SETTINGS_ENDPOINT, (req, res) => { res.sendStatus(200); }); rest_server.get(LIST_NETWORKS_ENDPOINT, (req, res) => { - if (countWifiScanPoll++ === 4) { - console.log('done, have list'); + if (countWifiScanPoll++ === 3) { + // console.log('done, have list'); res.json(list_networks); // send list } else { - console.log('...waiting #' + countWifiScanPoll); + // console.log('...waiting #' + countWifiScanPoll); res.sendStatus(200); // waiting.... } }); @@ -2124,6 +2124,7 @@ rest_server.post(UPLOAD_FILE_ENDPOINT, (req, res) => { res.sendStatus(200); }); rest_server.post(SIGN_IN_ENDPOINT, (req, res) => { + // res.sendStatus(401); // test bad user console.log('Signed in as ' + req.body.username); res.json(signin); // watch out, this has a return value });