mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-05-05 21:45:52 +00:00
Remove useMemo/useCallback across the web UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { ToastContainer, Zoom } from 'react-toastify';
|
import { ToastContainer, Zoom } from 'react-toastify';
|
||||||
|
|
||||||
import AppRouting from 'AppRouting';
|
import AppRouting from 'AppRouting';
|
||||||
@@ -46,19 +46,17 @@ const App = memo(() => {
|
|||||||
const [wasLoaded, setWasLoaded] = useState(false);
|
const [wasLoaded, setWasLoaded] = useState(false);
|
||||||
const [locale, setLocale] = useState<Locales>('en');
|
const [locale, setLocale] = useState<Locales>('en');
|
||||||
|
|
||||||
// Memoize locale initialization to prevent unnecessary re-runs
|
useEffect(() => {
|
||||||
const initializeLocale = useCallback(async () => {
|
const initializeLocale = async () => {
|
||||||
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
|
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
|
||||||
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
|
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
|
||||||
localStorage.setItem('lang', newLocale);
|
localStorage.setItem('lang', newLocale);
|
||||||
setLocale(newLocale);
|
setLocale(newLocale);
|
||||||
await loadLocaleAsync(newLocale);
|
await loadLocaleAsync(newLocale);
|
||||||
setWasLoaded(true);
|
setWasLoaded(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void initializeLocale();
|
void initializeLocale();
|
||||||
}, [initializeLocale]);
|
}, []);
|
||||||
|
|
||||||
if (!wasLoaded) return null;
|
if (!wasLoaded) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ const SignIn = memo(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize callback to prevent recreation on every render
|
|
||||||
const updateLoginRequestValue = useMemo(
|
const updateLoginRequestValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateValue((updater) =>
|
updateValue((updater) =>
|
||||||
@@ -65,7 +64,7 @@ const SignIn = memo(() => {
|
|||||||
});
|
});
|
||||||
}, [callSignIn, signInRequest, LL]);
|
}, [callSignIn, signInRequest, LL]);
|
||||||
|
|
||||||
const validateAndSignIn = useCallback(async () => {
|
const validateAndSignIn = async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
SIGN_IN_REQUEST_VALIDATOR.messages({
|
SIGN_IN_REQUEST_VALIDATOR.messages({
|
||||||
required: LL.IS_REQUIRED('%s')
|
required: LL.IS_REQUIRED('%s')
|
||||||
@@ -77,7 +76,7 @@ const SignIn = memo(() => {
|
|||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
}, [signInRequest, signIn, LL]);
|
};
|
||||||
|
|
||||||
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
|
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -57,20 +57,18 @@ const CustomEntities = () => {
|
|||||||
initialData: []
|
initialData: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const intervalCallback = useCallback(() => {
|
useInterval(() => {
|
||||||
if (!dialogOpen && !numChanges) {
|
if (!dialogOpen && !numChanges) {
|
||||||
void fetchEntities();
|
void fetchEntities();
|
||||||
}
|
}
|
||||||
}, [dialogOpen, numChanges, fetchEntities]);
|
});
|
||||||
|
|
||||||
useInterval(intervalCallback);
|
|
||||||
|
|
||||||
const { send: writeEntities } = useRequest(
|
const { send: writeEntities } = useRequest(
|
||||||
(data: Entities) => writeCustomEntities(data),
|
(data: Entities) => writeCustomEntities(data),
|
||||||
{ immediate: false }
|
{ immediate: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasEntityChanged = useCallback((ei: EntityItem) => {
|
const hasEntityChanged = (ei: EntityItem) => {
|
||||||
return (
|
return (
|
||||||
ei.id !== ei.o_id ||
|
ei.id !== ei.o_id ||
|
||||||
ei.ram !== ei.o_ram ||
|
ei.ram !== ei.o_ram ||
|
||||||
@@ -86,11 +84,9 @@ const CustomEntities = () => {
|
|||||||
ei.deleted !== ei.o_deleted ||
|
ei.deleted !== ei.o_deleted ||
|
||||||
(ei.value || '') !== (ei.o_value || '')
|
(ei.value || '') !== (ei.o_value || '')
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const entity_theme = useMemo(
|
const entity_theme = useTheme({
|
||||||
() =>
|
|
||||||
useTheme({
|
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
|
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
|
||||||
`,
|
`,
|
||||||
@@ -140,11 +136,9 @@ const CustomEntities = () => {
|
|||||||
background-color: #177ac9;
|
background-color: #177ac9;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveEntities = useCallback(async () => {
|
const saveEntities = async () => {
|
||||||
await writeEntities({
|
await writeEntities({
|
||||||
entities: entities
|
entities: entities
|
||||||
.filter((ei: EntityItem) => !ei.deleted)
|
.filter((ei: EntityItem) => !ei.deleted)
|
||||||
@@ -173,26 +167,25 @@ const CustomEntities = () => {
|
|||||||
await fetchEntities();
|
await fetchEntities();
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [entities, writeEntities, LL, fetchEntities]);
|
};
|
||||||
|
|
||||||
const editEntityItem = useCallback((ei: EntityItem) => {
|
const editEntityItem = (ei: EntityItem) => {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
setSelectedEntityItem(ei);
|
setSelectedEntityItem(ei);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogClose = useCallback(() => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogCancel = useCallback(async () => {
|
const onDialogCancel = async () => {
|
||||||
await fetchEntities().then(() => {
|
await fetchEntities().then(() => {
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [fetchEntities]);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: EntityItem) => {
|
||||||
(updatedItem: EntityItem) => {
|
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
||||||
const new_data = creating
|
const new_data = creating
|
||||||
@@ -206,11 +199,9 @@ const CustomEntities = () => {
|
|||||||
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[creating, hasEntityChanged]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDialogDup = useCallback((item: EntityItem) => {
|
const onDialogDup = (item: EntityItem) => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setSelectedEntityItem({
|
setSelectedEntityItem({
|
||||||
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
||||||
@@ -228,9 +219,9 @@ const CustomEntities = () => {
|
|||||||
value: item.value
|
value: item.value
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const addEntityItem = useCallback(() => {
|
const addEntityItem = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setSelectedEntityItem({
|
setSelectedEntityItem({
|
||||||
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
||||||
@@ -248,30 +239,27 @@ const CustomEntities = () => {
|
|||||||
value: ''
|
value: ''
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const formatValue = useCallback((value: unknown, uom: number) => {
|
const formatValue = (value: unknown, uom: number) => {
|
||||||
return value === undefined
|
return value === undefined
|
||||||
? ''
|
? ''
|
||||||
: typeof value === 'number'
|
: typeof value === 'number'
|
||||||
? new Intl.NumberFormat().format(value) +
|
? new Intl.NumberFormat().format(value) +
|
||||||
(uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`)
|
(uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`)
|
||||||
: `${value as string}${uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`}`;
|
: `${value as string}${uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`}`;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const showHex = useCallback((value: number, digit: number) => {
|
const showHex = (value: number, digit: number) => {
|
||||||
return `0x${value.toString(16).toUpperCase().padStart(digit, '0')}`;
|
return `0x${value.toString(16).toUpperCase().padStart(digit, '0')}`;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const filteredAndSortedEntities = useMemo(
|
const filteredAndSortedEntities =
|
||||||
() =>
|
|
||||||
entities
|
entities
|
||||||
?.filter((ei: EntityItem) => !ei.deleted)
|
?.filter((ei: EntityItem) => !ei.deleted)
|
||||||
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [],
|
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [];
|
||||||
[entities]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderEntity = useCallback(() => {
|
const renderEntity = () => {
|
||||||
if (!entities) {
|
if (!entities) {
|
||||||
return (
|
return (
|
||||||
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
|
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
|
||||||
@@ -328,17 +316,7 @@ const CustomEntities = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
entities,
|
|
||||||
error,
|
|
||||||
fetchEntities,
|
|
||||||
entity_theme,
|
|
||||||
editEntityItem,
|
|
||||||
LL,
|
|
||||||
filteredAndSortedEntities,
|
|
||||||
showHex,
|
|
||||||
formatValue
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -68,6 +68,7 @@ const CustomEntitiesDialog = ({
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
// Stable handler reference so the memoized ValidatedTextField can skip re-renders
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateValue(
|
updateValue(
|
||||||
@@ -105,16 +106,16 @@ const CustomEntitiesDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
_event: React.SyntheticEvent,
|
||||||
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
|
) => {
|
||||||
if (reason !== 'backdropClick') {
|
if (reason !== 'backdropClick') {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
@@ -138,27 +139,21 @@ const CustomEntitiesDialog = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const remove = useCallback(() => {
|
const remove = () => {
|
||||||
const itemWithDeleted = { ...editItem, deleted: true };
|
onSave({ ...editItem, deleted: true });
|
||||||
onSave(itemWithDeleted);
|
};
|
||||||
}, [editItem, onSave]);
|
|
||||||
|
|
||||||
const dup = useCallback(() => {
|
const dup = () => {
|
||||||
onDup(editItem);
|
onDup(editItem);
|
||||||
}, [editItem, onDup]);
|
};
|
||||||
|
|
||||||
// Memoize UOM menu items to avoid recreating on every render
|
const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
|
||||||
const uomMenuItems = useMemo(
|
|
||||||
() =>
|
|
||||||
DeviceValueUOM_s.map((val, i) => (
|
|
||||||
<MenuItem key={val} value={i}>
|
<MenuItem key={val} value={i}>
|
||||||
{val}
|
{val}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)),
|
));
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useBlocker, useLocation } from 'react-router';
|
import { useBlocker, useLocation } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -171,9 +171,7 @@ const Customizations = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const entities_theme = useMemo(
|
const entities_theme = useTheme({
|
||||||
() =>
|
|
||||||
useTheme({
|
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||||
`,
|
`,
|
||||||
@@ -236,9 +234,7 @@ const Customizations = () => {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
function hasEntityChanged(de: DeviceEntity) {
|
function hasEntityChanged(de: DeviceEntity) {
|
||||||
return (
|
return (
|
||||||
@@ -287,12 +283,11 @@ const Customizations = () => {
|
|||||||
return value as string;
|
return value as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCommand = useCallback((de: DeviceEntity) => {
|
const isCommand = (de: DeviceEntity) => {
|
||||||
return de.n && de.n[0] === '!';
|
return de.n && de.n[0] === '!';
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const formatName = useCallback(
|
const formatName = (de: DeviceEntity, withShortname: boolean) => {
|
||||||
(de: DeviceEntity, withShortname: boolean) => {
|
|
||||||
let name: string;
|
let name: string;
|
||||||
if (isCommand(de)) {
|
if (isCommand(de)) {
|
||||||
name = de.t
|
name = de.t
|
||||||
@@ -304,9 +299,7 @@ const Customizations = () => {
|
|||||||
name = de.t ? `${de.t} ${de.n}` : de.n || '';
|
name = de.t ? `${de.t} ${de.n}` : de.n || '';
|
||||||
}
|
}
|
||||||
return withShortname ? `${name} ${de.id}` : name;
|
return withShortname ? `${name} ${de.id}` : name;
|
||||||
},
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMaskNumber = (newMask: string[]) => {
|
const getMaskNumber = (newMask: string[]) => {
|
||||||
let new_mask = 0;
|
let new_mask = 0;
|
||||||
@@ -336,15 +329,11 @@ const Customizations = () => {
|
|||||||
return new_masks;
|
return new_masks;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter_entity = useCallback(
|
const filter_entity = (de: DeviceEntity) =>
|
||||||
(de: DeviceEntity) =>
|
|
||||||
(de.m & selectedFilters || !selectedFilters) &&
|
(de.m & selectedFilters || !selectedFilters) &&
|
||||||
formatName(de, true).toLowerCase().includes(search.toLowerCase()),
|
formatName(de, true).toLowerCase().includes(search.toLowerCase());
|
||||||
[selectedFilters, search, formatName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const maskDisabled = useCallback(
|
const maskDisabled = (set: boolean) => {
|
||||||
(set: boolean) => {
|
|
||||||
setDeviceEntities((prev) =>
|
setDeviceEntities((prev) =>
|
||||||
prev.map((de) => {
|
prev.map((de) => {
|
||||||
if (filter_entity(de)) {
|
if (filter_entity(de)) {
|
||||||
@@ -358,11 +347,9 @@ const Customizations = () => {
|
|||||||
return de;
|
return de;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
[filter_entity]
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetCustomization = useCallback(async () => {
|
const resetCustomization = async () => {
|
||||||
try {
|
try {
|
||||||
await sendResetCustomizations();
|
await sendResetCustomizations();
|
||||||
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
||||||
@@ -372,30 +359,27 @@ const Customizations = () => {
|
|||||||
setConfirmReset(false);
|
setConfirmReset(false);
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
}
|
}
|
||||||
}, [sendResetCustomizations, LL]);
|
};
|
||||||
|
|
||||||
const onDialogClose = () => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => {
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
setDeviceEntities(
|
setDeviceEntities(
|
||||||
(prev) =>
|
(prev) =>
|
||||||
prev?.map((de) =>
|
prev?.map((de) =>
|
||||||
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
||||||
) ?? []
|
) ?? []
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: DeviceEntity) => {
|
||||||
(updatedItem: DeviceEntity) => {
|
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
updateDeviceEntity(updatedItem);
|
updateDeviceEntity(updatedItem);
|
||||||
},
|
};
|
||||||
[updateDeviceEntity]
|
|
||||||
);
|
|
||||||
|
|
||||||
const editDeviceEntity = useCallback((de: DeviceEntity) => {
|
const editDeviceEntity = (de: DeviceEntity) => {
|
||||||
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -406,9 +390,9 @@ const Customizations = () => {
|
|||||||
|
|
||||||
setSelectedDeviceEntity(de);
|
setSelectedDeviceEntity(de);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const saveCustomization = useCallback(async () => {
|
const saveCustomization = async () => {
|
||||||
if (!devices || !deviceEntities || selectedDevice === -1) {
|
if (!devices || !deviceEntities || selectedDevice === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -441,9 +425,9 @@ const Customizations = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setOriginalSettings(deviceEntities);
|
setOriginalSettings(deviceEntities);
|
||||||
});
|
});
|
||||||
}, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]);
|
};
|
||||||
|
|
||||||
const renameDevice = useCallback(async () => {
|
const renameDevice = async () => {
|
||||||
await sendDeviceName({
|
await sendDeviceName({
|
||||||
id: selectedDevice,
|
id: selectedDevice,
|
||||||
name: selectedDeviceName,
|
name: selectedDeviceName,
|
||||||
@@ -459,14 +443,7 @@ const Customizations = () => {
|
|||||||
setRename(false);
|
setRename(false);
|
||||||
await fetchCoreData();
|
await fetchCoreData();
|
||||||
});
|
});
|
||||||
}, [
|
};
|
||||||
selectedDevice,
|
|
||||||
selectedDeviceName,
|
|
||||||
selectedDeviceBrand,
|
|
||||||
sendDeviceName,
|
|
||||||
LL,
|
|
||||||
fetchCoreData
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderDeviceList = () => (
|
const renderDeviceList = () => (
|
||||||
<>
|
<>
|
||||||
@@ -562,10 +539,7 @@ const Customizations = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredEntities = useMemo(
|
const filteredEntities = deviceEntities.filter((de) => filter_entity(de));
|
||||||
() => deviceEntities.filter((de) => filter_entity(de)),
|
|
||||||
[deviceEntities, filter_entity]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderDeviceData = () => {
|
const renderDeviceData = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
@@ -57,23 +57,16 @@ const CustomizationsDialog = ({
|
|||||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue(
|
||||||
() =>
|
|
||||||
updateValue(
|
|
||||||
setEditItem as unknown as React.Dispatch<
|
setEditItem as unknown as React.Dispatch<
|
||||||
React.SetStateAction<Record<string, unknown>>
|
React.SetStateAction<Record<string, unknown>>
|
||||||
>
|
>
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isWriteableNumber = useMemo(
|
const isWriteableNumber =
|
||||||
() =>
|
|
||||||
typeof editItem.v === 'number' &&
|
typeof editItem.v === 'number' &&
|
||||||
editItem.w &&
|
editItem.w &&
|
||||||
!(editItem.m & DeviceEntityMask.DV_READONLY),
|
!(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||||
[editItem.v, editItem.w, editItem.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -82,16 +75,16 @@ const CustomizationsDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
_event: React.SyntheticEvent,
|
||||||
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
|
) => {
|
||||||
if (reason !== 'backdropClick') {
|
if (reason !== 'backdropClick') {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = useCallback(() => {
|
const save = () => {
|
||||||
if (
|
if (
|
||||||
isWriteableNumber &&
|
isWriteableNumber &&
|
||||||
editItem.mi &&
|
editItem.mi &&
|
||||||
@@ -102,34 +95,31 @@ const CustomizationsDialog = ({
|
|||||||
} else {
|
} else {
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
}
|
}
|
||||||
}, [isWriteableNumber, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => {
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
setEditItem((prev) => ({ ...prev, m: updatedItem.m }));
|
setEditItem((prev) => ({ ...prev, m: updatedItem.m }));
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(() => `${LL.EDIT()} ${LL.ENTITY()}`, [LL]);
|
|
||||||
|
|
||||||
const writeableIcon = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.w ? (
|
|
||||||
<DoneIcon color="success" sx={{ fontSize: ICON_SIZE }} />
|
|
||||||
) : (
|
|
||||||
<CloseIcon color="error" sx={{ fontSize: ICON_SIZE }} />
|
|
||||||
),
|
|
||||||
[editItem.w]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{`${LL.EDIT()} ${LL.ENTITY()}`}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<LabelValue label={LL.ID_OF(LL.ENTITY())} value={editItem.id} />
|
<LabelValue label={LL.ID_OF(LL.ENTITY())} value={editItem.id} />
|
||||||
<LabelValue
|
<LabelValue
|
||||||
label={`${LL.DEFAULT(1)} ${LL.ENTITY_NAME(1)}`}
|
label={`${LL.DEFAULT(1)} ${LL.ENTITY_NAME(1)}`}
|
||||||
value={editItem.n}
|
value={editItem.n}
|
||||||
/>
|
/>
|
||||||
<LabelValue label={LL.WRITEABLE()} value={writeableIcon} />
|
<LabelValue
|
||||||
|
label={LL.WRITEABLE()}
|
||||||
|
value={
|
||||||
|
editItem.w ? (
|
||||||
|
<DoneIcon color="success" sx={{ fontSize: ICON_SIZE }} />
|
||||||
|
) : (
|
||||||
|
<CloseIcon color="error" sx={{ fontSize: ICON_SIZE }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box sx={{ mt: 1, mb: 2 }}>
|
<Box sx={{ mt: 1, mb: 2 }}>
|
||||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import { memo, useContext, useEffect, useState } from 'react';
|
||||||
import { IconContext } from 'react-icons/lib';
|
import { IconContext } from 'react-icons/lib';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -77,8 +77,7 @@ const Dashboard = memo(() => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const deviceValueDialogSave = useCallback(
|
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||||
async (devicevalue: DeviceValue) => {
|
|
||||||
if (!selectedDashboardItem) {
|
if (!selectedDashboardItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,13 +93,9 @@ const Dashboard = memo(() => {
|
|||||||
setDeviceValueDialogOpen(false);
|
setDeviceValueDialogOpen(false);
|
||||||
setSelectedDashboardItem(undefined);
|
setSelectedDashboardItem(undefined);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[selectedDashboardItem, sendDeviceValue, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dashboard_theme = useMemo(
|
const dashboard_theme = useTheme({
|
||||||
() =>
|
|
||||||
useTheme({
|
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
|
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
|
||||||
`,
|
`,
|
||||||
@@ -128,9 +123,7 @@ const Dashboard = memo(() => {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tree = useTree(
|
const tree = useTree(
|
||||||
{ nodes: [...data.nodes] },
|
{ nodes: [...data.nodes] },
|
||||||
@@ -164,19 +157,14 @@ const Dashboard = memo(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodeIds = useMemo(
|
|
||||||
() => data.nodes.map((item: DashboardItem) => item.id),
|
|
||||||
[data.nodes]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const nodeIds = data.nodes.map((item: DashboardItem) => item.id);
|
||||||
showAll
|
showAll
|
||||||
? tree.fns.onAddAll(nodeIds) // expand tree
|
? tree.fns.onAddAll(nodeIds) // expand tree
|
||||||
: tree.fns.onRemoveAll(); // collapse tree
|
: tree.fns.onRemoveAll(); // collapse tree
|
||||||
}, [parentNodes]);
|
}, [parentNodes]);
|
||||||
|
|
||||||
const showType = useCallback(
|
const showType = (n?: string, t?: number) => {
|
||||||
(n?: string, t?: number) => {
|
|
||||||
// if we have a name show it
|
// if we have a name show it
|
||||||
if (n) {
|
if (n) {
|
||||||
return n;
|
return n;
|
||||||
@@ -197,12 +185,9 @@ const Dashboard = memo(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showName = useCallback(
|
const showName = (di: DashboardItem) => {
|
||||||
(di: DashboardItem) => {
|
|
||||||
if (di.id < 100) {
|
if (di.id < 100) {
|
||||||
// if its a device (parent node) and has entities
|
// if its a device (parent node) and has entities
|
||||||
if (di.nodes?.length) {
|
if (di.nodes?.length) {
|
||||||
@@ -219,24 +204,17 @@ const Dashboard = memo(() => {
|
|||||||
return <span>{di.dv.id.slice(2)}</span>;
|
return <span>{di.dv.id.slice(2)}</span>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
};
|
||||||
[showType]
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasMask = useCallback(
|
const hasMask = (id: string, mask: number) =>
|
||||||
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const editDashboardValue = useCallback(
|
const editDashboardValue = (di: DashboardItem) => {
|
||||||
(di: DashboardItem) => {
|
|
||||||
if (me.admin && di.dv?.c) {
|
if (me.admin && di.dv?.c) {
|
||||||
setSelectedDashboardItem(di);
|
setSelectedDashboardItem(di);
|
||||||
setDeviceValueDialogOpen(true);
|
setDeviceValueDialogOpen(true);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[me.admin]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleShowAll = (
|
const handleShowAll = (
|
||||||
_event: React.MouseEvent<HTMLElement>,
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
@@ -248,10 +226,9 @@ const Dashboard = memo(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFavEntities = useMemo(
|
const hasFavEntities = data.nodes.filter(
|
||||||
() => data.nodes.filter((item: DashboardItem) => item.id <= 90).length,
|
(item: DashboardItem) => item.id <= 90
|
||||||
[data.nodes]
|
).length;
|
||||||
);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { IconContext } from 'react-icons';
|
import { IconContext } from 'react-icons';
|
||||||
@@ -133,17 +132,15 @@ const Devices = memo(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const leftOffset = useCallback(() => {
|
const leftOffset = () => {
|
||||||
const devicesWindow = document.getElementById('devices-window');
|
const devicesWindow = document.getElementById('devices-window');
|
||||||
if (!devicesWindow) return 0;
|
if (!devicesWindow) return 0;
|
||||||
const { left, right } = devicesWindow.getBoundingClientRect();
|
const { left, right } = devicesWindow.getBoundingClientRect();
|
||||||
if (!left || !right) return 0;
|
if (!left || !right) return 0;
|
||||||
return left + (right - left < 400 ? 0 : 200);
|
return left + (right - left < 400 ? 0 : 200);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const common_theme = useMemo(
|
const common_theme = useTheme({
|
||||||
() =>
|
|
||||||
useTheme({
|
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`,
|
`,
|
||||||
@@ -165,13 +162,9 @@ const Devices = memo(() => {
|
|||||||
background-color: #177ac9;
|
background-color: #177ac9;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const device_theme = useMemo(
|
const device_theme = useTheme([
|
||||||
() =>
|
|
||||||
useTheme([
|
|
||||||
common_theme,
|
common_theme,
|
||||||
{
|
{
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
@@ -196,13 +189,9 @@ const Devices = memo(() => {
|
|||||||
},
|
},
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
]),
|
]);
|
||||||
[common_theme]
|
|
||||||
);
|
|
||||||
|
|
||||||
const data_theme = useMemo(
|
const data_theme = useTheme([
|
||||||
() =>
|
|
||||||
useTheme([
|
|
||||||
common_theme,
|
common_theme,
|
||||||
{
|
{
|
||||||
Table: `
|
Table: `
|
||||||
@@ -244,9 +233,7 @@ const Devices = memo(() => {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
]),
|
]);
|
||||||
[common_theme]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSortIcon = (state: State, sortKey: unknown) => {
|
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||||
if (state.sortKey === sortKey && state.reverse) {
|
if (state.sortKey === sortKey && state.reverse) {
|
||||||
@@ -345,10 +332,8 @@ const Devices = memo(() => {
|
|||||||
return sc;
|
return sc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMask = useCallback(
|
const hasMask = (id: string, mask: number) =>
|
||||||
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDownloadCsv = () => {
|
const handleDownloadCsv = () => {
|
||||||
const deviceIndex = coreData.devices.findIndex(
|
const deviceIndex = coreData.devices.findIndex(
|
||||||
@@ -607,13 +592,12 @@ const Devices = memo(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDeviceValue = useCallback((dv: DeviceValue) => {
|
const showDeviceValue = (dv: DeviceValue) => {
|
||||||
setSelectedDeviceValue(dv);
|
setSelectedDeviceValue(dv);
|
||||||
setDeviceValueDialogOpen(true);
|
setDeviceValueDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const renderNameCell = useCallback(
|
const renderNameCell = (dv: DeviceValue) => (
|
||||||
(dv: DeviceValue) => (
|
|
||||||
<>
|
<>
|
||||||
{dv.id.slice(2)}
|
{dv.id.slice(2)}
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||||
@@ -626,22 +610,17 @@ const Devices = memo(() => {
|
|||||||
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
|
||||||
[hasMask]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const shown_data = useMemo(() => {
|
const shown_data = onlyFav
|
||||||
if (onlyFav) {
|
? deviceData.nodes.filter(
|
||||||
return deviceData.nodes.filter(
|
|
||||||
(dv: DeviceValue) =>
|
(dv: DeviceValue) =>
|
||||||
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
|
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
|
||||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||||
);
|
)
|
||||||
}
|
: deviceData.nodes.filter((dv: DeviceValue) =>
|
||||||
return deviceData.nodes.filter((dv: DeviceValue) =>
|
|
||||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||||
);
|
);
|
||||||
}, [deviceData.nodes, onlyFav, search]);
|
|
||||||
|
|
||||||
const deviceIndex = coreData.devices.findIndex(
|
const deviceIndex = coreData.devices.findIndex(
|
||||||
(d: Device) => d.id === device_select.state.id
|
(d: Device) => d.id === device_select.state.id
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } 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';
|
||||||
@@ -52,6 +52,7 @@ const DevicesDialog = ({
|
|||||||
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
|
// Stable handler reference so the memoized ValidatedTextField can skip re-renders
|
||||||
const updateFormValue = useMemo(() => updateValue(setEditItem), [setEditItem]);
|
const updateFormValue = useMemo(() => updateValue(setEditItem), [setEditItem]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -61,7 +62,7 @@ const DevicesDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
@@ -69,10 +70,9 @@ const DevicesDialog = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const setUom = useCallback(
|
const setUom = (uom?: DeviceValueUOM) => {
|
||||||
(uom?: DeviceValueUOM) => {
|
|
||||||
if (uom === undefined) {
|
if (uom === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -86,11 +86,9 @@ const DevicesDialog = ({
|
|||||||
default:
|
default:
|
||||||
return DeviceValueUOM_s[uom];
|
return DeviceValueUOM_s[uom];
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showHelperText = useCallback((dv: DeviceValue) => {
|
const showHelperText = (dv: DeviceValue) => {
|
||||||
if (dv.h) return dv.h;
|
if (dv.h) return dv.h;
|
||||||
if (dv.l) return dv.l.join(' | ');
|
if (dv.l) return dv.l.join(' | ');
|
||||||
if (dv.m !== undefined && dv.x !== undefined) {
|
if (dv.m !== undefined && dv.x !== undefined) {
|
||||||
@@ -101,26 +99,16 @@ const DevicesDialog = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const isCommand = useMemo(
|
const isCommand = selectedItem.v === '' && selectedItem.c;
|
||||||
() => selectedItem.v === '' && selectedItem.c,
|
const dialogTitle = isCommand
|
||||||
[selectedItem.v, selectedItem.c]
|
? LL.RUN_COMMAND()
|
||||||
);
|
: writeable
|
||||||
|
? LL.CHANGE_VALUE()
|
||||||
const dialogTitle = useMemo(() => {
|
: LL.VALUE(0);
|
||||||
if (isCommand) return LL.RUN_COMMAND();
|
const buttonLabel = isCommand ? LL.EXECUTE() : LL.UPDATE();
|
||||||
return writeable ? LL.CHANGE_VALUE() : LL.VALUE(0);
|
const helperText = showHelperText(editItem);
|
||||||
}, [isCommand, writeable, LL]);
|
|
||||||
|
|
||||||
const buttonLabel = useMemo(() => {
|
|
||||||
return isCommand ? LL.EXECUTE() : LL.UPDATE();
|
|
||||||
}, [isCommand, LL]);
|
|
||||||
|
|
||||||
const helperText = useMemo(
|
|
||||||
() => showHelperText(editItem),
|
|
||||||
[editItem, showHelperText]
|
|
||||||
);
|
|
||||||
|
|
||||||
const valueLabel = LL.VALUE(0);
|
const valueLabel = LL.VALUE(0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||||
|
|
||||||
import OptionIcon from './OptionIcon';
|
import OptionIcon from './OptionIcon';
|
||||||
@@ -11,7 +9,6 @@ interface EntityMaskToggleProps {
|
|||||||
de: DeviceEntity;
|
de: DeviceEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available mask values
|
|
||||||
const MASK_VALUES = [
|
const MASK_VALUES = [
|
||||||
DeviceEntityMask.DV_WEB_EXCLUDE, // 1
|
DeviceEntityMask.DV_WEB_EXCLUDE, // 1
|
||||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE, // 2
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE, // 2
|
||||||
@@ -20,35 +17,21 @@ const MASK_VALUES = [
|
|||||||
DeviceEntityMask.DV_DELETED // 128
|
DeviceEntityMask.DV_DELETED // 128
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
const getMaskNumber = (newMask: string[]): number =>
|
||||||
* Converts an array of mask strings to a bitmask number
|
newMask.reduce((mask, entry) => mask | Number(entry), 0);
|
||||||
*/
|
|
||||||
const getMaskNumber = (newMask: string[]): number => {
|
|
||||||
return newMask.reduce((mask, entry) => mask | Number(entry), 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const getMaskString = (mask: number): string[] =>
|
||||||
* Converts a bitmask number to an array of mask strings
|
MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
|
||||||
*/
|
|
||||||
const getMaskString = (mask: number): string[] => {
|
|
||||||
return MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
|
|
||||||
String(value)
|
String(value)
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a specific mask bit is set
|
|
||||||
*/
|
|
||||||
const hasMask = (mask: number, flag: number): boolean => (mask & flag) === flag;
|
const hasMask = (mask: number, flag: number): boolean => (mask & flag) === flag;
|
||||||
|
|
||||||
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
||||||
const handleChange = useCallback(
|
const handleChange = (_event: unknown, mask: string[]) => {
|
||||||
(_event: unknown, mask: string[]) => {
|
|
||||||
// Convert selected masks to a number
|
|
||||||
const newMask = getMaskNumber(mask);
|
const newMask = getMaskNumber(mask);
|
||||||
const updatedDe = { ...de };
|
const updatedDe = { ...de };
|
||||||
|
|
||||||
// Apply business logic for mask interactions
|
|
||||||
// If entity has no name and is set to readonly, also exclude from web
|
// If entity has no name and is set to readonly, also exclude from web
|
||||||
if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) {
|
if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) {
|
||||||
updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE;
|
updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||||
@@ -62,81 +45,67 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUpdate(updatedDe);
|
onUpdate(updatedDe);
|
||||||
},
|
};
|
||||||
[de, onUpdate]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize mask string value
|
|
||||||
const maskStringValue = useMemo(() => getMaskString(de.m), [de.m]);
|
|
||||||
|
|
||||||
// Memoize disabled states
|
|
||||||
const isFavoriteDisabled = useMemo(
|
|
||||||
() =>
|
|
||||||
hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_DELETED) ||
|
|
||||||
de.n === undefined,
|
|
||||||
[de.m, de.n]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isReadonlyDisabled = useMemo(
|
|
||||||
() =>
|
|
||||||
!de.w ||
|
|
||||||
hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_FAVORITE),
|
|
||||||
[de.w, de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isApiMqttExcludeDisabled = useMemo(
|
|
||||||
() => de.n === '' || hasMask(de.m, DeviceEntityMask.DV_DELETED),
|
|
||||||
[de.n, de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isWebExcludeDisabled = useMemo(
|
|
||||||
() => de.n === undefined || hasMask(de.m, DeviceEntityMask.DV_DELETED),
|
|
||||||
[de.n, de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize mask flag checks
|
|
||||||
const isFavoriteSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_FAVORITE),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isReadonlySet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_READONLY),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isApiMqttExcludeSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_API_MQTT_EXCLUDE),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isWebExcludeSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isDeletedSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_DELETED),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
value={maskStringValue}
|
value={getMaskString(de.m)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<ToggleButton value="8" disabled={isFavoriteDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="favorite" isSet={isFavoriteSet} />
|
value="8"
|
||||||
|
disabled={
|
||||||
|
hasMask(
|
||||||
|
de.m,
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_DELETED
|
||||||
|
) || de.n === undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="favorite"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_FAVORITE)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="4" disabled={isReadonlyDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="readonly" isSet={isReadonlySet} />
|
value="4"
|
||||||
|
disabled={
|
||||||
|
!de.w ||
|
||||||
|
hasMask(
|
||||||
|
de.m,
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_FAVORITE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="readonly"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_READONLY)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="2" disabled={isApiMqttExcludeDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={isApiMqttExcludeSet} />
|
value="2"
|
||||||
|
disabled={de.n === '' || hasMask(de.m, DeviceEntityMask.DV_DELETED)}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="api_mqtt_exclude"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_API_MQTT_EXCLUDE)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="1" disabled={isWebExcludeDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="web_exclude" isSet={isWebExcludeSet} />
|
value="1"
|
||||||
|
disabled={de.n === undefined || hasMask(de.m, DeviceEntityMask.DV_DELETED)}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="web_exclude"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="128">
|
<ToggleButton value="128">
|
||||||
<OptionIcon type="deleted" isSet={isDeletedSet} />
|
<OptionIcon
|
||||||
|
type="deleted"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_DELETED)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useMemo, useState } from 'react';
|
import { memo, useContext, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -60,6 +60,8 @@ const AVATAR_STYLES: SxProps<Theme> = {
|
|||||||
bgcolor: '#72caf9'
|
bgcolor: '#72caf9'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SYSTEM_INFO_API: APIcall = { device: 'system', cmd: 'info', id: 0 };
|
||||||
|
|
||||||
const HelpComponent = () => {
|
const HelpComponent = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
useLayoutTitle(LL.HELP());
|
useLayoutTitle(LL.HELP());
|
||||||
@@ -72,12 +74,7 @@ const HelpComponent = () => {
|
|||||||
});
|
});
|
||||||
const [imgError, setImgError] = useState<boolean>(false);
|
const [imgError, setImgError] = useState<boolean>(false);
|
||||||
|
|
||||||
const getCustomSupportMethod = useMemo(
|
useRequest(callAction({ action: 'getCustomSupport' })).onSuccess((event) => {
|
||||||
() => callAction({ action: 'getCustomSupport' }),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useRequest(getCustomSupportMethod).onSuccess((event) => {
|
|
||||||
if (event?.data && Object.keys(event.data).length !== 0) {
|
if (event?.data && Object.keys(event.data).length !== 0) {
|
||||||
const { Support } = event.data as {
|
const { Support } = event.data as {
|
||||||
Support: { img_url?: string; html?: string[] };
|
Support: { img_url?: string; html?: string[] };
|
||||||
@@ -100,20 +97,7 @@ const HelpComponent = () => {
|
|||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optimize API call memoization
|
const helpLinks: HelpLink[] = [
|
||||||
const apiCall = useMemo(() => ({ device: 'system', cmd: 'info', id: 0 }), []);
|
|
||||||
|
|
||||||
const handleDownloadSystemInfo = useCallback(() => {
|
|
||||||
void sendAPI(apiCall);
|
|
||||||
}, [sendAPI, apiCall]);
|
|
||||||
|
|
||||||
const handleImageError = useCallback(() => {
|
|
||||||
setImgError(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Memoize help links to prevent recreation on every render
|
|
||||||
const helpLinks: HelpLink[] = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
{
|
||||||
href: 'https://emsesp.org',
|
href: 'https://emsesp.org',
|
||||||
icon: <MenuBookIcon />,
|
icon: <MenuBookIcon />,
|
||||||
@@ -129,18 +113,10 @@ const HelpComponent = () => {
|
|||||||
icon: <GitHubIcon />,
|
icon: <GitHubIcon />,
|
||||||
label: () => LL.HELP_INFORMATION_3()
|
label: () => LL.HELP_INFORMATION_3()
|
||||||
}
|
}
|
||||||
],
|
];
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isAdmin = useMemo(() => me?.admin ?? false, [me?.admin]);
|
const imageSrc =
|
||||||
|
imgError || !customSupport.img_url ? DEFAULT_IMAGE_URL : customSupport.img_url;
|
||||||
// Memoize image source computation
|
|
||||||
const imageSrc = useMemo(
|
|
||||||
() =>
|
|
||||||
imgError || !customSupport.img_url ? DEFAULT_IMAGE_URL : customSupport.img_url,
|
|
||||||
[imgError, customSupport.img_url]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
@@ -157,13 +133,13 @@ const HelpComponent = () => {
|
|||||||
component="img"
|
component="img"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
sx={IMAGE_STYLES}
|
sx={IMAGE_STYLES}
|
||||||
onError={handleImageError}
|
onError={() => setImgError(true)}
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAdmin && (
|
{me?.admin && (
|
||||||
<List>
|
<List>
|
||||||
{helpLinks.map(({ href, icon, label }) => (
|
{helpLinks.map(({ href, icon, label }) => (
|
||||||
<ListItem key={href}>
|
<ListItem key={href}>
|
||||||
@@ -191,7 +167,7 @@ const HelpComponent = () => {
|
|||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDownloadSystemInfo}
|
onClick={() => void sendAPI(SYSTEM_INFO_API)}
|
||||||
>
|
>
|
||||||
{LL.SUPPORT_INFORMATION(0)}
|
{LL.SUPPORT_INFORMATION(0)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -214,7 +190,6 @@ const HelpComponent = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memoize the component to prevent unnecessary re-renders
|
|
||||||
const Help = memo(HelpComponent);
|
const Help = memo(HelpComponent);
|
||||||
|
|
||||||
export default Help;
|
export default Help;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -69,9 +69,7 @@ const Modules = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const modules_theme = useTheme(
|
const modules_theme = useTheme({
|
||||||
useMemo(
|
|
||||||
() => ({
|
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
|
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
|
||||||
`,
|
`,
|
||||||
@@ -111,16 +109,13 @@ const Modules = () => {
|
|||||||
background-color: #303030;
|
background-color: #303030;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDialogClose = useCallback(() => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const updateModuleItem = useCallback((updatedItem: ModuleItem) => {
|
const updateModuleItem = (updatedItem: ModuleItem) => {
|
||||||
void updateState(readModules(), (data: ModuleItem[]) => {
|
void updateState(readModules(), (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
|
||||||
@@ -128,28 +123,25 @@ const Modules = () => {
|
|||||||
setNumChanges(new_data.filter(hasModulesChanged).length);
|
setNumChanges(new_data.filter(hasModulesChanged).length);
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: ModuleItem) => {
|
||||||
(updatedItem: ModuleItem) => {
|
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
updateModuleItem(updatedItem);
|
updateModuleItem(updatedItem);
|
||||||
},
|
};
|
||||||
[updateModuleItem]
|
|
||||||
);
|
|
||||||
|
|
||||||
const editModuleItem = useCallback((mi: ModuleItem) => {
|
const editModuleItem = (mi: ModuleItem) => {
|
||||||
setSelectedModuleItem(mi);
|
setSelectedModuleItem(mi);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onCancel = useCallback(async () => {
|
const onCancel = async () => {
|
||||||
await fetchModules().then(() => {
|
await fetchModules().then(() => {
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [fetchModules]);
|
};
|
||||||
|
|
||||||
const saveModules = useCallback(async () => {
|
const saveModules = async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
modules.map((condensed_mi: ModuleItem) =>
|
modules.map((condensed_mi: ModuleItem) =>
|
||||||
@@ -167,9 +159,9 @@ const Modules = () => {
|
|||||||
await fetchModules();
|
await fetchModules();
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
}
|
}
|
||||||
}, [modules, updateModules, LL, fetchModules]);
|
};
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const renderContent = () => {
|
||||||
if (!modules) {
|
if (!modules) {
|
||||||
return (
|
return (
|
||||||
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
|
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
|
||||||
@@ -262,22 +254,12 @@ const Modules = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
modules,
|
|
||||||
fetchModules,
|
|
||||||
error,
|
|
||||||
modules_theme,
|
|
||||||
editModuleItem,
|
|
||||||
LL,
|
|
||||||
numChanges,
|
|
||||||
onCancel,
|
|
||||||
saveModules
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content}
|
{renderContent()}
|
||||||
{selectedModuleItem && (
|
{selectedModuleItem && (
|
||||||
<ModulesDialog
|
<ModulesDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import DoneIcon from '@mui/icons-material/Done';
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
@@ -37,14 +37,10 @@ const ModulesDialog = ({
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
|
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue(
|
||||||
() =>
|
|
||||||
updateValue(
|
|
||||||
setEditItem as unknown as React.Dispatch<
|
setEditItem as unknown as React.Dispatch<
|
||||||
React.SetStateAction<Record<string, unknown>>
|
React.SetStateAction<Record<string, unknown>>
|
||||||
>
|
>
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync form state when dialog opens or selected item changes
|
// Sync form state when dialog opens or selected item changes
|
||||||
@@ -54,18 +50,13 @@ const ModulesDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = () => {
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
}, [editItem, onSave]);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(
|
|
||||||
() => `${LL.EDIT()} ${editItem.key}`,
|
|
||||||
[LL, editItem.key]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
|
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{`${LL.EDIT()} ${editItem.key}`}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ const Scheduler = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasScheduleChanged = useCallback((si: ScheduleItem) => {
|
const hasScheduleChanged = (si: ScheduleItem) => {
|
||||||
return (
|
return (
|
||||||
si.id !== si.o_id ||
|
si.id !== si.o_id ||
|
||||||
(si.name || '') !== (si.o_name || '') ||
|
(si.name || '') !== (si.o_name || '') ||
|
||||||
@@ -143,15 +143,13 @@ const Scheduler = () => {
|
|||||||
si.cmd !== si.o_cmd ||
|
si.cmd !== si.o_cmd ||
|
||||||
si.value !== si.o_value
|
si.value !== si.o_value
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const intervalCallback = useCallback(() => {
|
useInterval(() => {
|
||||||
if (numChanges === 0) {
|
if (numChanges === 0) {
|
||||||
void fetchSchedule();
|
void fetchSchedule();
|
||||||
}
|
}
|
||||||
}, [numChanges, fetchSchedule]);
|
}, INTERVAL_DELAY);
|
||||||
|
|
||||||
useInterval(intervalCallback, INTERVAL_DELAY);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formatter = new Intl.DateTimeFormat(locale, {
|
const formatter = new Intl.DateTimeFormat(locale, {
|
||||||
@@ -169,7 +167,7 @@ const Scheduler = () => {
|
|||||||
|
|
||||||
const schedule_theme = useTheme(scheduleTheme);
|
const schedule_theme = useTheme(scheduleTheme);
|
||||||
|
|
||||||
const saveSchedule = useCallback(async () => {
|
const saveSchedule = async () => {
|
||||||
try {
|
try {
|
||||||
await updateSchedule({
|
await updateSchedule({
|
||||||
schedule: schedule
|
schedule: schedule
|
||||||
@@ -192,29 +190,28 @@ const Scheduler = () => {
|
|||||||
await fetchSchedule();
|
await fetchSchedule();
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
}
|
}
|
||||||
}, [LL, schedule, updateSchedule, fetchSchedule]);
|
};
|
||||||
|
|
||||||
const editScheduleItem = useCallback((si: ScheduleItem) => {
|
const editScheduleItem = (si: ScheduleItem) => {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
setSelectedScheduleItem(si);
|
setSelectedScheduleItem(si);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
if (si.o_name === undefined) {
|
if (si.o_name === undefined) {
|
||||||
si.o_name = si.name;
|
si.o_name = si.name;
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogClose = useCallback(() => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogCancel = useCallback(async () => {
|
const onDialogCancel = async () => {
|
||||||
await fetchSchedule().then(() => {
|
await fetchSchedule().then(() => {
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [fetchSchedule]);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||||
(updatedItem: ScheduleItem) => {
|
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
void updateState(readSchedule(), (data: ScheduleItem[]) => {
|
void updateState(readSchedule(), (data: ScheduleItem[]) => {
|
||||||
const new_data = creating
|
const new_data = creating
|
||||||
@@ -227,11 +224,9 @@ const Scheduler = () => {
|
|||||||
|
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[creating, hasScheduleChanged]
|
|
||||||
);
|
|
||||||
|
|
||||||
const addScheduleItem = useCallback(() => {
|
const addScheduleItem = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
const newItem: ScheduleItem = {
|
const newItem: ScheduleItem = {
|
||||||
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
||||||
@@ -239,18 +234,13 @@ const Scheduler = () => {
|
|||||||
};
|
};
|
||||||
setSelectedScheduleItem(newItem);
|
setSelectedScheduleItem(newItem);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const filteredAndSortedSchedule = useMemo(
|
const filteredAndSortedSchedule = schedule
|
||||||
() =>
|
|
||||||
schedule
|
|
||||||
.filter((si: ScheduleItem) => !si.deleted)
|
.filter((si: ScheduleItem) => !si.deleted)
|
||||||
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags),
|
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags);
|
||||||
[schedule]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dayBox = useCallback(
|
const dayBox = (si: ScheduleItem, flag: number) => {
|
||||||
(si: ScheduleItem, flag: number) => {
|
|
||||||
const dayIndex = Math.log(flag) / LOG_2;
|
const dayIndex = Math.log(flag) / LOG_2;
|
||||||
const isActive = (si.flags & flag) === flag;
|
const isActive = (si.flags & flag) === flag;
|
||||||
|
|
||||||
@@ -264,11 +254,9 @@ const Scheduler = () => {
|
|||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
[dow]
|
|
||||||
);
|
|
||||||
|
|
||||||
const scheduleType = useCallback((si: ScheduleItem) => {
|
const scheduleType = (si: ScheduleItem) => {
|
||||||
const label = scheduleTypeLabels[si.flags];
|
const label = scheduleTypeLabels[si.flags];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -278,9 +266,9 @@ const Scheduler = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const renderSchedule = useCallback(() => {
|
const renderSchedule = () => {
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
return (
|
return (
|
||||||
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
|
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
|
||||||
@@ -343,17 +331,7 @@ const Scheduler = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
schedule,
|
|
||||||
error,
|
|
||||||
fetchSchedule,
|
|
||||||
filteredAndSortedSchedule,
|
|
||||||
schedule_theme,
|
|
||||||
editScheduleItem,
|
|
||||||
LL,
|
|
||||||
dayBox,
|
|
||||||
scheduleType
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -60,6 +60,12 @@ const FLAG_VALUES = [
|
|||||||
ScheduleFlag.SCHEDULE_SAT
|
ScheduleFlag.SCHEDULE_SAT
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const getFlagDOWnumber = (flags: string[]) =>
|
||||||
|
flags.reduce((acc, flag) => acc | Number(flag), 0) & FLAG_MASK_127;
|
||||||
|
|
||||||
|
const getFlagDOWstring = (f: number) =>
|
||||||
|
FLAG_VALUES.filter((flag) => (f & flag) === flag).map((flag) => String(flag));
|
||||||
|
|
||||||
interface SchedulerDialogProps {
|
interface SchedulerDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
@@ -84,6 +90,7 @@ const SchedulerDialog = ({
|
|||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const [scheduleType, setScheduleType] = useState<ScheduleFlag>();
|
const [scheduleType, setScheduleType] = useState<ScheduleFlag>();
|
||||||
|
|
||||||
|
// Stable handler reference so the memoized ValidatedTextField can skip re-renders
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateValue(
|
updateValue(
|
||||||
@@ -112,9 +119,7 @@ const SchedulerDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
// Helper function to handle save operations
|
const handleSave = async (itemToSave: ScheduleItem) => {
|
||||||
const handleSave = useCallback(
|
|
||||||
async (itemToSave: ScheduleItem) => {
|
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, itemToSave);
|
await validate(validator, itemToSave);
|
||||||
@@ -122,36 +127,21 @@ const SchedulerDialog = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[validator, onSave]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
await handleSave(editItem);
|
await handleSave(editItem);
|
||||||
}, [editItem, handleSave]);
|
};
|
||||||
|
|
||||||
const saveandactivate = useCallback(async () => {
|
const saveandactivate = async () => {
|
||||||
await handleSave({ ...editItem, active: true });
|
await handleSave({ ...editItem, active: true });
|
||||||
}, [editItem, handleSave]);
|
};
|
||||||
|
|
||||||
const remove = useCallback(() => {
|
const remove = () => {
|
||||||
onSave({ ...editItem, deleted: true });
|
onSave({ ...editItem, deleted: true });
|
||||||
}, [editItem, onSave]);
|
};
|
||||||
|
|
||||||
// Optimize DOW flag conversion
|
const DayOfWeekButton = (flag: number) => {
|
||||||
const getFlagDOWnumber = useCallback((flags: string[]) => {
|
|
||||||
return flags.reduce((acc, flag) => acc | Number(flag), 0) & FLAG_MASK_127;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getFlagDOWstring = useCallback((f: number) => {
|
|
||||||
return FLAG_VALUES.filter((flag) => (f & flag) === flag).map((flag) =>
|
|
||||||
String(flag)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Day of week display component
|
|
||||||
const DayOfWeekButton = useCallback(
|
|
||||||
(flag: number) => {
|
|
||||||
const dayIndex = Math.log2(flag);
|
const dayIndex = Math.log2(flag);
|
||||||
const isSelected = (editItem.flags & flag) === flag;
|
const isSelected = (editItem.flags & flag) === flag;
|
||||||
return (
|
return (
|
||||||
@@ -162,21 +152,21 @@ const SchedulerDialog = ({
|
|||||||
{dow[dayIndex]}
|
{dow[dayIndex]}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
[editItem.flags, dow]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
_event: React.SyntheticEvent,
|
||||||
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
|
) => {
|
||||||
if (reason !== 'backdropClick') {
|
if (reason !== 'backdropClick') {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleScheduleTypeChange = useCallback(
|
const handleScheduleTypeChange = (
|
||||||
(_event: React.SyntheticEvent<HTMLElement>, flag: ScheduleFlag | null) => {
|
_event: React.SyntheticEvent<HTMLElement>,
|
||||||
|
flag: ScheduleFlag | null
|
||||||
|
) => {
|
||||||
if (flag !== null) {
|
if (flag !== null) {
|
||||||
setFieldErrors(undefined); // clear any validation errors
|
setFieldErrors(undefined); // clear any validation errors
|
||||||
setScheduleType(flag);
|
setScheduleType(flag);
|
||||||
@@ -185,56 +175,39 @@ const SchedulerDialog = ({
|
|||||||
const newFlags = flag === ScheduleFlag.SCHEDULE_DAY ? FLAG_ALL_DAYS : flag;
|
const newFlags = flag === ScheduleFlag.SCHEDULE_DAY ? FLAG_ALL_DAYS : flag;
|
||||||
setEditItem((prev) => ({ ...prev, time: '', flags: newFlags }));
|
setEditItem((prev) => ({ ...prev, time: '', flags: newFlags }));
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDOWChange = useCallback(
|
const handleDOWChange = (
|
||||||
(_event: React.SyntheticEvent<HTMLElement>, flags: string[]) => {
|
_event: React.SyntheticEvent<HTMLElement>,
|
||||||
|
flags: string[]
|
||||||
|
) => {
|
||||||
const newFlags =
|
const newFlags =
|
||||||
getFlagDOWnumber(flags) === 0 ? FLAG_ALL_DAYS : getFlagDOWnumber(flags);
|
getFlagDOWnumber(flags) === 0 ? FLAG_ALL_DAYS : getFlagDOWnumber(flags);
|
||||||
setEditItem((prev) => ({ ...prev, flags: newFlags }));
|
setEditItem((prev) => ({ ...prev, flags: newFlags }));
|
||||||
},
|
};
|
||||||
[getFlagDOWnumber]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize derived values
|
const isDaySchedule = scheduleType === ScheduleFlag.SCHEDULE_DAY;
|
||||||
const isDaySchedule = useMemo(
|
const isTimerSchedule = scheduleType === ScheduleFlag.SCHEDULE_TIMER;
|
||||||
() => scheduleType === ScheduleFlag.SCHEDULE_DAY,
|
const isImmediateSchedule = scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE;
|
||||||
[scheduleType]
|
const needsTimeField = isDaySchedule || isTimerSchedule;
|
||||||
);
|
|
||||||
const isTimerSchedule = useMemo(
|
|
||||||
() => scheduleType === ScheduleFlag.SCHEDULE_TIMER,
|
|
||||||
[scheduleType]
|
|
||||||
);
|
|
||||||
const isImmediateSchedule = useMemo(
|
|
||||||
() => scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE,
|
|
||||||
[scheduleType]
|
|
||||||
);
|
|
||||||
const needsTimeField = useMemo(
|
|
||||||
() => isDaySchedule || isTimerSchedule,
|
|
||||||
[isDaySchedule, isTimerSchedule]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dowFlags = useMemo(
|
const dowFlags = getFlagDOWstring(editItem.flags);
|
||||||
() => getFlagDOWstring(editItem.flags),
|
|
||||||
[editItem.flags, getFlagDOWstring]
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeFieldValue = useMemo(() => {
|
const timeFieldValue = needsTimeField
|
||||||
if (needsTimeField) {
|
? editItem.time === ''
|
||||||
return editItem.time === '' ? DEFAULT_TIME : editItem.time;
|
? DEFAULT_TIME
|
||||||
}
|
: editItem.time
|
||||||
return editItem.time === DEFAULT_TIME ? '' : editItem.time;
|
: editItem.time === DEFAULT_TIME
|
||||||
}, [editItem.time, needsTimeField]);
|
? ''
|
||||||
|
: editItem.time;
|
||||||
|
|
||||||
const timeFieldLabel = useMemo(() => {
|
const timeFieldLabel = (() => {
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_TIMER) return LL.TIMER(1);
|
if (scheduleType === ScheduleFlag.SCHEDULE_TIMER) return LL.TIMER(1);
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION();
|
if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION();
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE();
|
if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE();
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE();
|
if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE();
|
||||||
return LL.TIME(1);
|
return LL.TIME(1);
|
||||||
}, [scheduleType, LL]);
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext, useMemo, useRef, useState } from 'react';
|
import { useContext, useRef, useState } 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';
|
||||||
@@ -158,18 +158,16 @@ const Sensors = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const intervalCallback = useCallback(() => {
|
useInterval(() => {
|
||||||
if (!temperatureDialogOpen && !analogDialogOpen) {
|
if (!temperatureDialogOpen && !analogDialogOpen) {
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
}
|
}
|
||||||
}, [temperatureDialogOpen, analogDialogOpen, fetchSensorData]);
|
});
|
||||||
|
|
||||||
useInterval(intervalCallback);
|
|
||||||
|
|
||||||
const temperature_theme = useTheme([common_theme, temperature_theme_config]);
|
const temperature_theme = useTheme([common_theme, temperature_theme_config]);
|
||||||
const analog_theme = useTheme([common_theme, analog_theme_config]);
|
const analog_theme = useTheme([common_theme, analog_theme_config]);
|
||||||
|
|
||||||
const getSortIcon = useCallback((state: State, sortKey: unknown) => {
|
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||||
if (state.sortKey === sortKey && state.reverse) {
|
if (state.sortKey === sortKey && state.reverse) {
|
||||||
return <KeyboardArrowDownOutlinedIcon />;
|
return <KeyboardArrowDownOutlinedIcon />;
|
||||||
}
|
}
|
||||||
@@ -177,7 +175,7 @@ const Sensors = () => {
|
|||||||
return <KeyboardArrowUpOutlinedIcon />;
|
return <KeyboardArrowUpOutlinedIcon />;
|
||||||
}
|
}
|
||||||
return <UnfoldMoreOutlinedIcon />;
|
return <UnfoldMoreOutlinedIcon />;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const analog_sort = useSort(
|
const analog_sort = useSort(
|
||||||
{ nodes: sensorData.as },
|
{ nodes: sensorData.as },
|
||||||
@@ -234,8 +232,7 @@ const Sensors = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.SENSORS());
|
useLayoutTitle(LL.SENSORS());
|
||||||
|
|
||||||
const formatDurationMin = useCallback(
|
const formatDurationMin = (duration_min: number) => {
|
||||||
(duration_min: number) => {
|
|
||||||
const totalMs = duration_min * MS_PER_MINUTE;
|
const totalMs = duration_min * MS_PER_MINUTE;
|
||||||
const days = Math.trunc(totalMs / MS_PER_DAY);
|
const days = Math.trunc(totalMs / MS_PER_DAY);
|
||||||
const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24;
|
const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24;
|
||||||
@@ -252,12 +249,9 @@ const Sensors = () => {
|
|||||||
parts.push(LL.NUM_MINUTES({ num: minutes }));
|
parts.push(LL.NUM_MINUTES({ num: minutes }));
|
||||||
}
|
}
|
||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
},
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatValue = useCallback(
|
const formatValue = (value: unknown, uom: DeviceValueUOM) => {
|
||||||
(value: unknown, uom: DeviceValueUOM) => {
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -286,28 +280,22 @@ const Sensors = () => {
|
|||||||
default:
|
default:
|
||||||
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[formatDurationMin, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateTemperatureSensor = useCallback(
|
const updateTemperatureSensor = (ts: TemperatureSensor) => {
|
||||||
(ts: TemperatureSensor) => {
|
|
||||||
if (me.admin) {
|
if (me.admin) {
|
||||||
ts.o_n = ts.n;
|
ts.o_n = ts.n;
|
||||||
setSelectedTemperatureSensor(ts);
|
setSelectedTemperatureSensor(ts);
|
||||||
setTemperatureDialogOpen(true);
|
setTemperatureDialogOpen(true);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[me.admin]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTemperatureDialogClose = useCallback(() => {
|
const onTemperatureDialogClose = () => {
|
||||||
setTemperatureDialogOpen(false);
|
setTemperatureDialogOpen(false);
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
}, [fetchSensorData]);
|
};
|
||||||
|
|
||||||
const onTemperatureDialogSave = useCallback(
|
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
|
||||||
async (ts: TemperatureSensor) => {
|
|
||||||
await sendTemperatureSensor({
|
await sendTemperatureSensor({
|
||||||
id: ts.id,
|
id: ts.id,
|
||||||
name: ts.n,
|
name: ts.n,
|
||||||
@@ -325,28 +313,23 @@ const Sensors = () => {
|
|||||||
setSelectedTemperatureSensor(undefined);
|
setSelectedTemperatureSensor(undefined);
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[sendTemperatureSensor, LL, fetchSensorData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateAnalogSensor = useCallback(
|
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||||
(as: AnalogSensor) => {
|
|
||||||
if (me.admin) {
|
if (me.admin) {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
as.o_n = as.n;
|
as.o_n = as.n;
|
||||||
setSelectedAnalogSensor(as);
|
setSelectedAnalogSensor(as);
|
||||||
setAnalogDialogOpen(true);
|
setAnalogDialogOpen(true);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[me.admin]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onAnalogDialogClose = useCallback(() => {
|
const onAnalogDialogClose = () => {
|
||||||
setAnalogDialogOpen(false);
|
setAnalogDialogOpen(false);
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
}, [fetchSensorData]);
|
};
|
||||||
|
|
||||||
const addAnalogSensor = useCallback(() => {
|
const addAnalogSensor = () => {
|
||||||
if (firstAvailableGPIO.current === undefined) {
|
if (firstAvailableGPIO.current === undefined) {
|
||||||
toast.error(LL.NO_GPIO());
|
toast.error(LL.NO_GPIO());
|
||||||
return;
|
return;
|
||||||
@@ -366,10 +349,9 @@ const Sensors = () => {
|
|||||||
o_n: ''
|
o_n: ''
|
||||||
});
|
});
|
||||||
setAnalogDialogOpen(true);
|
setAnalogDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onAnalogDialogSave = useCallback(
|
const onAnalogDialogSave = async (as: AnalogSensor) => {
|
||||||
async (as: AnalogSensor) => {
|
|
||||||
await sendAnalogSensor({
|
await sendAnalogSensor({
|
||||||
id: as.id,
|
id: as.id,
|
||||||
gpio: as.g,
|
gpio: as.g,
|
||||||
@@ -392,12 +374,9 @@ const Sensors = () => {
|
|||||||
setSelectedAnalogSensor(undefined);
|
setSelectedAnalogSensor(undefined);
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[sendAnalogSensor, LL, fetchSensorData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const RenderAnalogSensors = useMemo(
|
const RenderAnalogSensors = (
|
||||||
() => (
|
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: sensorData.as }}
|
data={{ nodes: sensorData.as }}
|
||||||
theme={analog_theme}
|
theme={analog_theme}
|
||||||
@@ -443,9 +422,7 @@ const Sensors = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
style={HEADER_BUTTON_STYLE_END}
|
style={HEADER_BUTTON_STYLE_END}
|
||||||
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
|
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
|
||||||
onClick={() =>
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||||
analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{LL.VALUE(0)}
|
{LL.VALUE(0)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -478,20 +455,9 @@ const Sensors = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
),
|
|
||||||
[
|
|
||||||
analog_sort,
|
|
||||||
analog_theme,
|
|
||||||
getSortIcon,
|
|
||||||
sensorData.as,
|
|
||||||
LL,
|
|
||||||
updateAnalogSensor,
|
|
||||||
formatValue
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const RenderTemperatureSensors = useMemo(
|
const RenderTemperatureSensors = (
|
||||||
() => (
|
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: sensorData.ts }}
|
data={{ nodes: sensorData.ts }}
|
||||||
theme={temperature_theme}
|
theme={temperature_theme}
|
||||||
@@ -544,16 +510,6 @@ const Sensors = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
),
|
|
||||||
[
|
|
||||||
temperature_sort,
|
|
||||||
temperature_theme,
|
|
||||||
getSortIcon,
|
|
||||||
sensorData.ts,
|
|
||||||
LL,
|
|
||||||
updateTemperatureSensor,
|
|
||||||
formatValue
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import DoneIcon from '@mui/icons-material/Done';
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
@@ -53,6 +53,7 @@ const SensorsAnalogDialog = ({
|
|||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
|
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
|
||||||
|
|
||||||
|
// Stable handler reference so the memoized ValidatedTextField can skip re-renders
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateValue((updater) =>
|
updateValue((updater) =>
|
||||||
@@ -66,49 +67,29 @@ const SensorsAnalogDialog = ({
|
|||||||
[setEditItem]
|
[setEditItem]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize helper functions to check sensor type conditions
|
const isCounterOrRate =
|
||||||
const isCounterOrRate = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.COUNTER ||
|
editItem.t === AnalogType.COUNTER ||
|
||||||
editItem.t === AnalogType.RATE ||
|
editItem.t === AnalogType.RATE ||
|
||||||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2),
|
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
|
||||||
[editItem.t]
|
const isCounter =
|
||||||
);
|
|
||||||
const isCounter = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.COUNTER ||
|
editItem.t === AnalogType.COUNTER ||
|
||||||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2),
|
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
|
||||||
[editItem.t]
|
const isFreqType =
|
||||||
);
|
editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2;
|
||||||
const isFreqType = useMemo(
|
const isPWM =
|
||||||
() => editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2,
|
|
||||||
[editItem.t]
|
|
||||||
);
|
|
||||||
const isPWM = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.PWM_0 ||
|
editItem.t === AnalogType.PWM_0 ||
|
||||||
editItem.t === AnalogType.PWM_1 ||
|
editItem.t === AnalogType.PWM_1 ||
|
||||||
editItem.t === AnalogType.PWM_2,
|
editItem.t === AnalogType.PWM_2;
|
||||||
[editItem.t]
|
const isDACOutGPIO =
|
||||||
);
|
|
||||||
const isDACOutGPIO = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.DIGITAL_OUT &&
|
editItem.t === AnalogType.DIGITAL_OUT &&
|
||||||
(editItem.g === 25 || editItem.g === 26),
|
(editItem.g === 25 || editItem.g === 26);
|
||||||
[editItem.t, editItem.g]
|
const isDigitalOutGPIO =
|
||||||
);
|
editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26;
|
||||||
const isDigitalOutGPIO = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.DIGITAL_OUT &&
|
|
||||||
editItem.g !== 25 &&
|
|
||||||
editItem.g !== 26,
|
|
||||||
[editItem.t, editItem.g]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize menu items to avoid recreation on each render
|
const analogTypeMenuItems = AnalogTypeNames.map((val, i) => ({
|
||||||
const analogTypeMenuItems = useMemo(
|
name: val,
|
||||||
() =>
|
value: i + 1
|
||||||
AnalogTypeNames.map((val, i) => ({ name: val, value: i + 1 }))
|
}))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map(({ name, value }) => (
|
.map(({ name, value }) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -118,19 +99,13 @@ const SensorsAnalogDialog = ({
|
|||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)),
|
));
|
||||||
[disabledTypeList]
|
|
||||||
);
|
|
||||||
|
|
||||||
const uomMenuItems = useMemo(
|
const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
|
||||||
() =>
|
|
||||||
DeviceValueUOM_s.map((val, i) => (
|
|
||||||
<MenuItem key={val} value={i}>
|
<MenuItem key={val} value={i}>
|
||||||
{val}
|
{val}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)),
|
));
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const analogGPIOMenuItems = () =>
|
const analogGPIOMenuItems = () =>
|
||||||
// add selectedItem.g to the list
|
// add selectedItem.g to the list
|
||||||
@@ -157,16 +132,16 @@ const SensorsAnalogDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
_event: React.SyntheticEvent,
|
||||||
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
|
) => {
|
||||||
if (reason !== 'backdropClick') {
|
if (reason !== 'backdropClick') {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
@@ -174,17 +149,13 @@ const SensorsAnalogDialog = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const remove = useCallback(() => {
|
const remove = () => {
|
||||||
onSave({ ...editItem, d: true });
|
onSave({ ...editItem, d: true });
|
||||||
}, [editItem, onSave]);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(
|
const dialogTitle = `${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`;
|
||||||
() =>
|
|
||||||
`${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`,
|
|
||||||
[creating, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import DoneIcon from '@mui/icons-material/Done';
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
@@ -50,6 +50,7 @@ const SensorsTemperatureDialog = ({
|
|||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
||||||
|
|
||||||
|
// Stable handler reference so the memoized ValidatedTextField can skip re-renders
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateValue(
|
updateValue(
|
||||||
@@ -69,16 +70,13 @@ const SensorsTemperatureDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (_event: React.SyntheticEvent, reason?: string) => {
|
||||||
(_event: React.SyntheticEvent, reason?: string) => {
|
|
||||||
if (reason !== 'backdropClick') {
|
if (reason !== 'backdropClick') {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
@@ -86,29 +84,11 @@ const SensorsTemperatureDialog = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(() => `${LL.EDIT()} ${LL.TEMP_SENSOR()}`, [LL]);
|
|
||||||
|
|
||||||
const offsetValue = useMemo(() => numberValue(editItem.o), [editItem.o]);
|
|
||||||
|
|
||||||
const slotProps = useMemo(
|
|
||||||
() => ({
|
|
||||||
input: {
|
|
||||||
startAdornment: <InputAdornment position="start">{TEMP_UNIT}</InputAdornment>
|
|
||||||
},
|
|
||||||
htmlInput: {
|
|
||||||
min: OFFSET_MIN,
|
|
||||||
max: OFFSET_MAX,
|
|
||||||
step: OFFSET_STEP
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{`${LL.EDIT()} ${LL.TEMP_SENSOR()}`}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography sx={{ mb: 2 }} color="warning" variant="body2">
|
<Typography sx={{ mb: 2 }} color="warning" variant="body2">
|
||||||
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
||||||
@@ -128,12 +108,23 @@ const SensorsTemperatureDialog = ({
|
|||||||
<TextField
|
<TextField
|
||||||
name="o"
|
name="o"
|
||||||
label={LL.OFFSET()}
|
label={LL.OFFSET()}
|
||||||
value={offsetValue}
|
value={numberValue(editItem.o)}
|
||||||
sx={{ width: '11ch' }}
|
sx={{ width: '11ch' }}
|
||||||
type="number"
|
type="number"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
slotProps={slotProps}
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">{TEMP_UNIT}</InputAdornment>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
htmlInput: {
|
||||||
|
min: OFFSET_MIN,
|
||||||
|
max: OFFSET_MAX,
|
||||||
|
step: OFFSET_STEP
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
|
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import {
|
import {
|
||||||
@@ -23,9 +23,9 @@ const UserProfileComponent = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.USER_PROFILE());
|
useLayoutTitle(LL.USER_PROFILE());
|
||||||
|
|
||||||
const handleSignOut = useCallback(() => {
|
const handleSignOut = () => {
|
||||||
signOut(true);
|
signOut(true);
|
||||||
}, [signOut]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } 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';
|
||||||
@@ -63,22 +63,16 @@ const APSettings = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValueDirty(
|
||||||
() =>
|
|
||||||
updateValueDirty(
|
|
||||||
origData as unknown as Record<string, unknown>,
|
origData as unknown as Record<string, unknown>,
|
||||||
dirtyFlags,
|
dirtyFlags,
|
||||||
setDirtyFlags,
|
setDirtyFlags,
|
||||||
updateDataValue as (value: unknown) => void
|
updateDataValue as (value: unknown) => void
|
||||||
),
|
|
||||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize AP enabled state
|
const apEnabled = data ? isAPEnabled(data) : false;
|
||||||
const apEnabled = useMemo(() => (data ? isAPEnabled(data) : false), [data]);
|
|
||||||
|
|
||||||
// Memoize validation and submit handler
|
const validateAndSubmit = async () => {
|
||||||
const validateAndSubmit = useCallback(async () => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -88,7 +82,7 @@ const APSettings = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } 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';
|
||||||
@@ -106,49 +106,36 @@ const ApplicationSettings = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoized input props to prevent recreation on every render
|
const SecondsInputProps = {
|
||||||
const SecondsInputProps = useMemo(
|
|
||||||
() => ({
|
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
}),
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const MinutesInputProps = useMemo(
|
const MinutesInputProps = {
|
||||||
() => ({
|
|
||||||
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||||
}),
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const HoursInputProps = useMemo(
|
const HoursInputProps = {
|
||||||
() => ({
|
|
||||||
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
||||||
}),
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
(error: Error) => {
|
(error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
const updateBoardProfile = useCallback(
|
const updateBoardProfile = async (board_profile: string) => {
|
||||||
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);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[readBoardProfile]
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutTitle(LL.APPLICATION());
|
useLayoutTitle(LL.APPLICATION());
|
||||||
|
|
||||||
const validateAndSubmit = useCallback(async () => {
|
const validateAndSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(createSettingsValidator(data), data);
|
await validate(createSettingsValidator(data), data);
|
||||||
@@ -157,10 +144,9 @@ const ApplicationSettings = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
await saveData();
|
await saveData();
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
const changeBoardProfile = useCallback(
|
const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const boardProfile = event.target.value;
|
const boardProfile = event.target.value;
|
||||||
updateFormValue(event);
|
updateFormValue(event);
|
||||||
if (boardProfile === 'CUSTOM') {
|
if (boardProfile === 'CUSTOM') {
|
||||||
@@ -171,17 +157,14 @@ const ApplicationSettings = () => {
|
|||||||
} else {
|
} else {
|
||||||
void updateBoardProfile(boardProfile);
|
void updateBoardProfile(boardProfile);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[data, updateBoardProfile, updateFormValue, updateDataValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
const restart = useCallback(async () => {
|
const restart = async () => {
|
||||||
await validateAndSubmit();
|
await validateAndSubmit();
|
||||||
await doRestart();
|
await doRestart();
|
||||||
}, [validateAndSubmit, doRestart]);
|
};
|
||||||
|
|
||||||
// Memoize board profile select items to prevent recreation
|
const boardProfileItems = boardProfileSelectItems();
|
||||||
const boardProfileItems = useMemo(() => boardProfileSelectItems(), []);
|
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data || !hardwareData) {
|
if (!data || !hardwareData) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } 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';
|
||||||
@@ -57,7 +57,7 @@ const DownloadUpload = () => {
|
|||||||
|
|
||||||
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
try {
|
try {
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 });
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 });
|
||||||
@@ -65,16 +65,33 @@ const DownloadUpload = () => {
|
|||||||
toast.error((error as Error).message);
|
toast.error((error as Error).message);
|
||||||
setRestarting(false);
|
setRestarting(false);
|
||||||
}
|
}
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
|
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
|
||||||
|
|
||||||
const handleCloseBackupDialog = useCallback(() => {
|
const handleCloseBackupDialog = () => {
|
||||||
setConfirmBackup(false);
|
setConfirmBackup(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const renderBackupDialog = useMemo(
|
const handleDownload = (type: string) => () => {
|
||||||
() => (
|
void sendExportData(type);
|
||||||
|
setConfirmBackup(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (restarting) {
|
||||||
|
return <SystemMonitor />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
<Dialog
|
<Dialog
|
||||||
sx={dialogStyle}
|
sx={dialogStyle}
|
||||||
open={confirmBackup}
|
open={confirmBackup}
|
||||||
@@ -98,40 +115,13 @@ const DownloadUpload = () => {
|
|||||||
<Button
|
<Button
|
||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => handleDownload('systembackup')()}
|
onClick={handleDownload('systembackup')}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{LL.DOWNLOAD(0)}
|
{LL.DOWNLOAD(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
),
|
|
||||||
[confirmBackup, handleCloseBackupDialog, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDownload = useCallback(
|
|
||||||
(type: string) => () => {
|
|
||||||
void sendExportData(type);
|
|
||||||
setConfirmBackup(false);
|
|
||||||
},
|
|
||||||
[sendExportData]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (restarting) {
|
|
||||||
return <SystemMonitor />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<SectionContent>
|
|
||||||
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
|
||||||
</SectionContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionContent>
|
|
||||||
{renderBackupDialog}
|
|
||||||
|
|
||||||
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
||||||
{LL.DOWNLOAD(0)}
|
{LL.DOWNLOAD(0)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } 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';
|
||||||
@@ -57,7 +57,7 @@ const MqttSettings = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const sendResetMQTT = useCallback(() => {
|
const sendResetMQTT = () => {
|
||||||
void callAction({ action: 'resetMQTT' })
|
void callAction({ action: 'resetMQTT' })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('MQTT ' + LL.REFRESH() + ' successful');
|
toast.success('MQTT ' + LL.REFRESH() + ' successful');
|
||||||
@@ -65,29 +65,20 @@ const MqttSettings = () => {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValueDirty(
|
||||||
() =>
|
|
||||||
updateValueDirty(
|
|
||||||
origData as unknown as Record<string, unknown>,
|
origData as unknown as Record<string, unknown>,
|
||||||
dirtyFlags,
|
dirtyFlags,
|
||||||
setDirtyFlags,
|
setDirtyFlags,
|
||||||
updateDataValue as (value: unknown) => void
|
updateDataValue as (value: unknown) => void
|
||||||
),
|
|
||||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecondsInputProps = useMemo(
|
const SecondsInputProps = {
|
||||||
() => ({
|
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
}),
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const emptyFieldErrors = useMemo(() => ({}), []);
|
const validateAndSubmit = async () => {
|
||||||
|
|
||||||
const validateAndSubmit = useCallback(async () => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
@@ -96,10 +87,9 @@ const MqttSettings = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
const publishIntervalFields = useMemo(
|
const publishIntervalFields = [
|
||||||
() => [
|
|
||||||
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
|
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
|
||||||
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
|
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
|
||||||
{
|
{
|
||||||
@@ -112,9 +102,7 @@ const MqttSettings = () => {
|
|||||||
{ name: 'publish_time_water', label: LL.MQTT_INT_WATER(), validated: false },
|
{ name: 'publish_time_water', label: LL.MQTT_INT_WATER(), validated: false },
|
||||||
{ name: 'publish_time_sensor', label: LL.SENSORS(), validated: false },
|
{ name: 'publish_time_sensor', label: LL.SENSORS(), validated: false },
|
||||||
{ name: 'publish_time_other', label: LL.DEFAULT(0), validated: false }
|
{ name: 'publish_time_other', label: LL.DEFAULT(0), validated: false }
|
||||||
],
|
];
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +142,7 @@ const MqttSettings = () => {
|
|||||||
<Grid container spacing={2} rowSpacing={0}>
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="host"
|
name="host"
|
||||||
label={LL.ADDRESS_OF(LL.BROKER())}
|
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||||
multiline
|
multiline
|
||||||
@@ -166,7 +154,7 @@ const MqttSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="port"
|
name="port"
|
||||||
label="Port"
|
label="Port"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -178,7 +166,7 @@ const MqttSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="base"
|
name="base"
|
||||||
label={LL.BASE_TOPIC()}
|
label={LL.BASE_TOPIC()}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -219,7 +207,7 @@ const MqttSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="keep_alive"
|
name="keep_alive"
|
||||||
label="Keep Alive"
|
label="Keep Alive"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -438,7 +426,7 @@ const MqttSettings = () => {
|
|||||||
<Grid key={field.name}>
|
<Grid key={field.name}>
|
||||||
{field.validated ? (
|
{field.validated ? (
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } 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';
|
||||||
@@ -61,14 +61,11 @@ const NTPSettings = () => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
useLayoutTitle('NTP');
|
useLayoutTitle('NTP');
|
||||||
|
|
||||||
// Memoized timezone select items for better performance
|
|
||||||
const timeZoneItems = useTimeZoneSelectItems();
|
const timeZoneItems = useTimeZoneSelectItems();
|
||||||
|
|
||||||
// Memoized selected timezone value
|
const selectedTzValue = data
|
||||||
const selectedTzValue = useMemo(
|
? selectedTimeZone(data.tz_label, data.tz_format)
|
||||||
() => (data ? selectedTimeZone(data.tz_label, data.tz_format) : undefined),
|
: undefined;
|
||||||
[data?.tz_label, data?.tz_format]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [localTime, setLocalTime] = useState<string>('');
|
const [localTime, setLocalTime] = useState<string>('');
|
||||||
const [settingTime, setSettingTime] = useState<boolean>(false);
|
const [settingTime, setSettingTime] = useState<boolean>(false);
|
||||||
@@ -82,32 +79,22 @@ const NTPSettings = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize updateFormValue to prevent recreation on every render
|
const updateFormValue = updateValueDirty(
|
||||||
const updateFormValue = useMemo(
|
|
||||||
() =>
|
|
||||||
updateValueDirty(
|
|
||||||
origData as unknown as Record<string, unknown>,
|
origData as unknown as Record<string, unknown>,
|
||||||
dirtyFlags,
|
dirtyFlags,
|
||||||
setDirtyFlags,
|
setDirtyFlags,
|
||||||
updateDataValue as (value: unknown) => void
|
updateDataValue as (value: unknown) => void
|
||||||
),
|
|
||||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize updateLocalTime handler
|
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
const updateLocalTime = useCallback(
|
setLocalTime(event.target.value);
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize openSetTime handler
|
const openSetTime = () => {
|
||||||
const openSetTime = useCallback(() => {
|
|
||||||
setLocalTime(formatLocalDateTime(new Date()));
|
setLocalTime(formatLocalDateTime(new Date()));
|
||||||
setSettingTime(true);
|
setSettingTime(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
// Memoize configureTime handler
|
const configureTime = async () => {
|
||||||
const configureTime = useCallback(async () => {
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -120,13 +107,11 @@ const NTPSettings = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
}, [localTime, updateTime, LL, loadData]);
|
};
|
||||||
|
|
||||||
// Memoize close dialog handler
|
const handleCloseSetTime = () => setSettingTime(false);
|
||||||
const handleCloseSetTime = useCallback(() => setSettingTime(false), []);
|
|
||||||
|
|
||||||
// Memoize validate and submit handler
|
const validateAndSubmit = async () => {
|
||||||
const validateAndSubmit = useCallback(async () => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
@@ -135,23 +120,18 @@ const NTPSettings = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
// Memoize timezone change handler
|
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const changeTimeZone = useCallback(
|
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
|
void updateState(readNTPSettings(), (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]
|
||||||
}));
|
}));
|
||||||
updateFormValue(event);
|
updateFormValue(event);
|
||||||
},
|
};
|
||||||
[updateFormValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize render content to prevent unnecessary re-renders
|
const renderContent = () => {
|
||||||
const renderContent = useMemo(() => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
|
||||||
}
|
}
|
||||||
@@ -236,26 +216,12 @@ const NTPSettings = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
data,
|
|
||||||
errorMessage,
|
|
||||||
loadData,
|
|
||||||
updateFormValue,
|
|
||||||
fieldErrors,
|
|
||||||
selectedTzValue,
|
|
||||||
changeTimeZone,
|
|
||||||
timeZoneItems,
|
|
||||||
dirtyFlags,
|
|
||||||
openSetTime,
|
|
||||||
saving,
|
|
||||||
validateAndSubmit,
|
|
||||||
LL
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{renderContent}
|
{renderContent()}
|
||||||
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
|
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
|
||||||
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, 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';
|
||||||
@@ -43,24 +43,27 @@ const Settings = () => {
|
|||||||
immediate: false
|
immediate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const doFormat = useCallback(async () => {
|
const doFormat = async () => {
|
||||||
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
|
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
setConfirmFactoryReset(false);
|
setConfirmFactoryReset(false);
|
||||||
});
|
});
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
const handleFactoryResetClose = useCallback(() => {
|
const handleFactoryResetClose = () => {
|
||||||
setConfirmFactoryReset(false);
|
setConfirmFactoryReset(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const handleFactoryResetClick = useCallback(() => {
|
const handleFactoryResetClick = () => {
|
||||||
setConfirmFactoryReset(true);
|
setConfirmFactoryReset(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
|
if (restarting) {
|
||||||
|
return <SystemMonitor />;
|
||||||
|
}
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SectionContent>
|
||||||
<List>
|
<List>
|
||||||
<ListMenuItem
|
<ListMenuItem
|
||||||
icon={TuneIcon}
|
icon={TuneIcon}
|
||||||
@@ -173,18 +176,8 @@ const Settings = () => {
|
|||||||
{LL.FACTORY_RESET()}
|
{LL.FACTORY_RESET()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}, [
|
|
||||||
LL,
|
|
||||||
handleFactoryResetClick,
|
|
||||||
handleFactoryResetClose,
|
|
||||||
doFormat,
|
|
||||||
confirmFactoryReset,
|
|
||||||
restarting
|
|
||||||
]);
|
|
||||||
|
|
||||||
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { MenuItem } from '@mui/material';
|
import { MenuItem } from '@mui/material';
|
||||||
|
|
||||||
export const TIME_ZONES: Record<string, string> = {
|
export const TIME_ZONES: Record<string, string> = {
|
||||||
@@ -472,26 +470,16 @@ export function selectedTimeZone(label: string, format: string) {
|
|||||||
return TIME_ZONES[label] === format ? label : undefined;
|
return TIME_ZONES[label] === format ? label : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoized version for use in components
|
|
||||||
export function useTimeZoneSelectItems() {
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
TIME_ZONE_LABELS.map((label) => (
|
|
||||||
<MenuItem key={label} value={label}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
)),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback export for backward compatibility - now memoized
|
|
||||||
const precomputedTimeZoneItems = TIME_ZONE_LABELS.map((label) => (
|
const precomputedTimeZoneItems = TIME_ZONE_LABELS.map((label) => (
|
||||||
<MenuItem key={label} value={label}>
|
<MenuItem key={label} value={label}>
|
||||||
{label}
|
{label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
export function useTimeZoneSelectItems() {
|
||||||
|
return precomputedTimeZoneItems;
|
||||||
|
}
|
||||||
|
|
||||||
export function timeZoneSelectItems() {
|
export function timeZoneSelectItems() {
|
||||||
return precomputedTimeZoneItems;
|
return precomputedTimeZoneItems;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Navigate,
|
Navigate,
|
||||||
Route,
|
Route,
|
||||||
@@ -40,26 +40,20 @@ const Network = () => {
|
|||||||
|
|
||||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||||
|
|
||||||
const selectNetwork = useCallback(
|
const selectNetwork = (network: WiFiNetwork) => {
|
||||||
(network: WiFiNetwork) => {
|
|
||||||
setSelectedNetwork(network);
|
setSelectedNetwork(network);
|
||||||
void navigate('/settings/network/settings');
|
void navigate('/settings/network/settings');
|
||||||
},
|
};
|
||||||
[navigate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deselectNetwork = useCallback(() => {
|
const deselectNetwork = () => {
|
||||||
setSelectedNetwork(undefined);
|
setSelectedNetwork(undefined);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = {
|
||||||
() => ({
|
|
||||||
...(selectedNetwork && { selectedNetwork }),
|
...(selectedNetwork && { selectedNetwork }),
|
||||||
selectNetwork,
|
selectNetwork,
|
||||||
deselectNetwork
|
deselectNetwork
|
||||||
}),
|
};
|
||||||
[selectedNetwork, selectNetwork, deselectNetwork]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WiFiConnectionContext.Provider value={contextValue}>
|
<WiFiConnectionContext.Provider value={contextValue}>
|
||||||
|
|||||||
@@ -121,19 +121,19 @@ const NetworkSettings = () => {
|
|||||||
deselectNetwork();
|
deselectNetwork();
|
||||||
}, [data, saveData, deselectNetwork]);
|
}, [data, saveData, deselectNetwork]);
|
||||||
|
|
||||||
const setCancel = useCallback(async () => {
|
const setCancel = async () => {
|
||||||
deselectNetwork();
|
deselectNetwork();
|
||||||
await loadData();
|
await loadData();
|
||||||
}, [deselectNetwork, loadData]);
|
};
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
(error: Error) => {
|
(error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useRef, useState } from 'react';
|
import { memo, useRef, useState } 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';
|
||||||
@@ -48,12 +48,12 @@ const WiFiNetworkScanner = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderNetworkScanner = useCallback(() => {
|
const renderNetworkScanner = () => {
|
||||||
if (!networkList) {
|
if (!networkList) {
|
||||||
return <FormLoader errorMessage={errorMessage || ''} />;
|
return <FormLoader errorMessage={errorMessage || ''} />;
|
||||||
}
|
}
|
||||||
return <WiFiNetworkSelector networkList={networkList} />;
|
return <WiFiNetworkSelector networkList={networkList} />;
|
||||||
}, [networkList, errorMessage]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext } from 'react';
|
import { memo, useContext } 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';
|
||||||
@@ -63,8 +63,7 @@ const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList })
|
|||||||
|
|
||||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||||
|
|
||||||
const renderNetwork = useCallback(
|
const renderNetwork = (network: WiFiNetwork) => (
|
||||||
(network: WiFiNetwork) => (
|
|
||||||
<ListItem
|
<ListItem
|
||||||
key={network.bssid}
|
key={network.bssid}
|
||||||
onClick={() => wifiConnectionContext.selectNetwork(network)}
|
onClick={() => wifiConnectionContext.selectNetwork(network)}
|
||||||
@@ -89,8 +88,6 @@ const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList })
|
|||||||
</Badge>
|
</Badge>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
),
|
|
||||||
[wifiConnectionContext, theme]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (networkList.networks.length === 0) {
|
if (networkList.networks.length === 0) {
|
||||||
|
|||||||
@@ -99,34 +99,28 @@ const ManageUsers = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const noAdminConfigured = useCallback(
|
const noAdminConfigured = () => !data?.users.find((u) => u.admin);
|
||||||
() => !data?.users.find((u) => u.admin),
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeUser = useCallback(
|
const removeUser = (toRemove: UserType) => {
|
||||||
(toRemove: UserType) => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||||
updateDataValue({ ...data, users });
|
updateDataValue({ ...data, users });
|
||||||
setChanged(changed + 1);
|
setChanged(changed + 1);
|
||||||
},
|
};
|
||||||
[data, updateDataValue, changed]
|
|
||||||
);
|
|
||||||
|
|
||||||
const createUser = useCallback(() => {
|
const createUser = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setUser({
|
setUser({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
admin: true
|
admin: true
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const editUser = useCallback((toEdit: UserType) => {
|
const editUser = (toEdit: UserType) => {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
setUser({ ...toEdit });
|
setUser({ ...toEdit });
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const cancelEditingUser = useCallback(() => {
|
const cancelEditingUser = useCallback(() => {
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
@@ -150,20 +144,20 @@ const ManageUsers = () => {
|
|||||||
setGeneratingToken(undefined);
|
setGeneratingToken(undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const generateTokenForUser = useCallback((username: string) => {
|
const generateTokenForUser = (username: string) => {
|
||||||
setGeneratingToken(username);
|
setGeneratingToken(username);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onSubmit = useCallback(async () => {
|
const onSubmit = async () => {
|
||||||
await saveData();
|
await saveData();
|
||||||
await authenticatedContext.refresh();
|
await authenticatedContext.refresh();
|
||||||
setChanged(0);
|
setChanged(0);
|
||||||
}, [saveData, authenticatedContext]);
|
};
|
||||||
|
|
||||||
const onCancelSubmit = useCallback(async () => {
|
const onCancelSubmit = async () => {
|
||||||
await loadData();
|
await loadData();
|
||||||
setChanged(0);
|
setChanged(0);
|
||||||
}, [loadData]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -177,15 +171,10 @@ const ManageUsers = () => {
|
|||||||
admin: boolean;
|
admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add id to the type, needed for the table
|
const user_table = data.users.map((u) => ({
|
||||||
const user_table = useMemo(
|
|
||||||
() =>
|
|
||||||
data.users.map((u) => ({
|
|
||||||
...u,
|
...u,
|
||||||
id: u.username
|
id: u.username
|
||||||
})) as UserType2[],
|
})) as UserType2[];
|
||||||
[data.users]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
|
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
|
||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
@@ -15,9 +15,7 @@ const Security = () => {
|
|||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const matchedRoutes = useMemo(
|
const matchedRoutes = matchRoutes(
|
||||||
() =>
|
|
||||||
matchRoutes(
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
path: '/settings/security/settings',
|
path: '/settings/security/settings',
|
||||||
@@ -26,8 +24,6 @@ const Security = () => {
|
|||||||
{ path: '/settings/security/users', element: <SecuritySettings /> }
|
{ path: '/settings/security/users', element: <SecuritySettings /> }
|
||||||
],
|
],
|
||||||
location
|
location
|
||||||
),
|
|
||||||
[location]
|
|
||||||
);
|
);
|
||||||
const routerTab = matchedRoutes?.[0]?.route.path || false;
|
const routerTab = matchedRoutes?.[0]?.route.path || false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -62,7 +62,7 @@ const User: FC<UserFormProps> = ({
|
|||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const validateAndDone = useCallback(async () => {
|
const validateAndDone = async () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
@@ -72,7 +72,7 @@ const User: FC<UserFormProps> = ({
|
|||||||
setFieldErrors((error as ValidationError).fieldErrors);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [user, validator, onDoneEditing]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Cell,
|
Cell,
|
||||||
@@ -36,9 +34,7 @@ const SystemActivity = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.DATA_TRAFFIC());
|
useLayoutTitle(LL.DATA_TRAFFIC());
|
||||||
|
|
||||||
const stats_theme = tableTheme(
|
const stats_theme = tableTheme({
|
||||||
useMemo(
|
|
||||||
() => ({
|
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||||
`,
|
`,
|
||||||
@@ -74,21 +70,15 @@ const SystemActivity = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const showName = useCallback(
|
const showName = (id: number) => {
|
||||||
(id: number) => {
|
|
||||||
const name: keyof Translation['STATUS_NAMES'] =
|
const name: keyof Translation['STATUS_NAMES'] =
|
||||||
id.toString() as keyof Translation['STATUS_NAMES'];
|
id.toString() as keyof Translation['STATUS_NAMES'];
|
||||||
return LL.STATUS_NAMES[name]();
|
return LL.STATUS_NAMES[name]();
|
||||||
},
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showQuality = useCallback((stat: Stat) => {
|
const showQuality = (stat: Stat) => {
|
||||||
if (stat.q === 0 || stat.s + stat.f === 0) {
|
if (stat.q === 0 || stat.s + stat.f === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,14 +90,18 @@ const SystemActivity = () => {
|
|||||||
} else {
|
} else {
|
||||||
return <div style={{ color: QUALITY_COLORS.POOR }}>{stat.q}%</div>;
|
return <div style={{ color: QUALITY_COLORS.POOR }}>{stat.q}%</div>;
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SectionContent>
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: data.stats }}
|
data={{ nodes: data.stats }}
|
||||||
theme={stats_theme}
|
theme={stats_theme}
|
||||||
@@ -136,10 +130,8 @@ const SystemActivity = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}, [data, loadData, error?.message, stats_theme, LL, showName, showQuality]);
|
|
||||||
|
|
||||||
return <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SystemActivity;
|
export default SystemActivity;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, memo, useMemo } from 'react';
|
import { type FC, memo } 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';
|
||||||
@@ -127,16 +127,15 @@ const MqttStatus = () => {
|
|||||||
void loadData();
|
void loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize error message separately to avoid re-renders on error object changes
|
|
||||||
const errorMessage = error?.message || '';
|
const errorMessage = error?.message || '';
|
||||||
|
|
||||||
const mqttStatusText = useMemo(() => {
|
const mqttStatusText = !data
|
||||||
if (!data) return '';
|
? ''
|
||||||
if (!data.enabled) return LL.NOT_ENABLED();
|
: !data.enabled
|
||||||
return data.connected
|
? LL.NOT_ENABLED()
|
||||||
|
: data.connected
|
||||||
? `${LL.CONNECTED(0)} (${data.connect_count})`
|
? `${LL.CONNECTED(0)} (${data.connect_count})`
|
||||||
: `${LL.DISCONNECTED()} (${data.connect_count})`;
|
: `${LL.DISCONNECTED()} (${data.connect_count})`;
|
||||||
}, [data, LL]);
|
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||||
import DnsIcon from '@mui/icons-material/Dns';
|
import DnsIcon from '@mui/icons-material/Dns';
|
||||||
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
|
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
|
||||||
@@ -67,12 +65,16 @@ const NTPStatus = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SectionContent>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@@ -121,10 +123,8 @@ const NTPStatus = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}, [data, error, loadData, LL, theme]);
|
|
||||||
|
|
||||||
return <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NTPStatus;
|
export default NTPStatus;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useMemo } 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';
|
||||||
@@ -124,9 +122,12 @@ const NetworkStatus = () => {
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusText = getNetworkStatusText(data.status, data.reconnect_count, LL);
|
const statusText = getNetworkStatusText(data.status, data.reconnect_count, LL);
|
||||||
@@ -134,6 +135,7 @@ const NetworkStatus = () => {
|
|||||||
const qualityColor = networkQualityHighlight(data, theme);
|
const qualityColor = networkQualityHighlight(data, theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SectionContent>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@@ -227,10 +229,8 @@ const NetworkStatus = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}, [data, error, loadData, LL, theme]);
|
|
||||||
|
|
||||||
return <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NetworkStatus;
|
export default NetworkStatus;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
import { useContext, useState } 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';
|
||||||
@@ -43,7 +43,6 @@ import { formatDateTime } from 'utils/time';
|
|||||||
|
|
||||||
import SystemMonitor from './SystemMonitor';
|
import SystemMonitor from './SystemMonitor';
|
||||||
|
|
||||||
// Pure functions moved outside component to avoid recreation on each render
|
|
||||||
const formatNumber = (num: number) => new Intl.NumberFormat().format(num);
|
const formatNumber = (num: number) => new Intl.NumberFormat().format(num);
|
||||||
|
|
||||||
const formatDurationSec = (
|
const formatDurationSec = (
|
||||||
@@ -97,10 +96,8 @@ const SystemStatus = () => {
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
// Memoize derived status values to avoid recalculation on every render
|
const busStatus = (() => {
|
||||||
const busStatus = useMemo(() => {
|
|
||||||
if (!data) return 'EMS state unknown';
|
if (!data) return 'EMS state unknown';
|
||||||
|
|
||||||
switch (data.bus_status) {
|
switch (data.bus_status) {
|
||||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||||
return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`;
|
return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`;
|
||||||
@@ -111,12 +108,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return 'EMS state unknown';
|
return 'EMS state unknown';
|
||||||
}
|
}
|
||||||
}, [data?.bus_status, data?.bus_uptime, LL]);
|
})();
|
||||||
|
|
||||||
// Memoize derived status values to avoid recalculation on every render
|
const systemStatus = (() => {
|
||||||
const systemStatus = useMemo(() => {
|
|
||||||
if (!data) return '??';
|
if (!data) return '??';
|
||||||
|
|
||||||
switch (data.status) {
|
switch (data.status) {
|
||||||
case SystemStatusCodes.SYSTEM_STATUS_PENDING_UPLOAD:
|
case SystemStatusCodes.SYSTEM_STATUS_PENDING_UPLOAD:
|
||||||
case SystemStatusCodes.SYSTEM_STATUS_UPLOADING:
|
case SystemStatusCodes.SYSTEM_STATUS_UPLOADING:
|
||||||
@@ -129,14 +124,12 @@ const SystemStatus = () => {
|
|||||||
case SystemStatusCodes.SYSTEM_STATUS_INVALID_GPIO:
|
case SystemStatusCodes.SYSTEM_STATUS_INVALID_GPIO:
|
||||||
return LL.GPIO_OF(LL.FAILED(0));
|
return LL.GPIO_OF(LL.FAILED(0));
|
||||||
default:
|
default:
|
||||||
// SystemStatusCodes.SYSTEM_STATUS_NORMAL
|
|
||||||
return 'OK';
|
return 'OK';
|
||||||
}
|
}
|
||||||
}, [data?.status, LL]);
|
})();
|
||||||
|
|
||||||
const busStatusHighlight = useMemo(() => {
|
const busStatusHighlight = (() => {
|
||||||
if (!data) return theme.palette.warning.main;
|
if (!data) return theme.palette.warning.main;
|
||||||
|
|
||||||
switch (data.bus_status) {
|
switch (data.bus_status) {
|
||||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
@@ -147,11 +140,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}, [data?.bus_status, theme.palette]);
|
})();
|
||||||
|
|
||||||
const ntpStatus = useMemo(() => {
|
const ntpStatus = (() => {
|
||||||
if (!data) return LL.UNKNOWN();
|
if (!data) return LL.UNKNOWN();
|
||||||
|
|
||||||
switch (data.ntp_status) {
|
switch (data.ntp_status) {
|
||||||
case NTPSyncStatus.NTP_DISABLED:
|
case NTPSyncStatus.NTP_DISABLED:
|
||||||
return LL.NOT_ENABLED();
|
return LL.NOT_ENABLED();
|
||||||
@@ -164,11 +156,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return LL.UNKNOWN();
|
return LL.UNKNOWN();
|
||||||
}
|
}
|
||||||
}, [data?.ntp_status, data?.ntp_time, LL]);
|
})();
|
||||||
|
|
||||||
const ntpStatusHighlight = useMemo(() => {
|
const ntpStatusHighlight = (() => {
|
||||||
if (!data) return theme.palette.error.main;
|
if (!data) return theme.palette.error.main;
|
||||||
|
|
||||||
switch (data.ntp_status) {
|
switch (data.ntp_status) {
|
||||||
case NTPSyncStatus.NTP_DISABLED:
|
case NTPSyncStatus.NTP_DISABLED:
|
||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
@@ -179,11 +170,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
}
|
||||||
}, [data?.ntp_status, theme.palette]);
|
})();
|
||||||
|
|
||||||
const networkStatusHighlight = useMemo(() => {
|
const networkStatusHighlight = (() => {
|
||||||
if (!data) return theme.palette.warning.main;
|
if (!data) return theme.palette.warning.main;
|
||||||
|
|
||||||
switch (data.network_status) {
|
switch (data.network_status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
@@ -198,11 +188,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}, [data?.network_status, theme.palette]);
|
})();
|
||||||
|
|
||||||
const networkStatus = useMemo(() => {
|
const networkStatus = (() => {
|
||||||
if (!data) return LL.UNKNOWN();
|
if (!data) return LL.UNKNOWN();
|
||||||
|
|
||||||
switch (data.network_status) {
|
switch (data.network_status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||||
return LL.INACTIVE(1);
|
return LL.INACTIVE(1);
|
||||||
@@ -223,15 +212,12 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return LL.UNKNOWN();
|
return LL.UNKNOWN();
|
||||||
}
|
}
|
||||||
}, [data?.network_status, data?.wifi_rssi, LL]);
|
})();
|
||||||
|
|
||||||
const activeHighlight = useCallback(
|
const activeHighlight = (value: boolean) =>
|
||||||
(value: boolean) =>
|
value ? theme.palette.success.main : theme.palette.info.main;
|
||||||
value ? theme.palette.success.main : theme.palette.info.main,
|
|
||||||
[theme.palette]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
setConfirmRestart(false);
|
setConfirmRestart(false);
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
@@ -239,14 +225,123 @@ const SystemStatus = () => {
|
|||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
const handleCloseRestartDialog = useCallback(() => {
|
const handleCloseRestartDialog = () => setConfirmRestart(false);
|
||||||
setConfirmRestart(false);
|
|
||||||
}, []);
|
if (restarting) {
|
||||||
|
return <SystemMonitor />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || !LL) {
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<List>
|
||||||
|
<ListMenuItem
|
||||||
|
icon={BuildIcon}
|
||||||
|
bgcolor="#72caf9"
|
||||||
|
label="EMS-ESP Firmware"
|
||||||
|
text={`v${data.emsesp_version || ''}`}
|
||||||
|
to="version"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
|
||||||
|
<MonitorHeartIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={LL.STATUS_OF(LL.SYSTEM(0))}
|
||||||
|
secondary={`${systemStatus} (${LL.UPTIME()}: ${formatDurationSec(data.uptime, LL)})`}
|
||||||
|
/>
|
||||||
|
{me.admin && (
|
||||||
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={() => setConfirmRestart(true)}
|
||||||
|
>
|
||||||
|
{LL.RESTART()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={MemoryIcon}
|
||||||
|
bgcolor="#68374d"
|
||||||
|
label={LL.HARDWARE()}
|
||||||
|
text={`${formatNumber(data.free_heap)} KB ${LL.FREE_MEMORY()}`}
|
||||||
|
to="/status/hardwarestatus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={DirectionsBusIcon}
|
||||||
|
bgcolor={busStatusHighlight}
|
||||||
|
label={LL.DATA_TRAFFIC()}
|
||||||
|
text={busStatus}
|
||||||
|
to="/status/activity"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={
|
||||||
|
data.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
||||||
|
? WifiIcon
|
||||||
|
: RouterIcon
|
||||||
|
}
|
||||||
|
bgcolor={networkStatusHighlight}
|
||||||
|
label={LL.NETWORK(1)}
|
||||||
|
text={networkStatus}
|
||||||
|
to="/status/network"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={DeviceHubIcon}
|
||||||
|
bgcolor={activeHighlight(data.mqtt_status)}
|
||||||
|
label="MQTT"
|
||||||
|
text={data.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)}
|
||||||
|
to="/status/mqtt"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={AccessTimeIcon}
|
||||||
|
bgcolor={ntpStatusHighlight}
|
||||||
|
label="NTP"
|
||||||
|
text={ntpStatus}
|
||||||
|
to="/status/ntp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={SettingsInputAntennaIcon}
|
||||||
|
bgcolor={activeHighlight(data.ap_status)}
|
||||||
|
label={LL.ACCESS_POINT(0)}
|
||||||
|
text={data.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)}
|
||||||
|
to="/status/ap"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={LogoDevIcon}
|
||||||
|
bgcolor="#40828f"
|
||||||
|
label={LL.LOG_OF(LL.SYSTEM(0))}
|
||||||
|
text={LL.VIEW_LOG()}
|
||||||
|
to="/status/log"
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
|
||||||
const renderRestartDialog = useMemo(
|
|
||||||
() => (
|
|
||||||
<Dialog
|
<Dialog
|
||||||
sx={dialogStyle}
|
sx={dialogStyle}
|
||||||
open={confirmRestart}
|
open={confirmRestart}
|
||||||
@@ -273,177 +368,8 @@ const SystemStatus = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
),
|
</SectionContent>
|
||||||
[confirmRestart, handleCloseRestartDialog, doRestart, LL]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize formatted values
|
|
||||||
const firmwareVersion = useMemo(
|
|
||||||
() => `v${data?.emsesp_version || ''}`,
|
|
||||||
[data?.emsesp_version]
|
|
||||||
);
|
|
||||||
|
|
||||||
const uptimeText = useMemo(
|
|
||||||
() => (data ? formatDurationSec(data.uptime, LL) : ''),
|
|
||||||
[data?.uptime, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const freeMemoryText = useMemo(
|
|
||||||
() => (data ? `${formatNumber(data.free_heap)} KB ${LL.FREE_MEMORY()}` : ''),
|
|
||||||
[data?.free_heap, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const networkIcon = useMemo(
|
|
||||||
() =>
|
|
||||||
data?.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
|
||||||
? WifiIcon
|
|
||||||
: RouterIcon,
|
|
||||||
[data?.network_status]
|
|
||||||
);
|
|
||||||
|
|
||||||
const mqttStatusText = useMemo(
|
|
||||||
() => (data?.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)),
|
|
||||||
[data?.mqtt_status, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const apStatusText = useMemo(
|
|
||||||
() => (data?.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)),
|
|
||||||
[data?.ap_status, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRestartClick = useCallback(() => {
|
|
||||||
setConfirmRestart(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data || !LL) {
|
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<List>
|
|
||||||
<ListMenuItem
|
|
||||||
icon={BuildIcon}
|
|
||||||
bgcolor="#72caf9"
|
|
||||||
label="EMS-ESP Firmware"
|
|
||||||
text={firmwareVersion}
|
|
||||||
to="version"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
|
|
||||||
<MonitorHeartIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={LL.STATUS_OF(LL.SYSTEM(0))}
|
|
||||||
secondary={`${systemStatus} (${LL.UPTIME()}: ${uptimeText})`}
|
|
||||||
/>
|
|
||||||
{me.admin && (
|
|
||||||
<Button
|
|
||||||
startIcon={<PowerSettingsNewIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={handleRestartClick}
|
|
||||||
>
|
|
||||||
{LL.RESTART()}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={MemoryIcon}
|
|
||||||
bgcolor="#68374d"
|
|
||||||
label={LL.HARDWARE()}
|
|
||||||
text={freeMemoryText}
|
|
||||||
to="/status/hardwarestatus"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={DirectionsBusIcon}
|
|
||||||
bgcolor={busStatusHighlight}
|
|
||||||
label={LL.DATA_TRAFFIC()}
|
|
||||||
text={busStatus}
|
|
||||||
to="/status/activity"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={networkIcon}
|
|
||||||
bgcolor={networkStatusHighlight}
|
|
||||||
label={LL.NETWORK(1)}
|
|
||||||
text={networkStatus}
|
|
||||||
to="/status/network"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={DeviceHubIcon}
|
|
||||||
bgcolor={activeHighlight(data.mqtt_status)}
|
|
||||||
label="MQTT"
|
|
||||||
text={mqttStatusText}
|
|
||||||
to="/status/mqtt"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={AccessTimeIcon}
|
|
||||||
bgcolor={ntpStatusHighlight}
|
|
||||||
label="NTP"
|
|
||||||
text={ntpStatus}
|
|
||||||
to="/status/ntp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={SettingsInputAntennaIcon}
|
|
||||||
bgcolor={activeHighlight(data.ap_status)}
|
|
||||||
label={LL.ACCESS_POINT(0)}
|
|
||||||
text={apStatusText}
|
|
||||||
to="/status/ap"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={LogoDevIcon}
|
|
||||||
bgcolor="#40828f"
|
|
||||||
label={LL.LOG_OF(LL.SYSTEM(0))}
|
|
||||||
text={LL.VIEW_LOG()}
|
|
||||||
to="/status/log"
|
|
||||||
/>
|
|
||||||
</List>
|
|
||||||
|
|
||||||
{renderRestartDialog}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
data,
|
|
||||||
LL,
|
|
||||||
firmwareVersion,
|
|
||||||
uptimeText,
|
|
||||||
freeMemoryText,
|
|
||||||
networkIcon,
|
|
||||||
mqttStatusText,
|
|
||||||
apStatusText,
|
|
||||||
busStatus,
|
|
||||||
busStatusHighlight,
|
|
||||||
networkStatusHighlight,
|
|
||||||
networkStatus,
|
|
||||||
ntpStatusHighlight,
|
|
||||||
ntpStatus,
|
|
||||||
activeHighlight,
|
|
||||||
me.admin,
|
|
||||||
handleRestartClick,
|
|
||||||
error,
|
|
||||||
loadData,
|
|
||||||
renderRestartDialog
|
|
||||||
]);
|
|
||||||
|
|
||||||
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SystemStatus;
|
export default SystemStatus;
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
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';
|
||||||
@@ -185,8 +178,7 @@ const SystemLog = () => {
|
|||||||
};
|
};
|
||||||
}, [data]); // Recalculate when data changes (in case layout shifts)
|
}, [data]); // Recalculate when data changes (in case layout shifts)
|
||||||
|
|
||||||
// Memoize message handler to avoid recreating on every render
|
const handleLogMessage = (message: { data: string }) => {
|
||||||
const handleLogMessage = useCallback((message: { data: string }) => {
|
|
||||||
const rawData = message.data;
|
const rawData = message.data;
|
||||||
const logentry = JSON.parse(rawData) as LogEntry;
|
const logentry = JSON.parse(rawData) as LogEntry;
|
||||||
setLogEntries((log) => {
|
setLogEntries((log) => {
|
||||||
@@ -200,7 +192,7 @@ const SystemLog = () => {
|
|||||||
const newLog = [...log, logentry];
|
const newLog = [...log, logentry];
|
||||||
return newLog;
|
return newLog;
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
useSSE(fetchLogES, {
|
useSSE(fetchLogES, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@@ -211,7 +203,7 @@ const SystemLog = () => {
|
|||||||
toast.error('No connection to Log service');
|
toast.error('No connection to Log service');
|
||||||
});
|
});
|
||||||
|
|
||||||
const onDownload = useCallback(() => {
|
const onDownload = () => {
|
||||||
const result = logEntries
|
const result = logEntries
|
||||||
.map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`)
|
.map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
@@ -225,11 +217,11 @@ const SystemLog = () => {
|
|||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
}, [logEntries]);
|
};
|
||||||
|
|
||||||
const saveSettings = useCallback(async () => {
|
const saveSettings = async () => {
|
||||||
await saveData();
|
await saveData();
|
||||||
}, [saveData]);
|
};
|
||||||
|
|
||||||
// handle scrolling - optimized to only scroll when needed
|
// handle scrolling - optimized to only scroll when needed
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -246,7 +238,7 @@ const SystemLog = () => {
|
|||||||
}
|
}
|
||||||
}, [logEntries.length, autoscroll]);
|
}, [logEntries.length, autoscroll]);
|
||||||
|
|
||||||
const sendReadCommand = useCallback(() => {
|
const sendReadCommand = () => {
|
||||||
if (readValue === '') {
|
if (readValue === '') {
|
||||||
setReadOpen(!readOpen);
|
setReadOpen(!readOpen);
|
||||||
return;
|
return;
|
||||||
@@ -257,7 +249,7 @@ const SystemLog = () => {
|
|||||||
setReadOpen(false);
|
setReadOpen(false);
|
||||||
setReadValue('');
|
setReadValue('');
|
||||||
}
|
}
|
||||||
}, [readValue, readOpen, send]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import { Box, Button, Typography } from '@mui/material';
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
@@ -57,10 +57,9 @@ const SystemMonitor = () => {
|
|||||||
void send();
|
void send();
|
||||||
}, 1000); // check every 1 second
|
}, 1000); // check every 1 second
|
||||||
|
|
||||||
const { statusMessage, isUploading, progressValue } = useMemo(() => {
|
|
||||||
const status = data?.status;
|
const status = data?.status;
|
||||||
|
|
||||||
const message =
|
const statusMessage =
|
||||||
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
|
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
|
||||||
? LL.WAIT_FIRMWARE()
|
? LL.WAIT_FIRMWARE()
|
||||||
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
|
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
|
||||||
@@ -71,25 +70,18 @@ const SystemMonitor = () => {
|
|||||||
? 'Upload Failed'
|
? 'Upload Failed'
|
||||||
: LL.RESTARTING_POST();
|
: LL.RESTARTING_POST();
|
||||||
|
|
||||||
const uploading =
|
const isUploading =
|
||||||
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
|
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
|
||||||
const progress =
|
const progressValue =
|
||||||
uploading && status
|
isUploading && status
|
||||||
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
|
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return {
|
const onCancel = async () => {
|
||||||
statusMessage: message,
|
|
||||||
isUploading: uploading,
|
|
||||||
progressValue: progress
|
|
||||||
};
|
|
||||||
}, [data?.status, LL]);
|
|
||||||
|
|
||||||
const onCancel = useCallback(async () => {
|
|
||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
||||||
document.location.href = '/';
|
document.location.href = '/';
|
||||||
}, [setSystemStatus]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useMemo, useState } from 'react';
|
import { memo, useContext, useState } from 'react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -105,9 +105,9 @@ const VersionInfoDialog = memo(
|
|||||||
onClose
|
onClose
|
||||||
}: {
|
}: {
|
||||||
showVersionInfo: number;
|
showVersionInfo: number;
|
||||||
latestVersion?: VersionInfo;
|
latestVersion: VersionInfo | undefined;
|
||||||
latestDevVersion?: VersionInfo;
|
latestDevVersion: VersionInfo | undefined;
|
||||||
partitionVersion?: VersionInfo | undefined;
|
partitionVersion: VersionInfo | undefined;
|
||||||
partition: string;
|
partition: string;
|
||||||
currentPartition: string;
|
currentPartition: string;
|
||||||
size: number;
|
size: number;
|
||||||
@@ -224,7 +224,7 @@ const VersionInfoDialog = memo(
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{version.date && (
|
{version && version.date && (
|
||||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||||
<TableCell
|
<TableCell
|
||||||
component="th"
|
component="th"
|
||||||
@@ -283,8 +283,8 @@ const InstallDialog = memo(
|
|||||||
}: {
|
}: {
|
||||||
openInstallDialog: boolean;
|
openInstallDialog: boolean;
|
||||||
fetchDevVersion: boolean;
|
fetchDevVersion: boolean;
|
||||||
latestVersion?: VersionInfo;
|
latestVersion: VersionInfo | undefined;
|
||||||
latestDevVersion?: VersionInfo;
|
latestDevVersion: VersionInfo | undefined;
|
||||||
upgradeImportantMessageType: number;
|
upgradeImportantMessageType: number;
|
||||||
downloadOnly: boolean;
|
downloadOnly: boolean;
|
||||||
platform: string;
|
platform: string;
|
||||||
@@ -292,16 +292,14 @@ const InstallDialog = memo(
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onInstall: (url: string) => void;
|
onInstall: (url: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const binURL = useMemo(() => {
|
const binURL = (() => {
|
||||||
if (!latestVersion || !latestDevVersion) return '';
|
if (!latestVersion || !latestDevVersion) return '';
|
||||||
|
|
||||||
const version = fetchDevVersion ? latestDevVersion : latestVersion;
|
const version = fetchDevVersion ? latestDevVersion : latestVersion;
|
||||||
const filename = `EMS-ESP-${version.version.replaceAll('.', '_')}-${platform}.bin`;
|
const filename = `EMS-ESP-${version.version.replaceAll('.', '_')}-${platform}.bin`;
|
||||||
|
|
||||||
return fetchDevVersion
|
return fetchDevVersion
|
||||||
? `${DEV_URL}${filename}`
|
? `${DEV_URL}${filename}`
|
||||||
: `${STABLE_URL}v${version.version}/${filename}`;
|
: `${STABLE_URL}v${version.version}/${filename}`;
|
||||||
}, [fetchDevVersion, latestVersion, latestDevVersion, platform]);
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
|
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
|
||||||
@@ -532,18 +530,13 @@ const Version = () => {
|
|||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
|
const platform = data ? getPlatform(data) : '';
|
||||||
|
|
||||||
const otherPartitions = useMemo(
|
const otherPartitions =
|
||||||
() => data?.partitions.filter((p) => p.partition !== data.partition) ?? [],
|
data?.partitions.filter((p) => p.partition !== data.partition) ?? [];
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setPartitionVersionInfo = useCallback(
|
const setPartitionVersionInfo = (partition: string) => {
|
||||||
(partition: string) => {
|
|
||||||
setShowVersionInfo(3);
|
setShowVersionInfo(3);
|
||||||
|
|
||||||
// search for the partition in the data.partitions array
|
|
||||||
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
||||||
if (partitionData) {
|
if (partitionData) {
|
||||||
setPartitionVersion({
|
setPartitionVersion({
|
||||||
@@ -553,77 +546,64 @@ const Version = () => {
|
|||||||
setPartition(partitionData.partition);
|
setPartition(partitionData.partition);
|
||||||
setFirmwareSize(partitionData.size);
|
setFirmwareSize(partitionData.size);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
(error: Error) => {
|
(error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
const installFirmwareURL = useCallback(
|
const installFirmwareURL = async (url: string) => {
|
||||||
async (url: string) => {
|
|
||||||
await sendUploadURL(url).catch((error: Error) => {
|
await sendUploadURL(url).catch((error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
});
|
});
|
||||||
await doRestart();
|
await doRestart();
|
||||||
},
|
};
|
||||||
[sendUploadURL, doRestart]
|
|
||||||
);
|
|
||||||
|
|
||||||
const installPartitionFirmware = useCallback(
|
const installPartitionFirmware = async (partition: string) => {
|
||||||
async (partition: string) => {
|
|
||||||
await sendSetPartition(partition).catch((error: Error) => {
|
await sendSetPartition(partition).catch((error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
});
|
});
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
},
|
};
|
||||||
[sendSetPartition]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showPartitionDialog = useCallback(
|
const showPartitionDialog = (
|
||||||
(version: string, partition: string, install_date: string) => {
|
version: string,
|
||||||
|
partition: string,
|
||||||
|
install_date: string
|
||||||
|
) => {
|
||||||
setOpenInstallPartitionDialog(true);
|
setOpenInstallPartitionDialog(true);
|
||||||
setPartitionVersion({ version: version, date: install_date });
|
setPartitionVersion({ version: version, date: install_date });
|
||||||
setPartition(partition);
|
setPartition(partition);
|
||||||
},
|
};
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showFirmwareDialog = useCallback(
|
const showFirmwareDialog = (useDevVersion: boolean) => {
|
||||||
(useDevVersion: boolean) => {
|
|
||||||
setFetchDevVersion(useDevVersion);
|
setFetchDevVersion(useDevVersion);
|
||||||
void checkUpgradeImportantMessages(
|
const targetVersion = useDevVersion
|
||||||
useDevVersion ? latestDevVersion?.version : latestVersion?.version
|
? latestDevVersion?.version
|
||||||
);
|
: latestVersion?.version;
|
||||||
|
if (targetVersion) {
|
||||||
|
void checkUpgradeImportantMessages(targetVersion);
|
||||||
|
}
|
||||||
setOpenInstallDialog(true);
|
setOpenInstallDialog(true);
|
||||||
},
|
};
|
||||||
[latestDevVersion, latestVersion, fetchDevVersion]
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeInstallDialog = useCallback(() => {
|
const closeInstallDialog = () => setOpenInstallDialog(false);
|
||||||
setOpenInstallDialog(false);
|
const closeInstallPartitionDialog = () => setOpenInstallPartitionDialog(false);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeInstallPartitionDialog = useCallback(() => {
|
const handleVersionInfoClose = () => {
|
||||||
setOpenInstallPartitionDialog(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleVersionInfoClose = useCallback(() => {
|
|
||||||
setShowVersionInfo(0);
|
setShowVersionInfo(0);
|
||||||
setPartitionVersion(undefined);
|
setPartitionVersion(undefined);
|
||||||
setPartition('');
|
setPartition('');
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
useLayoutTitle('EMS-ESP Firmware');
|
useLayoutTitle('EMS-ESP Firmware');
|
||||||
|
|
||||||
const showButtons = useCallback(
|
const showButtons = (showingDev: boolean) => {
|
||||||
(showingDev: boolean) => {
|
|
||||||
const choice = showingDev
|
const choice = showingDev
|
||||||
? !usingDevVersion
|
? !usingDevVersion
|
||||||
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
|
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
|
||||||
@@ -671,24 +651,22 @@ const Version = () => {
|
|||||||
{choice}
|
{choice}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
[
|
|
||||||
usingDevVersion,
|
if (restarting) {
|
||||||
devUpgradeAvailable,
|
return <SystemMonitor />;
|
||||||
stableUpgradeAvailable,
|
}
|
||||||
me.admin,
|
|
||||||
LL,
|
|
||||||
showFirmwareDialog
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SectionContent>
|
||||||
<Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
|
<Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
|
||||||
<Typography sx={{ mb: 1 }} variant="h6" color="primary">
|
<Typography sx={{ mb: 1 }} variant="h6" color="primary">
|
||||||
{LL.THIS_VERSION()}
|
{LL.THIS_VERSION()}
|
||||||
@@ -771,9 +749,7 @@ const Version = () => {
|
|||||||
{otherPartitions.length > 0 && data.developer_mode && (
|
{otherPartitions.length > 0 && data.developer_mode && (
|
||||||
<>
|
<>
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
<Grid size={{ xs: 4, md: 2 }}>
|
||||||
<Typography color="secondary">
|
<Typography color="secondary">{LL.STORED_VERSIONS()}</Typography>
|
||||||
{LL.STORED_VERSIONS()}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
{otherPartitions.map((partition) => (
|
{otherPartitions.map((partition) => (
|
||||||
@@ -785,10 +761,7 @@ const Version = () => {
|
|||||||
}
|
}
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
>
|
>
|
||||||
<InfoOutlinedIcon
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
color="primary"
|
|
||||||
sx={{ fontSize: 18 }}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Button
|
<Button
|
||||||
sx={{ ml: 0 }}
|
sx={{ ml: 0 }}
|
||||||
@@ -889,39 +862,8 @@ const Version = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}, [
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
loadData,
|
|
||||||
LL,
|
|
||||||
platform,
|
|
||||||
internetLive,
|
|
||||||
latestVersion,
|
|
||||||
latestDevVersion,
|
|
||||||
showVersionInfo,
|
|
||||||
locale,
|
|
||||||
openInstallDialog,
|
|
||||||
fetchDevVersion,
|
|
||||||
downloadOnly,
|
|
||||||
me.admin,
|
|
||||||
showButtons,
|
|
||||||
handleVersionInfoClose,
|
|
||||||
closeInstallDialog,
|
|
||||||
installFirmwareURL,
|
|
||||||
doRestart,
|
|
||||||
otherPartitions,
|
|
||||||
setPartitionVersionInfo,
|
|
||||||
showPartitionDialog,
|
|
||||||
partitionVersion,
|
|
||||||
partition,
|
|
||||||
firmwareSize,
|
|
||||||
closeInstallPartitionDialog,
|
|
||||||
installPartitionFirmware
|
|
||||||
]);
|
|
||||||
|
|
||||||
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(Version);
|
export default memo(Version);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, type PropsWithChildren, memo, useMemo } from 'react';
|
import { type FC, type PropsWithChildren, memo } from 'react';
|
||||||
|
|
||||||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
@@ -38,19 +38,18 @@ const MessageBox: FC<PropsWithChildren<MessageBoxProps>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { Icon, backgroundColor } = useMemo(() => {
|
|
||||||
const Icon = LEVEL_ICONS[level];
|
const Icon = LEVEL_ICONS[level];
|
||||||
const palettePath = LEVEL_PALETTE_PATHS[level];
|
const palettePath = LEVEL_PALETTE_PATHS[level];
|
||||||
const [key, shade] = palettePath.split('.') as [
|
const [paletteKeyName, shade] = palettePath.split('.') as [
|
||||||
keyof typeof theme.palette,
|
keyof typeof theme.palette,
|
||||||
string
|
string
|
||||||
];
|
];
|
||||||
const paletteKey = theme.palette[key] as unknown as Record<string, string>;
|
const paletteKey = theme.palette[paletteKeyName] as unknown as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>;
|
||||||
const backgroundColor = paletteKey[shade];
|
const backgroundColor = paletteKey[shade];
|
||||||
|
|
||||||
return { Icon, backgroundColor };
|
|
||||||
}, [level, theme]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
@@ -44,27 +44,14 @@ const LANGUAGE_OPTIONS: LanguageOption[] = [
|
|||||||
const LanguageSelector = () => {
|
const LanguageSelector = () => {
|
||||||
const { setLocale, locale, LL } = useContext(I18nContext);
|
const { setLocale, locale, LL } = useContext(I18nContext);
|
||||||
|
|
||||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = useCallback(
|
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
|
||||||
async ({ target }) => {
|
target
|
||||||
|
}) => {
|
||||||
const loc = target.value as Locales;
|
const loc = target.value as Locales;
|
||||||
localStorage.setItem('lang', loc);
|
localStorage.setItem('lang', loc);
|
||||||
await loadLocaleAsync(loc);
|
await loadLocaleAsync(loc);
|
||||||
setLocale(loc);
|
setLocale(loc);
|
||||||
},
|
};
|
||||||
[setLocale]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize menu items to prevent recreation on every render
|
|
||||||
const menuItems = useMemo(
|
|
||||||
() =>
|
|
||||||
LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
|
|
||||||
<MenuItem key={key} value={key}>
|
|
||||||
<img src={flag} style={flagStyle} alt={label} />
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
)),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
@@ -76,7 +63,12 @@ const LanguageSelector = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
select
|
select
|
||||||
>
|
>
|
||||||
{menuItems}
|
{LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
|
||||||
|
<MenuItem key={key} value={key}>
|
||||||
|
<img src={flag} style={flagStyle} alt={label} />
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
@@ -13,9 +13,9 @@ type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
|
|||||||
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) => {
|
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) => {
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
|
||||||
const togglePasswordVisibility = useCallback(() => {
|
const togglePasswordVisibility = () => {
|
||||||
setShowPassword((prev) => !prev);
|
setShowPassword((prev) => !prev);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
const [title, setTitle] = useState(PROJECT_NAME);
|
const [title, setTitle] = useState(PROJECT_NAME);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
// Memoize drawer toggle handler to prevent unnecessary re-renders
|
|
||||||
const handleDrawerToggle = useCallback(() => {
|
const handleDrawerToggle = useCallback(() => {
|
||||||
setMobileOpen((prev) => !prev);
|
setMobileOpen((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -28,7 +27,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
setMobileOpen(false);
|
setMobileOpen(false);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
// Memoize context value to prevent unnecessary re-renders
|
|
||||||
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
|
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router';
|
import { Link, useLocation, useNavigate } from 'react-router';
|
||||||
|
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
@@ -39,14 +39,11 @@ const LayoutAppBarComponent = ({ title, onToggleDrawer }: LayoutAppBarProps) =>
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const pathnames = useMemo(
|
const pathnames = location.pathname.split('/').filter((x) => x);
|
||||||
() => location.pathname.split('/').filter((x) => x),
|
|
||||||
[location.pathname]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleBackClick = useCallback(() => {
|
const handleBackClick = () => {
|
||||||
void navigate('/' + pathnames[0]);
|
void navigate('/' + pathnames[0]);
|
||||||
}, [navigate, pathnames]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="fixed" sx={appBarStyles}>
|
<AppBar position="fixed" sx={appBarStyles}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
||||||
|
|
||||||
@@ -24,9 +24,7 @@ interface LayoutDrawerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
||||||
// Memoize drawer content to prevent unnecessary re-renders
|
const drawer = (
|
||||||
const drawer = useMemo(
|
|
||||||
() => (
|
|
||||||
<>
|
<>
|
||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
|
||||||
@@ -38,8 +36,6 @@ const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<LayoutMenu />
|
<LayoutMenu />
|
||||||
</>
|
</>
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useState } from 'react';
|
import { memo, useContext, useState } 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';
|
||||||
@@ -22,9 +22,9 @@ const LayoutMenuComponent = () => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [menuOpen, setMenuOpen] = useState(true);
|
const [menuOpen, setMenuOpen] = useState(true);
|
||||||
|
|
||||||
const handleMenuToggle = useCallback(() => {
|
const handleMenuToggle = () => {
|
||||||
setMenuOpen((prev) => !prev);
|
setMenuOpen((prev) => !prev);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router';
|
import { Link, useLocation } from 'react-router';
|
||||||
|
|
||||||
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
||||||
@@ -21,11 +21,9 @@ const LayoutMenuItemComponent = ({
|
|||||||
}: LayoutMenuItemProps) => {
|
}: LayoutMenuItemProps) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const selected = useMemo(() => routeMatches(to, pathname), [to, pathname]);
|
const selected = routeMatches(to, pathname);
|
||||||
|
|
||||||
// Memoize dynamic styles based on selected state
|
const buttonStyles: SxProps<Theme> = {
|
||||||
const buttonStyles: SxProps<Theme> = useMemo(
|
|
||||||
() => ({
|
|
||||||
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||||
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
|
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
@@ -43,28 +41,20 @@ const LayoutMenuItemComponent = ({
|
|||||||
backgroundColor: '#90caf9',
|
backgroundColor: '#90caf9',
|
||||||
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
||||||
}
|
}
|
||||||
}),
|
};
|
||||||
[selected]
|
|
||||||
);
|
|
||||||
|
|
||||||
const iconStyles: SxProps<Theme> = useMemo(
|
const iconStyles: SxProps<Theme> = {
|
||||||
() => ({
|
|
||||||
color: selected ? '#90caf9' : '#9e9e9e',
|
color: selected ? '#90caf9' : '#9e9e9e',
|
||||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||||
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
||||||
transitionProperty: 'color, transform'
|
transitionProperty: 'color, transform'
|
||||||
}),
|
};
|
||||||
[selected]
|
|
||||||
);
|
|
||||||
|
|
||||||
const textStyles: SxProps<Theme> = useMemo(
|
const textStyles: SxProps<Theme> = {
|
||||||
() => ({
|
|
||||||
color: selected ? '#90caf9' : '#f5f5f5',
|
color: selected ? '#90caf9' : '#f5f5f5',
|
||||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||||
transitionProperty: 'color, font-weight'
|
transitionProperty: 'color, font-weight'
|
||||||
}),
|
};
|
||||||
[selected]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
import type { Blocker } from 'react-router';
|
import type { Blocker } from 'react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -15,13 +15,13 @@ import { useI18nContext } from 'i18n/i18n-react';
|
|||||||
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
|
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = () => {
|
||||||
blocker.reset?.();
|
blocker.reset?.();
|
||||||
}, [blocker]);
|
};
|
||||||
|
|
||||||
const handleProceed = useCallback(() => {
|
const handleProceed = () => {
|
||||||
blocker.proceed?.();
|
blocker.proceed?.();
|
||||||
}, [blocker]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}>
|
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
@@ -16,12 +16,9 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
|
const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
const handleTabChange = useCallback(
|
const handleTabChange = (_event: unknown, path: string) => {
|
||||||
(_event: unknown, path: string) => {
|
|
||||||
void navigate(path);
|
void navigate(path);
|
||||||
},
|
};
|
||||||
[navigate]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
void refresh();
|
void refresh();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
// cache object to prevent re-renders
|
|
||||||
const obj = useMemo(
|
const obj = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
signIn,
|
signIn,
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const usePersistState = <T>(
|
export const usePersistState = <T>(
|
||||||
initial_value: T,
|
initial_value: T,
|
||||||
id: string
|
id: string
|
||||||
): [T, (new_state: T) => void] => {
|
): [T, (new_state: T) => void] => {
|
||||||
// Set initial value - only computed once on mount
|
const [state, setState] = useState<T>(() => {
|
||||||
const _initial_value = useMemo(() => {
|
|
||||||
try {
|
try {
|
||||||
const local_storage_value_str = localStorage.getItem(`state:${id}`);
|
const stored = localStorage.getItem(`state:${id}`);
|
||||||
// If there is a value stored in localStorage, use that
|
if (stored) {
|
||||||
if (local_storage_value_str) {
|
return JSON.parse(stored) as T;
|
||||||
return JSON.parse(local_storage_value_str) as T;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If parsing fails, fall back to initial_value
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed to parse localStorage value for key "state:${id}"`,
|
`Failed to parse localStorage value for key "state:${id}"`,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Otherwise use initial_value that was passed to the function
|
|
||||||
return initial_value;
|
return initial_value;
|
||||||
}, [id]); // initial_value intentionally omitted - only read on first mount
|
});
|
||||||
|
|
||||||
const [state, setState] = useState(_initial_value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const state_str = JSON.stringify(state);
|
localStorage.setItem(`state:${id}`, JSON.stringify(state));
|
||||||
localStorage.setItem(`state:${id}`, state_str);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed to save state to localStorage for key "state:${id}"`,
|
`Failed to save state to localStorage for key "state:${id}"`,
|
||||||
|
|||||||
Reference in New Issue
Block a user