mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-11 02:09:57 +03:00
refactor web file structure and seperate settings from status
This commit is contained in:
236
interface/src/app/settings/APSettings.tsx
Normal file
236
interface/src/app/settings/APSettings.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import { useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
|
||||
import * as APApi from 'api/ap';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { range } from 'lodash-es';
|
||||
import type { APSettingsType } from 'types';
|
||||
import { APProvisionMode } from 'types';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { createAPSettingsValidator, validate } from 'validators';
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
|
||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
|
||||
const APSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<APSettingsType>({
|
||||
read: APApi.readAPSettings,
|
||||
update: APApi.updateAPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
useLayoutTitle(LL.SETTINGS_OF(LL.ACCESS_POINT(0)));
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createAPSettingsValidator(data), data);
|
||||
await saveData();
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="provision_mode"
|
||||
label={LL.AP_PROVIDE() + '...'}
|
||||
value={data.provision_mode}
|
||||
fullWidth
|
||||
select
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>
|
||||
{LL.AP_PROVIDE_TEXT_1()}
|
||||
</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
|
||||
{LL.AP_PROVIDE_TEXT_2()}
|
||||
</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>
|
||||
{LL.AP_PROVIDE_TEXT_3()}
|
||||
</MenuItem>
|
||||
</ValidatedTextField>
|
||||
{isAPEnabled(data) && (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="ssid"
|
||||
label={LL.ACCESS_POINT(2) + ' SSID'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.ssid}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label={LL.ACCESS_POINT(2) + ' ' + LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="channel"
|
||||
label={LL.AP_PREFERRED_CHANNEL()}
|
||||
value={numberValue(data.channel)}
|
||||
fullWidth
|
||||
select
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
{range(1, 14).map((i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{i}
|
||||
</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="ssid_hidden"
|
||||
checked={data.ssid_hidden}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.AP_HIDE_SSID()}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="max_clients"
|
||||
label={LL.AP_MAX_CLIENTS()}
|
||||
value={numberValue(data.max_clients)}
|
||||
fullWidth
|
||||
select
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
>
|
||||
{range(1, 9).map((i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{i}
|
||||
</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="local_ip"
|
||||
label={LL.AP_LOCAL_IP()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.local_ip}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="gateway_ip"
|
||||
label={LL.NETWORK_GATEWAY()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.gateway_ip}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="subnet_mask"
|
||||
label={LL.NETWORK_SUBNET()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.subnet_mask}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default APSettings;
|
||||
946
interface/src/app/settings/ApplicationSettings.tsx
Normal file
946
interface/src/app/settings/ApplicationSettings.tsx
Normal file
@@ -0,0 +1,946 @@
|
||||
import { useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import { useRequest } from 'alova';
|
||||
import RestartMonitor from 'app/status/RestartMonitor';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
ValidatedTextField,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
import * as EMSESP from '../main/api';
|
||||
import { BOARD_PROFILES } from '../main/types';
|
||||
import type { Settings } from '../main/types';
|
||||
import { createSettingsValidator } from '../main/validators';
|
||||
|
||||
export function boardProfileSelectItems() {
|
||||
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||
<MenuItem key={code} value={code}>
|
||||
{BOARD_PROFILES[code]}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
|
||||
const ApplicationSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saveData,
|
||||
saving,
|
||||
updateDataValue,
|
||||
data,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
errorMessage,
|
||||
restartNeeded
|
||||
} = useRest<Settings>({
|
||||
read: EMSESP.readSettings,
|
||||
update: EMSESP.writeSettings
|
||||
});
|
||||
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const {
|
||||
loading: processingBoard,
|
||||
send: readBoardProfile,
|
||||
onSuccess: onSuccessBoardProfile
|
||||
} = useRequest((boardProfile: string) => EMSESP.getBoardProfile(boardProfile), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
onSuccessBoardProfile((event) => {
|
||||
const response = event.data as Settings;
|
||||
updateDataValue({
|
||||
...data,
|
||||
board_profile: response.board_profile,
|
||||
led_gpio: response.led_gpio,
|
||||
dallas_gpio: response.dallas_gpio,
|
||||
rx_gpio: response.rx_gpio,
|
||||
tx_gpio: response.tx_gpio,
|
||||
pbutton_gpio: response.pbutton_gpio,
|
||||
phy_type: response.phy_type,
|
||||
eth_power: response.eth_power,
|
||||
eth_phy_addr: response.eth_phy_addr,
|
||||
eth_clock_mode: response.eth_clock_mode
|
||||
});
|
||||
});
|
||||
|
||||
const updateBoardProfile = async (board_profile: string) => {
|
||||
await readBoardProfile(board_profile).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.SETTINGS_OF(LL.APPLICATION()));
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createSettingsValidator(data), data);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
} finally {
|
||||
await saveData();
|
||||
}
|
||||
};
|
||||
|
||||
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 = async () => {
|
||||
await validateAndSubmit();
|
||||
await restartCommand().catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pb: 1 }} variant="h6" color="primary">
|
||||
{LL.INTERFACE_BOARD_PROFILE()}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography variant="body2">{LL.BOARD_PROFILE_TEXT()}</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
name="board_profile"
|
||||
label={LL.BOARD_PROFILE()}
|
||||
value={data.board_profile}
|
||||
disabled={processingBoard}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={changeBoardProfile}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
{boardProfileSelectItems()}
|
||||
<Divider />
|
||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||
{LL.CUSTOM()}…
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
{data.board_profile === 'CUSTOM' && (
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
sx={{ pt: 1 }}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="rx_gpio"
|
||||
label={LL.GPIO_OF('Rx')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.rx_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="tx_gpio"
|
||||
label={LL.GPIO_OF('Tx')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.tx_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="pbutton_gpio"
|
||||
label={LL.GPIO_OF(LL.BUTTON())}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.pbutton_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="dallas_gpio"
|
||||
label={
|
||||
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
|
||||
}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.dallas_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="led_gpio"
|
||||
label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.led_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="phy_type"
|
||||
label={LL.PHY_TYPE()}
|
||||
disabled={saving}
|
||||
value={data.phy_type}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
|
||||
<MenuItem value={1}>LAN8720</MenuItem>
|
||||
<MenuItem value={2}>TLK110</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.phy_type !== 0 && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
sx={{ pt: 1 }}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="eth_power"
|
||||
label={LL.GPIO_OF('PHY Power') + ' (-1=' + LL.DISABLED(1) + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.eth_power)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="eth_phy_addr"
|
||||
label={LL.ADDRESS_OF('PHY I²C')}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.eth_phy_addr)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="eth_clock_mode"
|
||||
label="PHY Clk"
|
||||
disabled={saving}
|
||||
value={data.eth_clock_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>GPIO0_IN</MenuItem>
|
||||
<MenuItem value={1}>GPIO0_OUT</MenuItem>
|
||||
<MenuItem value={2}>GPIO16_OUT</MenuItem>
|
||||
<MenuItem value={3}>GPIO17_OUT</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
|
||||
</Typography>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
name="tx_mode"
|
||||
label={LL.TX_MODE()}
|
||||
disabled={saving}
|
||||
value={data.tx_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>EMS</MenuItem>
|
||||
<MenuItem value={2}>EMS+</MenuItem>
|
||||
<MenuItem value={3}>HT3</MenuItem>
|
||||
<MenuItem value={4}>{LL.HARDWARE()}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
name="ems_bus_id"
|
||||
label={LL.ID_OF(LL.EMS_BUS(1))}
|
||||
disabled={saving}
|
||||
value={data.ems_bus_id}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
|
||||
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
|
||||
<MenuItem value={0x0e}>Converter (0x0E)</MenuItem>
|
||||
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
|
||||
<MenuItem value={0x48}>Gateway 1 (0x48)</MenuItem>
|
||||
<MenuItem value={0x49}>Gateway 2 (0x49)</MenuItem>
|
||||
<MenuItem value={0x4a}>Gateway 3 (0x4A)</MenuItem>
|
||||
<MenuItem value={0x4b}>Gateway 4 (0x4B)</MenuItem>
|
||||
<MenuItem value={0x4c}>Gateway 5 (0x4C)</MenuItem>
|
||||
<MenuItem value={0x4d}>Gateway 7 (0x4D)</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.GENERAL_OPTIONS()}
|
||||
</Typography>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="locale"
|
||||
label={LL.LANGUAGE_ENTITIES()}
|
||||
disabled={saving}
|
||||
value={data.locale}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value="de">Deutsch (DE)</MenuItem>
|
||||
<MenuItem value="en">English (EN)</MenuItem>
|
||||
<MenuItem value="fr">Français (FR)</MenuItem>
|
||||
<MenuItem value="it">Italiano (IT)</MenuItem>
|
||||
<MenuItem value="nl">Nederlands (NL)</MenuItem>
|
||||
<MenuItem value="no">Norsk (NO)</MenuItem>
|
||||
<MenuItem value="pl">Polski (PL)</MenuItem>
|
||||
<MenuItem value="sk">Slovenčina (SK)</MenuItem>
|
||||
<MenuItem value="sv">Svenska (SV)</MenuItem>
|
||||
<MenuItem value="tr">Türk (TR)</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
{data.led_gpio !== 0 && (
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.hide_led}
|
||||
onChange={updateFormValue}
|
||||
name="hide_led"
|
||||
/>
|
||||
}
|
||||
label={LL.HIDE_LED()}
|
||||
disabled={saving}
|
||||
/>
|
||||
)}
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.telnet_enabled}
|
||||
onChange={updateFormValue}
|
||||
name="telnet_enabled"
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_TELNET()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.analog_enabled}
|
||||
onChange={updateFormValue}
|
||||
name="analog_enabled"
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_ANALOG()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.fahrenheit}
|
||||
onChange={updateFormValue}
|
||||
name="fahrenheit"
|
||||
/>
|
||||
}
|
||||
label={LL.CONVERT_FAHRENHEIT()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.notoken_api}
|
||||
onChange={updateFormValue}
|
||||
name="notoken_api"
|
||||
/>
|
||||
}
|
||||
label={LL.BYPASS_TOKEN()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.readonly_mode}
|
||||
onChange={updateFormValue}
|
||||
name="readonly_mode"
|
||||
/>
|
||||
}
|
||||
label={LL.READONLY()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.low_clock}
|
||||
onChange={updateFormValue}
|
||||
name="low_clock"
|
||||
/>
|
||||
}
|
||||
label={LL.UNDERCLOCK_CPU()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.boiler_heatingoff}
|
||||
onChange={updateFormValue}
|
||||
name="boiler_heatingoff"
|
||||
/>
|
||||
}
|
||||
label={LL.HEATINGOFF()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.remote_timeout_en}
|
||||
onChange={updateFormValue}
|
||||
name="remote_timeout_en"
|
||||
/>
|
||||
}
|
||||
label={LL.REMOTE_TIMEOUT_EN()}
|
||||
/>
|
||||
</Grid>
|
||||
{data.remote_timeout_en && (
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="remote_timeout"
|
||||
label={LL.REMOTE_TIMEOUT()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.remote_timeout)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_timer}
|
||||
onChange={updateFormValue}
|
||||
name="shower_timer"
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_SHOWER_TIMER()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_alert}
|
||||
onChange={updateFormValue}
|
||||
name="shower_alert"
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_SHOWER_ALERT()}
|
||||
disabled={!data.shower_timer}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
sx={{ pt: 2 }}
|
||||
rowSpacing={3}
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
{data.shower_timer && (
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="shower_min_duration"
|
||||
label={LL.MIN_DURATION()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
variant="outlined"
|
||||
value={numberValue(data.shower_min_duration)}
|
||||
fullWidth
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data.shower_alert && (
|
||||
<>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="shower_alert_trigger"
|
||||
label={LL.TRIGGER_TIME()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
variant="outlined"
|
||||
value={numberValue(data.shower_alert_trigger)}
|
||||
fullWidth
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
disabled={!data.shower_timer}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="shower_alert_coldshot"
|
||||
label={LL.COLD_SHOT_DURATION()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
variant="outlined"
|
||||
value={numberValue(data.shower_alert_coldshot)}
|
||||
fullWidth
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
disabled={!data.shower_timer}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<Typography sx={{ pt: 3 }} variant="h6" color="primary">
|
||||
{LL.FORMATTING_OPTIONS()}
|
||||
</Typography>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="bool_dashboard"
|
||||
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
|
||||
value={data.bool_dashboard}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
|
||||
<MenuItem value={3}>true/false</MenuItem>
|
||||
<MenuItem value={5}>1/0</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="bool_format"
|
||||
label={LL.BOOLEAN_FORMAT_API()}
|
||||
value={data.bool_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
|
||||
<MenuItem value={3}>"true"/"false"</MenuItem>
|
||||
<MenuItem value={4}>true/false</MenuItem>
|
||||
<MenuItem value={5}>"1"/"0"</MenuItem>
|
||||
<MenuItem value={6}>1/0</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="enum_format"
|
||||
label={LL.ENUM_FORMAT()}
|
||||
value={data.enum_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>{LL.VALUE(5)}</MenuItem>
|
||||
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.dallas_gpio !== 0 && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.TEMP_SENSORS()}
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.dallas_parasite}
|
||||
onChange={updateFormValue}
|
||||
name="dallas_parasite"
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_PARASITE()}
|
||||
disabled={saving}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.LOGGING()}
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.trace_raw}
|
||||
onChange={updateFormValue}
|
||||
name="trace_raw"
|
||||
/>
|
||||
}
|
||||
label={LL.LOG_HEX()}
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.syslog_enabled}
|
||||
onChange={updateFormValue}
|
||||
name="syslog_enabled"
|
||||
disabled={saving}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_SYSLOG()}
|
||||
/>
|
||||
{data.syslog_enabled && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_host}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.syslog_port)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
name="syslog_level"
|
||||
label={LL.LOG_LEVEL()}
|
||||
value={data.syslog_level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
disabled={saving}
|
||||
>
|
||||
<MenuItem value={-1}>OFF</MenuItem>
|
||||
<MenuItem value={3}>ERR</MenuItem>
|
||||
<MenuItem value={5}>NOTICE</MenuItem>
|
||||
<MenuItem value={6}>INFO</MenuItem>
|
||||
<MenuItem value={7}>DEBUG</MenuItem>
|
||||
<MenuItem value={9}>ALL</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_mark_interval"
|
||||
label={LL.MARK_INTERVAL()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.syslog_mark_interval)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Modbus
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.modbus_enabled}
|
||||
onChange={updateFormValue}
|
||||
name="modbus_enabled"
|
||||
disabled={saving}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_MODBUS()}
|
||||
/>
|
||||
{data.modbus_enabled && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="modbus_max_clients"
|
||||
label={LL.AP_MAX_CLIENTS()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.modbus_max_clients)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="modbus_port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.modbus_port)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="modbus_timeout"
|
||||
label="Timeout"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">ms</InputAdornment>
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.modbus_timeout)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={restart}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
{!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationSettings;
|
||||
578
interface/src/app/settings/MqttSettings.tsx
Normal file
578
interface/src/app/settings/MqttSettings.tsx
Normal file
@@ -0,0 +1,578 @@
|
||||
import { useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import * as MqttApi from 'api/mqtt';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { MqttSettingsType } from 'types';
|
||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||
import { createMqttSettingsValidator, validate } from 'validators';
|
||||
|
||||
const MqttSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<MqttSettingsType>({
|
||||
read: MqttApi.readMqttSettings,
|
||||
update: MqttApi.updateMqttSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF('MQTT'));
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createMqttSettingsValidator(data), data);
|
||||
await saveData();
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="enabled"
|
||||
checked={data.enabled}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_MQTT()}
|
||||
/>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="host"
|
||||
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||
fullWidth
|
||||
multiline
|
||||
variant="outlined"
|
||||
value={data.host}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.port)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="base"
|
||||
label={LL.BASE_TOPIC()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.base}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
name="client_id"
|
||||
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.client_id}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
name="username"
|
||||
label={LL.USERNAME(0)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.username}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedPasswordField
|
||||
name="password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="keep_alive"
|
||||
label="Keep Alive"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.keep_alive)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
name="mqtt_qos"
|
||||
label="QoS"
|
||||
value={data.mqtt_qos}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>0</MenuItem>
|
||||
<MenuItem value={1}>1</MenuItem>
|
||||
<MenuItem value={2}>2</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.enableTLS !== undefined && (
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="enableTLS"
|
||||
checked={data.enableTLS}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_TLS()}
|
||||
/>
|
||||
)}
|
||||
{data.enableTLS === true && (
|
||||
<ValidatedPasswordField
|
||||
name="rootCA"
|
||||
label={LL.CERT()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.rootCA}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="clean_session"
|
||||
checked={data.clean_session}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_CLEAN_SESSION()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="mqtt_retain"
|
||||
checked={data.mqtt_retain}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_RETAIN_FLAG()}
|
||||
/>
|
||||
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.FORMATTING()}
|
||||
</Typography>
|
||||
<TextField
|
||||
name="nested_format"
|
||||
label={LL.MQTT_FORMAT()}
|
||||
value={data.nested_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
||||
</TextField>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="send_response"
|
||||
checked={data.send_response}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_RESPONSE()}
|
||||
/>
|
||||
{!data.ha_enabled && (
|
||||
<Grid
|
||||
container
|
||||
rowSpacing={-1}
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="publish_single"
|
||||
checked={data.publish_single}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_PUBLISH_TEXT_1()}
|
||||
/>
|
||||
</Grid>
|
||||
{data.publish_single && (
|
||||
<Grid item>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="publish_single2cmd"
|
||||
checked={data.publish_single2cmd}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_PUBLISH_TEXT_2()}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
{!data.publish_single && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="ha_enabled"
|
||||
checked={data.ha_enabled}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.MQTT_PUBLISH_TEXT_3()}
|
||||
/>
|
||||
</Grid>
|
||||
{data.ha_enabled && (
|
||||
<Grid
|
||||
container
|
||||
sx={{ pl: 1 }}
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="discovery_type"
|
||||
label={LL.MQTT_PUBLISH_TEXT_5()}
|
||||
value={data.discovery_type}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>Home Assistant</MenuItem>
|
||||
<MenuItem value={1}>Domoticz</MenuItem>
|
||||
<MenuItem value={2}>Domoticz (latest)</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="discovery_prefix"
|
||||
label={LL.MQTT_PUBLISH_TEXT_4()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.discovery_prefix}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="entity_format"
|
||||
label={LL.MQTT_ENTITY_FORMAT()}
|
||||
value={data.entity_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
|
||||
<MenuItem value={3}>
|
||||
{LL.MQTT_ENTITY_FORMAT_1()} (v3.6)
|
||||
</MenuItem>
|
||||
<MenuItem value={4}>
|
||||
{LL.MQTT_ENTITY_FORMAT_2()} (v3.6)
|
||||
</MenuItem>
|
||||
<MenuItem value={1}>{LL.MQTT_ENTITY_FORMAT_1()}</MenuItem>
|
||||
<MenuItem value={2}>{LL.MQTT_ENTITY_FORMAT_2()}</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||
</Typography>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="publish_time_heartbeat"
|
||||
label="Heartbeat"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_heartbeat)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="publish_time_boiler"
|
||||
label={LL.MQTT_INT_BOILER()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_boiler)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="publish_time_thermostat"
|
||||
label={LL.MQTT_INT_THERMOSTATS()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_thermostat)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="publish_time_solar"
|
||||
label={LL.MQTT_INT_SOLAR()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_solar)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="publish_time_mixer"
|
||||
label={LL.MQTT_INT_MIXER()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_mixer)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="publish_time_water"
|
||||
label={LL.MQTT_INT_WATER()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_water)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="publish_time_sensor"
|
||||
label={LL.TEMP_SENSORS()}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_sensor)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<TextField
|
||||
name="publish_time_other"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||
)
|
||||
}}
|
||||
label={LL.DEFAULT(0)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.publish_time_other)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default MqttSettings;
|
||||
155
interface/src/app/settings/NTPSettings.tsx
Normal file
155
interface/src/app/settings/NTPSettings.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||
|
||||
import * as NTPApi from 'api/ntp';
|
||||
|
||||
import { updateState } from 'alova';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedTextField,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { NTPSettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
||||
|
||||
import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ';
|
||||
|
||||
const NTPSettings: FC = () => {
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<NTPSettingsType>({
|
||||
read: NTPApi.readNTPSettings,
|
||||
update: NTPApi.updateNTPSettings
|
||||
});
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF('NTP'));
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(NTP_SETTINGS_VALIDATOR, data);
|
||||
await saveData();
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFormValue(event);
|
||||
|
||||
updateState('ntpSettings', (settings: NTPSettingsType) => ({
|
||||
...settings,
|
||||
tz_label: event.target.value,
|
||||
tz_format: TIME_ZONES[event.target.value]
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="enabled"
|
||||
checked={data.enabled}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.ENABLE_NTP()}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="server"
|
||||
label={LL.NTP_SERVER()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.server}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="tz_label"
|
||||
label={LL.TIME_ZONE()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedTimeZone(data.tz_label, data.tz_format)}
|
||||
onChange={changeTimeZone}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
|
||||
{timeZoneSelectItems()}
|
||||
</ValidatedTextField>
|
||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default NTPSettings;
|
||||
163
interface/src/app/settings/Settings.tsx
Normal file
163
interface/src/app/settings/Settings.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { type FC, useState } from 'react';
|
||||
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
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 * as SystemApi from 'api/system';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
const Settings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS(0));
|
||||
|
||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||
|
||||
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
const factoryReset = async () => {
|
||||
await factoryResetCommand();
|
||||
setConfirmFactoryReset(false);
|
||||
};
|
||||
|
||||
const renderFactoryResetDialog = () => (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
open={confirmFactoryReset}
|
||||
onClose={() => setConfirmFactoryReset(false)}
|
||||
>
|
||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(false)}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={factoryReset}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => (
|
||||
<>
|
||||
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
|
||||
<ListMenuItem
|
||||
icon={TuneIcon}
|
||||
bgcolor="#134ba2"
|
||||
label={LL.SETTINGS_OF(LL.APPLICATION())}
|
||||
text={LL.APPLICATION_SETTINGS_1()}
|
||||
to="application"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsEthernetIcon}
|
||||
bgcolor="#40828f"
|
||||
label={LL.SETTINGS_OF(LL.NETWORK(0))}
|
||||
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
|
||||
to="network"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={SettingsInputAntennaIcon}
|
||||
bgcolor="#5f9a5f"
|
||||
label={LL.SETTINGS_OF(LL.ACCESS_POINT(0))}
|
||||
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
|
||||
to="ap"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={AccessTimeIcon}
|
||||
bgcolor="#c5572c"
|
||||
label={LL.SETTINGS_OF('NTP')}
|
||||
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
|
||||
to="ntp"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={DeviceHubIcon}
|
||||
bgcolor="#68374d"
|
||||
label={LL.SETTINGS_OF('MQTT')}
|
||||
text={LL.CONFIGURE('MQTT')}
|
||||
to="mqtt"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={LockIcon}
|
||||
label={LL.SETTINGS_OF(LL.SECURITY(0))}
|
||||
text={LL.SECURITY_1()}
|
||||
to="security"
|
||||
/>
|
||||
|
||||
<ListMenuItem
|
||||
icon={ViewModuleIcon}
|
||||
bgcolor="#efc34b"
|
||||
label="Modules"
|
||||
text="Activate or deactivate external modules"
|
||||
to="modules"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListMenuItem
|
||||
icon={ImportExportIcon}
|
||||
bgcolor="#5d89f7"
|
||||
label={LL.UPLOAD_DOWNLOAD()}
|
||||
text={LL.UPLOAD_DOWNLOAD_1()}
|
||||
to="upload"
|
||||
/>
|
||||
</List>
|
||||
|
||||
{renderFactoryResetDialog()}
|
||||
|
||||
<Box mt={2} display="flex" flexWrap="wrap">
|
||||
<Button
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setConfirmFactoryReset(true)}
|
||||
color="error"
|
||||
>
|
||||
{LL.FACTORY_RESET()}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
return <SectionContent>{content()}</SectionContent>;
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
478
interface/src/app/settings/TZ.tsx
Normal file
478
interface/src/app/settings/TZ.tsx
Normal file
@@ -0,0 +1,478 @@
|
||||
import { MenuItem } from '@mui/material';
|
||||
|
||||
type TimeZones = Record<string, string>;
|
||||
|
||||
export const TIME_ZONES: TimeZones = {
|
||||
'Africa/Abidjan': 'GMT0',
|
||||
'Africa/Accra': 'GMT0',
|
||||
'Africa/Addis_Ababa': 'EAT-3',
|
||||
'Africa/Algiers': 'CET-1',
|
||||
'Africa/Asmara': 'EAT-3',
|
||||
'Africa/Bamako': 'GMT0',
|
||||
'Africa/Bangui': 'WAT-1',
|
||||
'Africa/Banjul': 'GMT0',
|
||||
'Africa/Bissau': 'GMT0',
|
||||
'Africa/Blantyre': 'CAT-2',
|
||||
'Africa/Brazzaville': 'WAT-1',
|
||||
'Africa/Bujumbura': 'CAT-2',
|
||||
'Africa/Cairo': 'EET-2',
|
||||
'Africa/Casablanca': 'UNK-1',
|
||||
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Africa/Conakry': 'GMT0',
|
||||
'Africa/Dakar': 'GMT0',
|
||||
'Africa/Dar_es_Salaam': 'EAT-3',
|
||||
'Africa/Djibouti': 'EAT-3',
|
||||
'Africa/Douala': 'WAT-1',
|
||||
'Africa/El_Aaiun': 'UNK-1',
|
||||
'Africa/Freetown': 'GMT0',
|
||||
'Africa/Gaborone': 'CAT-2',
|
||||
'Africa/Harare': 'CAT-2',
|
||||
'Africa/Johannesburg': 'SAST-2',
|
||||
'Africa/Juba': 'EAT-3',
|
||||
'Africa/Kampala': 'EAT-3',
|
||||
'Africa/Khartoum': 'CAT-2',
|
||||
'Africa/Kigali': 'CAT-2',
|
||||
'Africa/Kinshasa': 'WAT-1',
|
||||
'Africa/Lagos': 'WAT-1',
|
||||
'Africa/Libreville': 'WAT-1',
|
||||
'Africa/Lome': 'GMT0',
|
||||
'Africa/Luanda': 'WAT-1',
|
||||
'Africa/Lubumbashi': 'CAT-2',
|
||||
'Africa/Lusaka': 'CAT-2',
|
||||
'Africa/Malabo': 'WAT-1',
|
||||
'Africa/Maputo': 'CAT-2',
|
||||
'Africa/Maseru': 'SAST-2',
|
||||
'Africa/Mbabane': 'SAST-2',
|
||||
'Africa/Mogadishu': 'EAT-3',
|
||||
'Africa/Monrovia': 'GMT0',
|
||||
'Africa/Nairobi': 'EAT-3',
|
||||
'Africa/Ndjamena': 'WAT-1',
|
||||
'Africa/Niamey': 'WAT-1',
|
||||
'Africa/Nouakchott': 'GMT0',
|
||||
'Africa/Ouagadougou': 'GMT0',
|
||||
'Africa/Porto-Novo': 'WAT-1',
|
||||
'Africa/Sao_Tome': 'GMT0',
|
||||
'Africa/Tripoli': 'EET-2',
|
||||
'Africa/Tunis': 'CET-1',
|
||||
'Africa/Windhoek': 'CAT-2',
|
||||
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
|
||||
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Anguilla': 'AST4',
|
||||
'America/Antigua': 'AST4',
|
||||
'America/Araguaina': 'UNK3',
|
||||
'America/Argentina/Buenos_Aires': 'UNK3',
|
||||
'America/Argentina/Catamarca': 'UNK3',
|
||||
'America/Argentina/Cordoba': 'UNK3',
|
||||
'America/Argentina/Jujuy': 'UNK3',
|
||||
'America/Argentina/La_Rioja': 'UNK3',
|
||||
'America/Argentina/Mendoza': 'UNK3',
|
||||
'America/Argentina/Rio_Gallegos': 'UNK3',
|
||||
'America/Argentina/Salta': 'UNK3',
|
||||
'America/Argentina/San_Juan': 'UNK3',
|
||||
'America/Argentina/San_Luis': 'UNK3',
|
||||
'America/Argentina/Tucuman': 'UNK3',
|
||||
'America/Argentina/Ushuaia': 'UNK3',
|
||||
'America/Aruba': 'AST4',
|
||||
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
|
||||
'America/Atikokan': 'EST5',
|
||||
'America/Bahia': 'UNK3',
|
||||
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Barbados': 'AST4',
|
||||
'America/Belem': 'UNK3',
|
||||
'America/Belize': 'CST6',
|
||||
'America/Blanc-Sablon': 'AST4',
|
||||
'America/Boa_Vista': 'UNK4',
|
||||
'America/Bogota': 'UNK5',
|
||||
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Campo_Grande': 'UNK4',
|
||||
'America/Cancun': 'EST5',
|
||||
'America/Caracas': 'UNK4',
|
||||
'America/Cayenne': 'UNK3',
|
||||
'America/Cayman': 'EST5',
|
||||
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
|
||||
'America/Costa_Rica': 'CST6',
|
||||
'America/Creston': 'MST7',
|
||||
'America/Cuiaba': 'UNK4',
|
||||
'America/Curacao': 'AST4',
|
||||
'America/Danmarkshavn': 'GMT0',
|
||||
'America/Dawson': 'MST7',
|
||||
'America/Dawson_Creek': 'MST7',
|
||||
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Dominica': 'AST4',
|
||||
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Eirunepe': 'UNK5',
|
||||
'America/El_Salvador': 'CST6',
|
||||
'America/Fort_Nelson': 'MST7',
|
||||
'America/Fortaleza': 'UNK3',
|
||||
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
|
||||
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Grenada': 'AST4',
|
||||
'America/Guadeloupe': 'AST4',
|
||||
'America/Guatemala': 'CST6',
|
||||
'America/Guayaquil': 'UNK5',
|
||||
'America/Guyana': 'UNK4',
|
||||
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
|
||||
'America/Hermosillo': 'MST7',
|
||||
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Knox': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Petersburg': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Vevay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Winamac': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Jamaica': 'EST5',
|
||||
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Kentucky/Louisville': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Kralendijk': 'AST4',
|
||||
'America/La_Paz': 'UNK4',
|
||||
'America/Lima': 'UNK5',
|
||||
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Lower_Princes': 'AST4',
|
||||
'America/Maceio': 'UNK3',
|
||||
'America/Managua': 'CST6',
|
||||
'America/Manaus': 'UNK4',
|
||||
'America/Marigot': 'AST4',
|
||||
'America/Martinique': 'AST4',
|
||||
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
|
||||
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
|
||||
'America/Moncton': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Montevideo': 'UNK3',
|
||||
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Montserrat': 'AST4',
|
||||
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/New_York': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Noronha': 'UNK2',
|
||||
'America/North_Dakota/Beulah': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/North_Dakota/Center': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/North_Dakota/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Panama': 'EST5',
|
||||
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Paramaribo': 'UNK3',
|
||||
'America/Phoenix': 'MST7',
|
||||
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Port_of_Spain': 'AST4',
|
||||
'America/Porto_Velho': 'UNK4',
|
||||
'America/Puerto_Rico': 'AST4',
|
||||
'America/Punta_Arenas': 'UNK3',
|
||||
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Recife': 'UNK3',
|
||||
'America/Regina': 'CST6',
|
||||
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Rio_Branco': 'UNK5',
|
||||
'America/Santarem': 'UNK3',
|
||||
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
|
||||
'America/Santo_Domingo': 'AST4',
|
||||
'America/Sao_Paulo': 'UNK3',
|
||||
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/St_Barthelemy': 'AST4',
|
||||
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
|
||||
'America/St_Kitts': 'AST4',
|
||||
'America/St_Lucia': 'AST4',
|
||||
'America/St_Thomas': 'AST4',
|
||||
'America/St_Vincent': 'AST4',
|
||||
'America/Swift_Current': 'CST6',
|
||||
'America/Tegucigalpa': 'CST6',
|
||||
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Thunder_Bay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Tortola': 'AST4',
|
||||
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Whitehorse': 'MST7',
|
||||
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'Antarctica/Casey': 'UNK-8',
|
||||
'Antarctica/Davis': 'UNK-7',
|
||||
'Antarctica/DumontDUrville': 'UNK-10',
|
||||
'Antarctica/Macquarie': 'UNK-11',
|
||||
'Antarctica/Mawson': 'UNK-5',
|
||||
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||
'Antarctica/Palmer': 'UNK3',
|
||||
'Antarctica/Rothera': 'UNK3',
|
||||
'Antarctica/Syowa': 'UNK-3',
|
||||
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
|
||||
'Antarctica/Vostok': 'UNK-6',
|
||||
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Asia/Aden': 'UNK-3',
|
||||
'Asia/Almaty': 'UNK-6',
|
||||
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
|
||||
'Asia/Anadyr': 'UNK-12',
|
||||
'Asia/Aqtau': 'UNK-5',
|
||||
'Asia/Aqtobe': 'UNK-5',
|
||||
'Asia/Ashgabat': 'UNK-5',
|
||||
'Asia/Atyrau': 'UNK-5',
|
||||
'Asia/Baghdad': 'UNK-3',
|
||||
'Asia/Bahrain': 'UNK-3',
|
||||
'Asia/Baku': 'UNK-4',
|
||||
'Asia/Bangkok': 'UNK-7',
|
||||
'Asia/Barnaul': 'UNK-7',
|
||||
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
|
||||
'Asia/Bishkek': 'UNK-6',
|
||||
'Asia/Brunei': 'UNK-8',
|
||||
'Asia/Chita': 'UNK-9',
|
||||
'Asia/Choibalsan': 'UNK-8',
|
||||
'Asia/Colombo': 'UNK-5:30',
|
||||
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
|
||||
'Asia/Dhaka': 'UNK-6',
|
||||
'Asia/Dili': 'UNK-9',
|
||||
'Asia/Dubai': 'UNK-4',
|
||||
'Asia/Dushanbe': 'UNK-5',
|
||||
'Asia/Famagusta': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Asia/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||
'Asia/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||
'Asia/Ho_Chi_Minh': 'UNK-7',
|
||||
'Asia/Hong_Kong': 'HKT-8',
|
||||
'Asia/Hovd': 'UNK-7',
|
||||
'Asia/Irkutsk': 'UNK-8',
|
||||
'Asia/Jakarta': 'WIB-7',
|
||||
'Asia/Jayapura': 'WIT-9',
|
||||
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
|
||||
'Asia/Kabul': 'UNK-4:30',
|
||||
'Asia/Kamchatka': 'UNK-12',
|
||||
'Asia/Karachi': 'PKT-5',
|
||||
'Asia/Kathmandu': 'UNK-5:45',
|
||||
'Asia/Khandyga': 'UNK-9',
|
||||
'Asia/Kolkata': 'IST-5:30',
|
||||
'Asia/Krasnoyarsk': 'UNK-7',
|
||||
'Asia/Kuala_Lumpur': 'UNK-8',
|
||||
'Asia/Kuching': 'UNK-8',
|
||||
'Asia/Kuwait': 'UNK-3',
|
||||
'Asia/Macau': 'CST-8',
|
||||
'Asia/Magadan': 'UNK-11',
|
||||
'Asia/Makassar': 'WITA-8',
|
||||
'Asia/Manila': 'PST-8',
|
||||
'Asia/Muscat': 'UNK-4',
|
||||
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Asia/Novokuznetsk': 'UNK-7',
|
||||
'Asia/Novosibirsk': 'UNK-7',
|
||||
'Asia/Omsk': 'UNK-6',
|
||||
'Asia/Oral': 'UNK-5',
|
||||
'Asia/Phnom_Penh': 'UNK-7',
|
||||
'Asia/Pontianak': 'WIB-7',
|
||||
'Asia/Pyongyang': 'KST-9',
|
||||
'Asia/Qatar': 'UNK-3',
|
||||
'Asia/Qyzylorda': 'UNK-5',
|
||||
'Asia/Riyadh': 'UNK-3',
|
||||
'Asia/Sakhalin': 'UNK-11',
|
||||
'Asia/Samarkand': 'UNK-5',
|
||||
'Asia/Seoul': 'KST-9',
|
||||
'Asia/Shanghai': 'CST-8',
|
||||
'Asia/Singapore': 'UNK-8',
|
||||
'Asia/Srednekolymsk': 'UNK-11',
|
||||
'Asia/Taipei': 'CST-8',
|
||||
'Asia/Tashkent': 'UNK-5',
|
||||
'Asia/Tbilisi': 'UNK-4',
|
||||
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
|
||||
'Asia/Thimphu': 'UNK-6',
|
||||
'Asia/Tokyo': 'JST-9',
|
||||
'Asia/Tomsk': 'UNK-7',
|
||||
'Asia/Ulaanbaatar': 'UNK-8',
|
||||
'Asia/Urumqi': 'UNK-6',
|
||||
'Asia/Ust-Nera': 'UNK-10',
|
||||
'Asia/Vientiane': 'UNK-7',
|
||||
'Asia/Vladivostok': 'UNK-10',
|
||||
'Asia/Yakutsk': 'UNK-9',
|
||||
'Asia/Yangon': 'UNK-6:30',
|
||||
'Asia/Yekaterinburg': 'UNK-5',
|
||||
'Asia/Yerevan': 'UNK-4',
|
||||
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Cape_Verde': 'UNK1',
|
||||
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Reykjavik': 'GMT0',
|
||||
'Atlantic/South_Georgia': 'UNK2',
|
||||
'Atlantic/St_Helena': 'GMT0',
|
||||
'Atlantic/Stanley': 'UNK3',
|
||||
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Brisbane': 'AEST-10',
|
||||
'Australia/Broken_Hill': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Darwin': 'ACST-9:30',
|
||||
'Australia/Eucla': 'UNK-8:45',
|
||||
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Lindeman': 'AEST-10',
|
||||
'Australia/Lord_Howe': 'UNK-10:30UNK-11,M10.1.0,M4.1.0',
|
||||
'Australia/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Perth': 'AWST-8',
|
||||
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Etc/GMT': 'GMT0',
|
||||
'Etc/GMT+0': 'GMT0',
|
||||
'Etc/GMT+1': 'UNK1',
|
||||
'Etc/GMT+10': 'UNK10',
|
||||
'Etc/GMT+11': 'UNK11',
|
||||
'Etc/GMT+12': 'UNK12',
|
||||
'Etc/GMT+2': 'UNK2',
|
||||
'Etc/GMT+3': 'UNK3',
|
||||
'Etc/GMT+4': 'UNK4',
|
||||
'Etc/GMT+5': 'UNK5',
|
||||
'Etc/GMT+6': 'UNK6',
|
||||
'Etc/GMT+7': 'UNK7',
|
||||
'Etc/GMT+8': 'UNK8',
|
||||
'Etc/GMT+9': 'UNK9',
|
||||
'Etc/GMT-0': 'GMT0',
|
||||
'Etc/GMT-1': 'UNK-1',
|
||||
'Etc/GMT-10': 'UNK-10',
|
||||
'Etc/GMT-11': 'UNK-11',
|
||||
'Etc/GMT-12': 'UNK-12',
|
||||
'Etc/GMT-13': 'UNK-13',
|
||||
'Etc/GMT-14': 'UNK-14',
|
||||
'Etc/GMT-2': 'UNK-2',
|
||||
'Etc/GMT-3': 'UNK-3',
|
||||
'Etc/GMT-4': 'UNK-4',
|
||||
'Etc/GMT-5': 'UNK-5',
|
||||
'Etc/GMT-6': 'UNK-6',
|
||||
'Etc/GMT-7': 'UNK-7',
|
||||
'Etc/GMT-8': 'UNK-8',
|
||||
'Etc/GMT-9': 'UNK-9',
|
||||
'Etc/GMT0': 'GMT0',
|
||||
'Etc/Greenwich': 'GMT0',
|
||||
'Etc/UCT': 'UTC0',
|
||||
'Etc/UTC': 'UTC0',
|
||||
'Etc/Universal': 'UTC0',
|
||||
'Etc/Zulu': 'UTC0',
|
||||
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Andorra': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Astrakhan': 'UNK-4',
|
||||
'Europe/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Berlin': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Busingen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
|
||||
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Helsinki': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Istanbul': 'UNK-3',
|
||||
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Kaliningrad': 'EET-2',
|
||||
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Kirov': 'UNK-3',
|
||||
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Madrid': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Minsk': 'UNK-3',
|
||||
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Moscow': 'MSK-3',
|
||||
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Paris': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Samara': 'UNK-4',
|
||||
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Sarajevo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Saratov': 'UNK-4',
|
||||
'Europe/Simferopol': 'MSK-3',
|
||||
'Europe/Skopje': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Ulyanovsk': 'UNK-4',
|
||||
'Europe/Uzhgorod': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Vatican': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Volgograd': 'UNK-4',
|
||||
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Zagreb': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Indian/Antananarivo': 'EAT-3',
|
||||
'Indian/Chagos': 'UNK-6',
|
||||
'Indian/Christmas': 'UNK-7',
|
||||
'Indian/Cocos': 'UNK-6:30',
|
||||
'Indian/Comoro': 'EAT-3',
|
||||
'Indian/Kerguelen': 'UNK-5',
|
||||
'Indian/Mahe': 'UNK-4',
|
||||
'Indian/Maldives': 'UNK-5',
|
||||
'Indian/Mauritius': 'UNK-4',
|
||||
'Indian/Mayotte': 'EAT-3',
|
||||
'Indian/Reunion': 'UNK-4',
|
||||
'Pacific/Apia': 'UNK-13UNK,M9.5.0/3,M4.1.0/4',
|
||||
'Pacific/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||
'Pacific/Bougainville': 'UNK-11',
|
||||
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
|
||||
'Pacific/Chuuk': 'UNK-10',
|
||||
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
|
||||
'Pacific/Efate': 'UNK-11',
|
||||
'Pacific/Enderbury': 'UNK-13',
|
||||
'Pacific/Fakaofo': 'UNK-13',
|
||||
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
|
||||
'Pacific/Funafuti': 'UNK-12',
|
||||
'Pacific/Galapagos': 'UNK6',
|
||||
'Pacific/Gambier': 'UNK9',
|
||||
'Pacific/Guadalcanal': 'UNK-11',
|
||||
'Pacific/Guam': 'ChST-10',
|
||||
'Pacific/Honolulu': 'HST10',
|
||||
'Pacific/Kiritimati': 'UNK-14',
|
||||
'Pacific/Kosrae': 'UNK-11',
|
||||
'Pacific/Kwajalein': 'UNK-12',
|
||||
'Pacific/Majuro': 'UNK-12',
|
||||
'Pacific/Marquesas': 'UNK9:30',
|
||||
'Pacific/Midway': 'SST11',
|
||||
'Pacific/Nauru': 'UNK-12',
|
||||
'Pacific/Niue': 'UNK11',
|
||||
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
|
||||
'Pacific/Noumea': 'UNK-11',
|
||||
'Pacific/Pago_Pago': 'SST11',
|
||||
'Pacific/Palau': 'UNK-9',
|
||||
'Pacific/Pitcairn': 'UNK8',
|
||||
'Pacific/Pohnpei': 'UNK-11',
|
||||
'Pacific/Port_Moresby': 'UNK-10',
|
||||
'Pacific/Rarotonga': 'UNK10',
|
||||
'Pacific/Saipan': 'ChST-10',
|
||||
'Pacific/Tahiti': 'UNK10',
|
||||
'Pacific/Tarawa': 'UNK-12',
|
||||
'Pacific/Tongatapu': 'UNK-13',
|
||||
'Pacific/Wake': 'UNK-12',
|
||||
'Pacific/Wallis': 'UNK-12'
|
||||
};
|
||||
|
||||
export function selectedTimeZone(label: string, format: string) {
|
||||
return TIME_ZONES[label] === format ? label : undefined;
|
||||
}
|
||||
|
||||
export function timeZoneSelectItems() {
|
||||
return Object.keys(TIME_ZONES).map((label) => (
|
||||
<MenuItem key={label} value={label}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
367
interface/src/app/settings/UploadDownload.tsx
Normal file
367
interface/src/app/settings/UploadDownload.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import { Box, Button, Divider, Link, Typography } from '@mui/material';
|
||||
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import * as EMSESP from 'app/main/api';
|
||||
import { useRequest } from 'alova';
|
||||
import type { APIcall } from 'app/main/types';
|
||||
import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
SingleUpload,
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import RestartMonitor from '../status/RestartMonitor';
|
||||
|
||||
const UploadDownload: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const [restarting, setRestarting] = useState<boolean>();
|
||||
const [md5, setMd5] = useState<string>();
|
||||
|
||||
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(
|
||||
EMSESP.getSettings(),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } =
|
||||
useRequest(EMSESP.getCustomizations(), {
|
||||
immediate: false
|
||||
});
|
||||
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(
|
||||
EMSESP.getEntities(),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(
|
||||
EMSESP.getSchedule(),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
|
||||
(data: APIcall) => EMSESP.API(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useRequest(SystemApi.readESPSystemStatus, { force: true });
|
||||
|
||||
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
|
||||
immediate: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
|
||||
immediate: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||
|
||||
const STABLE_RELNOTES_URL =
|
||||
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
||||
const DEV_RELNOTES_URL =
|
||||
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
||||
|
||||
const getBinURL = (v: string) =>
|
||||
'EMS-ESP-' +
|
||||
v.replaceAll('.', '_') +
|
||||
'-' +
|
||||
getPlatform().replaceAll('-', '_') +
|
||||
'.bin';
|
||||
|
||||
const getPlatform = () => {
|
||||
if (data.flash_chip_size === 16384) {
|
||||
return data.esp_platform + '-16M';
|
||||
}
|
||||
return data.esp_platform;
|
||||
};
|
||||
|
||||
const {
|
||||
loading: isUploading,
|
||||
uploading: progress,
|
||||
send: sendUpload,
|
||||
onSuccess: onSuccessUpload,
|
||||
abort: cancelUpload
|
||||
} = useRequest(SystemApi.uploadFile, {
|
||||
immediate: false,
|
||||
force: true
|
||||
});
|
||||
|
||||
onSuccessUpload(({ data }) => {
|
||||
if (data) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
setMd5(data.md5);
|
||||
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
|
||||
} else {
|
||||
setRestarting(true);
|
||||
}
|
||||
});
|
||||
|
||||
const startUpload = async (files: File[]) => {
|
||||
await sendUpload(files[0]).catch((error: Error) => {
|
||||
if (error.message === 'The user aborted a request') {
|
||||
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
|
||||
} else if (error.message === 'Network Error') {
|
||||
toast.warning('Invalid file extension or incompatible bin file');
|
||||
} else {
|
||||
toast.error(error.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveFile = (json: unknown, endpoint: string) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
anchor.download = 'emsesp_' + endpoint;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(anchor.href);
|
||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||
};
|
||||
|
||||
onSuccessGetSettings((event) => {
|
||||
saveFile(event.data, 'settings.json');
|
||||
});
|
||||
onSuccessGetCustomizations((event) => {
|
||||
saveFile(event.data, 'customizations.json');
|
||||
});
|
||||
onSuccessGetEntities((event) => {
|
||||
saveFile(event.data, 'entities.json');
|
||||
});
|
||||
onSuccessGetSchedule((event) => {
|
||||
saveFile(event.data, 'schedule.json');
|
||||
});
|
||||
onGetAPI((event) => {
|
||||
saveFile(
|
||||
event.data,
|
||||
|
||||
event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'
|
||||
);
|
||||
});
|
||||
|
||||
const downloadSettings = async () => {
|
||||
await getSettings().catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadCustomizations = async () => {
|
||||
await getCustomizations().catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadEntities = async () => {
|
||||
await getEntities().catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadSchedule = async () => {
|
||||
await getSchedule().catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
const callAPI = async (device: string, entity: string) => {
|
||||
await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutTitle(LL.UPLOAD_DOWNLOAD());
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
||||
{LL.EMS_ESP_VER()}
|
||||
</Typography>
|
||||
<Box p={2} border="2px solid grey" borderRadius={2}>
|
||||
{LL.VERSION_ON() + ' '}
|
||||
<b>{data.emsesp_version}</b> ({getPlatform()})
|
||||
<Divider />
|
||||
{latestVersion && (
|
||||
<Box mt={2}>
|
||||
{LL.THE_LATEST()} {LL.OFFICIAL()} {LL.RELEASE_IS()}
|
||||
<b>{latestVersion}</b>
|
||||
(
|
||||
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link
|
||||
target="_blank"
|
||||
href={
|
||||
STABLE_URL +
|
||||
'v' +
|
||||
latestVersion +
|
||||
'/' +
|
||||
getBinURL(latestVersion as string)
|
||||
}
|
||||
color="primary"
|
||||
>
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
{latestDevVersion && (
|
||||
<Box mt={2}>
|
||||
{LL.THE_LATEST()} {LL.DEVELOPMENT()} {LL.RELEASE_IS()}
|
||||
|
||||
<b>{latestDevVersion}</b>
|
||||
(
|
||||
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
||||
{LL.RELEASE_NOTES()}
|
||||
</Link>
|
||||
) (
|
||||
<Link
|
||||
target="_blank"
|
||||
href={DEV_URL + getBinURL(latestDevVersion as string)}
|
||||
color="primary"
|
||||
>
|
||||
{LL.DOWNLOAD(1)}
|
||||
</Link>
|
||||
)
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.UPLOAD()}
|
||||
</Typography>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">
|
||||
{LL.UPLOAD_TEXT()}
|
||||
<br />
|
||||
<br />
|
||||
{LL.RESTART_TEXT(1)}.
|
||||
</Typography>
|
||||
</Box>
|
||||
{md5 && (
|
||||
<Box mb={2}>
|
||||
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<SingleUpload
|
||||
onDrop={startUpload}
|
||||
onCancel={cancelUpload}
|
||||
isUploading={isUploading}
|
||||
progress={progress}
|
||||
/>
|
||||
{!isUploading && (
|
||||
<>
|
||||
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
{LL.HELP_INFORMATION_4()}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('system', 'info')}
|
||||
>
|
||||
{LL.SUPPORT_INFORMATION(0)}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => callAPI('system', 'allvalues')}
|
||||
>
|
||||
All Values
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadSettings}
|
||||
>
|
||||
{LL.SETTINGS_OF('')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadCustomizations}
|
||||
>
|
||||
{LL.CUSTOMIZATIONS()}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadEntities}
|
||||
>
|
||||
{LL.CUSTOM_ENTITIES(0)}
|
||||
</Button>
|
||||
<Box color="warning.main">
|
||||
<Typography mt={2} mb={1} variant="body2">
|
||||
{LL.DOWNLOAD_SCHEDULE_TEXT()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={downloadSchedule}
|
||||
>
|
||||
{LL.SCHEDULE(0)}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadDownload;
|
||||
58
interface/src/app/settings/network/Network.tsx
Normal file
58
interface/src/app/settings/network/Network.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { WiFiNetwork } from 'types';
|
||||
|
||||
import NetworkSettings from './NetworkSettings';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
|
||||
const Network: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF(LL.NETWORK(0)));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||
|
||||
const selectNetwork = useCallback(
|
||||
(network: WiFiNetwork) => {
|
||||
setSelectedNetwork(network);
|
||||
navigate('settings');
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const deselectNetwork = useCallback(() => {
|
||||
setSelectedNetwork(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WiFiConnectionContext.Provider
|
||||
value={{
|
||||
selectedNetwork,
|
||||
selectNetwork,
|
||||
deselectNetwork
|
||||
}}
|
||||
>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} />
|
||||
<Tab value="scan" label={LL.NETWORK_SCAN()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="scan" element={<WiFiNetworkScanner />} />
|
||||
<Route path="settings" element={<NetworkSettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</WiFiConnectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Network;
|
||||
406
interface/src/app/settings/network/NetworkSettings.tsx
Normal file
406
interface/src/app/settings/network/NetworkSettings.tsx
Normal file
@@ -0,0 +1,406 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Checkbox,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import * as NetworkApi from 'api/network';
|
||||
import * as SystemApi from 'api/system';
|
||||
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { NetworkSettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
import { createNetworkSettingsValidator } from 'validators/network';
|
||||
|
||||
import RestartMonitor from '../../status/RestartMonitor';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||
|
||||
const NetworkSettings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage,
|
||||
restartNeeded
|
||||
} = useRest<NetworkSettingsType>({
|
||||
read: NetworkApi.readNetworkSettings,
|
||||
update: NetworkApi.updateNetworkSettings
|
||||
});
|
||||
|
||||
const { send: restartCommand } = useRequest(SystemApi.restart(), {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized && data) {
|
||||
if (selectedNetwork) {
|
||||
updateState('networkSettings', (current_data: NetworkSettingsType) => ({
|
||||
ssid: selectedNetwork.ssid,
|
||||
bssid: selectedNetwork.bssid,
|
||||
password: current_data ? current_data.password : '',
|
||||
hostname: current_data?.hostname,
|
||||
static_ip_config: false,
|
||||
bandwidth20: false,
|
||||
tx_power: 0,
|
||||
nosleep: false,
|
||||
enableMDNS: true,
|
||||
enableCORS: false,
|
||||
CORSOrigin: '*'
|
||||
}));
|
||||
}
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [initialized, setInitialized, data, selectedNetwork]);
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
|
||||
useEffect(() => deselectNetwork, [deselectNetwork]);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createNetworkSettingsValidator(data), data);
|
||||
await saveData();
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
deselectNetwork();
|
||||
};
|
||||
|
||||
const setCancel = async () => {
|
||||
deselectNetwork();
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
await restartCommand().catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
setRestarting(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6" color="primary">
|
||||
WiFi
|
||||
</Typography>
|
||||
{selectedNetwork ? (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={selectedNetwork.ssid}
|
||||
secondary={
|
||||
'Security: ' +
|
||||
networkSecurityMode(selectedNetwork) +
|
||||
', Ch: ' +
|
||||
selectedNetwork.channel +
|
||||
', bssid: ' +
|
||||
selectedNetwork.bssid
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton onClick={setCancel}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</List>
|
||||
) : (
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="ssid"
|
||||
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.ssid}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="bssid"
|
||||
label={'BSSID (' + LL.NETWORK_BLANK_BSSID() + ')'}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.bssid}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
name="tx_power"
|
||||
label={LL.TX_POWER()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.tx_power}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>Auto</MenuItem>
|
||||
<MenuItem value={78}>19.5 dBm</MenuItem>
|
||||
<MenuItem value={76}>19 dBm</MenuItem>
|
||||
<MenuItem value={74}>18.5 dBm</MenuItem>
|
||||
<MenuItem value={68}>17 dBm</MenuItem>
|
||||
<MenuItem value={60}>15 dBm</MenuItem>
|
||||
<MenuItem value={52}>13 dBm</MenuItem>
|
||||
<MenuItem value={44}>11 dBm</MenuItem>
|
||||
<MenuItem value={34}>8.5 dBm</MenuItem>
|
||||
<MenuItem value={28}>7 dBm</MenuItem>
|
||||
<MenuItem value={20}>5 dBm</MenuItem>
|
||||
<MenuItem value={8}>2 dBm</MenuItem>
|
||||
</TextField>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="nosleep"
|
||||
checked={data.nosleep}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_DISABLE_SLEEP()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="bandwidth20"
|
||||
checked={data.bandwidth20}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_LOW_BAND()}
|
||||
/>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
{LL.GENERAL_OPTIONS()}
|
||||
</Typography>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="hostname"
|
||||
label={LL.HOSTNAME()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.hostname}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="enableMDNS"
|
||||
checked={data.enableMDNS}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_USE_DNS()}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="enableCORS"
|
||||
checked={data.enableCORS}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_ENABLE_CORS()}
|
||||
/>
|
||||
{data.enableCORS && (
|
||||
<TextField
|
||||
name="CORSOrigin"
|
||||
label={LL.NETWORK_CORS_ORIGIN()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.CORSOrigin}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
)}
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="static_ip_config"
|
||||
checked={data.static_ip_config}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.NETWORK_FIXED_IP()}
|
||||
/>
|
||||
{data.static_ip_config && (
|
||||
<>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="local_ip"
|
||||
label={LL.AP_LOCAL_IP()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.local_ip}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="gateway_ip"
|
||||
label={LL.NETWORK_GATEWAY()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.gateway_ip}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="subnet_mask"
|
||||
label={LL.NETWORK_SUBNET()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.subnet_mask}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="dns_ip_1"
|
||||
label="DNS #1"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.dns_ip_1}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="dns_ip_2"
|
||||
label="DNS #2"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.dns_ip_2}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||
<Button
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={restart}
|
||||
>
|
||||
{LL.RESTART()}
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
|
||||
{!restartNeeded &&
|
||||
(selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkSettings;
|
||||
14
interface/src/app/settings/network/WiFiConnectionContext.tsx
Normal file
14
interface/src/app/settings/network/WiFiConnectionContext.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { WiFiNetwork } from 'types';
|
||||
|
||||
export interface WiFiConnectionContextValue {
|
||||
selectedNetwork?: WiFiNetwork;
|
||||
selectNetwork: (network: WiFiNetwork) => void;
|
||||
deselectNetwork: () => void;
|
||||
}
|
||||
|
||||
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
|
||||
export const WiFiConnectionContext = createContext(
|
||||
WiFiConnectionContextDefaultValue
|
||||
);
|
||||
81
interface/src/app/settings/network/WiFiNetworkScanner.tsx
Normal file
81
interface/src/app/settings/network/WiFiNetworkScanner.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
import { Button } from '@mui/material';
|
||||
|
||||
import * as NetworkApi from 'api/network';
|
||||
|
||||
import { updateState, useRequest } from 'alova';
|
||||
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||
|
||||
const NUM_POLLS = 10;
|
||||
const POLLING_FREQUENCY = 1000;
|
||||
|
||||
const WiFiNetworkScanner: FC = () => {
|
||||
const pollCount = useRef(0);
|
||||
const { LL } = useI18nContext();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(
|
||||
NetworkApi.scanNetworks
|
||||
); // is called on page load to start network scan
|
||||
const {
|
||||
data: networkList,
|
||||
send: getNetworkList,
|
||||
onSuccess: onSuccessNetworkList
|
||||
} = useRequest(NetworkApi.listNetworks, {
|
||||
immediate: false
|
||||
});
|
||||
|
||||
onSuccessNetworkList((event) => {
|
||||
if (!event.data) {
|
||||
const completedPollCount = pollCount.current + 1;
|
||||
if (completedPollCount < NUM_POLLS) {
|
||||
pollCount.current = completedPollCount;
|
||||
setTimeout(getNetworkList, POLLING_FREQUENCY);
|
||||
} else {
|
||||
setErrorMessage(LL.PROBLEM_LOADING());
|
||||
pollCount.current = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onCompleteScanNetworks(() => {
|
||||
pollCount.current = 0;
|
||||
setErrorMessage(undefined);
|
||||
updateState('listNetworks', () => undefined);
|
||||
void getNetworkList();
|
||||
});
|
||||
|
||||
const renderNetworkScanner = () => {
|
||||
if (!networkList) {
|
||||
return (
|
||||
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
|
||||
);
|
||||
}
|
||||
return <WiFiNetworkSelector networkList={networkList} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{renderNetworkScanner()}
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<PermScanWifiIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={scanNetworks}
|
||||
disabled={!errorMessage && !networkList}
|
||||
>
|
||||
{LL.SCAN_AGAIN()}…
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default WiFiNetworkScanner;
|
||||
105
interface/src/app/settings/network/WiFiNetworkSelector.tsx
Normal file
105
interface/src/app/settings/network/WiFiNetworkSelector.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useContext } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material';
|
||||
|
||||
import { MessageBox } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { WiFiNetwork, WiFiNetworkList } from 'types';
|
||||
import { WiFiEncryptionType } from 'types';
|
||||
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
|
||||
interface WiFiNetworkSelectorProps {
|
||||
networkList: WiFiNetworkList;
|
||||
}
|
||||
|
||||
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
|
||||
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
||||
|
||||
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
||||
switch (encryption_type) {
|
||||
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
||||
return 'WEP';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
||||
return 'WPA';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
||||
return 'WPA2';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
||||
return 'WPA/WPA2';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
||||
return 'WPA2 Enterprise';
|
||||
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
||||
return 'None';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA3_PSK:
|
||||
return 'WPA3';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_WPA3_PSK:
|
||||
return 'WPA2/WPA3';
|
||||
default:
|
||||
return 'Unknown: ' + String(encryption_type);
|
||||
}
|
||||
};
|
||||
|
||||
const networkQualityHighlight = ({ rssi }: WiFiNetwork, theme: Theme) => {
|
||||
if (rssi <= -85) {
|
||||
return theme.palette.error.main;
|
||||
} else if (rssi <= -75) {
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
return theme.palette.success.main;
|
||||
};
|
||||
|
||||
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
|
||||
const { LL } = useI18nContext();
|
||||
const theme = useTheme();
|
||||
|
||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||
|
||||
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) {
|
||||
return <MessageBox mt={2} mb={1} message={LL.NETWORK_NO_WIFI()} level="info" />;
|
||||
}
|
||||
|
||||
return <List>{networkList.networks.map(renderNetwork)}</List>;
|
||||
};
|
||||
|
||||
export default WiFiNetworkSelector;
|
||||
90
interface/src/app/settings/security/GenerateToken.tsx
Normal file
90
interface/src/app/settings/security/GenerateToken.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
LinearProgress,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
import * as SecurityApi from 'api/security';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import { useRequest } from 'alova';
|
||||
import { MessageBox } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
interface GenerateTokenProps {
|
||||
username?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
||||
const { LL } = useI18nContext();
|
||||
const open = !!username;
|
||||
|
||||
const { data: token, send: generateToken } = useRequest(
|
||||
SecurityApi.generateToken(username),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
void generateToken();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
onClose={onClose}
|
||||
open={!!username}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle>{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{token ? (
|
||||
<>
|
||||
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
|
||||
<Box mt={2} mb={2}>
|
||||
<TextField
|
||||
label="Token"
|
||||
multiline
|
||||
value={token.token}
|
||||
fullWidth
|
||||
contentEditable={false}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box m={4} textAlign="center">
|
||||
<LinearProgress />
|
||||
<Typography variant="h6">{LL.GENERATING_TOKEN()}…</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CloseIcon />}
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CLOSE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateToken;
|
||||
286
interface/src/app/settings/security/ManageUsers.tsx
Normal file
286
interface/src/app/settings/security/ManageUsers.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Box, Button, IconButton } from '@mui/material';
|
||||
|
||||
import * as SecurityApi from 'api/security';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Cell,
|
||||
Header,
|
||||
HeaderCell,
|
||||
HeaderRow,
|
||||
Row,
|
||||
Table
|
||||
} from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SecuritySettingsType, UserType } from 'types';
|
||||
import { useRest } from 'utils';
|
||||
import { createUserValidator } from 'validators';
|
||||
|
||||
import GenerateToken from './GenerateToken';
|
||||
import User from './User';
|
||||
|
||||
const ManageUsers: FC = () => {
|
||||
const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
|
||||
useRest<SecuritySettingsType>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
|
||||
const [user, setUser] = useState<UserType>();
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
const [changed, setChanged] = useState<number>(0);
|
||||
const [generatingToken, setGeneratingToken] = useState<string>();
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
const blocker = useBlocker(changed !== 0);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const table_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
.th {
|
||||
padding: 8px;
|
||||
height: 36px;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
&:nth-of-type(even) .td {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(2) {
|
||||
text-align: center;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const noAdminConfigured = () => !data.users.find((u) => u.admin);
|
||||
|
||||
const removeUser = (toRemove: UserType) => {
|
||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||
updateDataValue({ ...data, users });
|
||||
setChanged(changed + 1);
|
||||
};
|
||||
|
||||
const createUser = () => {
|
||||
setCreating(true);
|
||||
setUser({
|
||||
username: '',
|
||||
password: '',
|
||||
admin: true
|
||||
});
|
||||
};
|
||||
|
||||
const editUser = (toEdit: UserType) => {
|
||||
setCreating(false);
|
||||
setUser({ ...toEdit });
|
||||
};
|
||||
|
||||
const cancelEditingUser = () => {
|
||||
setUser(undefined);
|
||||
};
|
||||
|
||||
const doneEditingUser = () => {
|
||||
if (user) {
|
||||
const users = [
|
||||
...data.users.filter(
|
||||
(u: { username: string }) => u.username !== user.username
|
||||
),
|
||||
user
|
||||
];
|
||||
updateDataValue({ ...data, users });
|
||||
setUser(undefined);
|
||||
setChanged(changed + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const closeGenerateToken = () => {
|
||||
setGeneratingToken(undefined);
|
||||
};
|
||||
|
||||
const generateToken = (username: string) => {
|
||||
setGeneratingToken(username);
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
await saveData();
|
||||
await authenticatedContext.refresh();
|
||||
setChanged(0);
|
||||
};
|
||||
|
||||
const onCancelSubmit = async () => {
|
||||
await loadData();
|
||||
setChanged(0);
|
||||
};
|
||||
|
||||
interface UserType2 {
|
||||
id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
// add id to the type, needed for the table
|
||||
const user_table = data.users.map((u) => ({
|
||||
...u,
|
||||
id: u.username
|
||||
})) as UserType2[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
data={{ nodes: user_table }}
|
||||
theme={table_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: UserType2[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>{LL.USERNAME(1)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.IS_ADMIN(0)}</HeaderCell>
|
||||
<HeaderCell stiff />
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.map((u: UserType2) => (
|
||||
<Row key={u.id} item={u}>
|
||||
<Cell>{u.username}</Cell>
|
||||
<Cell stiff>{u.admin ? <CheckIcon /> : <CloseIcon />}</Cell>
|
||||
<Cell stiff>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
onClick={() => generateToken(u.username)}
|
||||
>
|
||||
<VpnKeyIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => removeUser(u)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={() => editUser(u)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
|
||||
{noAdminConfigured() && (
|
||||
<MessageBox level="warning" message={LL.USER_WARNING()} my={2} />
|
||||
)}
|
||||
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
{changed !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={onCancelSubmit}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving || noAdminConfigured()}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(changed)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<PersonAddIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={createUser}
|
||||
>
|
||||
{LL.ADD(0)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<GenerateToken username={generatingToken} onClose={closeGenerateToken} />
|
||||
<User
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
creating={creating}
|
||||
onDoneEditing={doneEditingUser}
|
||||
onCancelEditing={cancelEditingUser}
|
||||
validator={createUserValidator(data.users, creating)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageUsers;
|
||||
33
interface/src/app/settings/security/Security.tsx
Normal file
33
interface/src/app/settings/security/Security.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { FC } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import ManageUsers from './ManageUsers';
|
||||
import SecuritySettings from './SecuritySettings';
|
||||
|
||||
const Security: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SETTINGS_OF(LL.SECURITY(0)));
|
||||
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
|
||||
<Tab value="users" label={LL.MANAGE_USERS()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="users" element={<ManageUsers />} />
|
||||
<Route path="settings" element={<SecuritySettings />} />
|
||||
<Route path="*" element={<Navigate replace to="settings" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Security;
|
||||
119
interface/src/app/settings/security/SecuritySettings.tsx
Normal file
119
interface/src/app/settings/security/SecuritySettings.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { Button } from '@mui/material';
|
||||
|
||||
import * as SecurityApi from 'api/security';
|
||||
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockNavigation,
|
||||
ButtonRow,
|
||||
FormLoader,
|
||||
MessageBox,
|
||||
SectionContent,
|
||||
ValidatedPasswordField
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { SecuritySettingsType } from 'types';
|
||||
import { updateValueDirty, useRest } from 'utils';
|
||||
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
|
||||
|
||||
const SecuritySettings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const {
|
||||
loadData,
|
||||
saving,
|
||||
data,
|
||||
updateDataValue,
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage
|
||||
} = useRest<SecuritySettingsType>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
});
|
||||
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
const updateFormValue = updateValueDirty(
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
updateDataValue
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(SECURITY_SETTINGS_VALIDATOR, data);
|
||||
await saveData();
|
||||
await authenticatedContext.refresh();
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="jwt_secret"
|
||||
label={LL.SU_PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.jwt_secret}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
|
||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={loadData}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
disabled={saving}
|
||||
variant="contained"
|
||||
color="info"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySettings;
|
||||
142
interface/src/app/settings/security/User.tsx
Normal file
142
interface/src/app/settings/security/User.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle
|
||||
} from '@mui/material';
|
||||
|
||||
import { dialogStyle } from 'CustomTheme';
|
||||
import type Schema from 'async-validator';
|
||||
import type { ValidateFieldsError } from 'async-validator';
|
||||
import {
|
||||
BlockFormControlLabel,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { UserType } from 'types';
|
||||
import { updateValue } from 'utils';
|
||||
import { validate } from 'validators';
|
||||
|
||||
interface UserFormProps {
|
||||
creating: boolean;
|
||||
validator: Schema;
|
||||
|
||||
user?: UserType;
|
||||
setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>;
|
||||
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
}
|
||||
|
||||
const User: FC<UserFormProps> = ({
|
||||
creating,
|
||||
validator,
|
||||
user,
|
||||
setUser,
|
||||
onDoneEditing,
|
||||
onCancelEditing
|
||||
}) => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const updateFormValue = updateValue(setUser);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const open = !!user;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setFieldErrors(undefined);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const validateAndDone = async () => {
|
||||
if (user) {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, user);
|
||||
onDoneEditing();
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
sx={dialogStyle}
|
||||
onClose={onCancelEditing}
|
||||
open={!!user}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
{user && (
|
||||
<>
|
||||
<DialogTitle id="user-form-dialog-title">
|
||||
{creating ? LL.ADD(1) : LL.MODIFY()} {LL.USER(1)}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="username"
|
||||
label={LL.USERNAME(1)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={user.username}
|
||||
disabled={!creating}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<ValidatedPasswordField
|
||||
fieldErrors={fieldErrors}
|
||||
name="password"
|
||||
label={LL.PASSWORD()}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={user.password}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="admin"
|
||||
checked={user.admin}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
}
|
||||
label={LL.IS_ADMIN(1)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={onCancelEditing}
|
||||
color="secondary"
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={creating ? <PersonAddIcon /> : <SaveIcon />}
|
||||
variant="outlined"
|
||||
onClick={validateAndDone}
|
||||
color="primary"
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
||||
Reference in New Issue
Block a user