mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-10 17:59:53 +03:00
alova 3
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -30,13 +29,13 @@ import {
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
|
||||
import { readCustomEntities, writeCustomEntities } from './api';
|
||||
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||
import type { Entities, EntityItem } from './types';
|
||||
import { entityItemValidation } from './validators';
|
||||
|
||||
const CustomEntities: FC = () => {
|
||||
const CustomEntities = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -50,13 +49,12 @@ const CustomEntities: FC = () => {
|
||||
data: entities,
|
||||
send: fetchEntities,
|
||||
error
|
||||
} = useRequest(EMSESP.readCustomEntities, {
|
||||
initialData: [],
|
||||
force: true
|
||||
} = useRequest(readCustomEntities, {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
const { send: writeEntities } = useRequest(
|
||||
(data: Entities) => EMSESP.writeCustomEntities(data),
|
||||
(data: Entities) => writeCustomEntities(data),
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
@@ -182,7 +180,7 @@ const CustomEntities: FC = () => {
|
||||
|
||||
const onDialogSave = (updatedItem: EntityItem) => {
|
||||
setDialogOpen(false);
|
||||
updateState('entities', (data: EntityItem[]) => {
|
||||
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
||||
const new_data = creating
|
||||
? [
|
||||
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
||||
|
||||
@@ -291,7 +291,11 @@ const CustomEntitiesDialog = ({
|
||||
fullWidth
|
||||
margin="normal"
|
||||
type="number"
|
||||
inputProps={{ min: '1', max: String(256 - editItem.offset), step: '1' }}
|
||||
inputProps={{
|
||||
min: '1',
|
||||
max: String(256 - editItem.offset),
|
||||
step: '1'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useBlocker, useLocation } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -52,7 +51,7 @@ import {
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import SettingsCustomizationDialog from './CustomizationDialog';
|
||||
import SettingsCustomizationsDialog from './CustomizationsDialog';
|
||||
import EntityMaskToggle from './EntityMaskToggle';
|
||||
import OptionIcon from './OptionIcon';
|
||||
import { DeviceEntityMask } from './types';
|
||||
@@ -60,7 +59,7 @@ import type { DeviceEntity, DeviceShort } from './types';
|
||||
|
||||
export const APIURL = window.location.origin + '/api/';
|
||||
|
||||
const Customization: FC = () => {
|
||||
const Customizations = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(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),
|
||||
{
|
||||
initialData: [],
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
).onSuccess((event) => {
|
||||
setOriginalSettings(event.data);
|
||||
});
|
||||
|
||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||
setDeviceEntities(
|
||||
@@ -126,10 +127,6 @@ const Customization: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
onSuccess((event) => {
|
||||
setOriginalSettings(event.data);
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
@@ -727,7 +724,7 @@ const Customization: FC = () => {
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : renderContent()}
|
||||
{selectedDeviceEntity && (
|
||||
<SettingsCustomizationDialog
|
||||
<SettingsCustomizationsDialog
|
||||
open={dialogOpen}
|
||||
onClose={onDialogClose}
|
||||
onSave={onDialogSave}
|
||||
@@ -738,4 +735,4 @@ const Customization: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Customization;
|
||||
export default Customizations;
|
||||
@@ -23,19 +23,19 @@ import EntityMaskToggle from './EntityMaskToggle';
|
||||
import { DeviceEntityMask } from './types';
|
||||
import type { DeviceEntity } from './types';
|
||||
|
||||
interface SettingsCustomizationDialogProps {
|
||||
interface SettingsCustomizationsDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (di: DeviceEntity) => void;
|
||||
selectedItem: DeviceEntity;
|
||||
}
|
||||
|
||||
const CustomizationDialog = ({
|
||||
const CustomizationsDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedItem
|
||||
}: SettingsCustomizationDialogProps) => {
|
||||
}: SettingsCustomizationsDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
@@ -175,4 +175,4 @@ const CustomizationDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomizationDialog;
|
||||
export default CustomizationsDialog;
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { FC } from 'react';
|
||||
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
|
||||
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
||||
import { FaSolarPanel } from 'react-icons/fa';
|
||||
@@ -16,11 +15,7 @@ import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
|
||||
import { DeviceType } from './types';
|
||||
|
||||
interface DeviceIconProps {
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
|
||||
export default function DeviceIcon({ type_id }) {
|
||||
switch (type_id as DeviceType) {
|
||||
case DeviceType.TEMPERATURESENSOR:
|
||||
case DeviceType.ANALOGSENSOR:
|
||||
@@ -60,6 +55,4 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default DeviceIcon;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
useLayoutEffect,
|
||||
useState
|
||||
} from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -70,7 +69,7 @@ import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
|
||||
import type { Device, DeviceValue } from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
|
||||
const Devices: FC = () => {
|
||||
const Devices = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CommentIcon from '@mui/icons-material/CommentTwoTone';
|
||||
@@ -21,27 +20,19 @@ import {
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import * as EMSESP from 'app/main/api';
|
||||
import { useAutoRequest, useRequest } from 'alova/client';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import type { APIcall } from './types';
|
||||
|
||||
const Help: FC = () => {
|
||||
const Help = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.HELP_OF(''));
|
||||
|
||||
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
|
||||
(data: APIcall) => EMSESP.API(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
// TODO check useAutoRequest - https://alova.js.org/tutorial/client/strategy/use-auto-request/#basic-usage
|
||||
const { data, loading } = useAutoRequest(SystemApi.readSystemStatus);
|
||||
|
||||
onGetAPI((event) => {
|
||||
const { send: getAPI } = useRequest((data: APIcall) => EMSESP.API(data), {
|
||||
immediate: false
|
||||
}).onSuccess((event) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(event.data, null, 2)], {
|
||||
@@ -56,14 +47,16 @@ const Help: FC = () => {
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
});
|
||||
|
||||
const { data, loading } = useRequest(SystemApi.readSystemStatus);
|
||||
|
||||
const callAPI = async (device: string, entity: string) => {
|
||||
await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
// TODO remove debug testing useRequest preact hook
|
||||
console.log('loading: ' + loading + ' data2: ' + data);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -8,8 +7,6 @@ import CircleIcon from '@mui/icons-material/Circle';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
|
||||
import { alovaInstance } from 'api/endpoints';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
@@ -30,11 +27,11 @@ import {
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import ModulesDialog from './ModulesDialog';
|
||||
import { readModules, writeModules } from './api';
|
||||
import type { ModuleItem, Modules } from './types';
|
||||
|
||||
const Modules: FC = () => {
|
||||
const Modules = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -46,13 +43,12 @@ const Modules: FC = () => {
|
||||
data: modules,
|
||||
send: fetchModules,
|
||||
error
|
||||
} = useRequest(EMSESP.readModules, {
|
||||
} = useRequest(readModules, {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
const { send: writeModules } = useRequest(
|
||||
(data: { key: string; enabled: boolean; license: string }) =>
|
||||
EMSESP.writeModules(data),
|
||||
const { send: updateModules } = useRequest(
|
||||
(data: { key: string; enabled: boolean; license: string }) => writeModules(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
@@ -124,23 +120,18 @@ const Modules: FC = () => {
|
||||
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) => {
|
||||
updateState<ModuleItem[]>(
|
||||
[alovaInstance.snapshots.match('modules', true)] as any,
|
||||
(data: ModuleItem[]) => {
|
||||
const new_data = data.map((mi) =>
|
||||
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
|
||||
);
|
||||
setNumChanges(new_data.filter((mi) => hasModulesChanged(mi)).length);
|
||||
return new_data;
|
||||
}
|
||||
);
|
||||
void updateState(readModules(), (data: ModuleItem[]) => {
|
||||
const new_data = data.map((mi) =>
|
||||
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
|
||||
);
|
||||
setNumChanges(new_data.filter((mi) => hasModulesChanged(mi)).length);
|
||||
return new_data;
|
||||
});
|
||||
};
|
||||
|
||||
const saveModules = async () => {
|
||||
await writeModules({
|
||||
await updateModules({
|
||||
modules: modules.map((condensed_mi) => ({
|
||||
key: condensed_mi.key,
|
||||
enabled: condensed_mi.enabled,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
@@ -32,18 +30,11 @@ const OPTION_ICONS: {
|
||||
favorite: [StarIcon, StarOutlineIcon]
|
||||
};
|
||||
|
||||
interface OptionIconProps {
|
||||
type: OptionType;
|
||||
isSet: boolean;
|
||||
}
|
||||
|
||||
const OptionIcon: FC<OptionIconProps> = ({ type, isSet }) => {
|
||||
export default function OptionIcon({ type, isSet }) {
|
||||
const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
|
||||
return isSet ? (
|
||||
<Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||
) : (
|
||||
<Icon sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionIcon;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -29,13 +28,13 @@ import {
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import SettingsSchedulerDialog from './SchedulerDialog';
|
||||
import { readSchedule, writeSchedule } from './api';
|
||||
import { ScheduleFlag } from './types';
|
||||
import type { Schedule, ScheduleItem } from './types';
|
||||
import { schedulerItemValidation } from './validators';
|
||||
|
||||
const Scheduler: FC = () => {
|
||||
const Scheduler = () => {
|
||||
const { LL, locale } = useI18nContext();
|
||||
const [numChanges, setNumChanges] = useState<number>(0);
|
||||
const blocker = useBlocker(numChanges !== 0);
|
||||
@@ -48,13 +47,12 @@ const Scheduler: FC = () => {
|
||||
data: schedule,
|
||||
send: fetchSchedule,
|
||||
error
|
||||
} = useRequest(EMSESP.readSchedule, {
|
||||
initialData: [],
|
||||
force: true
|
||||
} = useRequest(readSchedule, {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
const { send: writeSchedule } = useRequest(
|
||||
(data: Schedule) => EMSESP.writeSchedule(data),
|
||||
const { send: updateSchedule } = useRequest(
|
||||
(data: Schedule) => writeSchedule(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
@@ -131,7 +129,7 @@ const Scheduler: FC = () => {
|
||||
});
|
||||
|
||||
const saveSchedule = async () => {
|
||||
await writeSchedule({
|
||||
await updateSchedule({
|
||||
schedule: schedule
|
||||
.filter((si) => !si.deleted)
|
||||
.map((condensed_si) => ({
|
||||
@@ -177,7 +175,7 @@ const Scheduler: FC = () => {
|
||||
|
||||
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||
setDialogOpen(false);
|
||||
updateState('schedule', (data: ScheduleItem[]) => {
|
||||
void updateState(readSchedule(), (data: ScheduleItem[]) => {
|
||||
const new_data = creating
|
||||
? [
|
||||
...data.filter((si) => creating || si.o_id !== updatedItem.o_id),
|
||||
|
||||
@@ -208,7 +208,7 @@ const SchedulerDialog = ({
|
||||
scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE ? 'primary' : 'grey'
|
||||
}
|
||||
>
|
||||
{LL.ONCHANGE(0)}
|
||||
{LL.ONCHANGE()}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ScheduleFlag.SCHEDULE_CONDITION}>
|
||||
@@ -218,7 +218,7 @@ const SchedulerDialog = ({
|
||||
scheduleType === ScheduleFlag.SCHEDULE_CONDITION ? 'primary' : 'grey'
|
||||
}
|
||||
>
|
||||
{LL.CONDITION(0)}
|
||||
{LL.CONDITION()}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ScheduleFlag.SCHEDULE_IMMEDIATE}>
|
||||
@@ -228,7 +228,7 @@ const SchedulerDialog = ({
|
||||
scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE ? 'primary' : 'grey'
|
||||
}
|
||||
>
|
||||
{LL.IMMEDIATE(0)}
|
||||
{LL.IMMEDIATE()}
|
||||
</Typography>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
@@ -282,7 +282,7 @@ const SchedulerDialog = ({
|
||||
</Grid>
|
||||
<Grid container>
|
||||
{scheduleType === ScheduleFlag.SCHEDULE_DAY ||
|
||||
scheduleType === ScheduleFlag.SCHEDULE_TIMER ? (
|
||||
scheduleType === ScheduleFlag.SCHEDULE_TIMER ? (
|
||||
<>
|
||||
<TextField
|
||||
name="time"
|
||||
@@ -311,10 +311,10 @@ const SchedulerDialog = ({
|
||||
name="time"
|
||||
label={
|
||||
scheduleType === ScheduleFlag.SCHEDULE_CONDITION
|
||||
? LL.CONDITION(1)
|
||||
? LL.CONDITION()
|
||||
: scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE
|
||||
? LL.ONCHANGE(1)
|
||||
: LL.IMMEDIATE(1)
|
||||
? LL.ONCHANGE()
|
||||
: LL.IMMEDIATE()
|
||||
}
|
||||
multiline
|
||||
fullWidth
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
|
||||
@@ -46,7 +45,7 @@ import {
|
||||
temperatureSensorItemValidation
|
||||
} from './validators';
|
||||
|
||||
const Sensors: FC = () => {
|
||||
const Sensors = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ export const writeDeviceName = (data: { id: number; name: string }) =>
|
||||
// SettingsScheduler
|
||||
export const readSchedule = () =>
|
||||
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
|
||||
name: 'schedule',
|
||||
transform(data) {
|
||||
return (data as Schedule).schedule.map((si: ScheduleItem) => ({
|
||||
...si,
|
||||
@@ -109,7 +108,6 @@ export const writeSchedule = (data: Schedule) =>
|
||||
// Modules
|
||||
export const readModules = () =>
|
||||
alovaInstance.Get<ModuleItem[]>('/rest/modules', {
|
||||
name: 'modules',
|
||||
transform(data) {
|
||||
return (data as Modules).modules.map((mi: ModuleItem) => ({
|
||||
...mi,
|
||||
@@ -127,7 +125,6 @@ export const writeModules = (data: {
|
||||
// SettingsEntities
|
||||
export const readCustomEntities = () =>
|
||||
alovaInstance.Get<EntityItem[]>('/rest/customEntities', {
|
||||
name: 'entities',
|
||||
transform(data) {
|
||||
return (data as Entities).entities.map((ei: EntityItem) => ({
|
||||
...ei,
|
||||
|
||||
Reference in New Issue
Block a user