alova - add interceptor

This commit is contained in:
Proddy
2023-06-18 16:35:32 +02:00
parent b1d666d7b9
commit ce1b9f22cb
24 changed files with 200 additions and 331 deletions

View File

@@ -21,22 +21,23 @@ const AuthenticatedRouting: FC = () => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const handleApiResponseError = useCallback( // TODO fix this - how to redirect on a 401
(error: AxiosError) => { // const handleApiResponseError = useCallback(
if (error.response && error.response.status === 401) { // (error: AxiosError) => {
AuthenticationApi.storeLoginRedirect(location); // if (error.response && error.response.status === 401) {
navigate('/unauthorized'); // AuthenticationApi.storeLoginRedirect(location);
} // navigate('/unauthorized');
return Promise.reject(error); // }
}, // return Promise.reject(error);
[location, navigate] // },
); // [location, navigate]
// );
useEffect(() => { // useEffect(() => {
// TODO replace AXIOS.interceptors.response.use ??? // // TODO replace AXIOS.interceptors.response.use ???
const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError); // const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
return () => AXIOS.interceptors.response.eject(axiosHandlerId); // return () => AXIOS.interceptors.response.eject(axiosHandlerId);
}, [handleApiResponseError]); // }, [handleApiResponseError]);
return ( return (
<Layout> <Layout>

View File

@@ -1,5 +1,6 @@
import ForwardIcon from '@mui/icons-material/Forward'; import ForwardIcon from '@mui/icons-material/Forward';
import { Box, Fab, Paper, Typography, Button } from '@mui/material'; import { Box, Fab, Paper, Typography, Button } from '@mui/material';
import { useRequest } from 'alova';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import type { ValidateFieldsError } from 'async-validator'; 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 { ReactComponent as TRflag } from 'i18n/TR.svg';
import { I18nContext } from 'i18n/i18n-react'; import { I18nContext } from 'i18n/i18n-react';
import { loadLocaleAsync } from 'i18n/i18n-util.async'; 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'; import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
const SignIn: FC = () => { const SignIn: FC = () => {
@@ -38,23 +39,27 @@ const SignIn: FC = () => {
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
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 updateLoginRequestValue = updateValue(setSignInRequest);
const signIn = async () => { const signIn = async () => {
try { await callSignIn(signInRequest).catch((event) => {
// TODO move to Alova if (event.message === 'Unauthorized') {
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()); toast.warn(LL.INVALID_LOGIN());
}
} else { } else {
toast.error(extractErrorMessage(error, LL.ERROR())); toast.error(LL.ERROR() + ' ' + event.message);
} }
setProcessing(false); setProcessing(false);
} });
}; };
const validateAndSignIn = async () => { const validateAndSignIn = async () => {

View File

@@ -1,6 +1,5 @@
import jwtDecode from 'jwt-decode'; import jwtDecode from 'jwt-decode';
import { ACCESS_TOKEN, AXIOS } from './endpoints'; import { ACCESS_TOKEN, alovaInstance } from './endpoints';
import type { AxiosPromise } from 'axios';
import type * as H from 'history'; import type * as H from 'history';
import type { Path } from 'react-router-dom'; 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_PATHNAME = 'loginPathname';
export const SIGN_IN_SEARCH = 'loginSearch'; export const SIGN_IN_SEARCH = 'loginSearch';
// TODO move verifyAuthorization to Alova export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization');
export function verifyAuthorization(): AxiosPromise<void> { export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request);
return AXIOS.get('/verifyAuthorization');
}
// TODO move signIn to Alova
export function signIn(request: SignInRequest): AxiosPromise<SignInResponse> {
return AXIOS.post('/signIn', request);
}
export function getStorage() { export function getStorage() {
return localStorage || sessionStorage; return localStorage || sessionStorage;

View File

@@ -4,10 +4,7 @@ import ReactHook from 'alova/react';
import axios from 'axios'; import axios from 'axios';
import { unpack } from '../api/unpack'; import { unpack } from '../api/unpack';
// TODO axios can be removed
import type { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios'; 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'; export const ACCESS_TOKEN = 'access_token';
@@ -52,14 +49,26 @@ export const alovaInstance = createAlova({
return data; 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); 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({ export const AXIOS = axios.create({
baseURL: REST_BASE_URL, baseURL: '/rest/',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
@@ -78,49 +87,8 @@ export const AXIOS = axios.create({
] ]
}); });
export const AXIOS_API = axios.create({ // TODO fileupload move to alova
baseURL: API_BASE_URL, // see https://alova.js.org/next-step/download-upload-progress
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
export interface FileUploadConfig { export interface FileUploadConfig {
cancelToken?: CancelToken; cancelToken?: CancelToken;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;

View File

@@ -1,17 +1,13 @@
import { AXIOS } from './endpoints'; import { alovaInstance } from './endpoints';
import type { AxiosPromise } from 'axios';
import type { SecuritySettings, Token } from 'types'; import type { SecuritySettings, Token } from 'types';
// TODO move these 3 to Alova export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings');
export function readSecuritySettings(): AxiosPromise<SecuritySettings> {
return AXIOS.get('/securitySettings');
}
export function updateSecuritySettings(securitySettings: SecuritySettings): AxiosPromise<SecuritySettings> { export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
return AXIOS.post('/securitySettings', securitySettings); alovaInstance.Post('/rest/securitySettings', securitySettings);
}
export function generateToken(username?: string): AxiosPromise<Token> { export const generateToken = (username?: string) =>
return AXIOS.get('/generateToken', { params: { username } }); alovaInstance.Get<Token>('/rest/generateToken', {
} params: { username }
});

View File

@@ -1,8 +1,8 @@
import { alovaInstance, startUploadFile } from './endpoints'; import { alovaInstance, alovaInstanceGH, startUploadFile } from './endpoints';
import type { FileUploadConfig } from './endpoints'; import type { FileUploadConfig } from './endpoints';
import type { AxiosPromise } from 'axios'; 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 // SystemStatus - also used to ping in Restart monitor
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus'); export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
@@ -21,6 +21,28 @@ export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSe
export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data); export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data);
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog'); export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
// TODO fileupload move to Alova // Get versions from github
export const getStableVersion = () =>
alovaInstanceGH.Get<Version>('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<Version>('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<void> => export const uploadFile = (file: File, config?: FileUploadConfig): AxiosPromise<void> =>
startUploadFile('/uploadFile', file, config); startUploadFile('/uploadFile', file, config);

View File

@@ -6,7 +6,6 @@ import type { FileUploadConfig } from 'api/endpoints';
import type { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios'; import type { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
interface MediaUploadOptions { interface MediaUploadOptions {
// TODO fileupload move to alova // TODO fileupload move to alova
@@ -40,6 +39,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => {
[uploadCancelToken] [uploadCancelToken]
); );
// TODO fileupload move to alova
const uploadFile = async (images: File[]) => { const uploadFile = async (images: File[]) => {
try { try {
const cancelToken = axios.CancelToken.source(); const cancelToken = axios.CancelToken.source();
@@ -61,7 +61,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED()); toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else { } else {
resetUploadingStates(); resetUploadingStates();
toast.error(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED(0))); toast.error(LL.UPLOAD() + ' ' + LL.FAILED(0));
} }
} }
}; };

View File

@@ -1,3 +1,4 @@
import { useRequest } from 'alova';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -19,6 +20,10 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const [initialized, setInitialized] = useState<boolean>(false); const [initialized, setInitialized] = useState<boolean>(false);
const [me, setMe] = useState<Me>(); const [me, setMe] = useState<Me>();
const { send: verifyAuthorization } = useRequest(AuthenticationApi.verifyAuthorization(), {
immediate: false
});
const signIn = (accessToken: string) => { const signIn = (accessToken: string) => {
try { try {
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken); AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
@@ -42,18 +47,17 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN); const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
if (accessToken) { if (accessToken) {
try { await verifyAuthorization()
await AuthenticationApi.verifyAuthorization(); .then(() => {
setMe(AuthenticationApi.decodeMeJWT(accessToken)); setMe(AuthenticationApi.decodeMeJWT(accessToken));
setInitialized(true); setInitialized(true);
} catch (error) { })
setMe(undefined); .catch(() => {
setInitialized(true);
}
} else {
setMe(undefined); setMe(undefined);
setInitialized(true); setInitialized(true);
});
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@@ -20,7 +20,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { APProvisionMode } from 'types'; import { APProvisionMode } from 'types';
import { numberValue, updateValueDirty, useRest2 } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { createAPSettingsValidator, validate } from 'validators'; import { createAPSettingsValidator, validate } from 'validators';
@@ -39,7 +39,7 @@ const APSettingsForm: FC = () => {
blocker, blocker,
saveData, saveData,
errorMessage errorMessage
} = useRest2<APSettings>({ } = useRest<APSettings>({
read: APApi.readAPSettings, read: APApi.readAPSettings,
update: APApi.updateAPSettings update: APApi.updateAPSettings
}); });

View File

@@ -17,7 +17,7 @@ import {
BlockNavigation BlockNavigation
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest2 } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators'; import { createMqttSettingsValidator, validate } from 'validators';
@@ -33,7 +33,7 @@ const MqttSettingsForm: FC = () => {
blocker, blocker,
saveData, saveData,
errorMessage errorMessage
} = useRest2<MqttSettings>({ } = useRest<MqttSettings>({
read: MqttApi.readMqttSettings, read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings update: MqttApi.updateMqttSettings
}); });

View File

@@ -43,7 +43,7 @@ import {
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest2 } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network'; import { createNetworkSettingsValidator } from 'validators/network';
@@ -68,7 +68,7 @@ const WiFiSettingsForm: FC = () => {
saveData, saveData,
errorMessage, errorMessage,
restartNeeded restartNeeded
} = useRest2<NetworkSettings>({ } = useRest<NetworkSettings>({
read: NetworkApi.readNetworkSettings, read: NetworkApi.readNetworkSettings,
update: NetworkApi.updateNetworkSettings update: NetworkApi.updateNetworkSettings
}); });

View File

@@ -19,7 +19,7 @@ import {
BlockNavigation BlockNavigation
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { updateValueDirty, useRest2 } from 'utils'; import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
@@ -35,7 +35,7 @@ const NTPSettingsForm: FC = () => {
blocker, blocker,
saveData, saveData,
errorMessage errorMessage
} = useRest2<NTPSettings>({ } = useRest<NTPSettings>({
read: NTPApi.readNTPSettings, read: NTPApi.readNTPSettings,
update: NTPApi.updateNTPSettings update: NTPApi.updateNTPSettings
}); });

View File

@@ -10,16 +10,14 @@ import {
TextField, TextField,
Button Button
} from '@mui/material'; } 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 { FC } from 'react';
import type { Token } from 'types';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import { MessageBox } from 'components'; import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
interface GenerateTokenProps { interface GenerateTokenProps {
username?: string; username?: string;
@@ -27,24 +25,18 @@ interface GenerateTokenProps {
} }
const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => { const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const [token, setToken] = useState<Token>(); const { LL } = useI18nContext();
const open = !!username; const open = !!username;
const { LL } = useI18nContext(); const { data: token, send: generateToken } = useRequest(SecurityApi.generateToken(username), {
immediate: false
const getToken = useCallback(async () => { });
try {
setToken((await SecurityApi.generateToken(username)).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
}, [username, LL]);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
void getToken(); void generateToken();
} }
}, [open, getToken]); }, [open, generateToken]);
return ( return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open={!!username} fullWidth maxWidth="sm"> <Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open={!!username} fullWidth maxWidth="sm">

View File

@@ -23,8 +23,7 @@ import { useRest } from 'utils';
import { createUserValidator } from 'validators'; import { createUserValidator } from 'validators';
const ManageUsersForm: FC = () => { const ManageUsersForm: FC = () => {
// TODO move to Alova const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettings>({
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings, read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings update: SecurityApi.updateSecuritySettings
}); });
@@ -85,7 +84,7 @@ const ManageUsersForm: FC = () => {
const removeUser = (toRemove: User) => { const removeUser = (toRemove: User) => {
const users = data.users.filter((u) => u.username !== toRemove.username); const users = data.users.filter((u) => u.username !== toRemove.username);
setData({ ...data, users }); updateDataValue({ ...data, users });
}; };
const createUser = () => { const createUser = () => {
@@ -109,7 +108,7 @@ const ManageUsersForm: FC = () => {
const doneEditingUser = () => { const doneEditingUser = () => {
if (user) { if (user) {
const users = [...data.users.filter((u) => u.username !== user.username), user]; const users = [...data.users.filter((u) => u.username !== user.username), user];
setData({ ...data, users }); updateDataValue({ ...data, users });
setUser(undefined); setUser(undefined);
} }
}; };

View File

@@ -18,16 +18,25 @@ const SecuritySettingsForm: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
// TODO move to Alova const {
const { loadData, saving, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, saveData, errorMessage } = loadData,
useRest<SecuritySettings>({ saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings, read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings update: SecurityApi.updateSecuritySettings
}); });
const authenticatedContext = useContext(AuthenticatedContext); const authenticatedContext = useContext(AuthenticatedContext);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData); const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const content = () => { const content = () => {
if (!data) { if (!data) {

View File

@@ -18,7 +18,7 @@ import {
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest2 } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { OTA_SETTINGS_VALIDATOR } from 'validators/system'; import { OTA_SETTINGS_VALIDATOR } from 'validators/system';
@@ -35,7 +35,7 @@ const OTASettingsForm: FC = () => {
setDirtyFlags, setDirtyFlags,
blocker, blocker,
errorMessage errorMessage
} = useRest2<OTASettings>({ } = useRest<OTASettings>({
read: SystemApi.readOTASettings, read: SystemApi.readOTASettings,
update: SystemApi.updateOTASettings update: SystemApi.updateOTASettings
}); });

View File

@@ -16,7 +16,7 @@ import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } fr
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { LogLevel } from 'types'; import { LogLevel } from 'types';
import { updateValueDirty, useRest2 } from 'utils'; import { updateValueDirty, useRest } from 'utils';
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log'; export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
@@ -52,7 +52,7 @@ const SystemLog: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest2<LogSettings>({ useRest<LogSettings>({
read: SystemApi.readLogSettings, read: SystemApi.readLogSettings,
update: SystemApi.updateLogSettings update: SystemApi.updateLogSettings
}); });

View File

@@ -57,8 +57,6 @@ const SystemStatusForm: FC = () => {
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false); const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [showingVersion, setShowingVersion] = useState<boolean>(false); const [showingVersion, setShowingVersion] = useState<boolean>(false);
const [latestVersion, setLatestVersion] = useState<Version>();
const [latestDevVersion, setLatestDevVersion] = useState<Version>();
const [restarting, setRestarting] = useState<boolean>(); const [restarting, setRestarting] = useState<boolean>();
const { send: restartCommand } = useRequest(SystemApi.restart(), { const { send: restartCommand } = useRequest(SystemApi.restart(), {
@@ -73,24 +71,10 @@ const SystemStatusForm: FC = () => {
immediate: false 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(() => { const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
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 restart = async () => { const restart = async () => {
setProcessing(true); setProcessing(true);

View File

@@ -25,7 +25,7 @@ import {
import RestartMonitor from 'framework/system/RestartMonitor'; import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest2 } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
export function boardProfileSelectItems() { export function boardProfileSelectItems() {
@@ -49,7 +49,7 @@ const SettingsApplication: FC = () => {
blocker, blocker,
errorMessage, errorMessage,
restartNeeded restartNeeded
} = useRest2<Settings>({ } = useRest<Settings>({
read: EMSESP.readSettings, read: EMSESP.readSettings,
update: EMSESP.writeSettings update: EMSESP.writeSettings
}); });

View File

@@ -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;
};

View File

@@ -1,9 +1,6 @@
export * from './binding'; export * from './binding';
export * from './endpoints';
export * from './route'; export * from './route';
export * from './submit'; export * from './submit';
export * from './time'; export * from './time';
export * from './useRest'; export * from './useRest';
// TODO remove useRest2
export * from './useRest2';
export * from './props'; export * from './props';

View File

@@ -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 { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { extractErrorMessage } from '.';
import type { AxiosPromise } from 'axios';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
export interface RestRequestOptions<D> { export interface RestRequestOptions2<D> {
read: () => AxiosPromise<D>; read: () => Method<any, any, any, any, any, any, any>;
update?: (value: D) => AxiosPromise<D>; update: (value: D) => Method<any, any, any, any, any, any, any>;
} }
export const useRest = <D>({ read, update }: RestRequestOptions<D>) => { export const useRest = <D>({ read, update }: RestRequestOptions2<D>) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [data, setData] = useState<D>();
const [saving, setSaving] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const [restartNeeded, setRestartNeeded] = useState<boolean>(false); const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
const [origData, setOrigData] = useState<D>(); const [origData, setOrigData] = useState<D>();
const [dirtyFlags, setDirtyFlags] = useState<string[]>();
const blocker = useBlocker(dirtyFlags?.length !== 0); const [dirtyFlags, setDirtyFlags] = useState<string[]>([]);
const blocker = useBlocker(dirtyFlags.length !== 0);
const loadData = useCallback(async () => { const { data: data, send: readData, update: updateData, onComplete: onReadComplete } = useRequest(read());
setData(undefined);
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([]); setDirtyFlags([]);
setErrorMessage(undefined); setErrorMessage(undefined);
try { await readData().catch((error) => {
const fetch_data = (await read()).data; toast.error(error.message);
setData(fetch_data); setErrorMessage(error.message);
setOrigData(fetch_data); });
} catch (error) { };
const message = extractErrorMessage(error, LL.PROBLEM_LOADING());
toast.error(message);
setErrorMessage(message);
}
}, [read, LL]);
const save = useCallback( const saveData = async () => {
async (toSave: D) => { if (!data) {
if (!update) {
return; return;
} }
setSaving(true);
setRestartNeeded(false); setRestartNeeded(false);
setErrorMessage(undefined); setErrorMessage(undefined);
try { await writeData(data).catch((error) => {
const response = await update(toSave); if (error.message === 'Reboot required') {
setOrigData(response.data); setRestartNeeded(true);
setData(response.data);
if (response.status === 205) {
setRestartNeeded(true); // reboot required
} else { } else {
toast.success(LL.UPDATED_OF(LL.SETTINGS())); toast.error(error.message);
setErrorMessage(error.message);
} }
} 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 { return {
loadData, loadData,
saveData, saveData,
saving, saving,
setData, updateDataValue,
data, data,
origData, origData,
dirtyFlags, dirtyFlags,

View File

@@ -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<D> {
read: () => Method<any, any, any, any, any, any, any>;
update: (value: D) => Method<any, any, any, any, any, any, any>;
}
// TODO rename back to useRest
export const useRest2 = <D>({ read, update }: RestRequestOptions2<D>) => {
const { LL } = useI18nContext();
const [errorMessage, setErrorMessage] = useState<string>();
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
const [origData, setOrigData] = useState<D>();
const [dirtyFlags, setDirtyFlags] = useState<string[]>([]);
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;
};

View File

@@ -2024,11 +2024,11 @@ rest_server.post(NETWORK_SETTINGS_ENDPOINT, (req, res) => {
res.sendStatus(200); res.sendStatus(200);
}); });
rest_server.get(LIST_NETWORKS_ENDPOINT, (req, res) => { rest_server.get(LIST_NETWORKS_ENDPOINT, (req, res) => {
if (countWifiScanPoll++ === 4) { if (countWifiScanPoll++ === 3) {
console.log('done, have list'); // console.log('done, have list');
res.json(list_networks); // send list res.json(list_networks); // send list
} else { } else {
console.log('...waiting #' + countWifiScanPoll); // console.log('...waiting #' + countWifiScanPoll);
res.sendStatus(200); // waiting.... res.sendStatus(200); // waiting....
} }
}); });
@@ -2124,6 +2124,7 @@ rest_server.post(UPLOAD_FILE_ENDPOINT, (req, res) => {
res.sendStatus(200); res.sendStatus(200);
}); });
rest_server.post(SIGN_IN_ENDPOINT, (req, res) => { rest_server.post(SIGN_IN_ENDPOINT, (req, res) => {
// res.sendStatus(401); // test bad user
console.log('Signed in as ' + req.body.username); console.log('Signed in as ' + req.body.username);
res.json(signin); // watch out, this has a return value res.json(signin); // watch out, this has a return value
}); });