mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-06-14 03:46:49 +03:00
Merge remote-tracking branch 'origin/dev' into core3
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
@@ -62,22 +62,16 @@ const APSettings = () => {
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = useMemo(
|
||||
() =>
|
||||
updateValueDirty(
|
||||
origData as unknown as Record<string, unknown>,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue as (value: unknown) => void
|
||||
),
|
||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData as unknown as Record<string, unknown>,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue as (value: unknown) => void
|
||||
);
|
||||
|
||||
// Memoize AP enabled state
|
||||
const apEnabled = useMemo(() => (data ? isAPEnabled(data) : false), [data]);
|
||||
const apEnabled = data ? isAPEnabled(data) : false;
|
||||
|
||||
// Memoize validation and submit handler
|
||||
const validateAndSubmit = useCallback(async () => {
|
||||
const validateAndSubmit = async () => {
|
||||
if (!data) return;
|
||||
|
||||
try {
|
||||
@@ -87,7 +81,7 @@ const APSettings = () => {
|
||||
} catch (error) {
|
||||
setFieldErrors((error as ValidationError).fieldErrors);
|
||||
}
|
||||
}, [data, saveData]);
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
@@ -107,49 +107,36 @@ const ApplicationSettings = () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Memoized input props to prevent recreation on every render
|
||||
const SecondsInputProps = useMemo(
|
||||
() => ({
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}),
|
||||
[LL]
|
||||
);
|
||||
const SecondsInputProps = {
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
};
|
||||
|
||||
const MinutesInputProps = useMemo(
|
||||
() => ({
|
||||
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||
}),
|
||||
[LL]
|
||||
);
|
||||
const MinutesInputProps = {
|
||||
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||
};
|
||||
|
||||
const HoursInputProps = useMemo(
|
||||
() => ({
|
||||
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
||||
}),
|
||||
[LL]
|
||||
);
|
||||
const HoursInputProps = {
|
||||
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
||||
};
|
||||
|
||||
const doRestart = useCallback(async () => {
|
||||
const doRestart = async () => {
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
}, [sendAPI]);
|
||||
};
|
||||
|
||||
const updateBoardProfile = useCallback(
|
||||
async (board_profile: string) => {
|
||||
await readBoardProfile(board_profile).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
},
|
||||
[readBoardProfile]
|
||||
);
|
||||
const updateBoardProfile = async (board_profile: string) => {
|
||||
await readBoardProfile(board_profile).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.APPLICATION());
|
||||
|
||||
const validateAndSubmit = useCallback(async () => {
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createSettingsValidator(data), data);
|
||||
@@ -158,31 +145,27 @@ const ApplicationSettings = () => {
|
||||
} finally {
|
||||
await saveData();
|
||||
}
|
||||
}, [data, saveData]);
|
||||
};
|
||||
|
||||
const changeBoardProfile = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const boardProfile = event.target.value;
|
||||
updateFormValue(event);
|
||||
if (boardProfile === 'CUSTOM') {
|
||||
updateDataValue({
|
||||
...data,
|
||||
board_profile: boardProfile
|
||||
});
|
||||
} else {
|
||||
void updateBoardProfile(boardProfile);
|
||||
}
|
||||
},
|
||||
[data, updateBoardProfile, updateFormValue, updateDataValue]
|
||||
);
|
||||
const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const boardProfile = event.target.value;
|
||||
updateFormValue(event);
|
||||
if (boardProfile === 'CUSTOM') {
|
||||
updateDataValue({
|
||||
...data,
|
||||
board_profile: boardProfile
|
||||
});
|
||||
} else {
|
||||
void updateBoardProfile(boardProfile);
|
||||
}
|
||||
};
|
||||
|
||||
const restart = useCallback(async () => {
|
||||
const restart = async () => {
|
||||
await validateAndSubmit();
|
||||
await doRestart();
|
||||
}, [validateAndSubmit, doRestart]);
|
||||
};
|
||||
|
||||
// Memoize board profile select items to prevent recreation
|
||||
const boardProfileItems = useMemo(() => boardProfileSelectItems(), []);
|
||||
const boardProfileItems = boardProfileSelectItems();
|
||||
|
||||
const content = () => {
|
||||
if (!data || !hardwareData) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
@@ -57,7 +57,7 @@ const DownloadUpload = () => {
|
||||
|
||||
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
||||
|
||||
const doRestart = useCallback(async () => {
|
||||
const doRestart = async () => {
|
||||
setRestarting(true);
|
||||
try {
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 });
|
||||
@@ -65,16 +65,33 @@ const DownloadUpload = () => {
|
||||
toast.error((error as Error).message);
|
||||
setRestarting(false);
|
||||
}
|
||||
}, [sendAPI]);
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
|
||||
|
||||
const handleCloseBackupDialog = useCallback(() => {
|
||||
const handleCloseBackupDialog = () => {
|
||||
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
|
||||
sx={dialogStyle}
|
||||
open={confirmBackup}
|
||||
@@ -98,40 +115,13 @@ const DownloadUpload = () => {
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => handleDownload('systembackup')()}
|
||||
onClick={handleDownload('systembackup')}
|
||||
color="primary"
|
||||
>
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</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">
|
||||
{LL.DOWNLOAD(0)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
@@ -57,7 +57,7 @@ const MqttSettings = () => {
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const sendResetMQTT = useCallback(() => {
|
||||
const sendResetMQTT = () => {
|
||||
void callAction({ action: 'resetMQTT' })
|
||||
.then(() => {
|
||||
toast.success('MQTT ' + LL.REFRESH() + ' successful');
|
||||
@@ -65,29 +65,20 @@ const MqttSettings = () => {
|
||||
.catch((error) => {
|
||||
toast.error(String(error.error?.message || 'An error occurred'));
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const updateFormValue = useMemo(
|
||||
() =>
|
||||
updateValueDirty(
|
||||
origData as unknown as Record<string, unknown>,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue as (value: unknown) => void
|
||||
),
|
||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData as unknown as Record<string, unknown>,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue as (value: unknown) => void
|
||||
);
|
||||
|
||||
const SecondsInputProps = useMemo(
|
||||
() => ({
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
}),
|
||||
[LL]
|
||||
);
|
||||
const SecondsInputProps = {
|
||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
};
|
||||
|
||||
const emptyFieldErrors = useMemo(() => ({}), []);
|
||||
|
||||
const validateAndSubmit = useCallback(async () => {
|
||||
const validateAndSubmit = async () => {
|
||||
if (!data) return;
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
@@ -96,25 +87,22 @@ const MqttSettings = () => {
|
||||
} catch (error) {
|
||||
setFieldErrors((error as ValidationError).fieldErrors);
|
||||
}
|
||||
}, [data, saveData]);
|
||||
};
|
||||
|
||||
const publishIntervalFields = useMemo(
|
||||
() => [
|
||||
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
|
||||
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
|
||||
{
|
||||
name: 'publish_time_thermostat',
|
||||
label: LL.MQTT_INT_THERMOSTATS(),
|
||||
validated: false
|
||||
},
|
||||
{ name: 'publish_time_solar', label: LL.MQTT_INT_SOLAR(), validated: false },
|
||||
{ name: 'publish_time_mixer', label: LL.MQTT_INT_MIXER(), 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_other', label: LL.DEFAULT(0), validated: false }
|
||||
],
|
||||
[LL]
|
||||
);
|
||||
const publishIntervalFields = [
|
||||
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
|
||||
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
|
||||
{
|
||||
name: 'publish_time_thermostat',
|
||||
label: LL.MQTT_INT_THERMOSTATS(),
|
||||
validated: false
|
||||
},
|
||||
{ name: 'publish_time_solar', label: LL.MQTT_INT_SOLAR(), validated: false },
|
||||
{ name: 'publish_time_mixer', label: LL.MQTT_INT_MIXER(), 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_other', label: LL.DEFAULT(0), validated: false }
|
||||
];
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
@@ -154,7 +142,7 @@ const MqttSettings = () => {
|
||||
<Grid container spacing={2} rowSpacing={0}>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
||||
fieldErrors={fieldErrors ?? {}}
|
||||
name="host"
|
||||
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||
multiline
|
||||
@@ -166,7 +154,7 @@ const MqttSettings = () => {
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
||||
fieldErrors={fieldErrors ?? {}}
|
||||
name="port"
|
||||
label="Port"
|
||||
variant="outlined"
|
||||
@@ -178,7 +166,7 @@ const MqttSettings = () => {
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
||||
fieldErrors={fieldErrors ?? {}}
|
||||
name="base"
|
||||
label={LL.BASE_TOPIC()}
|
||||
variant="outlined"
|
||||
@@ -219,7 +207,7 @@ const MqttSettings = () => {
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
||||
fieldErrors={fieldErrors ?? {}}
|
||||
name="keep_alive"
|
||||
label="Keep Alive"
|
||||
slotProps={{
|
||||
@@ -438,7 +426,7 @@ const MqttSettings = () => {
|
||||
<Grid key={field.name}>
|
||||
{field.validated ? (
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
||||
fieldErrors={fieldErrors ?? {}}
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
slotProps={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
@@ -61,14 +61,11 @@ const NTPSettings = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle('NTP');
|
||||
|
||||
// Memoized timezone select items for better performance
|
||||
const timeZoneItems = useTimeZoneSelectItems();
|
||||
|
||||
// Memoized selected timezone value
|
||||
const selectedTzValue = useMemo(
|
||||
() => (data ? selectedTimeZone(data.tz_label, data.tz_format) : undefined),
|
||||
[data?.tz_label, data?.tz_format]
|
||||
);
|
||||
const selectedTzValue = data
|
||||
? selectedTimeZone(data.tz_label, data.tz_format)
|
||||
: undefined;
|
||||
|
||||
const [localTime, setLocalTime] = useState<string>('');
|
||||
const [settingTime, setSettingTime] = useState<boolean>(false);
|
||||
@@ -82,32 +79,22 @@ const NTPSettings = () => {
|
||||
}
|
||||
);
|
||||
|
||||
// Memoize updateFormValue to prevent recreation on every render
|
||||
const updateFormValue = useMemo(
|
||||
() =>
|
||||
updateValueDirty(
|
||||
origData as unknown as Record<string, unknown>,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue as (value: unknown) => void
|
||||
),
|
||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData as unknown as Record<string, unknown>,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue as (value: unknown) => void
|
||||
);
|
||||
|
||||
// Memoize updateLocalTime handler
|
||||
const updateLocalTime = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value),
|
||||
[]
|
||||
);
|
||||
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setLocalTime(event.target.value);
|
||||
|
||||
// Memoize openSetTime handler
|
||||
const openSetTime = useCallback(() => {
|
||||
const openSetTime = () => {
|
||||
setLocalTime(formatLocalDateTime(new Date()));
|
||||
setSettingTime(true);
|
||||
}, []);
|
||||
};
|
||||
|
||||
// Memoize configureTime handler
|
||||
const configureTime = useCallback(async () => {
|
||||
const configureTime = async () => {
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
@@ -120,13 +107,11 @@ const NTPSettings = () => {
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
}, [localTime, updateTime, LL, loadData]);
|
||||
};
|
||||
|
||||
// Memoize close dialog handler
|
||||
const handleCloseSetTime = useCallback(() => setSettingTime(false), []);
|
||||
const handleCloseSetTime = () => setSettingTime(false);
|
||||
|
||||
// Memoize validate and submit handler
|
||||
const validateAndSubmit = useCallback(async () => {
|
||||
const validateAndSubmit = async () => {
|
||||
if (!data) return;
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
@@ -135,23 +120,18 @@ const NTPSettings = () => {
|
||||
} catch (error) {
|
||||
setFieldErrors((error as ValidationError).fieldErrors);
|
||||
}
|
||||
}, [data, saveData]);
|
||||
};
|
||||
|
||||
// Memoize timezone change handler
|
||||
const changeTimeZone = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
|
||||
...settings,
|
||||
tz_label: event.target.value,
|
||||
tz_format: TIME_ZONES[event.target.value]
|
||||
}));
|
||||
updateFormValue(event);
|
||||
},
|
||||
[updateFormValue]
|
||||
);
|
||||
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
|
||||
...settings,
|
||||
tz_label: event.target.value,
|
||||
tz_format: TIME_ZONES[event.target.value]
|
||||
}));
|
||||
updateFormValue(event);
|
||||
};
|
||||
|
||||
// Memoize render content to prevent unnecessary re-renders
|
||||
const renderContent = useMemo(() => {
|
||||
const renderContent = () => {
|
||||
if (!data) {
|
||||
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 (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{renderContent}
|
||||
{renderContent()}
|
||||
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
|
||||
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
|
||||
@@ -1,190 +1,108 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import ImportExportIcon from '@mui/icons-material/ImportExport';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
List
|
||||
} from '@mui/material';
|
||||
import { List } from '@mui/material';
|
||||
|
||||
import { API } from 'api/app';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type { APIcall } from 'app/main/types';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import SystemMonitor from '../status/SystemMonitor';
|
||||
|
||||
const Settings = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { versions } = useContext(AuthenticatedContext);
|
||||
useLayoutTitle(LL.SETTINGS(0));
|
||||
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
const upgradeAvailable = versions?.current?.upgradeable ?? false;
|
||||
const firmwareText = versions?.current?.version
|
||||
? `v${versions.current.version}${upgradeAvailable ? ` (${LL.UPDATE_AVAILABLE()})` : ''}`
|
||||
: '';
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
return (
|
||||
<SectionContent>
|
||||
<List>
|
||||
<ListMenuItem
|
||||
icon={BuildIcon}
|
||||
bgcolor="#72caf9"
|
||||
label="EMS-ESP Firmware"
|
||||
text={firmwareText}
|
||||
to="/settings/version"
|
||||
badge={upgradeAvailable}
|
||||
/>
|
||||
|
||||
const doFormat = useCallback(async () => {
|
||||
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
|
||||
setRestarting(true);
|
||||
setConfirmFactoryReset(false);
|
||||
});
|
||||
}, [sendAPI]);
|
||||
<ListMenuItem
|
||||
icon={TuneIcon}
|
||||
bgcolor="#134ba2"
|
||||
label={LL.APPLICATION()}
|
||||
text={LL.APPLICATION_SETTINGS_1()}
|
||||
to="application"
|
||||
/>
|
||||
|
||||
const handleFactoryResetClose = useCallback(() => {
|
||||
setConfirmFactoryReset(false);
|
||||
}, []);
|
||||
<ListMenuItem
|
||||
icon={SettingsEthernetIcon}
|
||||
bgcolor="#40828f"
|
||||
label={LL.NETWORK(0)}
|
||||
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
|
||||
to="network"
|
||||
/>
|
||||
|
||||
const handleFactoryResetClick = useCallback(() => {
|
||||
setConfirmFactoryReset(true);
|
||||
}, []);
|
||||
<ListMenuItem
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor="#5f9a5f"
|
||||
label={LL.ACCESS_POINT(0)}
|
||||
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
|
||||
to="ap"
|
||||
/>
|
||||
|
||||
const content = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListMenuItem
|
||||
icon={TuneIcon}
|
||||
bgcolor="#134ba2"
|
||||
label={LL.APPLICATION()}
|
||||
text={LL.APPLICATION_SETTINGS_1()}
|
||||
to="application"
|
||||
/>
|
||||
<ListMenuItem
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor="#c5572c"
|
||||
label="NTP"
|
||||
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
|
||||
to="ntp"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsEthernetIcon}
|
||||
bgcolor="#40828f"
|
||||
label={LL.NETWORK(0)}
|
||||
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
|
||||
to="network"
|
||||
/>
|
||||
<ListMenuItem
|
||||
icon={DeviceHubIcon}
|
||||
bgcolor="#68374d"
|
||||
label="MQTT"
|
||||
text={LL.CONFIGURE('MQTT')}
|
||||
to="mqtt"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor="#5f9a5f"
|
||||
label={LL.ACCESS_POINT(0)}
|
||||
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
|
||||
to="ap"
|
||||
/>
|
||||
<ListMenuItem
|
||||
icon={LockIcon}
|
||||
label={LL.SECURITY(0)}
|
||||
text={LL.SECURITY_1()}
|
||||
to="security"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor="#c5572c"
|
||||
label="NTP"
|
||||
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
|
||||
to="ntp"
|
||||
/>
|
||||
<ListMenuItem
|
||||
icon={ViewModuleIcon}
|
||||
bgcolor="#efc34b"
|
||||
label={LL.MODULES()}
|
||||
text={LL.MODULES_1()}
|
||||
to="modules"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={DeviceHubIcon}
|
||||
bgcolor="#68374d"
|
||||
label="MQTT"
|
||||
text={LL.CONFIGURE('MQTT')}
|
||||
to="mqtt"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={LockIcon}
|
||||
label={LL.SECURITY(0)}
|
||||
text={LL.SECURITY_1()}
|
||||
to="security"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={ViewModuleIcon}
|
||||
bgcolor="#efc34b"
|
||||
label={LL.MODULES()}
|
||||
text={LL.MODULES_1()}
|
||||
to="modules"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={ImportExportIcon}
|
||||
bgcolor="#5d89f7"
|
||||
label={LL.DOWNLOAD_UPLOAD()}
|
||||
text={LL.DOWNLOAD_UPLOAD_1()}
|
||||
to="downloadUpload"
|
||||
/>
|
||||
</List>
|
||||
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmFactoryReset}
|
||||
onClose={handleFactoryResetClose}
|
||||
>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleFactoryResetClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={doFormat}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flexWrap: 'nowrap',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleFactoryResetClick}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
LL,
|
||||
handleFactoryResetClick,
|
||||
handleFactoryResetClose,
|
||||
doFormat,
|
||||
confirmFactoryReset,
|
||||
restarting
|
||||
]);
|
||||
|
||||
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
|
||||
<ListMenuItem
|
||||
icon={ImportExportIcon}
|
||||
bgcolor="#5d89f7"
|
||||
label={LL.DOWNLOAD_UPLOAD()}
|
||||
text={LL.DOWNLOAD_UPLOAD_1()}
|
||||
to="downloadUpload"
|
||||
/>
|
||||
</List>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { MenuItem } from '@mui/material';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) => (
|
||||
<MenuItem key={label} value={label}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
export function useTimeZoneSelectItems() {
|
||||
return precomputedTimeZoneItems;
|
||||
}
|
||||
|
||||
export function timeZoneSelectItems() {
|
||||
return precomputedTimeZoneItems;
|
||||
}
|
||||
|
||||
959
interface/src/app/settings/Version.tsx
Normal file
959
interface/src/app/settings/Version.tsx
Normal file
@@ -0,0 +1,959 @@
|
||||
import { memo, useContext, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import CheckIcon from '@mui/icons-material/Done';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
import { API, callAction } from 'api/app';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova/client';
|
||||
import type { APIcall } from 'app/main/types';
|
||||
import SystemMonitor from 'app/status/SystemMonitor';
|
||||
import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
SingleUpload,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { TranslationFunctions } from 'i18n/i18n-types';
|
||||
import type { VersionInfo } from 'types';
|
||||
import { prettyDateTime } from 'utils/time';
|
||||
|
||||
// Constants moved outside component to avoid recreation
|
||||
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
||||
const STABLE_RELNOTES_URL =
|
||||
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||
const DEV_RELNOTES_URL =
|
||||
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
||||
|
||||
// Types for better type safety
|
||||
interface PartitionData {
|
||||
partition: string;
|
||||
version: string;
|
||||
install_date?: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface VersionData {
|
||||
emsesp_version: string;
|
||||
arduino_version: string;
|
||||
esp_platform: string;
|
||||
flash_chip_size: number;
|
||||
psram: boolean;
|
||||
build_flags?: string;
|
||||
partition: string;
|
||||
partitions: PartitionData[];
|
||||
developer_mode: boolean;
|
||||
}
|
||||
|
||||
// Memoized components for better performance
|
||||
const VersionInfoDialog = memo(
|
||||
({
|
||||
showVersionInfo,
|
||||
latestVersion,
|
||||
latestDevVersion,
|
||||
partitionVersion,
|
||||
partition,
|
||||
currentPartition,
|
||||
size,
|
||||
locale,
|
||||
LL,
|
||||
onClose
|
||||
}: {
|
||||
showVersionInfo: number;
|
||||
latestVersion: VersionInfo | undefined;
|
||||
latestDevVersion: VersionInfo | undefined;
|
||||
partitionVersion: VersionInfo | undefined;
|
||||
partition: string;
|
||||
currentPartition: string;
|
||||
size: number;
|
||||
locale: string;
|
||||
LL: TranslationFunctions;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
if (showVersionInfo === 0) return null;
|
||||
|
||||
const isStable = showVersionInfo === 1;
|
||||
const isDev = showVersionInfo === 2;
|
||||
const isPartition = showVersionInfo === 3;
|
||||
|
||||
const version = isStable
|
||||
? latestVersion
|
||||
: isDev
|
||||
? latestDevVersion
|
||||
: partitionVersion;
|
||||
const relNotesUrl = isStable
|
||||
? STABLE_RELNOTES_URL
|
||||
: isDev
|
||||
? DEV_RELNOTES_URL
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
|
||||
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
|
||||
<TableBody>
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13
|
||||
}}
|
||||
>
|
||||
{LL.VERSION()}
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{isPartition
|
||||
? typeof version === 'string'
|
||||
? version
|
||||
: version?.version
|
||||
: version?.version}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13,
|
||||
width: 140
|
||||
}}
|
||||
>
|
||||
{isPartition ? LL.TYPE(0) : LL.RELEASE_TYPE()}
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{partition === currentPartition && LL.ACTIVE() + ' '}
|
||||
{isStable
|
||||
? LL.STABLE()
|
||||
: isDev
|
||||
? LL.DEVELOPMENT()
|
||||
: 'Partition ' + LL.VERSION()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isPartition && (
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13
|
||||
}}
|
||||
>
|
||||
Partition
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{partition}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{isPartition && (
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13
|
||||
}}
|
||||
>
|
||||
Size
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{size} KB
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{version && version.date && (
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13
|
||||
}}
|
||||
>
|
||||
{isPartition ? 'Install Date' : 'Build Date'}
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{prettyDateTime(locale, new Date(version.date))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!isPartition && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="a"
|
||||
href={relNotesUrl}
|
||||
target="_blank"
|
||||
color="primary"
|
||||
>
|
||||
Changelog
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outlined" onClick={onClose} color="secondary">
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const InstallDialog = memo(
|
||||
({
|
||||
openInstallDialog,
|
||||
fetchDevVersion,
|
||||
latestVersion,
|
||||
latestDevVersion,
|
||||
upgradeImportantMessageType,
|
||||
downloadOnly,
|
||||
platform,
|
||||
LL,
|
||||
onClose,
|
||||
onInstall
|
||||
}: {
|
||||
openInstallDialog: boolean;
|
||||
fetchDevVersion: boolean;
|
||||
latestVersion: VersionInfo | undefined;
|
||||
latestDevVersion: VersionInfo | undefined;
|
||||
upgradeImportantMessageType: number;
|
||||
downloadOnly: boolean;
|
||||
platform: string;
|
||||
LL: TranslationFunctions;
|
||||
onClose: () => void;
|
||||
onInstall: (url: string) => void;
|
||||
}) => {
|
||||
const binURL = (() => {
|
||||
if (!latestVersion || !latestDevVersion) return '';
|
||||
const version = fetchDevVersion ? latestDevVersion : latestVersion;
|
||||
const filename = `EMS-ESP-${version.version.replaceAll('.', '_')}-${platform}.bin`;
|
||||
return fetchDevVersion
|
||||
? `${DEV_URL}${filename}`
|
||||
: `${STABLE_URL}v${version.version}/${filename}`;
|
||||
})();
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
{`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography sx={{ mb: 2 }}>
|
||||
{LL.INSTALL_VERSION(
|
||||
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
|
||||
fetchDevVersion ? latestDevVersion?.version : latestVersion?.version
|
||||
)}
|
||||
</Typography>
|
||||
{upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()}
|
||||
{upgradeImportantMessageType === 1 && (
|
||||
<>
|
||||
{LL.UPGRADE_IMPORTANT_MESSAGES_1()}
|
||||
<Typography sx={{ mt: 2 }}>
|
||||
<Link to="/settings/downloadUpload" style={{ color: 'lightblue' }}>
|
||||
{LL.DOWNLOAD_SYSTEM_BACKUP()}
|
||||
</Link>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
<Typography sx={{ mt: 2 }}>
|
||||
<Link
|
||||
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: 'lightblue' }}
|
||||
>
|
||||
{LL.ONLINE_HELP()}
|
||||
</Link>
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="primary"
|
||||
>
|
||||
<Link
|
||||
to={binURL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: 'lightblue', textDecoration: 'none' }}
|
||||
>
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Link>
|
||||
</Button>
|
||||
{!downloadOnly && (
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="outlined"
|
||||
onClick={() => onInstall(binURL)}
|
||||
color="primary"
|
||||
>
|
||||
{LL.INSTALL()}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const InstallPartitionDialog = memo(
|
||||
({
|
||||
openInstallPartitionDialog,
|
||||
version,
|
||||
partition,
|
||||
LL,
|
||||
onClose,
|
||||
onInstall
|
||||
}: {
|
||||
openInstallPartitionDialog: boolean;
|
||||
version: string;
|
||||
partition: string;
|
||||
LL: TranslationFunctions;
|
||||
onClose: () => void;
|
||||
onInstall: (partition: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
{LL.INSTALL()} {LL.STORED_VERSIONS()}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography sx={{ mb: 2 }}>
|
||||
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="outlined"
|
||||
onClick={() => onInstall(partition)}
|
||||
color="primary"
|
||||
>
|
||||
{LL.INSTALL()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function moved outside component
|
||||
const getPlatform = (data: VersionData): string => {
|
||||
return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`;
|
||||
};
|
||||
|
||||
const Version = () => {
|
||||
const { LL, locale } = useI18nContext();
|
||||
const { me, versions } = useContext(AuthenticatedContext);
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>(false);
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
||||
|
||||
const [partitionVersion, setPartitionVersion] = useState<VersionInfo | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [partition, setPartition] = useState<string>('');
|
||||
const [openInstallPartitionDialog, setOpenInstallPartitionDialog] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
|
||||
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
|
||||
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
|
||||
const [firmwareSize, setFirmwareSize] = useState<number>(0);
|
||||
|
||||
const latestVersion = useMemo<VersionInfo | undefined>(
|
||||
() =>
|
||||
versions?.stable
|
||||
? { version: versions.stable.version, date: versions.stable.date }
|
||||
: undefined,
|
||||
[versions?.stable]
|
||||
);
|
||||
const latestDevVersion = useMemo<VersionInfo | undefined>(
|
||||
() =>
|
||||
versions?.dev
|
||||
? { version: versions.dev.version, date: versions.dev.date }
|
||||
: undefined,
|
||||
[versions?.dev]
|
||||
);
|
||||
const usingDevVersion = versions?.current?.type === 'dev';
|
||||
const stableUpgradeAvailable = versions?.stable?.upgradeable ?? false;
|
||||
const devUpgradeAvailable = versions?.dev?.upgradeable ?? false;
|
||||
const internetLive = Boolean(versions?.stable || versions?.dev);
|
||||
|
||||
const { send: sendSetPartition } = useRequest(
|
||||
(partition: string) => callAction({ action: 'setPartition', param: partition }),
|
||||
{ immediate: false }
|
||||
).onError((error) => {
|
||||
toast.error(String(error.error?.message || 'An error occurred'));
|
||||
});
|
||||
|
||||
const {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
|
||||
const systemData = event.data as VersionData;
|
||||
if (systemData.arduino_version.startsWith('Tasmota')) {
|
||||
setDownloadOnly(true);
|
||||
}
|
||||
});
|
||||
|
||||
const { send: sendUploadURL } = useRequest(
|
||||
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
|
||||
useState<number>(0);
|
||||
|
||||
const { send: checkUpgradeImportantMessages } = useRequest(
|
||||
(version: string) =>
|
||||
callAction({ action: 'upgradeImportantMessages', param: version }),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
)
|
||||
.onSuccess((event) => {
|
||||
const upgradeImportantMessageType_n = (
|
||||
event.data as { upgradeImportantMessageType: number }
|
||||
).upgradeImportantMessageType;
|
||||
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
|
||||
})
|
||||
.onError((error) => {
|
||||
toast.error(String(error.error?.message || 'An error occurred'));
|
||||
});
|
||||
|
||||
const platform = data ? getPlatform(data) : '';
|
||||
|
||||
const otherPartitions =
|
||||
data?.partitions.filter((p) => p.partition !== data.partition) ?? [];
|
||||
|
||||
const setPartitionVersionInfo = (partition: string) => {
|
||||
setShowVersionInfo(3);
|
||||
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
||||
if (partitionData) {
|
||||
setPartitionVersion({
|
||||
version: partitionData.version,
|
||||
date: partitionData.install_date ?? ''
|
||||
});
|
||||
setPartition(partitionData.partition);
|
||||
setFirmwareSize(partitionData.size);
|
||||
}
|
||||
};
|
||||
|
||||
const doRestart = async () => {
|
||||
setConfirmRestart(false);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
const doFormat = async () => {
|
||||
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
|
||||
setRestarting(true);
|
||||
setConfirmFactoryReset(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFactoryResetClose = () => setConfirmFactoryReset(false);
|
||||
const handleFactoryResetClick = () => setConfirmFactoryReset(true);
|
||||
const handleRestartClose = () => setConfirmRestart(false);
|
||||
const handleRestartClick = () => setConfirmRestart(true);
|
||||
|
||||
const installFirmwareURL = async (url: string) => {
|
||||
await sendUploadURL(url).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
await doRestart();
|
||||
};
|
||||
|
||||
const installPartitionFirmware = async (partition: string) => {
|
||||
await sendSetPartition(partition).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
const showPartitionDialog = (
|
||||
version: string,
|
||||
partition: string,
|
||||
install_date: string
|
||||
) => {
|
||||
setOpenInstallPartitionDialog(true);
|
||||
setPartitionVersion({ version: version, date: install_date });
|
||||
setPartition(partition);
|
||||
};
|
||||
|
||||
const showFirmwareDialog = (useDevVersion: boolean) => {
|
||||
setFetchDevVersion(useDevVersion);
|
||||
const targetVersion = useDevVersion
|
||||
? latestDevVersion?.version
|
||||
: latestVersion?.version;
|
||||
if (targetVersion) {
|
||||
void checkUpgradeImportantMessages(targetVersion);
|
||||
}
|
||||
setOpenInstallDialog(true);
|
||||
};
|
||||
|
||||
const closeInstallDialog = () => setOpenInstallDialog(false);
|
||||
const closeInstallPartitionDialog = () => setOpenInstallPartitionDialog(false);
|
||||
|
||||
const handleVersionInfoClose = () => {
|
||||
setShowVersionInfo(0);
|
||||
setPartitionVersion(undefined);
|
||||
setPartition('');
|
||||
};
|
||||
|
||||
useLayoutTitle('EMS-ESP Firmware');
|
||||
|
||||
const showButtons = (showingDev: boolean) => {
|
||||
const choice = showingDev
|
||||
? !usingDevVersion
|
||||
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
|
||||
: devUpgradeAvailable
|
||||
? LL.UPDATE_AVAILABLE()
|
||||
: undefined
|
||||
: usingDevVersion
|
||||
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
|
||||
: stableUpgradeAvailable
|
||||
? LL.UPDATE_AVAILABLE()
|
||||
: undefined;
|
||||
|
||||
if (!choice) {
|
||||
return (
|
||||
<>
|
||||
<CheckIcon
|
||||
color="success"
|
||||
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
|
||||
/>
|
||||
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
|
||||
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
||||
</span>
|
||||
<Button
|
||||
sx={{ ml: 1 }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => showFirmwareDialog(showingDev)}
|
||||
>
|
||||
{LL.REINSTALL()}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!me.admin) return null;
|
||||
|
||||
const isUpdateAvailable = choice === LL.UPDATE_AVAILABLE();
|
||||
|
||||
return (
|
||||
<Button
|
||||
sx={{ ml: 1 }}
|
||||
variant="outlined"
|
||||
color={isUpdateAvailable ? 'success' : 'warning'}
|
||||
size="small"
|
||||
onClick={() => showFirmwareDialog(showingDev)}
|
||||
>
|
||||
{choice}
|
||||
{isUpdateAvailable && (
|
||||
<Box
|
||||
component="span"
|
||||
aria-label="update available"
|
||||
sx={{
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 8,
|
||||
ml: 1,
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ffeb3b',
|
||||
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
if (restarting) {
|
||||
return <SystemMonitor />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<SectionContent>
|
||||
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
<Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
|
||||
<Typography sx={{ mb: 1 }} variant="h6" color="primary">
|
||||
{LL.THIS_VERSION()}
|
||||
</Typography>
|
||||
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'baseline'
|
||||
}}
|
||||
>
|
||||
<Grid size={{ xs: 4, md: 2 }}>
|
||||
<Typography color="secondary">{LL.VERSION()}</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 8, md: 10 }}>
|
||||
<Typography>
|
||||
{data.emsesp_version}
|
||||
{data.build_flags && (
|
||||
<Typography variant="caption">
|
||||
({data.build_flags})
|
||||
</Typography>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => setPartitionVersionInfo(data.partition)}
|
||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||
>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 4, md: 2 }}>
|
||||
<Typography color="secondary">{LL.PLATFORM()}</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 8, md: 10 }}>
|
||||
<Typography>
|
||||
{platform}
|
||||
<Typography variant="caption">
|
||||
(
|
||||
{data.psram ? (
|
||||
<CheckIcon
|
||||
color="success"
|
||||
sx={{
|
||||
fontSize: '1.5em',
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CloseIcon
|
||||
color="error"
|
||||
sx={{
|
||||
fontSize: '1.5em',
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
PSRAM)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{internetLive ? (
|
||||
<>
|
||||
<Typography sx={{ mt: 4, mb: 1 }} variant="h6" color="primary">
|
||||
{LL.AVAILABLE_VERSION()}
|
||||
</Typography>
|
||||
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
rowSpacing={1}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'baseline'
|
||||
}}
|
||||
>
|
||||
{otherPartitions.length > 0 && data.developer_mode && (
|
||||
<>
|
||||
<Grid size={{ xs: 4, md: 2 }}>
|
||||
<Typography color="secondary">{LL.STORED_VERSIONS()}</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 8, md: 10 }}>
|
||||
{otherPartitions.map((partition) => (
|
||||
<Typography key={partition.partition} sx={{ mb: 1 }}>
|
||||
{partition.version}
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setPartitionVersionInfo(partition.partition)
|
||||
}
|
||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||
>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
<Button
|
||||
sx={{ ml: 0 }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
showPartitionDialog(
|
||||
partition.version,
|
||||
partition.partition,
|
||||
partition.install_date ?? ''
|
||||
)
|
||||
}
|
||||
>
|
||||
{LL.INSTALL()}
|
||||
</Button>
|
||||
</Typography>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
<Grid size={{ xs: 4, md: 2 }}>
|
||||
<Typography color="secondary">{LL.STABLE()}</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 8, md: 10 }}>
|
||||
<Typography>
|
||||
{latestVersion?.version}
|
||||
<IconButton
|
||||
onClick={() => setShowVersionInfo(1)}
|
||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||
>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
{showButtons(false)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 4, md: 2 }}>
|
||||
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 8, md: 10 }}>
|
||||
<Typography>
|
||||
{latestDevVersion?.version}
|
||||
<IconButton
|
||||
onClick={() => setShowVersionInfo(2)}
|
||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||
>
|
||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
{showButtons(true)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
) : (
|
||||
<Typography sx={{ mt: 2 }} color="warning">
|
||||
<WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} />
|
||||
{LL.INTERNET_CONNECTION_REQUIRED()}
|
||||
</Typography>
|
||||
)}
|
||||
{me.admin && (
|
||||
<>
|
||||
<VersionInfoDialog
|
||||
showVersionInfo={showVersionInfo}
|
||||
latestVersion={latestVersion}
|
||||
latestDevVersion={latestDevVersion}
|
||||
partitionVersion={partitionVersion}
|
||||
locale={locale}
|
||||
partition={partition}
|
||||
currentPartition={data?.partition ?? ''}
|
||||
size={firmwareSize}
|
||||
LL={LL}
|
||||
onClose={handleVersionInfoClose}
|
||||
/>
|
||||
<InstallDialog
|
||||
openInstallDialog={openInstallDialog}
|
||||
fetchDevVersion={fetchDevVersion}
|
||||
latestVersion={latestVersion}
|
||||
latestDevVersion={latestDevVersion}
|
||||
upgradeImportantMessageType={upgradeImportantMessageType}
|
||||
downloadOnly={downloadOnly}
|
||||
platform={platform}
|
||||
LL={LL}
|
||||
onClose={closeInstallDialog}
|
||||
onInstall={installFirmwareURL}
|
||||
/>
|
||||
<InstallPartitionDialog
|
||||
openInstallPartitionDialog={openInstallPartitionDialog}
|
||||
version={partitionVersion?.version || ''}
|
||||
partition={partition}
|
||||
LL={LL}
|
||||
onClose={closeInstallPartitionDialog}
|
||||
onInstall={installPartitionFirmware}
|
||||
/>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
<SingleUpload doRestart={doRestart} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{me.admin && (
|
||||
<>
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmFactoryReset}
|
||||
onClose={handleFactoryResetClose}
|
||||
>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleFactoryResetClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={doFormat}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmRestart}
|
||||
onClose={handleRestartClose}
|
||||
>
|
||||
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleRestartClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={doRestart}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flexWrap: 'nowrap',
|
||||
whiteSpace: 'nowrap',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleRestartClick}
|
||||
color="error"
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
{data.developer_mode && (
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={handleFactoryResetClick}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Version);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
import {
|
||||
Navigate,
|
||||
Route,
|
||||
@@ -40,26 +40,20 @@ const Network = () => {
|
||||
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||
|
||||
const selectNetwork = useCallback(
|
||||
(network: WiFiNetwork) => {
|
||||
setSelectedNetwork(network);
|
||||
void navigate('/settings/network/settings');
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const selectNetwork = (network: WiFiNetwork) => {
|
||||
setSelectedNetwork(network);
|
||||
void navigate('/settings/network/settings');
|
||||
};
|
||||
|
||||
const deselectNetwork = useCallback(() => {
|
||||
const deselectNetwork = () => {
|
||||
setSelectedNetwork(undefined);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
...(selectedNetwork && { selectedNetwork }),
|
||||
selectNetwork,
|
||||
deselectNetwork
|
||||
}),
|
||||
[selectedNetwork, selectNetwork, deselectNetwork]
|
||||
);
|
||||
const contextValue = {
|
||||
...(selectedNetwork && { selectedNetwork }),
|
||||
selectNetwork,
|
||||
deselectNetwork
|
||||
};
|
||||
|
||||
return (
|
||||
<WiFiConnectionContext.Provider value={contextValue}>
|
||||
|
||||
@@ -121,19 +121,19 @@ const NetworkSettings = () => {
|
||||
deselectNetwork();
|
||||
}, [data, saveData, deselectNetwork]);
|
||||
|
||||
const setCancel = useCallback(async () => {
|
||||
const setCancel = async () => {
|
||||
deselectNetwork();
|
||||
await loadData();
|
||||
}, [deselectNetwork, loadData]);
|
||||
};
|
||||
|
||||
const doRestart = useCallback(async () => {
|
||||
const doRestart = async () => {
|
||||
setRestarting(true);
|
||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||
(error: Error) => {
|
||||
toast.error(error.message);
|
||||
}
|
||||
);
|
||||
}, [sendAPI]);
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
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 { Button } from '@mui/material';
|
||||
@@ -48,12 +48,12 @@ const WiFiNetworkScanner = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const renderNetworkScanner = useCallback(() => {
|
||||
const renderNetworkScanner = () => {
|
||||
if (!networkList) {
|
||||
return <FormLoader errorMessage={errorMessage || ''} />;
|
||||
}
|
||||
return <WiFiNetworkSelector networkList={networkList} />;
|
||||
}, [networkList, errorMessage]);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useContext } from 'react';
|
||||
import { memo, useContext } from 'react';
|
||||
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
@@ -63,34 +63,31 @@ const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList })
|
||||
|
||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||
|
||||
const renderNetwork = useCallback(
|
||||
(network: WiFiNetwork) => (
|
||||
<ListItem
|
||||
key={network.bssid}
|
||||
onClick={() => wifiConnectionContext.selectNetwork(network)}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={network.ssid}
|
||||
secondary={
|
||||
'Security: ' +
|
||||
networkSecurityMode(network) +
|
||||
', Ch: ' +
|
||||
network.channel +
|
||||
', bssid: ' +
|
||||
network.bssid
|
||||
}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent={network.rssi + 'dBm'}>
|
||||
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
),
|
||||
[wifiConnectionContext, theme]
|
||||
const renderNetwork = (network: WiFiNetwork) => (
|
||||
<ListItem
|
||||
key={network.bssid}
|
||||
onClick={() => wifiConnectionContext.selectNetwork(network)}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={network.ssid}
|
||||
secondary={
|
||||
'Security: ' +
|
||||
networkSecurityMode(network) +
|
||||
', Ch: ' +
|
||||
network.channel +
|
||||
', bssid: ' +
|
||||
network.bssid
|
||||
}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent={network.rssi + 'dBm'}>
|
||||
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
if (networkList.networks.length === 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useContext, useState } from 'react';
|
||||
import { useBlocker } from 'react-router';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
@@ -55,16 +55,14 @@ const ManageUsers = () => {
|
||||
const blocker = useBlocker(changed !== 0);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const table_theme = useMemo(
|
||||
() =>
|
||||
useTheme({
|
||||
Table: `
|
||||
const table_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
|
||||
`,
|
||||
BaseRow: `
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
@@ -74,7 +72,7 @@ const ManageUsers = () => {
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
Row: `
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
@@ -87,7 +85,7 @@ const ManageUsers = () => {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
BaseCell: `
|
||||
&:nth-of-type(2) {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -95,44 +93,36 @@ const ManageUsers = () => {
|
||||
text-align: right;
|
||||
}
|
||||
`
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
const noAdminConfigured = useCallback(
|
||||
() => !data?.users.find((u) => u.admin),
|
||||
[data]
|
||||
);
|
||||
const noAdminConfigured = () => !data?.users.find((u) => u.admin);
|
||||
|
||||
const removeUser = useCallback(
|
||||
(toRemove: UserType) => {
|
||||
if (!data) return;
|
||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||
updateDataValue({ ...data, users });
|
||||
setChanged(changed + 1);
|
||||
},
|
||||
[data, updateDataValue, changed]
|
||||
);
|
||||
const removeUser = (toRemove: UserType) => {
|
||||
if (!data) return;
|
||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||
updateDataValue({ ...data, users });
|
||||
setChanged(changed + 1);
|
||||
};
|
||||
|
||||
const createUser = useCallback(() => {
|
||||
const createUser = () => {
|
||||
setCreating(true);
|
||||
setUser({
|
||||
username: '',
|
||||
password: '',
|
||||
admin: true
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
const editUser = useCallback((toEdit: UserType) => {
|
||||
const editUser = (toEdit: UserType) => {
|
||||
setCreating(false);
|
||||
setUser({ ...toEdit });
|
||||
}, []);
|
||||
};
|
||||
|
||||
const cancelEditingUser = useCallback(() => {
|
||||
const cancelEditingUser = () => {
|
||||
setUser(undefined);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const doneEditingUser = useCallback(() => {
|
||||
const doneEditingUser = () => {
|
||||
if (user && data) {
|
||||
const users = [
|
||||
...data.users.filter(
|
||||
@@ -144,26 +134,26 @@ const ManageUsers = () => {
|
||||
setUser(undefined);
|
||||
setChanged(changed + 1);
|
||||
}
|
||||
}, [user, data, updateDataValue, changed]);
|
||||
};
|
||||
|
||||
const closeGenerateToken = useCallback(() => {
|
||||
setGeneratingToken(undefined);
|
||||
}, []);
|
||||
|
||||
const generateTokenForUser = useCallback((username: string) => {
|
||||
const generateTokenForUser = (username: string) => {
|
||||
setGeneratingToken(username);
|
||||
}, []);
|
||||
};
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
const onSubmit = async () => {
|
||||
await saveData();
|
||||
await authenticatedContext.refresh();
|
||||
setChanged(0);
|
||||
}, [saveData, authenticatedContext]);
|
||||
};
|
||||
|
||||
const onCancelSubmit = useCallback(async () => {
|
||||
const onCancelSubmit = async () => {
|
||||
await loadData();
|
||||
setChanged(0);
|
||||
}, [loadData]);
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
@@ -177,15 +167,10 @@ const ManageUsers = () => {
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
// add id to the type, needed for the table
|
||||
const user_table = useMemo(
|
||||
() =>
|
||||
data.users.map((u) => ({
|
||||
...u,
|
||||
id: u.username
|
||||
})) as UserType2[],
|
||||
[data.users]
|
||||
);
|
||||
const user_table = data.users.map((u) => ({
|
||||
...u,
|
||||
id: u.username
|
||||
})) as UserType2[];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
@@ -15,19 +15,15 @@ const Security = () => {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const matchedRoutes = useMemo(
|
||||
() =>
|
||||
matchRoutes(
|
||||
[
|
||||
{
|
||||
path: '/settings/security/settings',
|
||||
element: <ManageUsers />
|
||||
},
|
||||
{ path: '/settings/security/users', element: <SecuritySettings /> }
|
||||
],
|
||||
location
|
||||
),
|
||||
[location]
|
||||
const matchedRoutes = matchRoutes(
|
||||
[
|
||||
{
|
||||
path: '/settings/security/settings',
|
||||
element: <ManageUsers />
|
||||
},
|
||||
{ path: '/settings/security/users', element: <SecuritySettings /> }
|
||||
],
|
||||
location
|
||||
);
|
||||
const routerTab = matchedRoutes?.[0]?.route.path || false;
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ const SecuritySettings = () => {
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
|
||||
<MessageBox level="info" message={LL.SU_TEXT()} sx={{ mt: 1 }} />
|
||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
@@ -62,7 +62,7 @@ const User: FC<UserFormProps> = ({
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const validateAndDone = useCallback(async () => {
|
||||
const validateAndDone = async () => {
|
||||
if (user) {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
@@ -72,7 +72,7 @@ const User: FC<UserFormProps> = ({
|
||||
setFieldErrors((error as ValidationError).fieldErrors);
|
||||
}
|
||||
}
|
||||
}, [user, validator, onDoneEditing]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
||||
Reference in New Issue
Block a user