mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 00:39:50 +03:00
Merge branch 'dev2' of https://github.com/emsesp/EMS-ESP32 into dev2
This commit is contained in:
@@ -1,15 +1,10 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||
import Dashboard from './project/Dashboard';
|
||||
import Help from './project/Help';
|
||||
import Settings from './project/Settings';
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import * as AuthenticationApi from 'api/authentication';
|
||||
import { AXIOS } from 'api/endpoints';
|
||||
import { Layout, RequireAdmin } from 'components';
|
||||
|
||||
import AccessPoint from 'framework/ap/AccessPoint';
|
||||
import Mqtt from 'framework/mqtt/Mqtt';
|
||||
import NetworkConnection from 'framework/network/NetworkConnection';
|
||||
@@ -17,57 +12,54 @@ import NetworkTime from 'framework/ntp/NetworkTime';
|
||||
import Security from 'framework/security/Security';
|
||||
import System from 'framework/system/System';
|
||||
|
||||
const AuthenticatedRouting: FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const AuthenticatedRouting: FC = () => (
|
||||
// TODO not sure if this is needed, to redirect on 401. If so add incerceptor to Alova
|
||||
// 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]
|
||||
// );
|
||||
// useEffect(() => {
|
||||
// const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
|
||||
// return () => AXIOS.interceptors.response.eject(axiosHandlerId);
|
||||
// }, [handleApiResponseError]);
|
||||
|
||||
const handleApiResponseError = useCallback(
|
||||
(error: AxiosError) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
AuthenticationApi.storeLoginRedirect(location);
|
||||
navigate('/unauthorized');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
[location, navigate]
|
||||
);
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route
|
||||
path="/settings/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Settings />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/help/*" element={<Help />} />
|
||||
|
||||
useEffect(() => {
|
||||
const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
|
||||
return () => AXIOS.interceptors.response.eject(axiosHandlerId);
|
||||
}, [handleApiResponseError]);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route
|
||||
path="/settings/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Settings />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/help/*" element={<Help />} />
|
||||
|
||||
<Route path="/network/*" element={<NetworkConnection />} />
|
||||
<Route path="/ap/*" element={<AccessPoint />} />
|
||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
||||
<Route path="/mqtt/*" element={<Mqtt />} />
|
||||
<Route
|
||||
path="/security/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Security />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/system/*" element={<System />} />
|
||||
<Route path="/*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
<Route path="/network/*" element={<NetworkConnection />} />
|
||||
<Route path="/ap/*" element={<AccessPoint />} />
|
||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
||||
<Route path="/mqtt/*" element={<Mqtt />} />
|
||||
<Route
|
||||
path="/security/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Security />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/system/*" element={<System />} />
|
||||
<Route path="/*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default AuthenticatedRouting;
|
||||
|
||||
@@ -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';
|
||||
@@ -24,7 +25,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 = () => {
|
||||
@@ -39,22 +40,27 @@ const SignIn: FC = () => {
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
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 signIn = async () => {
|
||||
try {
|
||||
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 () => {
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { AXIOS } from './endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { APSettings, APStatus } from 'types';
|
||||
|
||||
export function readAPStatus(): AxiosPromise<APStatus> {
|
||||
return AXIOS.get('/apStatus');
|
||||
}
|
||||
|
||||
export function readAPSettings(): AxiosPromise<APSettings> {
|
||||
return AXIOS.get('/apSettings');
|
||||
}
|
||||
|
||||
export function updateAPSettings(apSettings: APSettings): AxiosPromise<APSettings> {
|
||||
return AXIOS.post('/apSettings', apSettings);
|
||||
}
|
||||
export const readAPStatus = () => alovaInstance.Get<APStatus>('/rest/apStatus');
|
||||
export const readAPSettings = () => alovaInstance.Get<APSettings>('/rest/apSettings');
|
||||
export const updateAPSettings = (data: APSettings) => alovaInstance.Post<APSettings>('/rest/apSettings', data);
|
||||
|
||||
@@ -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,13 +8,8 @@ import type { Me, SignInRequest, SignInResponse } from 'types';
|
||||
export const SIGN_IN_PATHNAME = 'loginPathname';
|
||||
export const SIGN_IN_SEARCH = 'loginSearch';
|
||||
|
||||
export function verifyAuthorization(): AxiosPromise<void> {
|
||||
return AXIOS.get('/verifyAuthorization');
|
||||
}
|
||||
|
||||
export function signIn(request: SignInRequest): AxiosPromise<SignInResponse> {
|
||||
return AXIOS.post('/signIn', request);
|
||||
}
|
||||
export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization');
|
||||
export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request);
|
||||
|
||||
export function getStorage() {
|
||||
return localStorage || sessionStorage;
|
||||
|
||||
@@ -1,95 +1,61 @@
|
||||
import axios from 'axios';
|
||||
import { unpack } from './unpack';
|
||||
import { xhrRequestAdapter } from '@alova/adapter-xhr';
|
||||
import { createAlova } from 'alova';
|
||||
import ReactHook from 'alova/react';
|
||||
import { unpack } from '../api/unpack';
|
||||
|
||||
import type { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios';
|
||||
|
||||
export const WS_BASE_URL = '/ws/';
|
||||
export const API_BASE_URL = '/rest/';
|
||||
export const ES_BASE_URL = '/es/';
|
||||
export const EMSESP_API_BASE_URL = '/api/';
|
||||
export const ACCESS_TOKEN = 'access_token';
|
||||
|
||||
const location = window.location;
|
||||
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
export const WEB_SOCKET_ROOT = webProtocol + '//' + location.host + WS_BASE_URL;
|
||||
export const EVENT_SOURCE_ROOT = location.protocol + '//' + location.host + ES_BASE_URL;
|
||||
const host = window.location.host;
|
||||
export const WEB_SOCKET_ROOT = 'ws://' + host + '/ws/';
|
||||
export const EVENT_SOURCE_ROOT = 'http://' + host + '/es/';
|
||||
|
||||
export const AXIOS = 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 alovaInstance = createAlova({
|
||||
statesHook: ReactHook,
|
||||
// timeout: 3000, // timeout not used because of uploading firmware
|
||||
// localCache: null,
|
||||
localCache: {
|
||||
GET: {
|
||||
mode: 'placeholder', // see https://alova.js.org/learning/response-cache/#cache-replaceholder-mode
|
||||
expire: 2000
|
||||
}
|
||||
]
|
||||
},
|
||||
requestAdapter: xhrRequestAdapter(),
|
||||
beforeRequest(method) {
|
||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||
method.config.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||
}
|
||||
},
|
||||
|
||||
responded: {
|
||||
onSuccess: async (response) => {
|
||||
// if (response.status === 202) {
|
||||
// throw new Error('Wait'); // wifi scan in progress
|
||||
// } else
|
||||
if (response.status === 205) {
|
||||
throw new Error('Reboot required');
|
||||
} else if (response.status === 400) {
|
||||
throw new Error('Request Failed');
|
||||
} else if (response.status >= 400) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const data = await response.data;
|
||||
if (response.data instanceof ArrayBuffer) {
|
||||
return unpack(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Interceptor for request failure. This interceptor will be entered when the request is wrong.
|
||||
// TODO how best to handle http errors like 401 (unauthorized)
|
||||
// but I think this is handled correctly in AppRouting? See AuthenticatedRouting()
|
||||
// onError: (error, method) => {
|
||||
// alert(error.message);
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
export const AXIOS_API = axios.create({
|
||||
baseURL: EMSESP_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 alovaInstanceGH = createAlova({
|
||||
baseURL: 'https://api.github.com/repos/emsesp/EMS-ESP32',
|
||||
statesHook: ReactHook,
|
||||
requestAdapter: xhrRequestAdapter()
|
||||
});
|
||||
|
||||
export const AXIOS_BIN = axios.create({
|
||||
baseURL: API_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) => decode(data)]
|
||||
transformResponse: [(data) => unpack(data)] // new using msgpackr
|
||||
});
|
||||
|
||||
export interface FileUploadConfig {
|
||||
cancelToken?: CancelToken;
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
}
|
||||
|
||||
export const startUploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise<void> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return AXIOS.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
...(config || {})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { AXIOS } from './endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { Features } from 'types';
|
||||
|
||||
export function readFeatures(): AxiosPromise<Features> {
|
||||
return AXIOS.get('/features');
|
||||
}
|
||||
export const readFeatures = () => alovaInstance.Get<Features>('/rest/features');
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { AXIOS } from './endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { alovaInstance } from './endpoints';
|
||||
import type { MqttSettings, MqttStatus } from 'types';
|
||||
|
||||
export function readMqttStatus(): AxiosPromise<MqttStatus> {
|
||||
return AXIOS.get('/mqttStatus');
|
||||
}
|
||||
|
||||
export function readMqttSettings(): AxiosPromise<MqttSettings> {
|
||||
return AXIOS.get('/mqttSettings');
|
||||
}
|
||||
|
||||
export function updateMqttSettings(mqttSettings: MqttSettings): AxiosPromise<MqttSettings> {
|
||||
return AXIOS.post('/mqttSettings', mqttSettings);
|
||||
}
|
||||
export const readMqttStatus = () => alovaInstance.Get<MqttStatus>('/rest/mqttStatus');
|
||||
export const readMqttSettings = () => alovaInstance.Get<MqttSettings>('/rest/mqttSettings');
|
||||
export const updateMqttSettings = (data: MqttSettings) => alovaInstance.Post<MqttSettings>('/rest/mqttSettings', data);
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import { AXIOS } from './endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types';
|
||||
|
||||
export function readNetworkStatus(): AxiosPromise<NetworkStatus> {
|
||||
return AXIOS.get('/networkStatus');
|
||||
}
|
||||
|
||||
export function scanNetworks(): AxiosPromise<void> {
|
||||
return AXIOS.get('/scanNetworks');
|
||||
}
|
||||
|
||||
export function listNetworks(): AxiosPromise<WiFiNetworkList> {
|
||||
return AXIOS.get('/listNetworks');
|
||||
}
|
||||
|
||||
export function readNetworkSettings(): AxiosPromise<NetworkSettings> {
|
||||
return AXIOS.get('/networkSettings');
|
||||
}
|
||||
|
||||
export function updateNetworkSettings(wifiSettings: NetworkSettings): AxiosPromise<NetworkSettings> {
|
||||
return AXIOS.post('/networkSettings', wifiSettings);
|
||||
}
|
||||
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatus>('/rest/networkStatus');
|
||||
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
||||
export const listNetworks = () =>
|
||||
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
||||
name: 'listNetworks'
|
||||
});
|
||||
export const readNetworkSettings = () =>
|
||||
alovaInstance.Get<NetworkSettings>('/rest/networkSettings', { name: 'networkSettings' });
|
||||
export const updateNetworkSettings = (wifiSettings: NetworkSettings) =>
|
||||
alovaInstance.Post<NetworkSettings>('/rest/networkSettings', wifiSettings);
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { AXIOS } from './endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { alovaInstance } from './endpoints';
|
||||
import type { NTPSettings, NTPStatus, Time } from 'types';
|
||||
|
||||
export function readNTPStatus(): AxiosPromise<NTPStatus> {
|
||||
return AXIOS.get('/ntpStatus');
|
||||
}
|
||||
export const readNTPStatus = () => alovaInstance.Get<NTPStatus>('/rest/ntpStatus');
|
||||
export const readNTPSettings = () =>
|
||||
alovaInstance.Get<NTPSettings>('/rest/ntpSettings', {
|
||||
name: 'ntpSettings'
|
||||
});
|
||||
export const updateNTPSettings = (data: NTPSettings) => alovaInstance.Post<NTPSettings>('/rest/ntpSettings', data);
|
||||
|
||||
export function readNTPSettings(): AxiosPromise<NTPSettings> {
|
||||
return AXIOS.get('/ntpSettings');
|
||||
}
|
||||
|
||||
export function updateNTPSettings(ntpSettings: NTPSettings): AxiosPromise<NTPSettings> {
|
||||
return AXIOS.post('/ntpSettings', ntpSettings);
|
||||
}
|
||||
|
||||
export function updateTime(time: Time): AxiosPromise<Time> {
|
||||
return AXIOS.post('/time', time);
|
||||
}
|
||||
export const updateTime = (data: Time) => alovaInstance.Post<Time>('/rest/time', data);
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { AXIOS } from './endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { alovaInstance } from './endpoints';
|
||||
|
||||
import type { SecuritySettings, Token } from 'types';
|
||||
|
||||
export function readSecuritySettings(): AxiosPromise<SecuritySettings> {
|
||||
return AXIOS.get('/securitySettings');
|
||||
}
|
||||
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings');
|
||||
|
||||
export function updateSecuritySettings(securitySettings: SecuritySettings): AxiosPromise<SecuritySettings> {
|
||||
return AXIOS.post('/securitySettings', securitySettings);
|
||||
}
|
||||
export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
|
||||
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
||||
|
||||
export function generateToken(username?: string): AxiosPromise<Token> {
|
||||
return AXIOS.get('/generateToken', { params: { username } });
|
||||
}
|
||||
export const generateToken = (username?: string) =>
|
||||
alovaInstance.Get<Token>('/rest/generateToken', {
|
||||
params: { username }
|
||||
});
|
||||
|
||||
@@ -1,44 +1,50 @@
|
||||
import { AXIOS, AXIOS_BIN, startUploadFile } from './endpoints';
|
||||
import type { FileUploadConfig } from './endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
||||
import type { OTASettings, SystemStatus, LogSettings, Version } from 'types';
|
||||
|
||||
import type { OTASettings, SystemStatus, LogSettings, LogEntries } from 'types';
|
||||
// SystemStatus - also used to ping in Restart monitor
|
||||
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
||||
|
||||
export function readSystemStatus(timeout?: number): AxiosPromise<SystemStatus> {
|
||||
return AXIOS.get('/systemStatus', { timeout });
|
||||
}
|
||||
// commands
|
||||
export const restart = () => alovaInstance.Post('/rest/restart');
|
||||
export const partition = () => alovaInstance.Post('/rest/partition');
|
||||
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
|
||||
|
||||
export function restart(): AxiosPromise<void> {
|
||||
return AXIOS.post('/restart');
|
||||
}
|
||||
// OTA
|
||||
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
|
||||
export const updateOTASettings = (data: any) => alovaInstance.Post('/rest/otaSettings', data);
|
||||
|
||||
export function partition(): AxiosPromise<void> {
|
||||
return AXIOS.post('/partition');
|
||||
}
|
||||
// SystemLog
|
||||
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
||||
export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data);
|
||||
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
|
||||
|
||||
export function factoryReset(): AxiosPromise<void> {
|
||||
return AXIOS.post('/factoryReset');
|
||||
}
|
||||
// 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 function readOTASettings(): AxiosPromise<OTASettings> {
|
||||
return AXIOS.get('/otaSettings');
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export function updateOTASettings(otaSettings: OTASettings): AxiosPromise<OTASettings> {
|
||||
return AXIOS.post('/otaSettings', otaSettings);
|
||||
}
|
||||
|
||||
export const uploadFile = (file: File, config?: FileUploadConfig): AxiosPromise<void> =>
|
||||
startUploadFile('/uploadFile', file, config);
|
||||
|
||||
export function readLogSettings(): AxiosPromise<LogSettings> {
|
||||
return AXIOS.get('/logSettings');
|
||||
}
|
||||
|
||||
export function updateLogSettings(logSettings: LogSettings): AxiosPromise<LogSettings> {
|
||||
return AXIOS.post('/logSettings', logSettings);
|
||||
}
|
||||
|
||||
export function readLogEntries(): AxiosPromise<LogEntries> {
|
||||
return AXIOS_BIN.get('/fetchLog');
|
||||
}
|
||||
export const uploadFile = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return alovaInstance.Post('/rest/uploadFile', formData, {
|
||||
enableUpload: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -968,7 +968,6 @@ currentExtensions[0x69] = (data) => {
|
||||
if (!referenceMap) referenceMap = new Map();
|
||||
const token = src[position];
|
||||
let target;
|
||||
// TODO: handle Maps, Sets, and other types that can cycle; this is complicated, because you potentially need to read
|
||||
// ahead past references to record structure definitions
|
||||
if ((token >= 0x90 && token < 0xa0) || token == 0xdc || token == 0xdd) target = [];
|
||||
else target = {};
|
||||
@@ -1041,7 +1040,6 @@ currentExtensions[0xff] = (data) => {
|
||||
((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000
|
||||
);
|
||||
else if (data.length == 12)
|
||||
// TODO: Implement support for negative
|
||||
return new Date(
|
||||
((data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]) / 1000000 +
|
||||
((data[4] & 0x80 ? -0x1000000000000 : 0) +
|
||||
@@ -1070,7 +1068,6 @@ function saveState(callback) {
|
||||
const savedReferenceMap = referenceMap;
|
||||
const savedBundledStrings = bundledStrings;
|
||||
|
||||
// TODO: We may need to revisit this if we do more external calls to user code (since it could be slow)
|
||||
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
|
||||
const savedStructures = currentStructures;
|
||||
const savedStructuresContents = currentStructures.slice(0, currentStructures.length);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material
|
||||
import { Fragment } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { AxiosProgressEvent } from 'axios';
|
||||
import type { Progress } from 'alova';
|
||||
import type { FC } from 'react';
|
||||
import type { DropzoneState } from 'react-dropzone';
|
||||
|
||||
@@ -26,11 +26,13 @@ const getBorderColor = (theme: Theme, props: DropzoneState) => {
|
||||
export interface SingleUploadProps {
|
||||
onDrop: (acceptedFiles: File[]) => void;
|
||||
onCancel: () => void;
|
||||
uploading: boolean;
|
||||
progress?: AxiosProgressEvent;
|
||||
isUploading: boolean;
|
||||
progress: Progress;
|
||||
}
|
||||
|
||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, progress }) => {
|
||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, progress }) => {
|
||||
const uploading = isUploading && progress.total > 0;
|
||||
|
||||
const dropzoneState = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
@@ -38,20 +40,19 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
|
||||
'application/json': ['.json'],
|
||||
'text/plain': ['.md5']
|
||||
},
|
||||
disabled: uploading,
|
||||
disabled: isUploading,
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const { getRootProps, getInputProps } = dropzoneState;
|
||||
const theme = useTheme();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const progressText = () => {
|
||||
if (uploading) {
|
||||
if (progress?.total) {
|
||||
if (progress.total) {
|
||||
return LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%';
|
||||
}
|
||||
return LL.UPLOADING() + `\u2026`;
|
||||
}
|
||||
return LL.UPLOAD_DROP_TEXT();
|
||||
};
|
||||
@@ -81,8 +82,8 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
|
||||
<Fragment>
|
||||
<Box width="100%" p={2}>
|
||||
<LinearProgress
|
||||
variant={!progress || progress.total ? 'determinate' : 'indeterminate'}
|
||||
value={!progress ? 0 : progress.total ? Math.round((progress.loaded * 100) / progress.total) : 0}
|
||||
variant="determinate"
|
||||
value={progress.total === 0 ? 0 : Math.round((progress.loaded * 100) / progress.total)}
|
||||
/>
|
||||
</Box>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { default as SingleUpload } from './SingleUpload';
|
||||
export { default as useFileUpload } from './useFileUpload';
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
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 {
|
||||
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
||||
}
|
||||
|
||||
const useFileUpload = ({ upload }: MediaUploadOptions) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
const [md5, setMd5] = useState<string>('');
|
||||
const [uploadProgress, setUploadProgress] = useState<AxiosProgressEvent>();
|
||||
const [uploadCancelToken, setUploadCancelToken] = useState<CancelTokenSource>();
|
||||
|
||||
const resetUploadingStates = () => {
|
||||
setUploading(false);
|
||||
setUploadProgress(undefined);
|
||||
setUploadCancelToken(undefined);
|
||||
setMd5('');
|
||||
};
|
||||
|
||||
const cancelUpload = useCallback(() => {
|
||||
uploadCancelToken?.cancel();
|
||||
resetUploadingStates();
|
||||
}, [uploadCancelToken]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
uploadCancelToken?.cancel();
|
||||
},
|
||||
[uploadCancelToken]
|
||||
);
|
||||
|
||||
const uploadFile = async (images: File[]) => {
|
||||
try {
|
||||
const cancelToken = axios.CancelToken.source();
|
||||
setUploadCancelToken(cancelToken);
|
||||
setUploading(true);
|
||||
const response = await upload(images[0], {
|
||||
onUploadProgress: setUploadProgress,
|
||||
cancelToken: cancelToken.token
|
||||
});
|
||||
resetUploadingStates();
|
||||
if (response.status === 200) {
|
||||
toast.success(LL.UPLOAD() + ' ' + LL.SUCCESSFUL());
|
||||
} else if (response.status === 201) {
|
||||
setMd5(String(response.data));
|
||||
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
|
||||
} else {
|
||||
resetUploadingStates();
|
||||
toast.error(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED(0)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [uploadFile, cancelUpload, uploading, uploadProgress, md5] as const;
|
||||
};
|
||||
|
||||
export default useFileUpload;
|
||||
@@ -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<RequiredChildrenProps> = ({ children }) => {
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [me, setMe] = useState<Me>();
|
||||
|
||||
const { send: verifyAuthorization } = useRequest(AuthenticationApi.verifyAuthorization(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const signIn = (accessToken: string) => {
|
||||
try {
|
||||
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||
@@ -42,18 +47,20 @@ const Authentication: FC<RequiredChildrenProps> = ({ 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);
|
||||
}
|
||||
await verifyAuthorization()
|
||||
.then(() => {
|
||||
setMe(AuthenticationApi.decodeMeJWT(accessToken));
|
||||
setInitialized(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setMe(undefined);
|
||||
setInitialized(true);
|
||||
});
|
||||
} else {
|
||||
setMe(undefined);
|
||||
setInitialized(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useRequest } from 'alova';
|
||||
|
||||
import { FeaturesContext } from '.';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { Features } from 'types';
|
||||
import type { RequiredChildrenProps } from 'utils';
|
||||
import * as FeaturesApi from 'api/features';
|
||||
import { ApplicationError, LoadingSpinner } from 'components';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const FeaturesLoader: FC<RequiredChildrenProps> = (props) => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [features, setFeatures] = useState<Features>();
|
||||
|
||||
const loadFeatures = useCallback(async () => {
|
||||
try {
|
||||
const response = await FeaturesApi.readFeatures();
|
||||
setFeatures(response.data);
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, 'Failed to fetch application details.'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadFeatures();
|
||||
}, [loadFeatures]);
|
||||
const { data: features, error } = useRequest(FeaturesApi.readFeatures);
|
||||
|
||||
if (features) {
|
||||
return (
|
||||
@@ -38,8 +22,8 @@ const FeaturesLoader: FC<RequiredChildrenProps> = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return <ApplicationError message={errorMessage} />;
|
||||
if (error) {
|
||||
return <ApplicationError message={error?.message} />;
|
||||
}
|
||||
|
||||
return <LoadingSpinner height="100vh" />;
|
||||
|
||||
@@ -28,17 +28,27 @@ export const isAPEnabled = ({ provision_mode }: APSettings) =>
|
||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
|
||||
const APSettingsForm: FC = () => {
|
||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||
useRest<APSettings>({
|
||||
read: APApi.readAPSettings,
|
||||
update: APApi.updateAPSettings
|
||||
});
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<APSettings>({
|
||||
read: APApi.readAPSettings,
|
||||
update: APApi.updateAPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
@@ -12,7 +13,6 @@ import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { APNetworkStatus } from 'types';
|
||||
import { useRest } from 'utils';
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
@@ -28,7 +28,7 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
};
|
||||
|
||||
const APStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
|
||||
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -49,7 +49,7 @@ const APStatusForm: FC = () => {
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,17 +22,27 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { createMqttSettingsValidator, validate } from 'validators';
|
||||
|
||||
const MqttSettingsForm: FC = () => {
|
||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||
useRest<MqttSettings>({
|
||||
read: MqttApi.readMqttSettings,
|
||||
update: MqttApi.updateMqttSettings
|
||||
});
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<MqttSettings>({
|
||||
read: MqttApi.readMqttSettings,
|
||||
update: MqttApi.updateMqttSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ReportIcon from '@mui/icons-material/Report';
|
||||
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
@@ -12,7 +13,6 @@ import * as MqttApi from 'api/mqtt';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { MqttDisconnectReason } from 'types';
|
||||
import { useRest } from 'utils';
|
||||
|
||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||
if (!enabled) {
|
||||
@@ -26,7 +26,6 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
|
||||
|
||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
||||
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||
|
||||
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||
|
||||
return theme.palette.error.main;
|
||||
@@ -39,7 +38,7 @@ export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatus, theme: Theme) =>
|
||||
};
|
||||
|
||||
const MqttStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<MqttStatus>({ read: MqttApi.readMqttStatus });
|
||||
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -80,7 +79,7 @@ const MqttStatusForm: FC = () => {
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
const renderConnectionStatus = () => (
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
InputAdornment,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from '../system/RestartMonitor';
|
||||
@@ -28,6 +30,7 @@ import type { FC } from 'react';
|
||||
|
||||
import type { NetworkSettings } from 'types';
|
||||
import * as NetworkApi from 'api/network';
|
||||
import * as SystemApi from 'api/system';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
ButtonRow,
|
||||
@@ -39,7 +42,7 @@ import {
|
||||
BlockNavigation
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import * as EMSESP from 'project/api';
|
||||
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
|
||||
import { validate } from 'validators';
|
||||
@@ -52,11 +55,12 @@ const WiFiSettingsForm: FC = () => {
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
setData,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
@@ -69,13 +73,17 @@ const WiFiSettingsForm: FC = () => {
|
||||
update: NetworkApi.updateNetworkSettings
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized && data) {
|
||||
if (selectedNetwork) {
|
||||
setData({
|
||||
updateState('networkSettings', (current_data) => ({
|
||||
ssid: selectedNetwork.ssid,
|
||||
password: '',
|
||||
hostname: data?.hostname,
|
||||
hostname: current_data?.hostname,
|
||||
static_ip_config: false,
|
||||
enableIPv6: false,
|
||||
bandwidth20: false,
|
||||
@@ -84,13 +92,13 @@ const WiFiSettingsForm: FC = () => {
|
||||
enableMDNS: true,
|
||||
enableCORS: false,
|
||||
CORSOrigin: '*'
|
||||
});
|
||||
}));
|
||||
}
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [initialized, setInitialized, data, setData, selectedNetwork]);
|
||||
}, [initialized, setInitialized, data, selectedNetwork]);
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
@@ -112,12 +120,10 @@ const WiFiSettingsForm: FC = () => {
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
}
|
||||
await restartCommand().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import type { Theme } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
|
||||
@@ -15,7 +16,6 @@ import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { NetworkConnectionStatus } from 'types';
|
||||
import { useRest } from 'utils';
|
||||
|
||||
const isConnected = ({ status }: NetworkStatus) =>
|
||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||
@@ -59,7 +59,7 @@ const IPs = (status: NetworkStatus) => {
|
||||
};
|
||||
|
||||
const NetworkStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<NetworkStatus>({ read: NetworkApi.readNetworkStatus });
|
||||
const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -90,7 +90,7 @@ const NetworkStatusForm: FC = () => {
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,82 +1,52 @@
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
import { Button } from '@mui/material';
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||
import type { FC } from 'react';
|
||||
import type { WiFiNetwork, WiFiNetworkList } from 'types';
|
||||
import * as NetworkApi from 'api/network';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const NUM_POLLS = 10;
|
||||
const POLLING_FREQUENCY = 500;
|
||||
|
||||
const compareNetworks = (network1: WiFiNetwork, network2: WiFiNetwork) => {
|
||||
if (network1.rssi < network2.rssi) return 1;
|
||||
if (network1.rssi > network2.rssi) return -1;
|
||||
return 0;
|
||||
};
|
||||
const POLLING_FREQUENCY = 1000;
|
||||
|
||||
const WiFiNetworkScanner: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const pollCount = useRef(0);
|
||||
const [networkList, setNetworkList] = useState<WiFiNetworkList>();
|
||||
const { LL } = useI18nContext();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const finishedWithError = useCallback((message: string) => {
|
||||
toast.error(message);
|
||||
setNetworkList(undefined);
|
||||
setErrorMessage(message);
|
||||
}, []);
|
||||
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(NetworkApi.scanNetworks); // is called on page load to start network scan
|
||||
const {
|
||||
data: networkList,
|
||||
send: getNetworkList,
|
||||
onSuccess: onSuccessNetworkList
|
||||
} = useRequest(NetworkApi.listNetworks, {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const pollNetworkList = useCallback(async () => {
|
||||
try {
|
||||
const response = await NetworkApi.listNetworks();
|
||||
if (response.status === 202) {
|
||||
const completedPollCount = pollCount.current + 1;
|
||||
if (completedPollCount < NUM_POLLS) {
|
||||
pollCount.current = completedPollCount;
|
||||
setTimeout(pollNetworkList, POLLING_FREQUENCY);
|
||||
} else {
|
||||
finishedWithError(LL.PROBLEM_LOADING());
|
||||
}
|
||||
onSuccessNetworkList((event) => {
|
||||
if (!event.data) {
|
||||
const completedPollCount = pollCount.current + 1;
|
||||
if (completedPollCount < NUM_POLLS) {
|
||||
pollCount.current = completedPollCount;
|
||||
setTimeout(getNetworkList, POLLING_FREQUENCY);
|
||||
} else {
|
||||
const newNetworkList = response.data;
|
||||
newNetworkList.networks.sort(compareNetworks);
|
||||
setNetworkList(newNetworkList);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
|
||||
} else {
|
||||
finishedWithError(LL.PROBLEM_LOADING());
|
||||
setErrorMessage(LL.PROBLEM_LOADING());
|
||||
pollCount.current = 0;
|
||||
}
|
||||
}
|
||||
}, [finishedWithError, LL]);
|
||||
});
|
||||
|
||||
const startNetworkScan = useCallback(async () => {
|
||||
onCompleteScanNetworks(() => {
|
||||
pollCount.current = 0;
|
||||
setNetworkList(undefined);
|
||||
setErrorMessage(undefined);
|
||||
try {
|
||||
await NetworkApi.scanNetworks();
|
||||
setTimeout(pollNetworkList, POLLING_FREQUENCY);
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
|
||||
} else {
|
||||
finishedWithError(LL.PROBLEM_LOADING());
|
||||
}
|
||||
}
|
||||
}, [finishedWithError, pollNetworkList, LL]);
|
||||
|
||||
useEffect(() => {
|
||||
void startNetworkScan();
|
||||
}, [startNetworkScan]);
|
||||
updateState('listNetworks', () => undefined);
|
||||
void getNetworkList();
|
||||
});
|
||||
|
||||
const renderNetworkScanner = () => {
|
||||
if (!networkList) {
|
||||
@@ -93,7 +63,7 @@ const WiFiNetworkScanner: FC = () => {
|
||||
startIcon={<PermScanWifiIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={startNetworkScan}
|
||||
onClick={scanNetworks}
|
||||
disabled={!errorMessage && !networkList}
|
||||
>
|
||||
{LL.SCAN_AGAIN()}…
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState } from 'alova';
|
||||
import { useState } from 'react';
|
||||
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
@@ -22,15 +24,25 @@ import { validate } from 'validators';
|
||||
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
||||
|
||||
const NTPSettingsForm: FC = () => {
|
||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||
useRest<NTPSettings>({
|
||||
read: NTPApi.readNTPSettings,
|
||||
update: NTPApi.updateNTPSettings
|
||||
});
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<NTPSettings>({
|
||||
read: NTPApi.readNTPSettings,
|
||||
update: NTPApi.updateNTPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
@@ -51,11 +63,12 @@ const NTPSettingsForm: FC = () => {
|
||||
|
||||
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFormValue(event);
|
||||
setData({
|
||||
...data,
|
||||
|
||||
updateState('ntpSettings', (settings) => ({
|
||||
...settings,
|
||||
tz_label: event.target.value,
|
||||
tz_format: TIME_ZONES[event.target.value]
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
useTheme,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { Theme } from '@mui/material';
|
||||
@@ -33,7 +34,7 @@ import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { NTPSyncStatus } from 'types';
|
||||
import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from 'utils';
|
||||
import { formatDateTime, formatLocalDateTime } from 'utils';
|
||||
|
||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
||||
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
|
||||
@@ -52,7 +53,8 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||
};
|
||||
|
||||
const NTPStatusForm: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<NTPStatus>({ read: NTPApi.readNTPStatus });
|
||||
const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
|
||||
|
||||
const [localTime, setLocalTime] = useState<string>('');
|
||||
const [settingTime, setSettingTime] = useState<boolean>(false);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
@@ -60,6 +62,12 @@ const NTPStatusForm: FC = () => {
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { send: updateTime } = useRequest((local_time) => NTPApi.updateTime(local_time), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
NTPApi.updateTime;
|
||||
|
||||
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
|
||||
|
||||
const openSetTime = () => {
|
||||
@@ -84,18 +92,19 @@ const NTPStatusForm: FC = () => {
|
||||
|
||||
const configureTime = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await NTPApi.updateTime({
|
||||
local_time: formatLocalDateTime(new Date(localTime))
|
||||
|
||||
await updateTime({ local_time: formatLocalDateTime(new Date(localTime)) })
|
||||
.then(async () => {
|
||||
toast.success(LL.TIME_SET());
|
||||
setSettingTime(false);
|
||||
await loadData();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
})
|
||||
.finally(() => {
|
||||
setProcessing(false);
|
||||
});
|
||||
toast.success(LL.TIME_SET());
|
||||
setSettingTime(false);
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSetTimeDialog = () => (
|
||||
@@ -136,7 +145,7 @@ const NTPStatusForm: FC = () => {
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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<GenerateTokenProps> = ({ username, onClose }) => {
|
||||
const [token, setToken] = useState<Token>();
|
||||
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 (
|
||||
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open={!!username} fullWidth maxWidth="sm">
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useRest } from 'utils';
|
||||
import { createUserValidator } from 'validators';
|
||||
|
||||
const ManageUsersForm: FC = () => {
|
||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<SecuritySettings>({
|
||||
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettings>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
@@ -88,7 +88,7 @@ const ManageUsersForm: FC = () => {
|
||||
|
||||
const removeUser = (toRemove: User) => {
|
||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||
setData({ ...data, users });
|
||||
updateDataValue({ ...data, users });
|
||||
setChanged(changed + 1);
|
||||
};
|
||||
|
||||
@@ -113,7 +113,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);
|
||||
setChanged(changed + 1);
|
||||
}
|
||||
|
||||
@@ -18,15 +18,25 @@ const SecuritySettingsForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const { loadData, saving, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, saveData, errorMessage } =
|
||||
useRest<SecuritySettings>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<SecuritySettings>({
|
||||
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) {
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import { Typography, Button, Box } from '@mui/material';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { FileUploadConfig } from 'api/endpoints';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { SingleUpload, useFileUpload } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import * as EMSESP from 'project/api';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
interface UploadFileProps {
|
||||
uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
||||
}
|
||||
|
||||
const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
|
||||
const [uploadFile, cancelUpload, uploading, uploadProgress, md5] = useFileUpload({ upload: uploadGeneralFile });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const saveFile = (json: any, endpoint: string) => {
|
||||
const a = document.createElement('a');
|
||||
const filename = 'emsesp_' + endpoint + '.json';
|
||||
a.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
a.setAttribute('download', filename);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
};
|
||||
|
||||
const downloadSettings = async () => {
|
||||
try {
|
||||
const response = await EMSESP.getSettings();
|
||||
if (response.status !== 200) {
|
||||
toast.error(LL.PROBLEM_LOADING());
|
||||
} else {
|
||||
saveFile(response.data, 'settings');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
};
|
||||
|
||||
const downloadCustomizations = async () => {
|
||||
try {
|
||||
const response = await EMSESP.getCustomizations();
|
||||
if (response.status !== 200) {
|
||||
toast.error(LL.PROBLEM_LOADING());
|
||||
} else {
|
||||
saveFile(response.data, 'customizations');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
};
|
||||
|
||||
const downloadEntities = async () => {
|
||||
try {
|
||||
const response = await EMSESP.getEntities();
|
||||
if (response.status !== 200) {
|
||||
toast.error(LL.PROBLEM_LOADING());
|
||||
} else {
|
||||
saveFile(response.data, 'entities');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
};
|
||||
|
||||
const downloadSchedule = async () => {
|
||||
try {
|
||||
const response = await EMSESP.getSchedule();
|
||||
if (response.status !== 200) {
|
||||
toast.error(LL.PROBLEM_LOADING());
|
||||
} else {
|
||||
saveFile(response.data, 'schedule');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!uploading && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.UPLOAD_TEXT()} </Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{md5 !== '' && (
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<SingleUpload onDrop={uploadFile} onCancel={cancelUpload} uploading={uploading} progress={uploadProgress} />
|
||||
|
||||
{!uploading && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSettings}>
|
||||
{LL.SETTINGS_OF('')}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadCustomizations}>
|
||||
{LL.CUSTOMIZATIONS()}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadEntities}
|
||||
>
|
||||
{LL.CUSTOM_ENTITIES(0)}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SCHEDULE_TEXT()}{' '}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSchedule}>
|
||||
{LL.SCHEDULE(0)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralFileUpload;
|
||||
@@ -24,15 +24,25 @@ import { validate } from 'validators';
|
||||
import { OTA_SETTINGS_VALIDATOR } from 'validators/system';
|
||||
|
||||
const OTASettingsForm: FC = () => {
|
||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||
useRest<OTASettings>({
|
||||
read: SystemApi.readOTASettings,
|
||||
update: SystemApi.updateOTASettings
|
||||
});
|
||||
const {
|
||||
loadData,
|
||||
saveData,
|
||||
saving,
|
||||
updateDataValue,
|
||||
data,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
errorMessage
|
||||
} = useRest<OTASettings>({
|
||||
read: SystemApi.readOTASettings,
|
||||
update: SystemApi.updateOTASettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { useRetriableRequest } from '@alova/scene-react';
|
||||
import { useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
@@ -6,35 +7,26 @@ import { FormLoader } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const RESTART_TIMEOUT = 2 * 60 * 1000;
|
||||
const POLL_TIMEOUT = 2000;
|
||||
const POLL_INTERVAL = 5000;
|
||||
|
||||
const RestartMonitor: FC = () => {
|
||||
const [failed, setFailed] = useState<boolean>(false);
|
||||
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
|
||||
const poll = useRef(async () => {
|
||||
try {
|
||||
await SystemApi.readSystemStatus(POLL_TIMEOUT);
|
||||
document.location.href = '/fileUpdated';
|
||||
} catch (error) {
|
||||
if (new Date().getTime() < timeoutAt.current) {
|
||||
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
|
||||
} else {
|
||||
setFailed(true);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const { onFail, onSuccess } = useRetriableRequest(SystemApi.readSystemStatus(), {
|
||||
retry: 10,
|
||||
backoff: {
|
||||
delay: 1500
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void poll.current();
|
||||
}, []);
|
||||
onFail(() => {
|
||||
setFailed(true);
|
||||
});
|
||||
|
||||
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
|
||||
onSuccess(() => {
|
||||
document.location.href = '/fileUpdated';
|
||||
});
|
||||
|
||||
return <FormLoader message={LL.APPLICATION_RESTARTING() + '...'} errorMessage={failed ? 'Timed out' : undefined} />;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, styled, Button, Checkbox, MenuItem, Grid, TextField } from '@mui/material';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
// eslint-disable-next-line import/named
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { LogSettings, LogEntry, LogEntries } from 'types';
|
||||
import type { LogSettings, LogEntry } from 'types';
|
||||
import { addAccessTokenParameter } from 'api/authentication';
|
||||
import { EVENT_SOURCE_ROOT } from 'api/endpoints';
|
||||
import * as SystemApi from 'api/system';
|
||||
@@ -14,7 +16,7 @@ import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } fr
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { LogLevel } from 'types';
|
||||
import { useRest, updateValueDirty, extractErrorMessage } from 'utils';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
|
||||
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
|
||||
|
||||
@@ -49,14 +51,20 @@ const levelLabel = (level: LogLevel) => {
|
||||
const SystemLog: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { loadData, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, setOrigData } = useRest<LogSettings>({
|
||||
read: SystemApi.readLogSettings
|
||||
});
|
||||
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||
useRest<LogSettings>({
|
||||
read: SystemApi.readLogSettings,
|
||||
update: SystemApi.updateLogSettings
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [logEntries, setLogEntries] = useState<LogEntries>({ events: [] });
|
||||
// called on page load to reset pointer and fetch all log entries
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { send: fetchLog } = useRequest(SystemApi.fetchLog());
|
||||
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
|
||||
const [lastIndex, setLastIndex] = useState<number>(0);
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
|
||||
const paddedLevelLabel = (level: LogLevel) => {
|
||||
const label = levelLabel(level);
|
||||
return data?.compact ? ' ' + label[0] : label.padStart(8, '\xa0');
|
||||
@@ -72,11 +80,9 @@ const SystemLog: FC = () => {
|
||||
return data?.compact ? label : label.padEnd(7, '\xa0');
|
||||
};
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||
|
||||
const onDownload = () => {
|
||||
let result = '';
|
||||
for (const i of logEntries.events) {
|
||||
for (const i of logEntries) {
|
||||
result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
@@ -93,29 +99,22 @@ const SystemLog: FC = () => {
|
||||
const logentry = JSON.parse(rawData as string) as LogEntry;
|
||||
if (logentry.i > lastIndex) {
|
||||
setLastIndex(logentry.i);
|
||||
setLogEntries((old) => ({ events: [...old.events, logentry] }));
|
||||
setLogEntries((log) => [...log, logentry]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLog = useCallback(async () => {
|
||||
try {
|
||||
await SystemApi.readLogEntries();
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchLog();
|
||||
}, [fetchLog]);
|
||||
const saveSettings = async () => {
|
||||
await saveData();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL));
|
||||
es.onmessage = onMessage;
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
window.location.reload();
|
||||
toast.error('EventSource failed');
|
||||
// window.location.reload();
|
||||
};
|
||||
|
||||
return () => {
|
||||
@@ -123,28 +122,6 @@ const SystemLog: FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (data) {
|
||||
try {
|
||||
const response = await SystemApi.updateLogSettings({
|
||||
level: data.level,
|
||||
max_messages: data.max_messages,
|
||||
compact: data.compact
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
} else {
|
||||
setOrigData(response.data);
|
||||
setDirtyFlags([]);
|
||||
toast.success(LL.UPDATED_OF(LL.SETTINGS()));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
@@ -230,17 +207,16 @@ const SystemLog: FC = () => {
|
||||
p: 1
|
||||
}}
|
||||
>
|
||||
{logEntries &&
|
||||
logEntries.events.map((e) => (
|
||||
<LogEntryLine key={e.i}>
|
||||
<span>{e.t}</span>
|
||||
{data.compact && <span>{paddedLevelLabel(e.l)} </span>}
|
||||
{!data.compact && <span>{paddedLevelLabel(e.l)} </span>}
|
||||
<span>{paddedIDLabel(e.i)} </span>
|
||||
<span>{paddedNameLabel(e.n)} </span>
|
||||
<span>{e.m}</span>
|
||||
</LogEntryLine>
|
||||
))}
|
||||
{logEntries.map((e) => (
|
||||
<LogEntryLine key={e.i}>
|
||||
<span>{e.t}</span>
|
||||
{data.compact && <span>{paddedLevelLabel(e.l)} </span>}
|
||||
{!data.compact && <span>{paddedLevelLabel(e.l)} </span>}
|
||||
<span>{paddedIDLabel(e.i)} </span>
|
||||
<span>{paddedNameLabel(e.n)} </span>
|
||||
<span>{e.m}</span>
|
||||
</LogEntryLine>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -28,18 +28,16 @@ import {
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import axios from 'axios';
|
||||
import { useContext, useState, useEffect } from 'react';
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import type { SystemStatus, Version } from 'types';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { ButtonRow, FormLoader, SectionContent, MessageBox } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage, useRest } from 'utils';
|
||||
|
||||
export const VERSIONCHECK_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/latest';
|
||||
export const VERSIONCHECK_DEV_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/latest';
|
||||
@@ -51,61 +49,75 @@ function formatNumber(num: number) {
|
||||
|
||||
const SystemStatusForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { loadData, data, errorMessage } = useRest<SystemStatus>({ read: SystemApi.readSystemStatus });
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [showingVersion, setShowingVersion] = useState<boolean>(false);
|
||||
const [latestVersion, setLatestVersion] = useState<Version>();
|
||||
const [latestDevVersion, setLatestDevVersion] = useState<Version>();
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
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 { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: partitionCommand } = useRequest(SystemApi.partition(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
// fetch versions from GH on load
|
||||
const { data: latestVersion } = useRequest(SystemApi.getStableVersion);
|
||||
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion);
|
||||
|
||||
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
|
||||
|
||||
const restart = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await SystemApi.restart();
|
||||
if (response.status === 200) {
|
||||
await restartCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
} finally {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const factoryReset = async () => {
|
||||
setProcessing(true);
|
||||
await factoryResetCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmFactoryReset(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const partition = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await SystemApi.partition();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
} finally {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
}
|
||||
await partitionCommand()
|
||||
.then(() => {
|
||||
setRestarting(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setConfirmRestart(false);
|
||||
setProcessing(false);
|
||||
});
|
||||
};
|
||||
|
||||
const renderRestartDialog = () => (
|
||||
@@ -200,19 +212,6 @@ const SystemStatusForm: FC = () => {
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const factoryReset = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await SystemApi.factoryReset();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setConfirmFactoryReset(false);
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFactoryResetDialog = () => (
|
||||
<Dialog open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
@@ -242,7 +241,7 @@ const SystemStatusForm: FC = () => {
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,30 +1,173 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import GeneralFileUpload from './GeneralFileUpload';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import { Typography, Button, Box } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, type FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import RestartMonitor from './RestartMonitor';
|
||||
import type { FileUploadConfig } from 'api/endpoints';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { SectionContent } from 'components';
|
||||
import { SectionContent, SingleUpload } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import * as EMSESP from 'project/api';
|
||||
|
||||
const UploadFileForm: FC = () => {
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
const [md5, setMd5] = useState<string>();
|
||||
|
||||
const uploadFile = useRef(async (file: File, config?: FileUploadConfig) => {
|
||||
const response = await SystemApi.uploadFile(file, config);
|
||||
if (response.status === 200) {
|
||||
setRestarting(true);
|
||||
}
|
||||
return response;
|
||||
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getCustomizations, onSuccess: onSuccessgetCustomizations } = useRequest(EMSESP.getCustomizations(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const {
|
||||
loading: isUploading,
|
||||
uploading: progress,
|
||||
send: sendUpload,
|
||||
onSuccess: onSuccessUpload,
|
||||
abort: cancelUpload
|
||||
} = useRequest(SystemApi.uploadFile, {
|
||||
immediate: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
onSuccessUpload(({ data }: any) => {
|
||||
if (data) {
|
||||
setMd5(data.md5);
|
||||
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
|
||||
} else {
|
||||
setRestarting(true);
|
||||
}
|
||||
});
|
||||
|
||||
const startUpload = async (files: File[]) => {
|
||||
await sendUpload(files[0]).catch((err) => {
|
||||
if (err.message === 'The user aborted a request') {
|
||||
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
|
||||
} else {
|
||||
toast.warning(err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveFile = (json: any, endpoint: string) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
anchor.download = 'emsesp_' + endpoint + '.json';
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(anchor.href);
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
};
|
||||
|
||||
onSuccessGetSettings((event) => {
|
||||
saveFile(event.data, 'settings');
|
||||
});
|
||||
onSuccessgetCustomizations((event) => {
|
||||
saveFile(event.data, 'customizations');
|
||||
});
|
||||
onSuccessGetEntities((event) => {
|
||||
saveFile(event.data, 'entities');
|
||||
});
|
||||
onSuccessGetSchedule((event) => {
|
||||
saveFile(event.data, 'schedule');
|
||||
});
|
||||
|
||||
const downloadSettings = async () => {
|
||||
await getSettings().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadCustomizations = async () => {
|
||||
await getCustomizations().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadEntities = async () => {
|
||||
await getEntities().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadSchedule = async () => {
|
||||
await getSchedule().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const content = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.UPLOAD_TEXT()} </Typography>
|
||||
</Box>
|
||||
{md5 && (
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
|
||||
{!isUploading && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSettings}>
|
||||
{LL.SETTINGS_OF('')}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadCustomizations}>
|
||||
{LL.CUSTOMIZATIONS()}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadEntities}
|
||||
>
|
||||
{LL.CUSTOM_ENTITIES(0)}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SCHEDULE_TEXT()}{' '}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSchedule}>
|
||||
{LL.SCHEDULE(0)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<SectionContent title={LL.UPLOAD_DOWNLOAD()} titleGutter>
|
||||
{restarting ? <RestartMonitor /> : <GeneralFileUpload uploadGeneralFile={uploadFile.current} />}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,7 +51,6 @@ const de: Translation = {
|
||||
REMOVE: 'Entfernen',
|
||||
PROBLEM_UPDATING: 'Problem beim Aktualisieren',
|
||||
PROBLEM_LOADING: 'Problem beim Laden',
|
||||
ACCESS_DENIED: 'Zugriff abgelehnt',
|
||||
ANALOG_SENSOR: 'Analogsensor',
|
||||
ANALOG_SENSORS: 'Analogsensoren',
|
||||
SETTINGS: 'Einstellungen',
|
||||
@@ -71,7 +70,6 @@ const de: Translation = {
|
||||
TEMP_SENSOR: 'Temperatursensor',
|
||||
TEMP_SENSORS: 'Temperatursensoren',
|
||||
WRITE_CMD_SENT: 'Befehl schreiben wurde gesendet',
|
||||
WRITE_CMD_FAILED: 'Befehl schreiben failed', // TODO translate
|
||||
EMS_BUS_WARNING: 'EMS-Bus getrennt. Wenn diese Warnung nach einigen Sekunden immer noch besteht, überprüfen Sie bitte die Einstellungen und das Board-Profil',
|
||||
EMS_BUS_SCANNING: 'Suche nach EMS Geräten...',
|
||||
CONNECTED: 'Verbunden',
|
||||
|
||||
@@ -51,7 +51,6 @@ const en: Translation = {
|
||||
REMOVE: 'Remove',
|
||||
PROBLEM_UPDATING: 'Problem updating',
|
||||
PROBLEM_LOADING: 'Problem loading',
|
||||
ACCESS_DENIED: 'Access Denied',
|
||||
ANALOG_SENSOR: 'Analog Sensor',
|
||||
ANALOG_SENSORS: 'Analog Sensors',
|
||||
SETTINGS: 'Settings',
|
||||
@@ -71,7 +70,6 @@ const en: Translation = {
|
||||
TEMP_SENSOR: 'Temperature Sensor',
|
||||
TEMP_SENSORS: 'Temperature Sensors',
|
||||
WRITE_CMD_SENT: 'Write command sent',
|
||||
WRITE_CMD_FAILED: 'Write command failed',
|
||||
EMS_BUS_WARNING: 'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
|
||||
EMS_BUS_SCANNING: 'Scanning for EMS devices...',
|
||||
CONNECTED: 'Connected',
|
||||
|
||||
@@ -51,7 +51,6 @@ const fr: Translation = {
|
||||
REMOVE: 'Enlever',
|
||||
PROBLEM_UPDATING: 'Problème lors de la mise à jour',
|
||||
PROBLEM_LOADING: 'Problème lors du chargement',
|
||||
ACCESS_DENIED: 'Accès refusé',
|
||||
ANALOG_SENSOR: 'Capteur analogique',
|
||||
ANALOG_SENSORS: 'Capteurs analogiques',
|
||||
SETTINGS: 'Paramètres',
|
||||
@@ -71,7 +70,6 @@ const fr: Translation = {
|
||||
TEMP_SENSOR: 'Capteur de température',
|
||||
TEMP_SENSORS: 'Capteurs de température',
|
||||
WRITE_CMD_SENT: 'Envoyer la commande sent', // TODO translate
|
||||
WRITE_CMD_FAILED: 'Envoyer la commande failed', // TODO translate
|
||||
EMS_BUS_WARNING: 'Bus EMS déconnecté. Si ce message persiste après quelques secondes, vérifiez les paramètres et la configuration de la carte.',
|
||||
EMS_BUS_SCANNING: 'Scan des appareils EMS...',
|
||||
CONNECTED: 'Connecté',
|
||||
|
||||
@@ -51,7 +51,6 @@ const nl: Translation = {
|
||||
REMOVE: 'Verwijderen',
|
||||
PROBLEM_UPDATING: 'Probleem met updaten',
|
||||
PROBLEM_LOADING: 'Probleem met laden',
|
||||
ACCESS_DENIED: 'Toegang geweigerd',
|
||||
ANALOG_SENSOR: 'Analoge sensor',
|
||||
ANALOG_SENSORS: 'Analoge Sensoren',
|
||||
SETTINGS: 'Instellingen',
|
||||
@@ -71,7 +70,6 @@ const nl: Translation = {
|
||||
TEMP_SENSOR: 'Temperatuur sensor',
|
||||
TEMP_SENSORS: 'Temperatuur Sensoren',
|
||||
WRITE_CMD_SENT: 'Schrijf commando sent', // TODO translate
|
||||
WRITE_CMD_FAILED: 'Schrijf commando failed', // TODO translate
|
||||
EMS_BUS_WARNING: 'EMS bus niet gevonden. Als deze waarschuwing blijft staan na een paar seconden dan loop de instellingen na en in het bijzonder het apparaat type profiel na.',
|
||||
EMS_BUS_SCANNING: 'Scannen naar EMS apparaten...',
|
||||
CONNECTED: 'Verbonden',
|
||||
|
||||
@@ -51,7 +51,6 @@ const no: Translation = {
|
||||
REMOVE: 'Fjern',
|
||||
PROBLEM_UPDATING: 'Problem med oppdatering',
|
||||
PROBLEM_LOADING: 'Problem med opplasting',
|
||||
ACCESS_DENIED: 'Tilgang nektet',
|
||||
ANALOG_SENSOR: 'Analog Sensor',
|
||||
ANALOG_SENSORS: 'Analoge Sensorer',
|
||||
SETTINGS: 'Innstillinger',
|
||||
@@ -71,7 +70,6 @@ const no: Translation = {
|
||||
TEMP_SENSOR: 'Temperatursensor',
|
||||
TEMP_SENSORS: 'Temperaturesensorer',
|
||||
WRITE_CMD_SENT: 'Skriv kommando sent',
|
||||
WRITE_CMD_FAILED: 'Skriv kommando som har feilet',
|
||||
EMS_BUS_WARNING: 'EMS bussen koblet ned. Hvis denne advarselen fortsetter etter noen f¨sekunder sjekk instillinger og prosessorkort',
|
||||
EMS_BUS_SCANNING: 'Søker etter EMS enheter...',
|
||||
CONNECTED: 'Tilkoblet',
|
||||
|
||||
@@ -51,7 +51,6 @@ const pl: BaseTranslation = {
|
||||
REMOVE: 'Usuń',
|
||||
PROBLEM_UPDATING: 'Problem z uaktualnieniem!',
|
||||
PROBLEM_LOADING: 'Problem z załadowaniem!',
|
||||
ACCESS_DENIED: 'Brak dostępu!',
|
||||
ANALOG_SENSOR: '{{u|u||ustawienia u|ustawień u}}rządzeni{{a podłączonego do EMS-ESP|e||a podłączonego do EMS-ESP|a podłączonego do EMS-ESP}}',
|
||||
ANALOG_SENSORS: 'Urządzenia podłączone do EMS-ESP',
|
||||
SETTINGS: 'ustawienia',
|
||||
@@ -71,7 +70,6 @@ const pl: BaseTranslation = {
|
||||
TEMP_SENSOR: 'czujnika temperatury',
|
||||
TEMP_SENSORS: 'Czujniki temperatury 1-Wire®',
|
||||
WRITE_CMD_SENT: 'Komenda zapisu została wysłana.',
|
||||
WRITE_CMD_FAILED: 'Komenda zapisu nie powiodła się!',
|
||||
EMS_BUS_WARNING: 'Brak połączenia z magistralą EMS. Jeśli ten błąd występuje dłużej niż kilka sekund, sprawdź ustawienia oraz profil płytki interfejsu.',
|
||||
EMS_BUS_SCANNING: 'Trwa skanowanie urządzeń na magistrali EMS...',
|
||||
CONNECTED: '{{połączono|połączenie|}}',
|
||||
|
||||
@@ -51,7 +51,6 @@ const sv: Translation = {
|
||||
REMOVE: 'Ta bort',
|
||||
PROBLEM_UPDATING: 'Problem vid uppdatering',
|
||||
PROBLEM_LOADING: 'Problem vid hämtning',
|
||||
ACCESS_DENIED: 'Åtkomst Nekad',
|
||||
ANALOG_SENSOR: 'Analog Sensor',
|
||||
ANALOG_SENSORS: 'Analoga Sensorer',
|
||||
SETTINGS: 'Inställningar',
|
||||
@@ -71,7 +70,6 @@ const sv: Translation = {
|
||||
TEMP_SENSOR: 'Temperatursensor',
|
||||
TEMP_SENSORS: 'Temperatursensorer',
|
||||
WRITE_CMD_SENT: 'Skrivkommandon skickade',
|
||||
WRITE_CMD_FAILED: 'Skrivkommandon misslyckade',
|
||||
EMS_BUS_WARNING: 'EMS-buss nedkopplad. Om denna varning kvarstår efter några sekunder, kontrollera inställningar och enhets-profil.',
|
||||
EMS_BUS_SCANNING: 'Söker efter EMS-enheter...',
|
||||
CONNECTED: 'Ansluten',
|
||||
|
||||
@@ -51,7 +51,6 @@ const tr: Translation = {
|
||||
REMOVE: 'Kaldır',
|
||||
PROBLEM_UPDATING: 'Güncelleme Sorunu',
|
||||
PROBLEM_LOADING: 'Yükleme Sorunu',
|
||||
ACCESS_DENIED: 'Erişim Reddedildi',
|
||||
ANALOG_SENSOR: 'Analog Sensör',
|
||||
ANALOG_SENSORS: 'Analog Sensörler',
|
||||
SETTINGS: 'Ayarlar',
|
||||
@@ -71,7 +70,6 @@ const tr: Translation = {
|
||||
TEMP_SENSOR: 'Sıcaklık Sensörü',
|
||||
TEMP_SENSORS: 'Sıcaklık Sensörleri',
|
||||
WRITE_CMD_SENT: 'Yazma komutu gönderildi',
|
||||
WRITE_CMD_FAILED: 'Yazma komutu başarısız oldu',
|
||||
EMS_BUS_WARNING: 'EMS hat bağlantısı kesildi. Eğer bu uyarı birkaç saniye sonra devam ediyorsa lütfen ayarları ve kart tipini kontrol edin',
|
||||
EMS_BUS_SCANNING: 'EMS cihazları aranıyor...',
|
||||
CONNECTED: 'Bağlandı',
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useRowSelect } from '@table-library/react-table-library/select';
|
||||
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react';
|
||||
|
||||
import { IconContext } from 'react-icons';
|
||||
@@ -42,27 +43,39 @@ import { formatValue } from './deviceValue';
|
||||
|
||||
import { DeviceValueUOM_s, DeviceEntityMask, DeviceType } from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
import type { Device, CoreData, DeviceData, DeviceValue } from './types';
|
||||
import type { Device, DeviceValue } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, SectionContent, MessageBox } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const DashboardDevices: FC = () => {
|
||||
const [size, setSize] = useState([0, 0]);
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const { LL } = useI18nContext();
|
||||
const [deviceData, setDeviceData] = useState<DeviceData>({ data: [] });
|
||||
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
|
||||
const [onlyFav, setOnlyFav] = useState(false);
|
||||
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
|
||||
const [showDeviceInfo, setShowDeviceInfo] = useState<boolean>(false);
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>();
|
||||
const [coreData, setCoreData] = useState<CoreData>({
|
||||
connected: true,
|
||||
devices: []
|
||||
|
||||
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
|
||||
initialData: {
|
||||
connected: true,
|
||||
devices: []
|
||||
}
|
||||
});
|
||||
|
||||
const { data: deviceData, send: readDeviceData } = useRequest((id) => EMSESP.readDeviceData(id), {
|
||||
initialData: {
|
||||
data: []
|
||||
},
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { loading: submitting, send: writeDeviceValue } = useRequest((data) => EMSESP.writeDeviceValue(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -148,7 +161,7 @@ const DashboardDevices: FC = () => {
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(0, 1fr) minmax(150px, auto) 40px;
|
||||
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
@@ -212,19 +225,10 @@ const DashboardDevices: FC = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const fetchDeviceData = async (id: number) => {
|
||||
try {
|
||||
setDeviceData((await EMSESP.readDeviceData({ id })).data);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
};
|
||||
|
||||
function onSelectChange(action: any, state: any) {
|
||||
setDeviceData({ data: [] });
|
||||
async function onSelectChange(action: any, state: any) {
|
||||
setSelectedDevice(state.id);
|
||||
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
|
||||
void fetchDeviceData(state.id);
|
||||
await readDeviceData(state.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,27 +261,14 @@ const DashboardDevices: FC = () => {
|
||||
};
|
||||
}, [escFunction]);
|
||||
|
||||
const fetchCoreData = useCallback(async () => {
|
||||
try {
|
||||
setSelectedDevice(undefined);
|
||||
setCoreData((await EMSESP.readCoreData()).data);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchCoreData();
|
||||
}, [fetchCoreData]);
|
||||
|
||||
const refreshData = () => {
|
||||
if (deviceValueDialogOpen) {
|
||||
return;
|
||||
}
|
||||
if (selectedDevice) {
|
||||
void fetchDeviceData(selectedDevice);
|
||||
void readDeviceData(selectedDevice);
|
||||
} else {
|
||||
void fetchCoreData();
|
||||
void readCoreData();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -346,27 +337,20 @@ const DashboardDevices: FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const deviceValueDialogSave = async (dv: DeviceValue) => {
|
||||
const selectedDeviceID = Number(device_select.state.id);
|
||||
try {
|
||||
const response = await EMSESP.writeDeviceValue({
|
||||
id: selectedDeviceID,
|
||||
devicevalue: dv
|
||||
});
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.WRITE_CMD_FAILED());
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||
const id = Number(device_select.state.id);
|
||||
await writeDeviceValue({ id, devicevalue })
|
||||
.then(() => {
|
||||
toast.success(LL.WRITE_CMD_SENT());
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setDeviceValueDialogOpen(false);
|
||||
await fetchDeviceData(selectedDeviceID);
|
||||
setSelectedDeviceValue(undefined);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
await readDeviceData(id);
|
||||
setSelectedDeviceValue(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
const renderDeviceDetails = () => {
|
||||
@@ -500,20 +484,11 @@ const DashboardDevices: FC = () => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{ border: '1px solid #177ac9' }}>
|
||||
<Grid container justifyContent="space-between">
|
||||
<Box color="warning.main" ml={1}>
|
||||
<Typography noWrap variant="h6">
|
||||
{coreData.devices[deviceIndex].n}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid item zeroMinWidth justifyContent="flex-end">
|
||||
<IconButton onClick={resetDeviceSelect}>
|
||||
<CancelIcon color="info" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ mx: 1 }}>
|
||||
{coreData.devices[deviceIndex].n}
|
||||
</Typography>
|
||||
|
||||
<Grid item xs>
|
||||
<Grid container justifyContent="space-between">
|
||||
<Typography sx={{ ml: 1 }} variant="subtitle2" color="primary">
|
||||
{shown_data.length + ' ' + LL.ENTITIES(shown_data.length)}
|
||||
<IconButton onClick={() => setShowDeviceInfo(true)}>
|
||||
@@ -533,6 +508,11 @@ const DashboardDevices: FC = () => {
|
||||
<RefreshIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
</Typography>
|
||||
<Grid item zeroMinWidth justifyContent="flex-end">
|
||||
<IconButton onClick={resetDeviceSelect}>
|
||||
<CancelIcon color="info" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
@@ -612,6 +592,7 @@ const DashboardDevices: FC = () => {
|
||||
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
||||
}
|
||||
validator={deviceValueItemValidation(selectedDeviceValue)}
|
||||
progress={submitting}
|
||||
/>
|
||||
)}
|
||||
<ButtonRow>
|
||||
|
||||
@@ -13,8 +13,10 @@ import {
|
||||
FormHelperText,
|
||||
Grid,
|
||||
Box,
|
||||
Typography
|
||||
Typography,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { green } from '@mui/material/colors';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
|
||||
@@ -35,6 +37,7 @@ type DashboardDevicesDialogProps = {
|
||||
selectedItem: DeviceValue;
|
||||
writeable: boolean;
|
||||
validator: Schema;
|
||||
progress: boolean;
|
||||
};
|
||||
|
||||
const DashboarDevicesDialog = ({
|
||||
@@ -43,7 +46,8 @@ const DashboarDevicesDialog = ({
|
||||
onSave,
|
||||
selectedItem,
|
||||
writeable,
|
||||
validator
|
||||
validator,
|
||||
progress
|
||||
}: DashboardDevicesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
||||
@@ -115,7 +119,8 @@ const DashboarDevicesDialog = ({
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
borderRadius: '12px'
|
||||
}
|
||||
},
|
||||
backdropFilter: 'blur(1px)'
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
@@ -184,14 +189,32 @@ const DashboarDevicesDialog = ({
|
||||
|
||||
<DialogActions>
|
||||
{writeable ? (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
'& button, & a, & .MuiCard-root': {
|
||||
mx: 0.6
|
||||
},
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
||||
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
||||
</Button>
|
||||
</>
|
||||
{progress && (
|
||||
<CircularProgress
|
||||
size={24}
|
||||
sx={{
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
right: '20%',
|
||||
marginTop: '6px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Button variant="outlined" onClick={close} color="secondary">
|
||||
{LL.CLOSE()}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { Button, Typography, Box } from '@mui/material';
|
||||
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useState, useContext, useCallback, useEffect } from 'react';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, useContext, useEffect } from 'react';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -17,24 +18,38 @@ import * as EMSESP from './api';
|
||||
|
||||
import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames } from './types';
|
||||
import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators';
|
||||
import type { SensorData, TemperatureSensor, AnalogSensor } from './types';
|
||||
import type { TemperatureSensor, AnalogSensor } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, SectionContent } from 'components';
|
||||
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const DashboardSensors: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const [sensorData, setSensorData] = useState<SensorData>({ ts: [], as: [], analog_enabled: false });
|
||||
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
|
||||
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
|
||||
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
|
||||
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
|
||||
const { data: sensorData, send: fetchSensorData } = useRequest(() => EMSESP.readSensorData(), {
|
||||
initialData: {
|
||||
ts: [],
|
||||
as: [],
|
||||
analog_enabled: false
|
||||
}
|
||||
});
|
||||
|
||||
const { send: writeTemperatureSensor } = useRequest((data) => EMSESP.writeTemperatureSensor(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: writeAnalogSensor } = useRequest((data) => EMSESP.writeAnalogSensor(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const isAdmin = me.admin;
|
||||
|
||||
const common_theme = useTheme({
|
||||
@@ -101,20 +116,6 @@ const DashboardSensors: FC = () => {
|
||||
}
|
||||
]);
|
||||
|
||||
const fetchSensorData = useCallback(async () => {
|
||||
if (!analogDialogOpen && !temperatureDialogOpen) {
|
||||
try {
|
||||
setSensorData((await EMSESP.readSensorData()).data);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}
|
||||
}, [LL, analogDialogOpen, temperatureDialogOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchSensorData();
|
||||
}, [fetchSensorData]);
|
||||
|
||||
const getSortIcon = (state: any, sortKey: any) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
return <KeyboardArrowDownOutlinedIcon />;
|
||||
@@ -229,26 +230,18 @@ const DashboardSensors: FC = () => {
|
||||
};
|
||||
|
||||
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
|
||||
try {
|
||||
const response = await EMSESP.writeTemperatureSensor({
|
||||
id: ts.id,
|
||||
name: ts.n,
|
||||
offset: ts.o
|
||||
});
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
await writeTemperatureSensor({ id: ts.id, name: ts.n, offset: ts.o })
|
||||
.then(() => {
|
||||
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setTemperatureDialogOpen(false);
|
||||
setSelectedTemperatureSensor(undefined);
|
||||
await fetchSensorData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
|
||||
})
|
||||
.finally(async () => {
|
||||
setTemperatureDialogOpen(false);
|
||||
setSelectedTemperatureSensor(undefined);
|
||||
await fetchSensorData();
|
||||
});
|
||||
};
|
||||
|
||||
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||
@@ -280,32 +273,27 @@ const DashboardSensors: FC = () => {
|
||||
};
|
||||
|
||||
const onAnalogDialogSave = async (as: AnalogSensor) => {
|
||||
try {
|
||||
const response = await EMSESP.writeAnalogSensor({
|
||||
id: as.id,
|
||||
gpio: as.g,
|
||||
name: as.n,
|
||||
offset: as.o,
|
||||
factor: as.f,
|
||||
uom: as.u,
|
||||
type: as.t,
|
||||
deleted: as.d
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
|
||||
} else if (response.status === 403) {
|
||||
toast.error(LL.ACCESS_DENIED());
|
||||
} else {
|
||||
await writeAnalogSensor({
|
||||
id: as.id,
|
||||
gpio: as.g,
|
||||
name: as.n,
|
||||
offset: as.o,
|
||||
factor: as.f,
|
||||
uom: as.u,
|
||||
type: as.t,
|
||||
deleted: as.d
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setAnalogDialogOpen(false);
|
||||
setSelectedAnalogSensor(undefined);
|
||||
await fetchSensorData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
|
||||
})
|
||||
.finally(async () => {
|
||||
setAnalogDialogOpen(false);
|
||||
setSelectedAnalogSensor(undefined);
|
||||
await fetchSensorData();
|
||||
});
|
||||
};
|
||||
|
||||
const RenderTemperatureSensors = () => (
|
||||
@@ -431,7 +419,7 @@ const DashboardSensors: FC = () => {
|
||||
{sensorData?.analog_enabled === true && (
|
||||
<>
|
||||
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
|
||||
{LL.ANALOG_SENSORS(0)}
|
||||
{LL.ANALOG_SENSORS()}
|
||||
</Typography>
|
||||
<RenderAnalogSensors />
|
||||
{selectedAnalogSensor && (
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
||||
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
|
||||
import { useRequest } from 'alova';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -32,7 +33,6 @@ import type { FC } from 'react';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage, useRest } from 'utils';
|
||||
|
||||
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||
|
||||
@@ -64,7 +64,7 @@ const showQuality = (stat: Stat) => {
|
||||
};
|
||||
|
||||
const DashboardStatus: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<Status>({ read: EMSESP.readStatus });
|
||||
const { data: data, send: loadData, error } = useRequest(EMSESP.readStatus);
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
@@ -73,6 +73,10 @@ const DashboardStatus: FC = () => {
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const stats_theme = tableTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||
@@ -158,14 +162,14 @@ const DashboardStatus: FC = () => {
|
||||
};
|
||||
|
||||
const scan = async () => {
|
||||
try {
|
||||
await EMSESP.scanDevices();
|
||||
toast.info(LL.SCANNING() + '...');
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setConfirmScan(false);
|
||||
}
|
||||
await scanDevices()
|
||||
.then(() => {
|
||||
toast.info(LL.SCANNING() + '...');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
setConfirmScan(false);
|
||||
};
|
||||
|
||||
const renderScanDialog = () => (
|
||||
@@ -185,7 +189,7 @@ const DashboardStatus: FC = () => {
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
|
||||
import { Typography, Button, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { toast } from 'react-toastify';
|
||||
import * as EMSESP from './api';
|
||||
import type { FC } from 'react';
|
||||
@@ -11,16 +12,19 @@ import type { FC } from 'react';
|
||||
import { SectionContent } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const HelpInformation: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const saveFile = (json: any, endpoint: string) => {
|
||||
const { send: API, onSuccess: onSuccessAPI } = useRequest((data) => EMSESP.API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
onSuccessAPI((event) => {
|
||||
const a = document.createElement('a');
|
||||
const filename = 'emsesp_' + endpoint + '.txt';
|
||||
const filename = 'emsesp_info.txt';
|
||||
a.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
new Blob([JSON.stringify(event.data, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
@@ -29,23 +33,12 @@ const HelpInformation: FC = () => {
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
};
|
||||
});
|
||||
|
||||
const callAPI = async (endpoint: string) => {
|
||||
try {
|
||||
const response = await EMSESP.API({
|
||||
device: 'system',
|
||||
entity: endpoint,
|
||||
id: 0
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
toast.error(LL.PROBLEM_LOADING());
|
||||
} else {
|
||||
saveFile(response.data, endpoint);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
const callAPI = async () => {
|
||||
await API({ device: 'system', entity: 'info', id: 0 }).catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -96,7 +89,7 @@ const HelpInformation: FC = () => {
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('info')}
|
||||
onClick={() => callAPI()}
|
||||
>
|
||||
{LL.SUPPORT_INFO()}
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment, TextField } from '@mui/material';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -11,6 +12,7 @@ import { createSettingsValidator } from './validators';
|
||||
import type { Settings } from './types';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import type { FC } from 'react';
|
||||
import * as SystemApi from 'api/system';
|
||||
import {
|
||||
SectionContent,
|
||||
FormLoader,
|
||||
@@ -23,7 +25,7 @@ import {
|
||||
|
||||
import RestartMonitor from 'framework/system/RestartMonitor';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, extractErrorMessage, updateValueDirty, useRest } from 'utils';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
export function boardProfileSelectItems() {
|
||||
@@ -39,7 +41,7 @@ const SettingsApplication: FC = () => {
|
||||
loadData,
|
||||
saveData,
|
||||
saving,
|
||||
setData,
|
||||
updateDataValue,
|
||||
data,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
@@ -51,39 +53,48 @@ const SettingsApplication: FC = () => {
|
||||
read: EMSESP.readSettings,
|
||||
update: EMSESP.writeSettings
|
||||
});
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const [processingBoard, setProcessingBoard] = useState<boolean>(false);
|
||||
|
||||
const updateBoardProfile = async (boardProfile: string) => {
|
||||
setProcessingBoard(true);
|
||||
try {
|
||||
const response = await EMSESP.getBoardProfile({ board_profile: boardProfile });
|
||||
if (data) {
|
||||
setData({
|
||||
...data,
|
||||
board_profile: boardProfile,
|
||||
led_gpio: response.data.led_gpio,
|
||||
dallas_gpio: response.data.dallas_gpio,
|
||||
rx_gpio: response.data.rx_gpio,
|
||||
tx_gpio: response.data.tx_gpio,
|
||||
pbutton_gpio: response.data.pbutton_gpio,
|
||||
phy_type: response.data.phy_type,
|
||||
eth_power: response.data.eth_power,
|
||||
eth_phy_addr: response.data.eth_phy_addr,
|
||||
eth_clock_mode: response.data.eth_clock_mode
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
} finally {
|
||||
setProcessingBoard(false);
|
||||
}
|
||||
const {
|
||||
loading: processingBoard,
|
||||
send: readBoardProfile,
|
||||
onSuccess: onSuccessBoardProfile
|
||||
} = useRequest((boardProfile) => EMSESP.getBoardProfile(boardProfile), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
onSuccessBoardProfile((event) => {
|
||||
const response = event.data as Settings;
|
||||
updateDataValue({
|
||||
...data,
|
||||
board_profile: response.board_profile,
|
||||
led_gpio: response.led_gpio,
|
||||
dallas_gpio: response.dallas_gpio,
|
||||
rx_gpio: response.rx_gpio,
|
||||
tx_gpio: response.tx_gpio,
|
||||
pbutton_gpio: response.pbutton_gpio,
|
||||
phy_type: response.phy_type,
|
||||
eth_power: response.eth_power,
|
||||
eth_phy_addr: response.eth_phy_addr,
|
||||
eth_clock_mode: response.eth_clock_mode
|
||||
});
|
||||
});
|
||||
|
||||
const updateBoardProfile = async (board_profile: string) => {
|
||||
await readBoardProfile(board_profile).catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
@@ -95,9 +106,10 @@ const SettingsApplication: FC = () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createSettingsValidator(data), data);
|
||||
await saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
} finally {
|
||||
await saveData();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,7 +117,7 @@ const SettingsApplication: FC = () => {
|
||||
const boardProfile = event.target.value;
|
||||
updateFormValue(event);
|
||||
if (boardProfile === 'CUSTOM') {
|
||||
setData({
|
||||
updateDataValue({
|
||||
...data,
|
||||
board_profile: boardProfile
|
||||
});
|
||||
@@ -116,12 +128,10 @@ const SettingsApplication: FC = () => {
|
||||
|
||||
const restart = async () => {
|
||||
await validateAndSubmit();
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
}
|
||||
await restartCommand().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useRequest } from 'alova';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -32,13 +33,13 @@ import SettingsCustomizationDialog from './SettingsCustomizationDialog';
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { DeviceEntityMask } from './types';
|
||||
import type { DeviceShort, Devices, DeviceEntity } from './types';
|
||||
import type { DeviceShort, DeviceEntity } from './types';
|
||||
import type { FC } from 'react';
|
||||
import { ButtonRow, FormLoader, SectionContent, MessageBox, BlockNavigation } from 'components';
|
||||
import * as SystemApi from 'api/system';
|
||||
import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
|
||||
|
||||
import RestartMonitor from 'framework/system/RestartMonitor';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
export const APIURL = window.location.origin + '/api/';
|
||||
|
||||
@@ -46,11 +47,10 @@ const SettingsCustomization: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>();
|
||||
const [devices, setDevices] = useState<Devices>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
|
||||
const [confirmReset, setConfirmReset] = useState<boolean>(false);
|
||||
const [selectedFilters, setSelectedFilters] = useState<number>(0);
|
||||
@@ -58,6 +58,31 @@ const SettingsCustomization: FC = () => {
|
||||
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { data: devices } = useRequest(EMSESP.readDevices);
|
||||
|
||||
const { send: writeCustomEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false });
|
||||
|
||||
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), {
|
||||
initialData: [],
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
|
||||
};
|
||||
|
||||
onSuccess((event) => {
|
||||
setOriginalSettings(event.data);
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const entities_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||
@@ -131,7 +156,7 @@ const SettingsCustomization: FC = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (deviceEntities) {
|
||||
if (deviceEntities.length) {
|
||||
setNumChanges(
|
||||
deviceEntities
|
||||
.filter((de) => hasEntityChanged(de))
|
||||
@@ -148,29 +173,11 @@ const SettingsCustomization: FC = () => {
|
||||
}
|
||||
}, [deviceEntities]);
|
||||
|
||||
const fetchDevices = useCallback(async () => {
|
||||
try {
|
||||
setDevices((await EMSESP.readDevices()).data);
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchDevices();
|
||||
}, [fetchDevices]);
|
||||
|
||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
|
||||
};
|
||||
|
||||
const fetchDeviceEntities = async (unique_id: number) => {
|
||||
try {
|
||||
const new_deviceEntities = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
|
||||
setOriginalSettings(new_deviceEntities);
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
const restart = async () => {
|
||||
await restartCommand().catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
function formatValue(value: any) {
|
||||
@@ -226,7 +233,7 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
const maskDisabled = (set: boolean) => {
|
||||
setDeviceEntities(
|
||||
deviceEntities?.map(function (de) {
|
||||
deviceEntities.map(function (de) {
|
||||
if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) {
|
||||
return {
|
||||
...de,
|
||||
@@ -246,31 +253,22 @@ const SettingsCustomization: FC = () => {
|
||||
const selected_device = parseInt(event.target.value, 10);
|
||||
setSelectedDevice(selected_device);
|
||||
setNumChanges(0);
|
||||
void fetchDeviceEntities(devices?.devices[selected_device].i);
|
||||
void readDeviceEntities(devices?.devices[selected_device].i);
|
||||
setRestartNeeded(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetCustomization = async () => {
|
||||
try {
|
||||
await EMSESP.resetCustomizations();
|
||||
await resetCustomizations();
|
||||
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
toast.error(error.message);
|
||||
} finally {
|
||||
setConfirmReset(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
setRestarting(true);
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
}
|
||||
};
|
||||
|
||||
const onDialogClose = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
@@ -300,7 +298,7 @@ const SettingsCustomization: FC = () => {
|
||||
const saveCustomization = async () => {
|
||||
if (devices && deviceEntities && selectedDevice !== -1) {
|
||||
const masked_entities = deviceEntities
|
||||
.filter((de) => hasEntityChanged(de))
|
||||
.filter((de: DeviceEntity) => hasEntityChanged(de))
|
||||
.map(
|
||||
(new_de) =>
|
||||
new_de.m.toString(16).padStart(2, '0') +
|
||||
@@ -318,72 +316,56 @@ const SettingsCustomization: FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await EMSESP.writeCustomEntities({
|
||||
id: devices?.devices[selectedDevice].i,
|
||||
entity_ids: masked_entities
|
||||
});
|
||||
if (response.status === 200) {
|
||||
toast.success(LL.CUSTOMIZATIONS_SAVED());
|
||||
} else if (response.status === 201) {
|
||||
setRestartNeeded(true);
|
||||
} else {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
await writeCustomEntities({ id: devices?.devices[selectedDevice].i, entity_ids: masked_entities }).catch(
|
||||
(error) => {
|
||||
if (error.message === 'Reboot required') {
|
||||
setRestartNeeded(true);
|
||||
} else {
|
||||
toast.error(error.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
}
|
||||
);
|
||||
setOriginalSettings(deviceEntities);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDeviceList = () => {
|
||||
if (!devices) {
|
||||
return <FormLoader errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||
<Typography variant="body2" mt={1}>
|
||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
name="device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedDevice}
|
||||
disabled={numChanges !== 0}
|
||||
onChange={changeSelectedDevice}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem disabled key={0} value={-1}>
|
||||
{LL.SELECT_DEVICE()}...
|
||||
const renderDeviceList = () => (
|
||||
<>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||
<Typography variant="body2" mt={1}>
|
||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
name="device"
|
||||
label={LL.EMS_DEVICE()}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedDevice}
|
||||
disabled={numChanges !== 0}
|
||||
onChange={changeSelectedDevice}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem disabled key={0} value={-1}>
|
||||
{LL.SELECT_DEVICE()}...
|
||||
</MenuItem>
|
||||
{devices.devices.map((device: DeviceShort, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
{device.s}
|
||||
</MenuItem>
|
||||
{devices.devices.map((device: DeviceShort, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
{device.s}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
))}
|
||||
</TextField>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderDeviceData = () => {
|
||||
if (!deviceEntities) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (devices?.devices.length === 0 || deviceEntities[0].id === '') {
|
||||
if (deviceEntities.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -521,7 +503,7 @@ const SettingsCustomization: FC = () => {
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DEVICE_ENTITIES()}
|
||||
</Typography>
|
||||
{renderDeviceList()}
|
||||
{devices && renderDeviceList()}
|
||||
{renderDeviceData()}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
|
||||
@@ -539,7 +521,7 @@ const SettingsCustomization: FC = () => {
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => devices && fetchDeviceEntities(devices.devices[selectedDevice].i)}
|
||||
onClick={() => devices && readDeviceEntities(devices.devices[selectedDevice].i)}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
|
||||
@@ -124,7 +124,7 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
|
||||
{LL.UPDATE(0)}
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,7 +4,9 @@ import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Typography, Box } from '@mui/material';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -18,18 +20,26 @@ import type { FC } from 'react';
|
||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const SettingsEntities: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
const [entities, setEntities] = useState<EntityItem[]>();
|
||||
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
data: entities,
|
||||
send: fetchEntities,
|
||||
error
|
||||
} = useRequest(EMSESP.readEntities, {
|
||||
initialData: [],
|
||||
force: true
|
||||
});
|
||||
|
||||
const { send: writeEntities } = useRequest((data) => EMSESP.writeEntities(data), { immediate: false });
|
||||
|
||||
function hasEntityChanged(ei: EntityItem) {
|
||||
return (
|
||||
ei.id !== ei.o_id ||
|
||||
@@ -45,12 +55,6 @@ const SettingsEntities: FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (entities) {
|
||||
setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0);
|
||||
}
|
||||
}, [entities]);
|
||||
|
||||
const entity_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px;
|
||||
@@ -105,62 +109,32 @@ const SettingsEntities: FC = () => {
|
||||
`
|
||||
});
|
||||
|
||||
const fetchEntities = useCallback(async () => {
|
||||
try {
|
||||
const response = await EMSESP.readEntities();
|
||||
setEntities(
|
||||
response.data.entities.map((ei) => ({
|
||||
...ei,
|
||||
o_id: ei.id,
|
||||
o_device_id: ei.device_id,
|
||||
o_type_id: ei.type_id,
|
||||
o_offset: ei.offset,
|
||||
o_factor: ei.factor,
|
||||
o_uom: ei.uom,
|
||||
o_value_type: ei.value_type,
|
||||
o_name: ei.name,
|
||||
o_writeable: ei.writeable,
|
||||
o_deleted: ei.deleted
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchEntities();
|
||||
}, [fetchEntities]);
|
||||
|
||||
const saveEntities = async () => {
|
||||
if (entities) {
|
||||
try {
|
||||
const response = await EMSESP.writeEntities({
|
||||
entities: entities
|
||||
.filter((ei) => !ei.deleted)
|
||||
.map((condensed_ei) => ({
|
||||
id: condensed_ei.id,
|
||||
name: condensed_ei.name,
|
||||
device_id: condensed_ei.device_id,
|
||||
type_id: condensed_ei.type_id,
|
||||
offset: condensed_ei.offset,
|
||||
factor: condensed_ei.factor,
|
||||
uom: condensed_ei.uom,
|
||||
writeable: condensed_ei.writeable,
|
||||
value_type: condensed_ei.value_type
|
||||
}))
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
toast.success(LL.ENTITIES_UPDATED());
|
||||
} else {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
}
|
||||
await writeEntities({
|
||||
entities: entities
|
||||
.filter((ei) => !ei.deleted)
|
||||
.map((condensed_ei) => ({
|
||||
id: condensed_ei.id,
|
||||
name: condensed_ei.name,
|
||||
device_id: condensed_ei.device_id,
|
||||
type_id: condensed_ei.type_id,
|
||||
offset: condensed_ei.offset,
|
||||
factor: condensed_ei.factor,
|
||||
uom: condensed_ei.uom,
|
||||
writeable: condensed_ei.writeable,
|
||||
value_type: condensed_ei.value_type
|
||||
}))
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.ENTITIES_UPDATED());
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
await fetchEntities();
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
}
|
||||
}
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
const editEntityItem = useCallback((ei: EntityItem) => {
|
||||
@@ -173,13 +147,22 @@ const SettingsEntities: FC = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const onDialogCancel = async () => {
|
||||
await fetchEntities().then(() => {
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
const onDialogSave = (updatedItem: EntityItem) => {
|
||||
setDialogOpen(false);
|
||||
if (entities && creating) {
|
||||
setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]);
|
||||
} else {
|
||||
setEntities(entities?.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei)));
|
||||
}
|
||||
|
||||
updateState('entities', (data) => {
|
||||
const new_data = creating
|
||||
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
|
||||
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
|
||||
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
||||
return new_data;
|
||||
});
|
||||
};
|
||||
|
||||
const addEntityItem = () => {
|
||||
@@ -208,14 +191,12 @@ const SettingsEntities: FC = () => {
|
||||
}
|
||||
|
||||
function showHex(value: number, digit: number) {
|
||||
return digit === 4
|
||||
? '0x' + ('000' + value.toString(16).toUpperCase()).slice(-4)
|
||||
: '0x' + ('0' + value.toString(16).toUpperCase()).slice(-2);
|
||||
return '0x' + value.toString(16).toUpperCase().padStart(digit, '0');
|
||||
}
|
||||
|
||||
const renderEntity = () => {
|
||||
if (!entities) {
|
||||
return <FormLoader errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -236,7 +217,7 @@ const SettingsEntities: FC = () => {
|
||||
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
||||
<Cell>{ei.name}</Cell>
|
||||
<Cell>{showHex(ei.device_id as number, 2)}</Cell>
|
||||
<Cell>{showHex(ei.type_id as number, 4)}</Cell>
|
||||
<Cell>{showHex(ei.type_id as number, 3)}</Cell>
|
||||
<Cell>{ei.offset}</Cell>
|
||||
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||
</Row>
|
||||
@@ -272,7 +253,7 @@ const SettingsEntities: FC = () => {
|
||||
<Box flexGrow={1}>
|
||||
{numChanges > 0 && (
|
||||
<ButtonRow>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={fetchEntities} color="secondary">
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -58,8 +58,8 @@ const SettingsEntitiesDialog = ({
|
||||
// convert to hex strings straight away
|
||||
setEditItem({
|
||||
...selectedItem,
|
||||
device_id: selectedItem.device_id.toString(16).toUpperCase().slice(-2),
|
||||
type_id: selectedItem.type_id.toString(16).toUpperCase().slice(-4)
|
||||
device_id: selectedItem.device_id.toString(16).toUpperCase(),
|
||||
type_id: selectedItem.type_id.toString(16).toUpperCase()
|
||||
});
|
||||
}
|
||||
}, [open, selectedItem]);
|
||||
|
||||
@@ -6,6 +6,8 @@ import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Typography, Divider, Stack, Button } from '@mui/material';
|
||||
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
// eslint-disable-next-line import/named
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -19,23 +21,31 @@ import type { FC } from 'react';
|
||||
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
|
||||
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { extractErrorMessage } from 'utils';
|
||||
|
||||
const SettingsScheduler: FC = () => {
|
||||
const { LL, locale } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
const [schedule, setSchedule] = useState<ScheduleItem[]>([]);
|
||||
const [selectedScheduleItem, setSelectedScheduleItem] = useState<ScheduleItem>();
|
||||
const [dow, setDow] = useState<string[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
data: schedule,
|
||||
send: fetchSchedule,
|
||||
error
|
||||
} = useRequest(EMSESP.readSchedule, {
|
||||
initialData: [],
|
||||
force: true
|
||||
});
|
||||
|
||||
const { send: writeSchedule } = useRequest((data) => EMSESP.writeSchedule(data), { immediate: false });
|
||||
|
||||
function hasScheduleChanged(si: ScheduleItem) {
|
||||
return (
|
||||
si.id !== si.o_id ||
|
||||
(si?.name || '') !== (si?.o_name || '') ||
|
||||
(si.name || '') !== (si.o_name || '') ||
|
||||
si.active !== si.o_active ||
|
||||
si.deleted !== si.o_deleted ||
|
||||
si.flags !== si.o_flags ||
|
||||
@@ -46,10 +56,13 @@ const SettingsScheduler: FC = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule) {
|
||||
setNumChanges(schedule ? schedule.filter((si) => hasScheduleChanged(si)).length : 0);
|
||||
}
|
||||
}, [schedule]);
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
|
||||
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
||||
const dd = day < 10 ? `0${day}` : day;
|
||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||
});
|
||||
setDow(days.map((date) => formatter.format(date)));
|
||||
}, [locale]);
|
||||
|
||||
const schedule_theme = useTheme({
|
||||
Table: `
|
||||
@@ -96,63 +109,30 @@ const SettingsScheduler: FC = () => {
|
||||
`
|
||||
});
|
||||
|
||||
const fetchSchedule = useCallback(async () => {
|
||||
try {
|
||||
const response = await EMSESP.readSchedule();
|
||||
setSchedule(
|
||||
response.data.schedule.map((si) => ({
|
||||
...si,
|
||||
o_id: si.id,
|
||||
o_active: si.active,
|
||||
o_deleted: si.deleted,
|
||||
o_flags: si.flags,
|
||||
o_time: si.time,
|
||||
o_cmd: si.cmd,
|
||||
o_value: si.value,
|
||||
o_name: si.name
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||
}
|
||||
}, [LL]);
|
||||
|
||||
useEffect(() => {
|
||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
|
||||
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
||||
const dd = day < 10 ? `0${day}` : day;
|
||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||
});
|
||||
setDow(days.map((date) => formatter.format(date)));
|
||||
void fetchSchedule();
|
||||
}, [locale, fetchSchedule]);
|
||||
|
||||
const saveSchedule = async () => {
|
||||
if (schedule) {
|
||||
try {
|
||||
const response = await EMSESP.writeSchedule({
|
||||
schedule: schedule
|
||||
.filter((si) => !si.deleted)
|
||||
.map((condensed_si) => ({
|
||||
id: condensed_si.id,
|
||||
active: condensed_si.active,
|
||||
flags: condensed_si.flags,
|
||||
time: condensed_si.time,
|
||||
cmd: condensed_si.cmd,
|
||||
value: condensed_si.value,
|
||||
name: condensed_si.name
|
||||
}))
|
||||
});
|
||||
if (response.status === 200) {
|
||||
toast.success(LL.SCHEDULE_UPDATED());
|
||||
} else {
|
||||
toast.error(LL.PROBLEM_UPDATING());
|
||||
}
|
||||
await writeSchedule({
|
||||
schedule: schedule
|
||||
.filter((si) => !si.deleted)
|
||||
.map((condensed_si) => ({
|
||||
id: condensed_si.id,
|
||||
active: condensed_si.active,
|
||||
flags: condensed_si.flags,
|
||||
time: condensed_si.time,
|
||||
cmd: condensed_si.cmd,
|
||||
value: condensed_si.value,
|
||||
name: condensed_si.name
|
||||
}))
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(LL.SCHEDULE_UPDATED());
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(async () => {
|
||||
await fetchSchedule();
|
||||
} catch (error) {
|
||||
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
|
||||
}
|
||||
}
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
const editScheduleItem = useCallback((si: ScheduleItem) => {
|
||||
@@ -165,13 +145,22 @@ const SettingsScheduler: FC = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const onDialogCancel = async () => {
|
||||
await fetchSchedule().then(() => {
|
||||
setNumChanges(0);
|
||||
});
|
||||
};
|
||||
|
||||
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||
setDialogOpen(false);
|
||||
if (schedule && creating) {
|
||||
setSchedule([...schedule.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]);
|
||||
} else {
|
||||
setSchedule(schedule?.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si)));
|
||||
}
|
||||
|
||||
updateState('schedule', (data) => {
|
||||
const new_data = creating
|
||||
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]
|
||||
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si));
|
||||
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
||||
return new_data;
|
||||
});
|
||||
};
|
||||
|
||||
const addScheduleItem = () => {
|
||||
@@ -191,7 +180,7 @@ const SettingsScheduler: FC = () => {
|
||||
|
||||
const renderSchedule = () => {
|
||||
if (!schedule) {
|
||||
return <FormLoader errorMessage={errorMessage} />;
|
||||
return <FormLoader onRetry={fetchSchedule} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
const dayBox = (si: ScheduleItem, flag: number) => (
|
||||
@@ -270,7 +259,7 @@ const SettingsScheduler: FC = () => {
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
selectedItem={selectedScheduleItem}
|
||||
validator={schedulerItemValidation()}
|
||||
validator={schedulerItemValidation(schedule, selectedScheduleItem)}
|
||||
dow={dow}
|
||||
/>
|
||||
)}
|
||||
@@ -279,7 +268,7 @@ const SettingsScheduler: FC = () => {
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={fetchSchedule} color="secondary">
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -215,7 +215,7 @@ const SettingsSchedulerDialog = ({
|
||||
/>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.VALUE(0)}
|
||||
label={LL.VALUE(1)}
|
||||
multiline
|
||||
margin="normal"
|
||||
fullWidth
|
||||
|
||||
@@ -1,121 +1,107 @@
|
||||
import type {
|
||||
BoardProfile,
|
||||
BoardProfileName,
|
||||
APIcall,
|
||||
Settings,
|
||||
Status,
|
||||
CoreData,
|
||||
Devices,
|
||||
DeviceData,
|
||||
DeviceEntity,
|
||||
UniqueID,
|
||||
CustomEntities,
|
||||
WriteDeviceValue,
|
||||
WriteTemperatureSensor,
|
||||
WriteAnalogSensor,
|
||||
SensorData,
|
||||
Schedule,
|
||||
Entities
|
||||
Entities,
|
||||
DeviceData,
|
||||
ScheduleItem,
|
||||
EntityItem
|
||||
} from './types';
|
||||
import type { AxiosPromise } from 'axios';
|
||||
import { AXIOS, AXIOS_API, AXIOS_BIN } from 'api/endpoints';
|
||||
import { alovaInstance } from 'api/endpoints';
|
||||
|
||||
export function restart(): AxiosPromise<void> {
|
||||
return AXIOS.post('/restart');
|
||||
}
|
||||
// DashboardDevices
|
||||
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
|
||||
export const readDeviceData = (id: number) =>
|
||||
alovaInstance.Get<DeviceData>('/rest/deviceData', {
|
||||
params: { id },
|
||||
responseType: 'arraybuffer' // uses msgpack
|
||||
});
|
||||
export const writeDeviceValue = (data: any) => alovaInstance.Post('/rest/writeDeviceValue', data);
|
||||
|
||||
export function readSettings(): AxiosPromise<Settings> {
|
||||
return AXIOS.get('/settings');
|
||||
}
|
||||
// SettingsApplication
|
||||
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
|
||||
export const writeSettings = (data: any) => alovaInstance.Post('/rest/settings', data);
|
||||
export const getBoardProfile = (boardProfile: string) =>
|
||||
alovaInstance.Get('/rest/boardProfile', {
|
||||
params: { boardProfile }
|
||||
});
|
||||
|
||||
export function writeSettings(settings: Settings): AxiosPromise<Settings> {
|
||||
return AXIOS.post('/settings', settings);
|
||||
}
|
||||
// DashboardSensors
|
||||
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
|
||||
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
|
||||
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
|
||||
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as);
|
||||
|
||||
export function getBoardProfile(boardProfile: BoardProfileName): AxiosPromise<BoardProfile> {
|
||||
return AXIOS.post('/boardProfile', boardProfile);
|
||||
}
|
||||
// DashboardStatus
|
||||
export const readStatus = () => alovaInstance.Get<Status>('/rest/status');
|
||||
export const scanDevices = () => alovaInstance.Post('/rest/scanDevices');
|
||||
|
||||
export function readStatus(): AxiosPromise<Status> {
|
||||
return AXIOS.get('/status');
|
||||
}
|
||||
// HelpInformation
|
||||
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
|
||||
|
||||
export function readCoreData(): AxiosPromise<CoreData> {
|
||||
return AXIOS.get('/coreData');
|
||||
}
|
||||
// UploadFileForm
|
||||
export const getSettings = () => alovaInstance.Get('/rest/getSettings');
|
||||
export const getCustomizations = () => alovaInstance.Get('/rest/getCustomizations');
|
||||
export const getEntities = () => alovaInstance.Get<Entities>('/rest/getEntities');
|
||||
export const getSchedule = () => alovaInstance.Get('/rest/getSchedule');
|
||||
|
||||
export function readDevices(): AxiosPromise<Devices> {
|
||||
return AXIOS.get('/devices');
|
||||
}
|
||||
// SettingsCustomization
|
||||
export const readDeviceEntities = (id: number) =>
|
||||
alovaInstance.Get<DeviceEntity[]>('/rest/deviceEntities', {
|
||||
params: { id },
|
||||
responseType: 'arraybuffer',
|
||||
transformData(data: any) {
|
||||
return data.map((de: DeviceEntity) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }));
|
||||
}
|
||||
});
|
||||
export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices');
|
||||
export const resetCustomizations = () => alovaInstance.Post('/rest/resetCustomizations');
|
||||
export const writeCustomEntities = (data: any) => alovaInstance.Post('/rest/customEntities', data);
|
||||
|
||||
export function scanDevices(): AxiosPromise<void> {
|
||||
return AXIOS.post('/scanDevices');
|
||||
}
|
||||
// SettingsScheduler
|
||||
export const readSchedule = () =>
|
||||
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
|
||||
name: 'schedule',
|
||||
transformData(data: any) {
|
||||
return data.schedule.map((si: ScheduleItem) => ({
|
||||
...si,
|
||||
o_id: si.id,
|
||||
o_active: si.active,
|
||||
o_deleted: si.deleted,
|
||||
o_flags: si.flags,
|
||||
o_time: si.time,
|
||||
o_cmd: si.cmd,
|
||||
o_value: si.value,
|
||||
o_name: si.name
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeSchedule = (data: any) => alovaInstance.Post('/rest/schedule', data);
|
||||
|
||||
export function readDeviceData(unique_id: UniqueID): AxiosPromise<DeviceData> {
|
||||
return AXIOS_BIN.post('/deviceData', unique_id);
|
||||
}
|
||||
|
||||
export function readSensorData(): AxiosPromise<SensorData> {
|
||||
return AXIOS.get('/sensorData');
|
||||
}
|
||||
|
||||
export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEntity[]> {
|
||||
return AXIOS_BIN.post('/deviceEntities', unique_id);
|
||||
}
|
||||
|
||||
export function writeCustomEntities(customEntities: CustomEntities): AxiosPromise<void> {
|
||||
return AXIOS.post('/customEntities', customEntities);
|
||||
}
|
||||
|
||||
export function writeDeviceValue(dv: WriteDeviceValue): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeDeviceValue', dv);
|
||||
}
|
||||
|
||||
export function writeTemperatureSensor(ts: WriteTemperatureSensor): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeTemperatureSensor', ts);
|
||||
}
|
||||
|
||||
export function writeAnalogSensor(as: WriteAnalogSensor): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeAnalogSensor', as);
|
||||
}
|
||||
|
||||
export function resetCustomizations(): AxiosPromise<void> {
|
||||
return AXIOS.post('/resetCustomizations');
|
||||
}
|
||||
|
||||
export function API(apiCall: APIcall): AxiosPromise<void> {
|
||||
return AXIOS_API.post('/', apiCall);
|
||||
}
|
||||
|
||||
export function getSettings(): AxiosPromise<void> {
|
||||
return AXIOS.get('/getSettings');
|
||||
}
|
||||
|
||||
export function getCustomizations(): AxiosPromise<void> {
|
||||
return AXIOS.get('/getCustomizations');
|
||||
}
|
||||
|
||||
export function getSchedule(): AxiosPromise<Schedule> {
|
||||
return AXIOS.get('/getSchedule');
|
||||
}
|
||||
|
||||
export function readSchedule(): AxiosPromise<Schedule> {
|
||||
return AXIOS.get('/schedule');
|
||||
}
|
||||
|
||||
export function writeSchedule(schedule: Schedule): AxiosPromise<void> {
|
||||
return AXIOS.post('/schedule', schedule);
|
||||
}
|
||||
|
||||
export function getEntities(): AxiosPromise<Entities> {
|
||||
return AXIOS.get('/getEntities');
|
||||
}
|
||||
|
||||
export function readEntities(): AxiosPromise<Entities> {
|
||||
return AXIOS.get('/entities');
|
||||
}
|
||||
|
||||
export function writeEntities(entities: Entities): AxiosPromise<void> {
|
||||
return AXIOS.post('/entities', entities);
|
||||
}
|
||||
// SettingsEntities
|
||||
export const readEntities = () =>
|
||||
alovaInstance.Get<EntityItem[]>('/rest/entities', {
|
||||
name: 'entities',
|
||||
transformData(data: any) {
|
||||
return data.entities.map((ei: EntityItem) => ({
|
||||
...ei,
|
||||
o_id: ei.id,
|
||||
o_device_id: ei.device_id,
|
||||
o_type_id: ei.type_id,
|
||||
o_offset: ei.offset,
|
||||
o_factor: ei.factor,
|
||||
o_uom: ei.uom,
|
||||
o_value_type: ei.value_type,
|
||||
o_name: ei.name,
|
||||
o_writeable: ei.writeable,
|
||||
o_deleted: ei.deleted
|
||||
}));
|
||||
}
|
||||
});
|
||||
export const writeEntities = (data: any) => alovaInstance.Post('/rest/entities', data);
|
||||
|
||||
@@ -131,7 +131,6 @@ export interface DeviceValue {
|
||||
m?: number; // min, optional
|
||||
x?: number; // max, optional
|
||||
}
|
||||
|
||||
export interface DeviceData {
|
||||
data: DeviceValue[];
|
||||
}
|
||||
@@ -151,15 +150,6 @@ export interface DeviceEntity {
|
||||
o_ma?: number; // original max value
|
||||
}
|
||||
|
||||
export interface CustomEntities {
|
||||
id: number;
|
||||
entity_ids: string[];
|
||||
}
|
||||
|
||||
export interface UniqueID {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export enum DeviceValueUOM {
|
||||
NONE = 0,
|
||||
DEGREES,
|
||||
@@ -256,10 +246,6 @@ export const BOARD_PROFILES: BoardProfiles = {
|
||||
S3MINI: 'Liligo S3'
|
||||
};
|
||||
|
||||
export interface BoardProfileName {
|
||||
board_profile: string;
|
||||
}
|
||||
|
||||
export interface BoardProfile {
|
||||
board_profile: string;
|
||||
led_gpio: number;
|
||||
@@ -278,12 +264,6 @@ export interface APIcall {
|
||||
entity: string;
|
||||
id: any;
|
||||
}
|
||||
|
||||
export interface WriteDeviceValue {
|
||||
id: number;
|
||||
devicevalue: DeviceValue;
|
||||
}
|
||||
|
||||
export interface WriteAnalogSensor {
|
||||
id: number;
|
||||
gpio: number;
|
||||
@@ -312,7 +292,7 @@ export interface ScheduleItem {
|
||||
time: string;
|
||||
cmd: string;
|
||||
value: string;
|
||||
name?: string; // optional
|
||||
name: string; // optional
|
||||
o_id?: number;
|
||||
o_active?: boolean;
|
||||
o_deleted?: boolean;
|
||||
@@ -323,10 +303,6 @@ export interface ScheduleItem {
|
||||
o_name?: string;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
schedule: ScheduleItem[];
|
||||
}
|
||||
|
||||
export enum ScheduleFlag {
|
||||
SCHEDULE_SUN = 1,
|
||||
SCHEDULE_MON = 2,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Schema from 'async-validator';
|
||||
import type { AnalogSensor, DeviceValue, Settings } from './types';
|
||||
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
|
||||
import type { InternalRuleItem } from 'async-validator';
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
||||
|
||||
@@ -86,14 +86,26 @@ export const createSettingsValidator = (settings: Settings) =>
|
||||
})
|
||||
});
|
||||
|
||||
export const schedulerItemValidation = () =>
|
||||
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
|
||||
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
|
||||
if ((o_name === undefined || o_name !== name) && schedule.find((si) => si.name === name)) {
|
||||
callback('Name already in use');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
|
||||
new Schema({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
pattern: /^[a-zA-Z0-9_\\.]{0,15}$/,
|
||||
message: "Must be <15 characters: alpha numeric, '_' or '.'"
|
||||
}
|
||||
},
|
||||
...[uniqueNameValidator(schedule, scheduleItem.o_name)]
|
||||
],
|
||||
cmd: [
|
||||
{ required: true, message: 'Command is required' },
|
||||
|
||||
@@ -42,10 +42,6 @@ export interface LogEntry {
|
||||
m: string;
|
||||
}
|
||||
|
||||
export interface LogEntries {
|
||||
events: LogEntry[];
|
||||
}
|
||||
|
||||
export interface LogSettings {
|
||||
level: number;
|
||||
max_messages: number;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
|
||||
|
||||
export const numberValue = (value: number) => (isNaN(value) ? '' : value.toString());
|
||||
|
||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -13,6 +11,8 @@ export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
}
|
||||
};
|
||||
|
||||
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
|
||||
|
||||
export const updateValue =
|
||||
<S>(updateEntity: UpdateEntity<S>) =>
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -23,11 +23,12 @@ export const updateValue =
|
||||
};
|
||||
|
||||
export const updateValueDirty =
|
||||
<S>(origData: any, dirtyFlags: any, setDirtyFlags: any, updateEntity: UpdateEntity<S>) =>
|
||||
(origData: any, dirtyFlags: any, setDirtyFlags: any, updateDataValue: any) =>
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updated_value = extractEventValue(event);
|
||||
const name = event.target.name;
|
||||
updateEntity((prevState) => ({
|
||||
|
||||
updateDataValue((prevState) => ({
|
||||
...prevState,
|
||||
[name]: updated_value
|
||||
}));
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './binding';
|
||||
export * from './endpoints';
|
||||
export * from './route';
|
||||
export * from './submit';
|
||||
export * from './time';
|
||||
|
||||
@@ -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<D> {
|
||||
read: () => AxiosPromise<D>;
|
||||
update?: (value: D) => AxiosPromise<D>;
|
||||
export interface RestRequestOptions2<D> {
|
||||
read: () => Method<any, any, any, any, any, any, any>;
|
||||
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 [data, setData] = useState<D>();
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
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 [dirtyFlags, setDirtyFlags] = useState<string[]>([]);
|
||||
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 === 202) {
|
||||
setRestartNeeded(true);
|
||||
} 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,
|
||||
|
||||
Reference in New Issue
Block a user