This commit is contained in:
proddy
2024-08-08 12:39:48 +02:00
parent dc53ff42f6
commit 3481a879c2
59 changed files with 259 additions and 453 deletions

View File

@@ -21,6 +21,7 @@ export default tseslint.config(
{ {
rules: { rules: {
'@typescript-eslint/no-unsafe-enum-comparison': 'off', '@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-misused-promises': [ '@typescript-eslint/no-misused-promises': [
'error', 'error',

View File

@@ -33,6 +33,7 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"preact": "^10.23.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
@@ -57,14 +58,13 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"eslint": "^9.8.0", "eslint": "^9.8.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"preact": "^10.23.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"terser": "^5.31.3", "terser": "^5.31.5",
"typescript-eslint": "8.0.1", "typescript-eslint": "8.0.1",
"vite": "^5.3.5", "vite": "^5.4.0",
"vite-plugin-imagemin": "^0.6.1", "vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^4.3.2" "vite-tsconfig-paths": "^5.0.0"
}, },
"packageManager": "yarn@4.2.1" "packageManager": "yarn@4.2.1"
} }

View File

@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { FC } from 'react';
import { Slide, ToastContainer } from 'react-toastify'; import { Slide, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css'; import 'react-toastify/dist/ReactToastify.min.css';
@@ -12,7 +11,7 @@ import { localStorageDetector } from 'typesafe-i18n/detectors';
const detectedLocale = detectLocale(localStorageDetector); const detectedLocale = detectLocale(localStorageDetector);
const App: FC = () => { const App = () => {
const [wasLoaded, setWasLoaded] = useState(false); const [wasLoaded, setWasLoaded] = useState(false);
useEffect(() => { useEffect(() => {

View File

@@ -37,7 +37,7 @@ export const RemoveTrailingSlashes = () => {
); );
}; };
const AppRouting: FC = () => { const AppRouting = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
return ( return (

View File

@@ -1,8 +1,8 @@
import { type FC, useContext } from 'react'; import { useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import CustomEntities from 'app/main/CustomEntities'; import CustomEntities from 'app/main/CustomEntities';
import Customization from 'app/main/Customization'; import Customizations from 'app/main/Customizations';
import Devices from 'app/main/Devices'; import Devices from 'app/main/Devices';
import Help from 'app/main/Help'; import Help from 'app/main/Help';
import Modules from 'app/main/Modules'; import Modules from 'app/main/Modules';
@@ -27,7 +27,7 @@ import SystemLog from 'app/status/SystemLog';
import { Layout } from 'components'; import { Layout } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
const AuthenticatedRouting: FC = () => { const AuthenticatedRouting = () => {
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
return ( return (
<Layout> <Layout>
@@ -59,7 +59,7 @@ const AuthenticatedRouting: FC = () => {
<Route path="/settings/network/*" element={<Network />} /> <Route path="/settings/network/*" element={<Network />} />
<Route path="/settings/security/*" element={<Security />} /> <Route path="/settings/security/*" element={<Security />} />
<Route path="/customizations" element={<Customization />} /> <Route path="/customizations" element={<Customizations />} />
<Route path="/scheduler" element={<Scheduler />} /> <Route path="/scheduler" element={<Scheduler />} />
<Route path="/customentities" element={<CustomEntities />} /> <Route path="/customentities" element={<CustomEntities />} />
</> </>

View File

@@ -1,5 +1,4 @@
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import ForwardIcon from '@mui/icons-material/Forward'; import ForwardIcon from '@mui/icons-material/Forward';
@@ -21,7 +20,7 @@ import type { SignInRequest } from 'types';
import { onEnterCallback, updateValue } from 'utils'; import { onEnterCallback, updateValue } from 'utils';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators'; import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
const SignIn: FC = () => { const SignIn = () => {
const authenticationContext = useContext(AuthenticationContext); const authenticationContext = useContext(AuthenticationContext);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -33,14 +32,12 @@ const SignIn: FC = () => {
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { send: callSignIn, onSuccess } = useRequest( const { send: callSignIn } = useRequest(
(request: SignInRequest) => AuthenticationApi.signIn(request), (request: SignInRequest) => AuthenticationApi.signIn(request),
{ {
immediate: false immediate: false
} }
); ).onSuccess((response) => {
onSuccess((response) => {
if (response.data) { if (response.data) {
authenticationContext.signIn(response.data.access_token); authenticationContext.signIn(response.data.access_token);
} }

View File

@@ -7,12 +7,9 @@ export const readNetworkStatus = () =>
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks'); export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
export const listNetworks = () => export const listNetworks = () =>
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', { alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
name: 'listNetworks',
timeout: 20000 // timeout 20 seconds timeout: 20000 // timeout 20 seconds
}); });
export const readNetworkSettings = () => export const readNetworkSettings = () =>
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings', { alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings');
name: 'networkSettings'
});
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) => export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings); alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);

View File

@@ -4,10 +4,9 @@ import { alovaInstance } from './endpoints';
export const readNTPStatus = () => export const readNTPStatus = () =>
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus'); alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
export const readNTPSettings = () => export const readNTPSettings = () =>
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', { alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {});
name: 'ntpSettings'
});
export const updateNTPSettings = (data: NTPSettingsType) => export const updateNTPSettings = (data: NTPSettingsType) =>
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data); alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom'; import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -30,13 +29,13 @@ import {
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog'; import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
import { readCustomEntities, writeCustomEntities } from './api';
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types'; import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import type { Entities, EntityItem } from './types'; import type { Entities, EntityItem } from './types';
import { entityItemValidation } from './validators'; import { entityItemValidation } from './validators';
const CustomEntities: FC = () => { const CustomEntities = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
@@ -50,13 +49,12 @@ const CustomEntities: FC = () => {
data: entities, data: entities,
send: fetchEntities, send: fetchEntities,
error error
} = useRequest(EMSESP.readCustomEntities, { } = useRequest(readCustomEntities, {
initialData: [], initialData: []
force: true
}); });
const { send: writeEntities } = useRequest( const { send: writeEntities } = useRequest(
(data: Entities) => EMSESP.writeCustomEntities(data), (data: Entities) => writeCustomEntities(data),
{ immediate: false } { immediate: false }
); );
@@ -182,7 +180,7 @@ const CustomEntities: FC = () => {
const onDialogSave = (updatedItem: EntityItem) => { const onDialogSave = (updatedItem: EntityItem) => {
setDialogOpen(false); setDialogOpen(false);
updateState('entities', (data: EntityItem[]) => { void updateState(readCustomEntities(), (data: EntityItem[]) => {
const new_data = creating const new_data = creating
? [ ? [
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), ...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),

View File

@@ -291,7 +291,11 @@ const CustomEntitiesDialog = ({
fullWidth fullWidth
margin="normal" margin="normal"
type="number" type="number"
inputProps={{ min: '1', max: String(256 - editItem.offset), step: '1' }} inputProps={{
min: '1',
max: String(256 - editItem.offset),
step: '1'
}}
/> />
</Grid> </Grid>
)} )}

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { useBlocker, useLocation } from 'react-router-dom'; import { useBlocker, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -52,7 +51,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api'; import * as EMSESP from './api';
import SettingsCustomizationDialog from './CustomizationDialog'; import SettingsCustomizationsDialog from './CustomizationsDialog';
import EntityMaskToggle from './EntityMaskToggle'; import EntityMaskToggle from './EntityMaskToggle';
import OptionIcon from './OptionIcon'; import OptionIcon from './OptionIcon';
import { DeviceEntityMask } from './types'; import { DeviceEntityMask } from './types';
@@ -60,7 +59,7 @@ import type { DeviceEntity, DeviceShort } from './types';
export const APIURL = window.location.origin + '/api/'; export const APIURL = window.location.origin + '/api/';
const Customization: FC = () => { const Customizations = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
@@ -106,13 +105,15 @@ const Customization: FC = () => {
} }
); );
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest( const { send: readDeviceEntities } = useRequest(
(data: number) => EMSESP.readDeviceEntities(data), (data: number) => EMSESP.readDeviceEntities(data),
{ {
initialData: [], initialData: [],
immediate: false immediate: false
} }
); ).onSuccess((event) => {
setOriginalSettings(event.data);
});
const setOriginalSettings = (data: DeviceEntity[]) => { const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities( setDeviceEntities(
@@ -126,10 +127,6 @@ const Customization: FC = () => {
); );
}; };
onSuccess((event) => {
setOriginalSettings(event.data);
});
const { send: restartCommand } = useRequest(SystemApi.restart(), { const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false immediate: false
}); });
@@ -727,7 +724,7 @@ const Customization: FC = () => {
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : renderContent()} {restarting ? <RestartMonitor /> : renderContent()}
{selectedDeviceEntity && ( {selectedDeviceEntity && (
<SettingsCustomizationDialog <SettingsCustomizationsDialog
open={dialogOpen} open={dialogOpen}
onClose={onDialogClose} onClose={onDialogClose}
onSave={onDialogSave} onSave={onDialogSave}
@@ -738,4 +735,4 @@ const Customization: FC = () => {
); );
}; };
export default Customization; export default Customizations;

View File

@@ -23,19 +23,19 @@ import EntityMaskToggle from './EntityMaskToggle';
import { DeviceEntityMask } from './types'; import { DeviceEntityMask } from './types';
import type { DeviceEntity } from './types'; import type { DeviceEntity } from './types';
interface SettingsCustomizationDialogProps { interface SettingsCustomizationsDialogProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSave: (di: DeviceEntity) => void; onSave: (di: DeviceEntity) => void;
selectedItem: DeviceEntity; selectedItem: DeviceEntity;
} }
const CustomizationDialog = ({ const CustomizationsDialog = ({
open, open,
onClose, onClose,
onSave, onSave,
selectedItem selectedItem
}: SettingsCustomizationDialogProps) => { }: SettingsCustomizationsDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem); const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
@@ -175,4 +175,4 @@ const CustomizationDialog = ({
); );
}; };
export default CustomizationDialog; export default CustomizationsDialog;

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai'; import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
import { CgSmartHomeBoiler } from 'react-icons/cg'; import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa'; import { FaSolarPanel } from 'react-icons/fa';
@@ -16,11 +15,7 @@ import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import { DeviceType } from './types'; import { DeviceType } from './types';
interface DeviceIconProps { export default function DeviceIcon({ type_id }) {
type_id: number;
}
const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
switch (type_id as DeviceType) { switch (type_id as DeviceType) {
case DeviceType.TEMPERATURESENSOR: case DeviceType.TEMPERATURESENSOR:
case DeviceType.ANALOGSENSOR: case DeviceType.ANALOGSENSOR:
@@ -60,6 +55,4 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
default: default:
return null; return null;
} }
}; }
export default DeviceIcon;

View File

@@ -5,7 +5,6 @@ import {
useLayoutEffect, useLayoutEffect,
useState useState
} from 'react'; } from 'react';
import type { FC } from 'react';
import { IconContext } from 'react-icons'; import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -70,7 +69,7 @@ import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
import type { Device, DeviceValue } from './types'; import type { Device, DeviceValue } from './types';
import { deviceValueItemValidation } from './validators'; import { deviceValueItemValidation } from './validators';
const Devices: FC = () => { const Devices = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CommentIcon from '@mui/icons-material/CommentTwoTone'; import CommentIcon from '@mui/icons-material/CommentTwoTone';
@@ -21,27 +20,19 @@ import {
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import * as EMSESP from 'app/main/api'; import * as EMSESP from 'app/main/api';
import { useAutoRequest, useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import { SectionContent, useLayoutTitle } from 'components'; import { SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { APIcall } from './types'; import type { APIcall } from './types';
const Help: FC = () => { const Help = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.HELP_OF('')); useLayoutTitle(LL.HELP_OF(''));
const { send: getAPI, onSuccess: onGetAPI } = useRequest( const { send: getAPI } = useRequest((data: APIcall) => EMSESP.API(data), {
(data: APIcall) => EMSESP.API(data),
{
immediate: false immediate: false
} }).onSuccess((event) => {
);
// TODO check useAutoRequest - https://alova.js.org/tutorial/client/strategy/use-auto-request/#basic-usage
const { data, loading } = useAutoRequest(SystemApi.readSystemStatus);
onGetAPI((event) => {
const anchor = document.createElement('a'); const anchor = document.createElement('a');
anchor.href = URL.createObjectURL( anchor.href = URL.createObjectURL(
new Blob([JSON.stringify(event.data, null, 2)], { new Blob([JSON.stringify(event.data, null, 2)], {
@@ -56,14 +47,16 @@ const Help: FC = () => {
toast.info(LL.DOWNLOAD_SUCCESSFUL()); toast.info(LL.DOWNLOAD_SUCCESSFUL());
}); });
const { data, loading } = useRequest(SystemApi.readSystemStatus);
const callAPI = async (device: string, entity: string) => { const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error: Error) => { await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
// TODO remove debug testing useRequest preact hook
console.log('loading: ' + loading + ' data2: ' + data); console.log('loading: ' + loading + ' data2: ' + data);
if (loading) { if (loading) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom'; import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -8,8 +7,6 @@ import CircleIcon from '@mui/icons-material/Circle';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Typography } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
import { alovaInstance } from 'api/endpoints';
import { import {
Body, Body,
Cell, Cell,
@@ -30,11 +27,11 @@ import {
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import ModulesDialog from './ModulesDialog'; import ModulesDialog from './ModulesDialog';
import { readModules, writeModules } from './api';
import type { ModuleItem, Modules } from './types'; import type { ModuleItem, Modules } from './types';
const Modules: FC = () => { const Modules = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
@@ -46,13 +43,12 @@ const Modules: FC = () => {
data: modules, data: modules,
send: fetchModules, send: fetchModules,
error error
} = useRequest(EMSESP.readModules, { } = useRequest(readModules, {
initialData: [] initialData: []
}); });
const { send: writeModules } = useRequest( const { send: updateModules } = useRequest(
(data: { key: string; enabled: boolean; license: string }) => (data: { key: string; enabled: boolean; license: string }) => writeModules(data),
EMSESP.writeModules(data),
{ {
immediate: false immediate: false
} }
@@ -124,23 +120,18 @@ const Modules: FC = () => {
return mi.enabled !== mi.o_enabled || mi.license !== mi.o_license; return mi.enabled !== mi.o_enabled || mi.license !== mi.o_license;
} }
// TODO example of how to use updateState
// TODO see https://alova.js.org/api/states/#updatestate
const updateModuleItem = (updatedItem: ModuleItem) => { const updateModuleItem = (updatedItem: ModuleItem) => {
updateState<ModuleItem[]>( void updateState(readModules(), (data: ModuleItem[]) => {
[alovaInstance.snapshots.match('modules', true)] as any,
(data: ModuleItem[]) => {
const new_data = data.map((mi) => const new_data = data.map((mi) =>
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
); );
setNumChanges(new_data.filter((mi) => hasModulesChanged(mi)).length); setNumChanges(new_data.filter((mi) => hasModulesChanged(mi)).length);
return new_data; return new_data;
} });
);
}; };
const saveModules = async () => { const saveModules = async () => {
await writeModules({ await updateModules({
modules: modules.map((condensed_mi) => ({ modules: modules.map((condensed_mi) => ({
key: condensed_mi.key, key: condensed_mi.key,
enabled: condensed_mi.enabled, enabled: condensed_mi.enabled,

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
@@ -32,18 +30,11 @@ const OPTION_ICONS: {
favorite: [StarIcon, StarOutlineIcon] favorite: [StarIcon, StarOutlineIcon]
}; };
interface OptionIconProps { export default function OptionIcon({ type, isSet }) {
type: OptionType;
isSet: boolean;
}
const OptionIcon: FC<OptionIconProps> = ({ type, isSet }) => {
const Icon = OPTION_ICONS[type][isSet ? 0 : 1]; const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
return isSet ? ( return isSet ? (
<Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} /> <Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
) : ( ) : (
<Icon sx={{ fontSize: 16, verticalAlign: 'middle' }} /> <Icon sx={{ fontSize: 16, verticalAlign: 'middle' }} />
); );
}; }
export default OptionIcon;

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom'; import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -29,13 +28,13 @@ import {
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import SettingsSchedulerDialog from './SchedulerDialog'; import SettingsSchedulerDialog from './SchedulerDialog';
import { readSchedule, writeSchedule } from './api';
import { ScheduleFlag } from './types'; import { ScheduleFlag } from './types';
import type { Schedule, ScheduleItem } from './types'; import type { Schedule, ScheduleItem } from './types';
import { schedulerItemValidation } from './validators'; import { schedulerItemValidation } from './validators';
const Scheduler: FC = () => { const Scheduler = () => {
const { LL, locale } = useI18nContext(); const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0); const blocker = useBlocker(numChanges !== 0);
@@ -48,13 +47,12 @@ const Scheduler: FC = () => {
data: schedule, data: schedule,
send: fetchSchedule, send: fetchSchedule,
error error
} = useRequest(EMSESP.readSchedule, { } = useRequest(readSchedule, {
initialData: [], initialData: []
force: true
}); });
const { send: writeSchedule } = useRequest( const { send: updateSchedule } = useRequest(
(data: Schedule) => EMSESP.writeSchedule(data), (data: Schedule) => writeSchedule(data),
{ {
immediate: false immediate: false
} }
@@ -131,7 +129,7 @@ const Scheduler: FC = () => {
}); });
const saveSchedule = async () => { const saveSchedule = async () => {
await writeSchedule({ await updateSchedule({
schedule: schedule schedule: schedule
.filter((si) => !si.deleted) .filter((si) => !si.deleted)
.map((condensed_si) => ({ .map((condensed_si) => ({
@@ -177,7 +175,7 @@ const Scheduler: FC = () => {
const onDialogSave = (updatedItem: ScheduleItem) => { const onDialogSave = (updatedItem: ScheduleItem) => {
setDialogOpen(false); setDialogOpen(false);
updateState('schedule', (data: ScheduleItem[]) => { void updateState(readSchedule(), (data: ScheduleItem[]) => {
const new_data = creating const new_data = creating
? [ ? [
...data.filter((si) => creating || si.o_id !== updatedItem.o_id), ...data.filter((si) => creating || si.o_id !== updatedItem.o_id),

View File

@@ -208,7 +208,7 @@ const SchedulerDialog = ({
scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE ? 'primary' : 'grey' scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE ? 'primary' : 'grey'
} }
> >
{LL.ONCHANGE(0)} {LL.ONCHANGE()}
</Typography> </Typography>
</ToggleButton> </ToggleButton>
<ToggleButton value={ScheduleFlag.SCHEDULE_CONDITION}> <ToggleButton value={ScheduleFlag.SCHEDULE_CONDITION}>
@@ -218,7 +218,7 @@ const SchedulerDialog = ({
scheduleType === ScheduleFlag.SCHEDULE_CONDITION ? 'primary' : 'grey' scheduleType === ScheduleFlag.SCHEDULE_CONDITION ? 'primary' : 'grey'
} }
> >
{LL.CONDITION(0)} {LL.CONDITION()}
</Typography> </Typography>
</ToggleButton> </ToggleButton>
<ToggleButton value={ScheduleFlag.SCHEDULE_IMMEDIATE}> <ToggleButton value={ScheduleFlag.SCHEDULE_IMMEDIATE}>
@@ -228,7 +228,7 @@ const SchedulerDialog = ({
scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE ? 'primary' : 'grey' scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE ? 'primary' : 'grey'
} }
> >
{LL.IMMEDIATE(0)} {LL.IMMEDIATE()}
</Typography> </Typography>
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
@@ -311,10 +311,10 @@ const SchedulerDialog = ({
name="time" name="time"
label={ label={
scheduleType === ScheduleFlag.SCHEDULE_CONDITION scheduleType === ScheduleFlag.SCHEDULE_CONDITION
? LL.CONDITION(1) ? LL.CONDITION()
: scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE : scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE
? LL.ONCHANGE(1) ? LL.ONCHANGE()
: LL.IMMEDIATE(1) : LL.IMMEDIATE()
} }
multiline multiline
fullWidth fullWidth

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
@@ -46,7 +45,7 @@ import {
temperatureSensorItemValidation temperatureSensorItemValidation
} from './validators'; } from './validators';
const Sensors: FC = () => { const Sensors = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);

View File

@@ -88,7 +88,6 @@ export const writeDeviceName = (data: { id: number; name: string }) =>
// SettingsScheduler // SettingsScheduler
export const readSchedule = () => export const readSchedule = () =>
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', { alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
name: 'schedule',
transform(data) { transform(data) {
return (data as Schedule).schedule.map((si: ScheduleItem) => ({ return (data as Schedule).schedule.map((si: ScheduleItem) => ({
...si, ...si,
@@ -109,7 +108,6 @@ export const writeSchedule = (data: Schedule) =>
// Modules // Modules
export const readModules = () => export const readModules = () =>
alovaInstance.Get<ModuleItem[]>('/rest/modules', { alovaInstance.Get<ModuleItem[]>('/rest/modules', {
name: 'modules',
transform(data) { transform(data) {
return (data as Modules).modules.map((mi: ModuleItem) => ({ return (data as Modules).modules.map((mi: ModuleItem) => ({
...mi, ...mi,
@@ -127,7 +125,6 @@ export const writeModules = (data: {
// SettingsEntities // SettingsEntities
export const readCustomEntities = () => export const readCustomEntities = () =>
alovaInstance.Get<EntityItem[]>('/rest/customEntities', { alovaInstance.Get<EntityItem[]>('/rest/customEntities', {
name: 'entities',
transform(data) { transform(data) {
return (data as Entities).entities.map((ei: EntityItem) => ({ return (data as Entities).entities.map((ei: EntityItem) => ({
...ei, ...ei,

View File

@@ -1,5 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
@@ -29,7 +28,7 @@ export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED; provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
const APSettings: FC = () => { const APSettings = () => {
const { const {
loadData, loadData,
saving, saving,

View File

@@ -1,5 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -49,9 +48,9 @@ export function boardProfileSelectItems() {
)); ));
} }
const ApplicationSettings: FC = () => { const ApplicationSettings = () => {
const { data: hardwareData } = useRequest(SystemApi.readHardwareStatus, { const { data: hardwareData } = useRequest(SystemApi.readHardwareStatus, {
force: true initialData: { psram: false }
}); });
const { const {
@@ -84,19 +83,12 @@ const ApplicationSettings: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { const { loading: processingBoard, send: readBoardProfile } = useRequest(
loading: processingBoard, (boardProfile: string) => EMSESP.getBoardProfile(boardProfile),
send: readBoardProfile, {
onSuccess: onSuccessBoardProfile
} = useRequest((boardProfile: string) => EMSESP.getBoardProfile(boardProfile), {
immediate: false immediate: false
}); }
).onSuccess((event) => {
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
onSuccessBoardProfile((event) => {
const response = event.data as Settings; const response = event.data as Settings;
updateDataValue({ updateDataValue({
...data, ...data,
@@ -113,6 +105,10 @@ const ApplicationSettings: FC = () => {
}); });
}); });
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const updateBoardProfile = async (board_profile: string) => { const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error: Error) => { await readBoardProfile(board_profile).catch((error: Error) => {
toast.error(error.message); toast.error(error.message);

View File

@@ -1,5 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
@@ -31,7 +30,7 @@ import type { MqttSettingsType } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators'; import { createMqttSettingsValidator, validate } from 'validators';
const MqttSettings: FC = () => { const MqttSettings = () => {
const { const {
loadData, loadData,
saving, saving,

View File

@@ -1,11 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Button, Checkbox, MenuItem } from '@mui/material'; import { Button, Checkbox, MenuItem } from '@mui/material';
import * as NTPApi from 'api/ntp'; import * as NTPApi from 'api/ntp';
import { readNTPSettings } from 'api/ntp';
import { updateState } from 'alova/client'; import { updateState } from 'alova/client';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
@@ -26,7 +26,7 @@ import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ'; import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ';
const NTPSettings: FC = () => { const NTPSettings = () => {
const { const {
loadData, loadData,
saving, saving,
@@ -72,9 +72,7 @@ const NTPSettings: FC = () => {
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => { const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFormValue(event); updateFormValue(event);
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
// TODO fix
updateState('ntpSettings', (settings: NTPSettingsType) => ({
...settings, ...settings,
tz_label: event.target.value, tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value] tz_format: TIME_ZONES[event.target.value]

View File

@@ -1,4 +1,4 @@
import { type FC, useState } from 'react'; import { useState } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -29,7 +29,7 @@ import { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const Settings: FC = () => { const Settings = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS(0)); useLayoutTitle(LL.SETTINGS(0));

View File

@@ -1,4 +1,4 @@
import { type FC, useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
@@ -19,61 +19,59 @@ import { useI18nContext } from 'i18n/i18n-react';
import RestartMonitor from '../status/RestartMonitor'; import RestartMonitor from '../status/RestartMonitor';
const UploadDownload: FC = () => { const UploadDownload = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>(); const [restarting, setRestarting] = useState<boolean>();
const [md5, setMd5] = useState<string>(); const [md5, setMd5] = useState<string>();
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest( const { send: getSettings } = useRequest(EMSESP.getSettings(), {
EMSESP.getSettings(),
{
immediate: false
}
);
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } =
useRequest(EMSESP.getCustomizations(), {
immediate: false immediate: false
}).onSuccess((event) => {
saveFile(event.data, 'settings.json');
}); });
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(
EMSESP.getEntities(), const { send: getCustomizations } = useRequest(EMSESP.getCustomizations(), {
{
immediate: false immediate: false
} }).onSuccess((event) => {
); saveFile(event.data, 'customizations.json');
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest( });
EMSESP.getSchedule(),
{ const { send: getEntities } = useRequest(EMSESP.getEntities(), {
immediate: false immediate: false
} }).onSuccess((event) => {
); saveFile(event.data, 'entities.json');
const { send: getAPI, onSuccess: onGetAPI } = useRequest( });
(data: APIcall) => EMSESP.API(data),
{ const { send: getSchedule } = useRequest(EMSESP.getSchedule(), {
immediate: false immediate: false
} }).onSuccess((event) => {
saveFile(event.data, 'schedule.json');
});
const { send: getAPI } = useRequest((data: APIcall) => EMSESP.API(data), {
immediate: false
}).onSuccess((event) => {
saveFile(
event.data,
String(event.args[0].device) + '_' + String(event.args[0].entity) + '.txt'
); );
});
const { const {
data: data, data: data,
send: loadData, send: loadData,
error error
} = useRequest(SystemApi.readHardwareStatus, { force: true }); } = useRequest(SystemApi.readHardwareStatus);
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, { // called immediately to get the latest version, on page load
immediate: true, const { data: latestVersion } = useRequest(SystemApi.getStableVersion);
force: true const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion);
});
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
immediate: true,
force: true
});
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/'; const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const STABLE_RELNOTES_URL = const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md'; 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL = const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md'; 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
@@ -95,16 +93,11 @@ const UploadDownload: FC = () => {
loading: isUploading, loading: isUploading,
uploading: progress, uploading: progress,
send: sendUpload, send: sendUpload,
onSuccess: onSuccessUpload,
abort: cancelUpload abort: cancelUpload
} = useRequest(SystemApi.uploadFile, { } = useRequest(SystemApi.uploadFile, {
immediate: false, immediate: false
force: true }).onSuccess(({ data }) => {
});
onSuccessUpload(({ data }) => {
if (data) { if (data) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setMd5(data.md5); setMd5(data.md5);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL()); toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
} else { } else {
@@ -137,22 +130,6 @@ const UploadDownload: FC = () => {
toast.info(LL.DOWNLOAD_SUCCESSFUL()); toast.info(LL.DOWNLOAD_SUCCESSFUL());
}; };
onSuccessGetSettings((event) => {
saveFile(event.data, 'settings.json');
});
onSuccessGetCustomizations((event) => {
saveFile(event.data, 'customizations.json');
});
onSuccessGetEntities((event) => {
saveFile(event.data, 'entities.json');
});
onSuccessGetSchedule((event) => {
saveFile(event.data, 'schedule.json');
});
onGetAPI((event) => {
saveFile(event.data, event.args[0].device + '_' + event.args[0].entity + '.txt');
});
const downloadSettings = async () => { const downloadSettings = async () => {
await getSettings().catch((error: Error) => { await getSettings().catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
@@ -211,11 +188,7 @@ const UploadDownload: FC = () => {
<Link <Link
target="_blank" target="_blank"
href={ href={
STABLE_URL + STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion)
'v' +
latestVersion +
'/' +
getBinURL(latestVersion as string)
} }
color="primary" color="primary"
> >
@@ -236,7 +209,7 @@ const UploadDownload: FC = () => {
)&nbsp;( )&nbsp;(
<Link <Link
target="_blank" target="_blank"
href={DEV_URL + getBinURL(latestDevVersion as string)} href={DEV_URL + getBinURL(latestDevVersion)}
color="primary" color="primary"
> >
{LL.DOWNLOAD(1)} {LL.DOWNLOAD(1)}

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'; import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
@@ -12,7 +11,7 @@ import NetworkSettings from './NetworkSettings';
import { WiFiConnectionContext } from './WiFiConnectionContext'; import { WiFiConnectionContext } from './WiFiConnectionContext';
import WiFiNetworkScanner from './WiFiNetworkScanner'; import WiFiNetworkScanner from './WiFiNetworkScanner';
const Network: FC = () => { const Network = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF(LL.NETWORK(0))); useLayoutTitle(LL.SETTINGS_OF(LL.NETWORK(0)));

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -48,7 +47,7 @@ import RestartMonitor from '../../status/RestartMonitor';
import { WiFiConnectionContext } from './WiFiConnectionContext'; import { WiFiConnectionContext } from './WiFiConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector'; import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
const NetworkSettings: FC = () => { const NetworkSettings = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext); const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
@@ -80,7 +79,9 @@ const NetworkSettings: FC = () => {
useEffect(() => { useEffect(() => {
if (!initialized && data) { if (!initialized && data) {
if (selectedNetwork) { if (selectedNetwork) {
updateState('networkSettings', (current_data: NetworkSettingsType) => ({ void updateState(
NetworkApi.readNetworkSettings(),
(current_data: NetworkSettingsType) => ({
ssid: selectedNetwork.ssid, ssid: selectedNetwork.ssid,
bssid: selectedNetwork.bssid, bssid: selectedNetwork.bssid,
password: current_data ? current_data.password : '', password: current_data ? current_data.password : '',
@@ -92,7 +93,8 @@ const NetworkSettings: FC = () => {
enableMDNS: true, enableMDNS: true,
enableCORS: false, enableCORS: false,
CORSOrigin: '*' CORSOrigin: '*'
})); })
);
} }
setInitialized(true); setInitialized(true);
} }

View File

@@ -1,5 +1,4 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import type { FC } from 'react';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi'; import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
@@ -15,23 +14,28 @@ import WiFiNetworkSelector from './WiFiNetworkSelector';
const NUM_POLLS = 10; const NUM_POLLS = 10;
const POLLING_FREQUENCY = 1000; const POLLING_FREQUENCY = 1000;
const WiFiNetworkScanner: FC = () => { const WiFiNetworkScanner = () => {
const pollCount = useRef(0); const pollCount = useRef(0);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest( // is called on page load to start network scan
NetworkApi.scanNetworks const { send: scanNetworks } = useRequest(NetworkApi.scanNetworks).onComplete(
); // is called on page load to start network scan () => {
const { pollCount.current = 0;
data: networkList, setErrorMessage(undefined);
send: getNetworkList, void updateState(NetworkApi.listNetworks(), () => undefined);
onSuccess: onSuccessNetworkList void getNetworkList();
} = useRequest(NetworkApi.listNetworks, { }
immediate: false );
});
onSuccessNetworkList((event) => { const { data: networkList, send: getNetworkList } = useRequest(
NetworkApi.listNetworks,
{
immediate: false
}
).onSuccess((event) => {
// is called when network scan is completed
if (!event.data) { if (!event.data) {
const completedPollCount = pollCount.current + 1; const completedPollCount = pollCount.current + 1;
if (completedPollCount < NUM_POLLS) { if (completedPollCount < NUM_POLLS) {
@@ -44,14 +48,6 @@ const WiFiNetworkScanner: FC = () => {
} }
}); });
onCompleteScanNetworks(() => {
pollCount.current = 0;
setErrorMessage(undefined);
// TODO fix
updateState('listNetworks', () => undefined);
void getNetworkList();
});
const renderNetworkScanner = () => { const renderNetworkScanner = () => {
if (!networkList) { if (!networkList) {
return ( return (

View File

@@ -1,5 +1,4 @@
import { useContext } from 'react'; import { useContext } from 'react';
import type { FC } from 'react';
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen'; import LockOpenIcon from '@mui/icons-material/LockOpen';
@@ -18,15 +17,11 @@ import type { Theme } from '@mui/material';
import { MessageBox } from 'components'; import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { WiFiNetwork, WiFiNetworkList } from 'types'; import type { WiFiNetwork } from 'types';
import { WiFiEncryptionType } from 'types'; import { WiFiEncryptionType } from 'types';
import { WiFiConnectionContext } from './WiFiConnectionContext'; import { WiFiConnectionContext } from './WiFiConnectionContext';
interface WiFiNetworkSelectorProps {
networkList: WiFiNetworkList;
}
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) => export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN; encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
@@ -62,7 +57,7 @@ const networkQualityHighlight = ({ rssi }: WiFiNetwork, theme: Theme) => {
return theme.palette.success.main; return theme.palette.success.main;
}; };
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => { function WiFiNetworkSelector({ networkList }) {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const theme = useTheme(); const theme = useTheme();
@@ -100,6 +95,6 @@ const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
} }
return <List>{networkList.networks.map(renderNetwork)}</List>; return <List>{networkList.networks.map(renderNetwork)}</List>;
}; }
export default WiFiNetworkSelector; export default WiFiNetworkSelector;

View File

@@ -1,5 +1,4 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { FC } from 'react';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { import {
@@ -21,12 +20,7 @@ import { useRequest } from 'alova/client';
import { MessageBox } from 'components'; import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
interface GenerateTokenProps { export default function GenerateToken({ username, onClose }) {
username?: string;
onClose: () => void;
}
const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const open = !!username; const open = !!username;
@@ -85,6 +79,4 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
}; }
export default GenerateToken;

View File

@@ -1,5 +1,4 @@
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom'; import { useBlocker } from 'react-router-dom';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -40,7 +39,7 @@ import { createUserValidator } from 'validators';
import GenerateToken from './GenerateToken'; import GenerateToken from './GenerateToken';
import User from './User'; import User from './User';
const ManageUsers: FC = () => { const ManageUsers = () => {
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
useRest<SecuritySettingsType>({ useRest<SecuritySettingsType>({
read: SecurityApi.readSecuritySettings, read: SecurityApi.readSecuritySettings,

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
@@ -9,7 +8,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import ManageUsers from './ManageUsers'; import ManageUsers from './ManageUsers';
import SecuritySettings from './SecuritySettings'; import SecuritySettings from './SecuritySettings';
const Security: FC = () => { const Security = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF(LL.SECURITY(0))); useLayoutTitle(LL.SETTINGS_OF(LL.SECURITY(0)));

View File

@@ -1,5 +1,4 @@
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
@@ -22,7 +21,7 @@ import type { SecuritySettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators'; import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
const SecuritySettings: FC = () => { const SecuritySettings = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();

View File

@@ -29,10 +29,8 @@ import { validate } from 'validators';
interface UserFormProps { interface UserFormProps {
creating: boolean; creating: boolean;
validator: Schema; validator: Schema;
user?: UserType; user?: UserType;
setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>; setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>;
onDoneEditing: () => void; onDoneEditing: () => void;
onCancelEditing: () => void; onCancelEditing: () => void;
} }

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import ComputerIcon from '@mui/icons-material/Computer'; import ComputerIcon from '@mui/icons-material/Computer';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
@@ -37,7 +35,7 @@ export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
} }
}; };
const APStatus: FC = () => { const APStatus = () => {
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus); const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,5 +1,4 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { FC } from 'react';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
@@ -22,7 +21,7 @@ import type { Translation } from 'i18n/i18n-types';
import * as EMSESP from '../main/api'; import * as EMSESP from '../main/api';
import type { Stat } from '../main/types'; import type { Stat } from '../main/types';
const SystemActivity: FC = () => { const SystemActivity = () => {
const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity); const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity);
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import AppsIcon from '@mui/icons-material/Apps'; import AppsIcon from '@mui/icons-material/Apps';
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard'; import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
import DevicesIcon from '@mui/icons-material/Devices'; import DevicesIcon from '@mui/icons-material/Devices';
@@ -32,7 +30,7 @@ function formatNumber(num: number) {
return new Intl.NumberFormat().format(num); return new Intl.NumberFormat().format(num);
} }
const HardwareStatus: FC = () => { const HardwareStatus = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(LL.HARDWARE())); useLayoutTitle(LL.STATUS_OF(LL.HARDWARE()));
@@ -41,7 +39,7 @@ const HardwareStatus: FC = () => {
data: data, data: data,
send: loadData, send: loadData,
error error
} = useRequest(SystemApi.readHardwareStatus, { force: true }); } = useRequest(SystemApi.readHardwareStatus);
const content = () => { const content = () => {
if (!data) { if (!data) {

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion'; import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
@@ -57,7 +55,7 @@ export const mqttQueueHighlight = (
return theme.palette.warning.main; return theme.palette.warning.main;
}; };
const MqttStatus: FC = () => { const MqttStatus = () => {
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus); const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,5 +1,4 @@
import { useState } from 'react'; import { useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
@@ -37,7 +36,7 @@ import type { NTPStatusType, Time } from 'types';
import { NTPSyncStatus } from 'types'; import { NTPSyncStatus } from 'types';
import { formatDateTime, formatLocalDateTime } from 'utils'; import { formatDateTime, formatLocalDateTime } from 'utils';
const NTPStatus: FC = () => { const NTPStatus = () => {
const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus); const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
const [localTime, setLocalTime] = useState<string>(''); const [localTime, setLocalTime] = useState<string>('');

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DnsIcon from '@mui/icons-material/Dns'; import DnsIcon from '@mui/icons-material/Dns';
import GiteIcon from '@mui/icons-material/Gite'; import GiteIcon from '@mui/icons-material/Gite';
@@ -84,7 +82,7 @@ const IPs = (status: NetworkStatusType) => {
return status.local_ip + ', ' + status.local_ipv6; return status.local_ip + ', ' + status.local_ipv6;
}; };
const NetworkStatus: FC = () => { const NetworkStatus = () => {
const { const {
data: data, data: data,
send: loadData, send: loadData,

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { FC } from 'react';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
@@ -10,11 +9,11 @@ import { useI18nContext } from 'i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000; const RESTART_TIMEOUT = 2 * 60 * 1000;
const POLL_INTERVAL = 3000; const POLL_INTERVAL = 3000;
const RestartMonitor: FC = () => { const RestartMonitor = () => {
const [failed, setFailed] = useState<boolean>(false); const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>(); const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { send } = useRequest(SystemApi.readSystemStatus, { force: true }); const { send } = useRequest(SystemApi.readSystemStatus);
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT); const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
const poll = useRef(async () => { const poll = useRef(async () => {

View File

@@ -1,4 +1,4 @@
import { type FC, useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -44,7 +44,7 @@ import { NTPSyncStatus, NetworkConnectionStatus } from 'types';
import RestartMonitor from './RestartMonitor'; import RestartMonitor from './RestartMonitor';
const SystemStatus: FC = () => { const SystemStatus = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -272,12 +272,13 @@ const SystemStatus: FC = () => {
); );
const content = () => { const content = () => {
// if (!data) { // TODO remove test code
// return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
// }
if (loading) { if (loading) {
return <>fddfdd</>; return <>not loaded!</>;
}
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
} }
return ( return (
@@ -411,7 +412,9 @@ const SystemStatus: FC = () => {
); );
}; };
return <SectionContent>{content()}</SectionContent>; return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
}; };
export default SystemStatus; export default SystemStatus;

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
@@ -71,7 +70,7 @@ const levelLabel = (level: LogLevel) => {
} }
}; };
const SystemLog: FC = () => { const SystemLog = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.LOG_OF(LL.SYSTEM(0))); useLayoutTitle(LL.LOG_OF(LL.SYSTEM(0)));

View File

@@ -1,4 +1,4 @@
import { type ChangeEventHandler, type FC, useContext } from 'react'; import { type ChangeEventHandler, useContext } from 'react';
import { MenuItem, TextField } from '@mui/material'; import { MenuItem, TextField } from '@mui/material';
@@ -16,7 +16,7 @@ import { I18nContext } from 'i18n/i18n-react';
import type { Locales } from 'i18n/i18n-types'; import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async'; import { loadLocaleAsync } from 'i18n/i18n-util.async';
const LanguageSelector: FC = () => { const LanguageSelector = () => {
const { setLocale, locale } = useContext(I18nContext); const { setLocale, locale } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -7,12 +6,7 @@ import { AppBar, IconButton, Toolbar, Typography } from '@mui/material';
export const DRAWER_WIDTH = 210; export const DRAWER_WIDTH = 210;
interface LayoutAppBarProps { export default function LayoutAppBar({ title, onToggleDrawer }) {
title: string;
onToggleDrawer: () => void;
}
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
const pathnames = useLocation() const pathnames = useLocation()
.pathname.split('/') .pathname.split('/')
.filter((x) => x); .filter((x) => x);
@@ -56,6 +50,4 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
); );
}; }
export default LayoutAppBar;

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material'; import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
import { PROJECT_NAME } from 'api/env'; import { PROJECT_NAME } from 'api/env';
@@ -18,12 +16,7 @@ const LayoutDrawerLogo = styled('img')(({ theme }) => ({
} }
})); }));
interface LayoutDrawerProps { export default function LayoutDrawerProps({ mobileOpen, onClose }) {
mobileOpen: boolean;
onClose: () => void;
}
const LayoutDrawer: FC<LayoutDrawerProps> = ({ mobileOpen, onClose }) => {
const drawer = ( const drawer = (
<> <>
<Toolbar disableGutters> <Toolbar disableGutters>
@@ -66,6 +59,4 @@ const LayoutDrawer: FC<LayoutDrawerProps> = ({ mobileOpen, onClose }) => {
</Drawer> </Drawer>
</Box> </Box>
); );
}; }
export default LayoutDrawer;

View File

@@ -1,5 +1,4 @@
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import type { FC } from 'react';
import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import AssessmentIcon from '@mui/icons-material/Assessment'; import AssessmentIcon from '@mui/icons-material/Assessment';
@@ -30,7 +29,7 @@ import LayoutMenuItem from 'components/layout/LayoutMenuItem';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const LayoutMenu: FC = () => { const LayoutMenu = () => {
const { me, signOut } = useContext(AuthenticatedContext); const { me, signOut } = useContext(AuthenticatedContext);
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,49 +0,0 @@
import type { FC } from 'react';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Paper, Typography } from '@mui/material';
interface ApplicationErrorProps {
message?: string;
}
const ApplicationError: FC<ApplicationErrorProps> = ({ message }) => (
<Box display="flex" height="100vh" justifyContent="center" flexDirection="column">
<Paper
elevation={10}
sx={{
textAlign: 'center',
padding: '280px 0 40px 0',
backgroundImage: 'url("/app/icon.png")',
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% 40px',
backgroundSize: '200px auto',
width: '100%',
borderRadius: 0
}}
>
<Box
display="flex"
flexDirection="row"
justifyContent="center"
alignItems="center"
mb={2}
>
<WarningIcon fontSize="large" color="error" />
<Box ml={2}>
<Typography variant="h4">Application Error</Typography>
</Box>
</Box>
<Typography variant="subtitle1" gutterBottom>
Failed to configure the application, please refresh to try again.
</Typography>
{message && (
<Typography variant="subtitle2" gutterBottom>
{message}
</Typography>
)}
</Paper>
</Box>
);
export default ApplicationError;

View File

@@ -1,3 +1,2 @@
export { default as ApplicationError } from './ApplicationError';
export { default as LoadingSpinner } from './LoadingSpinner'; export { default as LoadingSpinner } from './LoadingSpinner';
export { default as FormLoader } from './FormLoader'; export { default as FormLoader } from './FormLoader';

View File

@@ -1,6 +1,3 @@
import type { FC } from 'react';
import type { Blocker } from 'react-router-dom';
import { import {
Button, Button,
Dialog, Dialog,
@@ -12,11 +9,7 @@ import {
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
interface BlockNavigationProps { export default function BlockNavigation({ blocker }) {
blocker: Blocker;
}
const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
return ( return (
@@ -41,6 +34,4 @@ const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
}; }
export default BlockNavigation;

View File

@@ -8,7 +8,7 @@ import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material'; import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { Progress } from 'alova/client'; import type { Progress } from 'alova';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const getBorderColor = (theme: Theme, props: DropzoneState) => { const getBorderColor = (theme: Theme, props: DropzoneState) => {

View File

@@ -252,9 +252,9 @@ const en: Translation = {
TIME_ZONE: 'Time Zone', TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point', ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point', AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'always', AP_PROVIDE_TEXT_1: 'Always',
AP_PROVIDE_TEXT_2: 'when WiFi is disconnected', AP_PROVIDE_TEXT_2: 'When WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'never', AP_PROVIDE_TEXT_3: 'Never',
AP_PREFERRED_CHANNEL: 'Preferred Channel', AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID', AP_HIDE_SSID: 'Hide SSID',
AP_CLIENTS: 'AP Clients', AP_CLIENTS: 'AP Clients',

View File

@@ -24,30 +24,22 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
const { const {
data, data,
send: readData, send: readData,
update: updateData, update: updateData
// TODO refactor } = useRequest(read()).onComplete((event) => {
onComplete: onReadComplete setOrigData(event.data as D);
} = useRequest(read()); });
const { const { loading: saving, send: writeData } = useRequest(
loading: saving, (newData: D) => update(newData),
send: writeData, { immediate: false }
// TODO refactor ).onSuccess(() => {
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(1))); toast.success(LL.UPDATED_OF(LL.SETTINGS(1)));
setDirtyFlags([]); setDirtyFlags([]);
}); });
onReadComplete((event) => { const updateDataValue = (new_data: D) => {
setOrigData(event.data as D); updateData({ data: new_data });
}); };
const loadData = async () => { const loadData = async () => {
setDirtyFlags([]); setDirtyFlags([]);

View File

@@ -12,7 +12,7 @@ export const validate = <T extends object>(
options ? options : {}, options ? options : {},
(errors, fieldErrors) => { (errors, fieldErrors) => {
if (errors) { if (errors) {
reject(fieldErrors); reject(fieldErrors as Error);
} else { } else {
resolve(source as T); resolve(source as T);
} }

View File

@@ -1705,13 +1705,13 @@ __metadata:
react-router-dom: "npm:^6.26.0" react-router-dom: "npm:^6.26.0"
react-toastify: "npm:^10.0.5" react-toastify: "npm:^10.0.5"
rollup-plugin-visualizer: "npm:^5.12.0" rollup-plugin-visualizer: "npm:^5.12.0"
terser: "npm:^5.31.3" terser: "npm:^5.31.5"
typesafe-i18n: "npm:^5.26.2" typesafe-i18n: "npm:^5.26.2"
typescript: "npm:^5.5.4" typescript: "npm:^5.5.4"
typescript-eslint: "npm:8.0.1" typescript-eslint: "npm:8.0.1"
vite: "npm:^5.3.5" vite: "npm:^5.4.0"
vite-plugin-imagemin: "npm:^0.6.1" vite-plugin-imagemin: "npm:^0.6.1"
vite-tsconfig-paths: "npm:^4.3.2" vite-tsconfig-paths: "npm:^5.0.0"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -5611,14 +5611,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postcss@npm:^8.4.39": "postcss@npm:^8.4.40":
version: 8.4.40 version: 8.4.41
resolution: "postcss@npm:8.4.40" resolution: "postcss@npm:8.4.41"
dependencies: dependencies:
nanoid: "npm:^3.3.7" nanoid: "npm:^3.3.7"
picocolors: "npm:^1.0.1" picocolors: "npm:^1.0.1"
source-map-js: "npm:^1.2.0" source-map-js: "npm:^1.2.0"
checksum: 10c0/65ed67573e5443beaeb582282ff27a6be7c7fe3b4d9fa15761157616f2b97510cb1c335023c26220b005909f007337026d6e3ff092f25010b484ad484e80ea7f checksum: 10c0/c1828fc59e7ec1a3bf52b3a42f615dba53c67960ed82a81df6441b485fe43c20aba7f4e7c55425762fd99c594ecabbaaba8cf5b30fd79dfec5b52a9f63a2d690
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6686,9 +6686,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"terser@npm:^5.31.3": "terser@npm:^5.31.5":
version: 5.31.3 version: 5.31.5
resolution: "terser@npm:5.31.3" resolution: "terser@npm:5.31.5"
dependencies: dependencies:
"@jridgewell/source-map": "npm:^0.3.3" "@jridgewell/source-map": "npm:^0.3.3"
acorn: "npm:^8.8.2" acorn: "npm:^8.8.2"
@@ -6696,7 +6696,7 @@ __metadata:
source-map-support: "npm:~0.5.20" source-map-support: "npm:~0.5.20"
bin: bin:
terser: bin/terser terser: bin/terser
checksum: 10c0/eb2b525dada9febd3db74e94bd295f9cd7abd809e4f9c6bbc795a3048ad50fd327c15eab99db383fa820239680eef6d2dbd7dc05361769c204ddee5cf684d41e checksum: 10c0/6e7c66c1f4062ee098bff3dc3c396819ebf5f1740f0615be9de39b675a78c732d199f4dcfdcd15bd65f354e37c45bb944360f532a36fe7f7d22f800ca53c2d02
languageName: node languageName: node
linkType: hard linkType: hard
@@ -7025,9 +7025,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-tsconfig-paths@npm:^4.3.2": "vite-tsconfig-paths@npm:^5.0.0":
version: 4.3.2 version: 5.0.0
resolution: "vite-tsconfig-paths@npm:4.3.2" resolution: "vite-tsconfig-paths@npm:5.0.0"
dependencies: dependencies:
debug: "npm:^4.1.1" debug: "npm:^4.1.1"
globrex: "npm:^0.1.2" globrex: "npm:^0.1.2"
@@ -7037,23 +7037,24 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
vite: vite:
optional: true optional: true
checksum: 10c0/f390ac1d1c3992fc5ac50f9274c1090f8b55ab34a89ea88893db9a6924a3b26c9f64bc1163615150ad100749db73b6b2cf1d57f6cd60df6e762ceb5b8ad30024 checksum: 10c0/c29284abb92e829558f4913fb4e6e8ee2581e90c0ad13d6c88f4c16250d03cde2d14729af63df76d801babfc3ee9c6ccff5a421142d7af4f497beab988e9196c
languageName: node languageName: node
linkType: hard linkType: hard
"vite@npm:^5.3.5": "vite@npm:^5.4.0":
version: 5.3.5 version: 5.4.0
resolution: "vite@npm:5.3.5" resolution: "vite@npm:5.4.0"
dependencies: dependencies:
esbuild: "npm:^0.21.3" esbuild: "npm:^0.21.3"
fsevents: "npm:~2.3.3" fsevents: "npm:~2.3.3"
postcss: "npm:^8.4.39" postcss: "npm:^8.4.40"
rollup: "npm:^4.13.0" rollup: "npm:^4.13.0"
peerDependencies: peerDependencies:
"@types/node": ^18.0.0 || >=20.0.0 "@types/node": ^18.0.0 || >=20.0.0
less: "*" less: "*"
lightningcss: ^1.21.0 lightningcss: ^1.21.0
sass: "*" sass: "*"
sass-embedded: "*"
stylus: "*" stylus: "*"
sugarss: "*" sugarss: "*"
terser: ^5.4.0 terser: ^5.4.0
@@ -7069,6 +7070,8 @@ __metadata:
optional: true optional: true
sass: sass:
optional: true optional: true
sass-embedded:
optional: true
stylus: stylus:
optional: true optional: true
sugarss: sugarss:
@@ -7077,7 +7080,7 @@ __metadata:
optional: true optional: true
bin: bin:
vite: bin/vite.js vite: bin/vite.js
checksum: 10c0/795c7e0dbc94b96c4a0aff0d5d4b349dd28ad8b7b70979c1010f96b4d83f7d6c1700ebd6fed91de2e021b0a3689b9abc2d8017f6dfa8c9a6ca5c7af637d6afc6 checksum: 10c0/122de7795e1c3c08cd0acc7d77296f908398266b424492be7310400107f37a3cf4c9506f2b4b16619e57299ca2859b8ca187aac5e25f8e66d84f9204a1d72d18
languageName: node languageName: node
linkType: hard linkType: hard

View File

@@ -4356,7 +4356,7 @@ router
) )
.get(EMSESP_DEVICEENTITIES_ENDPOINT2, ({ params }) => (params.id ? deviceEntities(Number(params.id)) : status(404))) .get(EMSESP_DEVICEENTITIES_ENDPOINT2, ({ params }) => (params.id ? deviceEntities(Number(params.id)) : status(404)))
// Customization // Customizations
.post(EMSESP_CUSTOMIZATION_ENTITIES_ENDPOINT, async (request: any) => { .post(EMSESP_CUSTOMIZATION_ENTITIES_ENDPOINT, async (request: any) => {
const content = await request.json(); const content = await request.json();
const id = content.id; const id = content.id;

View File

@@ -210,7 +210,7 @@ void WebCustomizationService::device_entities(AsyncWebServerRequest * request) {
#endif #endif
#if defined(EMSESP_DEBUG) #if defined(EMSESP_DEBUG)
size_t length = response->setLength(); size_t length = response->setLength();
EMSESP::logger().debug("Customization buffer used: %d", length); EMSESP::logger().debug("Customizations buffer used: %d", length);
#else #else
response->setLength(); response->setLength();
#endif #endif