Multi-language/I18n support #22

This commit is contained in:
Proddy
2022-08-24 21:50:19 +02:00
parent 763337db3f
commit 1a4ce643fc
84 changed files with 5506 additions and 4196 deletions

View File

@@ -1,4 +1,4 @@
import { FC, createRef, createContext, useContext, RefObject } from 'react';
import { FC, createRef, createContext, useContext, useEffect, useState, RefObject } from 'react';
import { SnackbarProvider } from 'notistack';
import { IconButton } from '@mui/material';
@@ -9,6 +9,13 @@ import { FeaturesLoader } from './contexts/features';
import CustomTheme from './CustomTheme';
import AppRouting from './AppRouting';
import { localStorageDetector } from 'typesafe-i18n/detectors';
import TypesafeI18n from './i18n/i18n-react';
import { detectLocale } from './i18n/i18n-util';
import { loadLocaleAsync } from './i18n/i18n-util.async';
const detectedLocale = detectLocale(localStorageDetector);
const App: FC = () => {
const notistackRef: RefObject<any> = createRef();
@@ -20,24 +27,34 @@ const App: FC = () => {
const colorMode = useContext(ColorModeContext);
const [wasLoaded, setWasLoaded] = useState(false);
useEffect(() => {
loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
}, []);
if (!wasLoaded) return null;
return (
<ColorModeContext.Provider value={colorMode}>
<CustomTheme>
<SnackbarProvider
maxSnack={3}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} size="small">
<CloseIcon />
</IconButton>
)}
>
<FeaturesLoader>
<AppRouting />
</FeaturesLoader>
</SnackbarProvider>
</CustomTheme>
<TypesafeI18n locale={detectedLocale}>
<CustomTheme>
<SnackbarProvider
maxSnack={3}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} size="small">
<CloseIcon />
</IconButton>
)}
>
<FeaturesLoader>
<AppRouting />
</FeaturesLoader>
</SnackbarProvider>
</CustomTheme>
</TypesafeI18n>
</ColorModeContext.Provider>
);
};

View File

@@ -2,6 +2,8 @@ import { FC, useContext, useEffect } from 'react';
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
import { useSnackbar, VariantType } from 'notistack';
import { useI18nContext } from './i18n/i18n-react';
import { Authentication, AuthenticationContext } from './contexts/authentication';
import { FeaturesContext } from './contexts/features';
import { RequireAuthenticated, RequireUnauthenticated } from './components';
@@ -41,13 +43,14 @@ export const RemoveTrailingSlashes = () => {
const AppRouting: FC = () => {
const { features } = useContext(FeaturesContext);
const { LL } = useI18nContext();
return (
<Authentication>
<RemoveTrailingSlashes />
<Routes>
<Route path="/unauthorized" element={<RootRedirect message="Please sign in to continue" signOut />} />
<Route path="/fileUpdated" element={<RootRedirect message="Upload successful" variant="success" />} />
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
{features.security && (
<Route
path="/"

View File

@@ -1,8 +1,8 @@
import { FC, useContext, useState } from 'react';
import { FC, useContext, useState, ChangeEventHandler } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { useSnackbar } from 'notistack';
import { Box, Fab, Paper, Typography } from '@mui/material';
import { Box, Fab, Paper, Typography, MenuItem } from '@mui/material';
import ForwardIcon from '@mui/icons-material/Forward';
import * as AuthenticationApi from './api/authentication';
@@ -16,6 +16,11 @@ import { SignInRequest } from './types';
import { ValidatedTextField } from './components';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from './validators';
import { I18nContext } from './i18n/i18n-react';
import type { Locales } from './i18n/i18n-types';
import { locales } from './i18n/i18n-util';
import { loadLocaleAsync } from './i18n/i18n-util.async';
const SignIn: FC = () => {
const authenticationContext = useContext(AuthenticationContext);
const { enqueueSnackbar } = useSnackbar();
@@ -31,6 +36,9 @@ const SignIn: FC = () => {
const validateAndSignIn = async () => {
setProcessing(true);
SIGN_IN_REQUEST_VALIDATOR.messages({
required: '%s ' + LL.IS_REQUIRED()
});
try {
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
signIn();
@@ -47,7 +55,7 @@ const SignIn: FC = () => {
} catch (error: unknown) {
if (error instanceof AxiosError) {
if (error.response?.status === 401) {
enqueueSnackbar('Invalid login details', { variant: 'warning' });
enqueueSnackbar(LL.INVALID_LOGIN(), { variant: 'warning' });
}
} else {
enqueueSnackbar(extractErrorMessage(error, 'Unexpected error, please try again'), { variant: 'error' });
@@ -58,6 +66,15 @@ const SignIn: FC = () => {
const submitOnEnter = onEnterCallback(signIn);
const { locale, LL, setLocale } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
return (
<Box
display="flex"
@@ -81,11 +98,33 @@ const SignIn: FC = () => {
})}
>
<Typography variant="h4">{PROJECT_NAME}</Typography>
<Box
sx={{
'& .MuiTextField-root': { m: 2, width: '15ch' }
}}
>
<ValidatedTextField
name="locale"
label={LL.LANGUAGE()}
variant="outlined"
value={locale || ''}
onChange={onLocaleSelected}
margin="normal"
size="small"
select
>
{locales.map((loc) => (
<MenuItem key={loc} value={loc}>
{loc}
</MenuItem>
))}
</ValidatedTextField>
</Box>
<ValidatedTextField
fieldErrors={fieldErrors}
disabled={processing}
name="username"
label="Username"
label={LL.USERNAME()}
value={signInRequest.username}
onChange={updateLoginRequestValue}
margin="normal"
@@ -97,7 +136,7 @@ const SignIn: FC = () => {
disabled={processing}
type="password"
name="password"
label="Password"
label={LL.PASSWORD()}
value={signInRequest.password}
onChange={updateLoginRequestValue}
onKeyDown={submitOnEnter}
@@ -107,7 +146,7 @@ const SignIn: FC = () => {
/>
<Fab variant="extended" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
<ForwardIcon sx={{ mr: 1 }} />
Sign In
{LL.SIGN_IN()}
</Fab>
</Paper>
</Box>

View File

@@ -15,9 +15,12 @@ import ProjectMenu from '../../project/ProjectMenu';
import LayoutMenuItem from './LayoutMenuItem';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
const LayoutMenu: FC = () => {
const { features } = useContext(FeaturesContext);
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
return (
<>
@@ -28,11 +31,11 @@ const LayoutMenu: FC = () => {
</List>
)}
<List disablePadding component="nav">
<LayoutMenuItem icon={SettingsEthernetIcon} label="Network Connection" to="/network" />
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK_CONNECTION()} to="/network" />
<LayoutMenuItem icon={SettingsInputAntennaIcon} label="Access Point" to="/ap" />
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="Network Time" to="/ntp" />}
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label={LL.NETWORK_TIME()} to="/ntp" />}
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
<LayoutMenuItem icon={LockIcon} label="Security" to="/security" disabled={!authenticatedContext.me.admin} />
<LayoutMenuItem icon={LockIcon} label={LL.SECURITY()} to="/security" disabled={!authenticatedContext.me.admin} />
<LayoutMenuItem icon={SettingsIcon} label="System" to="/system" />
</List>
</>

View File

@@ -2,6 +2,8 @@ import { FC, useCallback, useContext, useEffect, useState } from 'react';
import { useSnackbar } from 'notistack';
import { useNavigate } from 'react-router-dom';
import { useI18nContext } from '../../i18n/i18n-react';
import * as AuthenticationApi from '../../api/authentication';
import { ACCESS_TOKEN } from '../../api/endpoints';
import { RequiredChildrenProps } from '../../utils';
@@ -12,6 +14,8 @@ import { AuthenticationContext } from './context';
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const { features } = useContext(FeaturesContext);
const { LL } = useI18nContext();
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
@@ -23,7 +27,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
setMe(decodedMe);
enqueueSnackbar(`Logged in as ${decodedMe.username}`, { variant: 'success' });
enqueueSnackbar(LL.LOGGED_IN({ name: decodedMe.username }), { variant: 'success' });
} catch (error: unknown) {
setMe(undefined);
throw new Error('Failed to parse JWT');

View File

@@ -1,7 +1,7 @@
import { FC, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { Button, Checkbox, MenuItem, Grid, Typography } from '@mui/material';
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import { MQTT_SETTINGS_VALIDATOR, validate } from '../../validators';
@@ -129,7 +129,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="keep_alive"
label="Keep Alive (seconds)"
label="Keep Alive"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.keep_alive)}
@@ -149,7 +152,7 @@ const MqttSettingsForm: FC = () => {
margin="normal"
select
>
<MenuItem value={0}>0 (default)</MenuItem>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
</ValidatedTextField>
@@ -227,7 +230,7 @@ const MqttSettingsForm: FC = () => {
</Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Publish Intervals (in seconds, 0=automatic)
Publish Intervals (0=auto)
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={4}>
@@ -235,6 +238,9 @@ const MqttSettingsForm: FC = () => {
fieldErrors={fieldErrors}
name="publish_time_boiler"
label="Boilers and Heat Pumps"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_boiler)}
@@ -248,6 +254,9 @@ const MqttSettingsForm: FC = () => {
fieldErrors={fieldErrors}
name="publish_time_thermostat"
label="Thermostats"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_thermostat)}
@@ -261,6 +270,9 @@ const MqttSettingsForm: FC = () => {
fieldErrors={fieldErrors}
name="publish_time_solar"
label="Solar Modules"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_solar)}
@@ -274,6 +286,9 @@ const MqttSettingsForm: FC = () => {
fieldErrors={fieldErrors}
name="publish_time_mixer"
label="Mixer Modules"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_mixer)}
@@ -287,6 +302,9 @@ const MqttSettingsForm: FC = () => {
fieldErrors={fieldErrors}
name="publish_time_sensor"
label="Temperature Sensors"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_sensor)}
@@ -299,6 +317,9 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_other"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
label="Default"
fullWidth
variant="outlined"

View File

@@ -11,12 +11,16 @@ import NetworkStatusForm from './NetworkStatusForm';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import NetworkSettingsForm from './NetworkSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const NetworkConnection: FC = () => {
useLayoutTitle('Network Connection');
const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK_CONNECTION());
const { routerTab } = useRouterTab();
const authenticatedContext = useContext(AuthenticatedContext);
const navigate = useNavigate();
const { routerTab } = useRouterTab();
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();

View File

@@ -10,7 +10,8 @@ import {
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
Typography
Typography,
InputAdornment
} from '@mui/material';
import LockOpenIcon from '@mui/icons-material/LockOpen';
@@ -135,7 +136,10 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="tx_power"
label="WiFi Tx Power (dBm)"
label="WiFi Tx Power"
InputProps={{
endAdornment: <InputAdornment position="end">dBm</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.tx_power)}

View File

@@ -9,8 +9,11 @@ import { AuthenticatedContext } from '../../contexts/authentication';
import NTPStatusForm from './NTPStatusForm';
import NTPSettingsForm from './NTPSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const NetworkTime: FC = () => {
useLayoutTitle('Network Time');
const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK_TIME());
const authenticatedContext = useContext(AuthenticatedContext);
const { routerTab } = useRouterTab();

View File

@@ -8,8 +8,11 @@ import { RouterTabs, useRouterTab, useLayoutTitle } from '../../components';
import SecuritySettingsForm from './SecuritySettingsForm';
import ManageUsersForm from './ManageUsersForm';
import { useI18nContext } from '../../i18n/i18n-react';
const Security: FC = () => {
useLayoutTitle('Security');
const { LL } = useI18nContext();
useLayoutTitle(LL.SECURITY());
const { routerTab } = useRouterTab();

View File

@@ -72,7 +72,7 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
{!uploading && (
<Box mb={2} color="warning.main">
<Typography variant="body2">
Upload a new firmware (.bin) file, settings or customizations (.json) file below.
Upload a new firmware (.bin) file, settings or customizations (.json) file below
</Typography>
</Box>
)}
@@ -86,7 +86,7 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
<Box color="warning.main">
<Typography mb={1} variant="body2">
Download the application settings. Be careful when sharing your settings as this file contains passwords
and other sensitive system information.
and other sensitive system information
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => downloadSettings()}>
@@ -95,7 +95,7 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
Download the entity customizations.
Download the entity customizations
</Typography>
</Box>
<Button

View File

@@ -0,0 +1,49 @@
import type { Translation } from '../i18n-types';
const de: Translation = {
LANGUAGE: 'Sprache',
IS_REQUIRED: 'ist nötig',
SIGN_IN: 'Einloggen',
USERNAME: 'Nutzername',
PASSWORD: 'Passwort',
DASHBOARD: 'Armaturenbrett',
SETTINGS: 'Einstellungen',
HELP: 'Hilfe',
LOGGED_IN: 'Eingeloggt als {name}',
PLEASE_SIGNIN: 'Bitte einloggen, um fortzufahren',
UPLOAD_SUCCESSFUL: 'Hochladen erfolgreich',
INVALID_LOGIN: 'Ungültige Login Daten',
NETWORK_CONNECTION: 'Netzwerkverbindung',
SECURITY: 'Sicherheit',
NETWORK_TIME: 'Netzwerkzeit',
ONOFF_CAP: 'AN/AUS',
ONOFF: 'an/aus',
TYPE: 'Typ',
DESCRIPTION: 'Bezeichnung',
ENTITIES: 'Entitäten',
REFRESH: 'Aktualisierung',
EXPORT: 'Export',
ENTITY_NAME: 'Entitätsname',
VALUE: 'Wert',
SHOW_FAV: 'nur Favoriten anzeigen',
DEVICE_SENSOR_DATA: 'Device und Sensordaten',
DEVICES_SENSORS: 'Devices & Sensoren',
ATTACHED_SENSORS: 'Angeschlossene EMS-ESP Sensoren',
RUN_COMMAND: 'Befehl ausführen',
CHANGE_VALUE: 'Wert ändern',
CANCEL: 'Absagen',
RESET: 'Zurücksetzen',
SEND: 'Senden',
SAVE: 'Speichern',
REMOVE: 'Entfernen',
PROBLEM_UPDATING: 'Problem beim Aktualisieren',
ACCESS_DENIED: 'Zugriff abgelehnt',
ANALOG_SENSOR: 'Analoger Sensor {cmd}',
TEMP_SENSOR: 'Temperatursensor {cmd}',
WRITE_COMMAND: 'Befehl schreiben {cmd}',
EMS_BUS_WARNING:
'EMS-Bus getrennt. Wenn diese Warnung nach einigen Sekunden immer noch besteht, überprüfen Sie bitte die Einstellungen und das Board-Profil',
EMS_BUS_SCANNING: 'Scannen nach EMS devices...'
};
export default de;

View File

@@ -0,0 +1,49 @@
import type { BaseTranslation } from '../i18n-types';
const en: BaseTranslation = {
LANGUAGE: 'Language',
IS_REQUIRED: 'is required',
SIGN_IN: 'Sign In',
USERNAME: 'Username',
PASSWORD: 'Password',
DASHBOARD: 'Dashboard',
SETTINGS: 'Settings',
HELP: 'Help',
LOGGED_IN: 'Logged in as {name}',
PLEASE_SIGNIN: 'Please sign in to continue',
UPLOAD_SUCCESSFUL: 'Upload successful',
INVALID_LOGIN: 'Invalid login details',
NETWORK_CONNECTION: 'Network Connection',
SECURITY: 'Security',
NETWORK_TIME: 'Network Time',
ONOFF_CAP: 'ON/OFF',
ONOFF: 'on/off',
TYPE: 'Type',
DESCRIPTION: 'Description',
ENTITIES: 'Entities',
REFRESH: 'Refresh',
EXPORT: 'Export',
ENTITY_NAME: 'Entity Name',
VALUE: 'Value',
SHOW_FAV: 'only show favorites',
DEVICE_SENSOR_DATA: 'Device and Sensor Data',
DEVICES_SENSORS: 'Devices & Sensors',
ATTACHED_SENSORS: 'Attached EMS-ESP Sensors',
RUN_COMMAND: 'Call Command',
CHANGE_VALUE: 'Change Value',
CANCEL: 'Cancel',
RESET: 'Reset',
SEND: 'Send',
SAVE: 'Save',
REMOVE: 'Remove',
PROBLEM_UPDATING: 'Problem updating',
ACCESS_DENIED: 'Access Denied',
ANALOG_SENSOR: 'Analog Sensor {cmd}',
TEMP_SENSOR: 'Temperature Sensor {cmd}',
WRITE_COMMAND: 'Write command {cmd}',
EMS_BUS_WARNING:
'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
EMS_BUS_SCANNING: 'Scanning for EMS devices...'
};
export default en;

View File

@@ -0,0 +1,11 @@
import type { FormattersInitializer } from 'typesafe-i18n';
import type { Locales, Formatters } from './i18n-types';
import { date } from 'typesafe-i18n/formatters';
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
const formatters: Formatters = {
weekday: date(locale, { weekday: 'long' })
};
return formatters;
};

View File

@@ -0,0 +1,16 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { useContext } from 'react'
import { initI18nReact } from 'typesafe-i18n/react'
import type { I18nContextType } from 'typesafe-i18n/react'
import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales } from './i18n-util'
const { component: TypesafeI18n, context: I18nContext } = initI18nReact<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
const useI18nContext = (): I18nContextType<Locales, Translations, TranslationFunctions> => useContext(I18nContext)
export { I18nContext, useI18nContext }
export default TypesafeI18n

View File

@@ -0,0 +1,362 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n'
export type BaseTranslation = BaseTranslationType
export type BaseLocale = 'en'
export type Locales =
| 'de'
| 'en'
export type Translation = RootTranslation
export type Translations = RootTranslation
type RootTranslation = {
/**
* Language
*/
LANGUAGE: string
/**
* is required
*/
IS_REQUIRED: string
/**
* Sign In
*/
SIGN_IN: string
/**
* Username
*/
USERNAME: string
/**
* Password
*/
PASSWORD: string
/**
* Dashboard
*/
DASHBOARD: string
/**
* Settings
*/
SETTINGS: string
/**
* Help
*/
HELP: string
/**
* Logged in as {name}
* @param {unknown} name
*/
LOGGED_IN: RequiredParams<'name'>
/**
* Please sign in to continue
*/
PLEASE_SIGNIN: string
/**
* Upload successful
*/
UPLOAD_SUCCESSFUL: string
/**
* Invalid login details
*/
INVALID_LOGIN: string
/**
* Network Connection
*/
NETWORK_CONNECTION: string
/**
* Security
*/
SECURITY: string
/**
* Network Time
*/
NETWORK_TIME: string
/**
* ON/OFF
*/
ONOFF_CAP: string
/**
* on/off
*/
ONOFF: string
/**
* Type
*/
TYPE: string
/**
* Description
*/
DESCRIPTION: string
/**
* Entities
*/
ENTITIES: string
/**
* Refresh
*/
REFRESH: string
/**
* Export
*/
EXPORT: string
/**
* Entity Name
*/
ENTITY_NAME: string
/**
* Value
*/
VALUE: string
/**
* only show favorites
*/
SHOW_FAV: string
/**
* Device and Sensor Data
*/
DEVICE_SENSOR_DATA: string
/**
* Devices & Sensors
*/
DEVICES_SENSORS: string
/**
* Attached EMS-ESP Sensors
*/
ATTACHED_SENSORS: string
/**
* Call Command
*/
RUN_COMMAND: string
/**
* Change Value
*/
CHANGE_VALUE: string
/**
* Cancel
*/
CANCEL: string
/**
* Reset
*/
RESET: string
/**
* Send
*/
SEND: string
/**
* Save
*/
SAVE: string
/**
* Remove
*/
REMOVE: string
/**
* Problem updating
*/
PROBLEM_UPDATING: string
/**
* Access Denied
*/
ACCESS_DENIED: string
/**
* Analog Sensor {cmd}
* @param {unknown} cmd
*/
ANALOG_SENSOR: RequiredParams<'cmd'>
/**
* Temperature Sensor {cmd}
* @param {unknown} cmd
*/
TEMP_SENSOR: RequiredParams<'cmd'>
/**
* Write command {cmd}
* @param {unknown} cmd
*/
WRITE_COMMAND: RequiredParams<'cmd'>
/**
* EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile
*/
EMS_BUS_WARNING: string
/**
* Scanning for EMS devices...
*/
EMS_BUS_SCANNING: string
}
export type TranslationFunctions = {
/**
* Language
*/
LANGUAGE: () => LocalizedString
/**
* is required
*/
IS_REQUIRED: () => LocalizedString
/**
* Sign In
*/
SIGN_IN: () => LocalizedString
/**
* Username
*/
USERNAME: () => LocalizedString
/**
* Password
*/
PASSWORD: () => LocalizedString
/**
* Dashboard
*/
DASHBOARD: () => LocalizedString
/**
* Settings
*/
SETTINGS: () => LocalizedString
/**
* Help
*/
HELP: () => LocalizedString
/**
* Logged in as {name}
*/
LOGGED_IN: (arg: { name: unknown }) => LocalizedString
/**
* Please sign in to continue
*/
PLEASE_SIGNIN: () => LocalizedString
/**
* Upload successful
*/
UPLOAD_SUCCESSFUL: () => LocalizedString
/**
* Invalid login details
*/
INVALID_LOGIN: () => LocalizedString
/**
* Network Connection
*/
NETWORK_CONNECTION: () => LocalizedString
/**
* Security
*/
SECURITY: () => LocalizedString
/**
* Network Time
*/
NETWORK_TIME: () => LocalizedString
/**
* ON/OFF
*/
ONOFF_CAP: () => LocalizedString
/**
* on/off
*/
ONOFF: () => LocalizedString
/**
* Type
*/
TYPE: () => LocalizedString
/**
* Description
*/
DESCRIPTION: () => LocalizedString
/**
* Entities
*/
ENTITIES: () => LocalizedString
/**
* Refresh
*/
REFRESH: () => LocalizedString
/**
* Export
*/
EXPORT: () => LocalizedString
/**
* Entity Name
*/
ENTITY_NAME: () => LocalizedString
/**
* Value
*/
VALUE: () => LocalizedString
/**
* only show favorites
*/
SHOW_FAV: () => LocalizedString
/**
* Device and Sensor Data
*/
DEVICE_SENSOR_DATA: () => LocalizedString
/**
* Devices & Sensors
*/
DEVICES_SENSORS: () => LocalizedString
/**
* Attached EMS-ESP Sensors
*/
ATTACHED_SENSORS: () => LocalizedString
/**
* Call Command
*/
RUN_COMMAND: () => LocalizedString
/**
* Change Value
*/
CHANGE_VALUE: () => LocalizedString
/**
* Cancel
*/
CANCEL: () => LocalizedString
/**
* Reset
*/
RESET: () => LocalizedString
/**
* Send
*/
SEND: () => LocalizedString
/**
* Save
*/
SAVE: () => LocalizedString
/**
* Remove
*/
REMOVE: () => LocalizedString
/**
* Problem updating
*/
PROBLEM_UPDATING: () => LocalizedString
/**
* Access Denied
*/
ACCESS_DENIED: () => LocalizedString
/**
* Analog Sensor {cmd}
*/
ANALOG_SENSOR: (arg: { cmd: unknown }) => LocalizedString
/**
* Temperature Sensor {cmd}
*/
TEMP_SENSOR: (arg: { cmd: unknown }) => LocalizedString
/**
* Write command {cmd}
*/
WRITE_COMMAND: (arg: { cmd: unknown }) => LocalizedString
/**
* EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile
*/
EMS_BUS_WARNING: () => LocalizedString
/**
* Scanning for EMS devices...
*/
EMS_BUS_SCANNING: () => LocalizedString
}
export type Formatters = {}

View File

@@ -0,0 +1,27 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales, locales } from './i18n-util'
const localeTranslationLoaders = {
de: () => import('./de'),
en: () => import('./en'),
}
const updateDictionary = (locale: Locales, dictionary: Partial<Translations>) =>
loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
export const importLocaleAsync = async (locale: Locales) =>
(await localeTranslationLoaders[locale]()).default as unknown as Translations
export const loadLocaleAsync = async (locale: Locales): Promise<void> => {
updateDictionary(locale, await importLocaleAsync(locale))
loadFormatters(locale)
}
export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync))
export const loadFormatters = (locale: Locales): void =>
void (loadedFormatters[locale] = initFormatters(locale))

View File

@@ -0,0 +1,26 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales, locales } from './i18n-util'
import de from './de'
import en from './en'
const localeTranslations = {
de,
en,
}
export const loadLocale = (locale: Locales): void => {
if (loadedLocales[locale]) return
loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
loadFormatters(locale)
}
export const loadAllLocales = (): void => locales.forEach(loadLocale)
export const loadFormatters = (locale: Locales): void =>
void (loadedFormatters[locale] = initFormatters(locale))

View File

@@ -0,0 +1,31 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
import type { LocaleDetector } from 'typesafe-i18n/detectors'
import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'
export const baseLocale: Locales = 'en'
export const locales: Locales[] = [
'de',
'en'
]
export const loadedLocales = {} as Record<Locales, Translations>
export const loadedFormatters = {} as Record<Locales, Formatters>
export const i18nString = (locale: Locales) => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale])
export const i18nObject = (locale: Locales) =>
initI18nObject<Locales, Translations, TranslationFunctions, Formatters>(
locale,
loadedLocales[locale],
loadedFormatters[locale]
)
export const i18n = () => initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
export const detectLocale = (...detectors: LocaleDetector[]) => detectLocaleFn<Locales>(baseLocale, locales, ...detectors)

View File

@@ -5,17 +5,21 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import DashboardStatus from './DashboardStatus';
import DashboardData from './DashboardData';
const Dashboard: FC = () => {
useLayoutTitle('Dashboard');
const { routerTab } = useRouterTab();
const { LL } = useI18nContext();
useLayoutTitle(LL.DASHBOARD());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="data" label="Devices &amp; Sensors" />
<Tab value="data" label={LL.DEVICES_SENSORS()} />
<Tab value="status" label="Status" />
</RouterTabs>
<Routes>

View File

@@ -74,12 +74,21 @@ import {
DeviceEntityMask
} from './types';
import { useI18nContext } from '../i18n/i18n-react';
const DashboardData: FC = () => {
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [coreData, setCoreData] = useState<CoreData>({ connected: true, devices: [], active_sensors: 0, analog_enabled: false });
const [coreData, setCoreData] = useState<CoreData>({
connected: true,
devices: [],
active_sensors: 0,
analog_enabled: false
});
const [deviceData, setDeviceData] = useState<DeviceData>({ label: '', data: [] });
const [sensorData, setSensorData] = useState<SensorData>({ sensors: [], analogs: [] });
const [deviceValue, setDeviceValue] = useState<DeviceValue>();
@@ -134,7 +143,7 @@ const DashboardData: FC = () => {
common_theme,
{
Table: `
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 80px 40px;
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 90px 40px;
`,
BaseRow: `
.td {
@@ -429,15 +438,15 @@ const DashboardData: FC = () => {
devicevalue: deviceValue
});
if (response.status === 204) {
enqueueSnackbar('Write command failed', { variant: 'error' });
enqueueSnackbar(LL.WRITE_COMMAND({ cmd: 'failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Write access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Write command sent', { variant: 'success' });
enqueueSnackbar(LL.WRITE_COMMAND({ cmd: 'send' }), { variant: 'success' });
}
setDeviceValue(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem writing value'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
refreshData();
setDeviceValue(undefined);
@@ -449,7 +458,7 @@ const DashboardData: FC = () => {
if (deviceValue) {
return (
<Dialog open={deviceValue !== undefined} onClose={() => setDeviceValue(undefined)}>
<DialogTitle>{isCmdOnly(deviceValue) ? 'Run Command' : 'Change Value'}</DialogTitle>
<DialogTitle>{isCmdOnly(deviceValue) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
<DialogContent dividers>
{deviceValue.l && (
<ValidatedTextField
@@ -491,7 +500,7 @@ const DashboardData: FC = () => {
onClick={() => setDeviceValue(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SendIcon />}
@@ -500,7 +509,7 @@ const DashboardData: FC = () => {
onClick={() => sendDeviceValue()}
color="warning"
>
Send
{LL.SEND()}
</Button>
</DialogActions>
</Dialog>
@@ -521,15 +530,15 @@ const DashboardData: FC = () => {
offset: sensor.o
});
if (response.status === 204) {
enqueueSnackbar('Sensor change failed', { variant: 'error' });
enqueueSnackbar(LL.TEMP_SENSOR({ cmd: 'change failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Sensor updated', { variant: 'success' });
enqueueSnackbar(LL.TEMP_SENSOR({ cmd: 'removed' }), { variant: 'success' });
}
setSensor(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating sensor'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setSensor(undefined);
fetchSensorData();
@@ -581,7 +590,7 @@ const DashboardData: FC = () => {
onClick={() => setSensor(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -590,7 +599,7 @@ const DashboardData: FC = () => {
onClick={() => sendSensor()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -640,17 +649,20 @@ const DashboardData: FC = () => {
const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message="EMSbus disconnected, check settings and board profile" />}
{coreData.connected && coreData.devices.length === 0 && <MessageBox my={2} level="warning" message="Scanning for EMS devices..." />}
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
{coreData.connected && coreData.devices.length === 0 && (
<MessageBox my={2} level="warning" message={LL.EMS_BUS_SCANNING()} />
)}
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff />
<HeaderCell stiff>TYPE</HeaderCell>
<HeaderCell resize>DESCRIPTION</HeaderCell>
<HeaderCell stiff>ENTITIES</HeaderCell>
<HeaderCell stiff>{LL.TYPE()}</HeaderCell>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
<HeaderCell stiff>{LL.ENTITIES()}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -676,7 +688,7 @@ const DashboardData: FC = () => {
<DeviceIcon type="Sensor" />
</Cell>
<Cell>Sensors</Cell>
<Cell>Attached EMS-ESP Sensors</Cell>
<Cell>{LL.ATTACHED_SENSORS()}</Cell>
<Cell>{coreData.active_sensors}</Cell>
<Cell>
<IconButton size="small" onClick={() => addAnalogSensor()}>
@@ -723,7 +735,7 @@ const DashboardData: FC = () => {
control={<Checkbox size="small" name="onlyFav" checked={onlyFav} onChange={() => setOnlyFav(!onlyFav)} />}
label={
<span style={{ fontSize: '12px' }}>
only show favorites&nbsp;
{LL.SHOW_FAV()}&nbsp;
<StarIcon color="primary" sx={{ fontSize: 12 }} />
</span>
}
@@ -749,7 +761,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'NAME')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
ENTITY NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell resize>
@@ -759,7 +771,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
>
VALUE
{LL.VALUE()}
</Button>
</HeaderCell>
<HeaderCell stiff />
@@ -943,14 +955,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog deletion failed', { variant: 'error' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'deletion failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor removed', { variant: 'success' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'removed' }), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog sensor'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -971,14 +983,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog sensor update failed', { variant: 'error' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'update failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor updated', { variant: 'success' });
enqueueSnackbar(LL.ANALOG_SENSOR({ cmd: 'updated' }), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -1131,7 +1143,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Dutycycle"
label="Duty Cycle"
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
type="number"
@@ -1153,7 +1165,7 @@ const DashboardData: FC = () => {
<DialogActions>
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={() => sendRemoveAnalog()}>
Remove
{LL.REMOVE()}
</Button>
</Box>
<Button
@@ -1162,7 +1174,7 @@ const DashboardData: FC = () => {
onClick={() => setAnalog(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -1171,7 +1183,7 @@ const DashboardData: FC = () => {
onClick={() => sendAnalog()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -1180,7 +1192,7 @@ const DashboardData: FC = () => {
};
return (
<SectionContent title="Device and Sensor Data" titleGutter>
<SectionContent title={LL.DEVICE_SENSOR_DATA()} titleGutter>
{renderCoreData()}
{renderDeviceData()}
{renderDeviceDialog()}
@@ -1191,11 +1203,11 @@ const DashboardData: FC = () => {
{renderAnalogDialog()}
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
Refresh
{LL.REFRESH()}
</Button>
{device_select.state.id && device_select.state.id !== 'sensor' && (
<Button startIcon={<DownloadIcon />} variant="outlined" onClick={handleDownloadCsv}>
Export
{LL.EXPORT()}
</Button>
)}
</ButtonRow>

View File

@@ -5,12 +5,16 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import HelpInformation from './HelpInformation';
const Help: FC = () => {
useLayoutTitle('Help');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.HELP());
return (
<>
<RouterTabs value={routerTab}>

View File

@@ -6,6 +6,8 @@ import { AuthenticatedContext } from '../contexts/authentication';
import { PROJECT_PATH } from '../api/env';
import { useI18nContext } from '../i18n/i18n-react';
import TuneIcon from '@mui/icons-material/Tune';
import DashboardIcon from '@mui/icons-material/Dashboard';
import LayoutMenuItem from '../components/layout/LayoutMenuItem';
@@ -13,17 +15,18 @@ import InfoIcon from '@mui/icons-material/Info';
const ProjectMenu: FC = () => {
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
return (
<List>
<LayoutMenuItem icon={DashboardIcon} label="Dashboard" to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem icon={DashboardIcon} label={LL.DASHBOARD()} to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem
icon={TuneIcon}
label="Settings"
label={LL.SETTINGS()}
to={`/${PROJECT_PATH}/settings`}
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={InfoIcon} label="Help" to={`/${PROJECT_PATH}/help`} />
<LayoutMenuItem icon={InfoIcon} label={LL.HELP()} to={`/${PROJECT_PATH}/help`} />
</List>
);
};

View File

@@ -5,13 +5,17 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import SettingsApplication from './SettingsApplication';
import SettingsCustomization from './SettingsCustomization';
const Settings: FC = () => {
useLayoutTitle('Settings');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.SETTINGS());
return (
<>
<RouterTabs value={routerTab}>

View File

@@ -3,7 +3,7 @@ import { ValidateFieldsError } from 'async-validator';
import { useSnackbar } from 'notistack';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider } from '@mui/material';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
@@ -24,6 +24,8 @@ import { numberValue, extractErrorMessage, updateValue, useRest } from '../utils
import * as EMSESP from './api';
import { Settings, BOARD_PROFILES } from './types';
import { useI18nContext } from '../i18n/i18n-react';
export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}>
@@ -38,6 +40,8 @@ const SettingsApplication: FC = () => {
update: EMSESP.writeSettings
});
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const updateFormValue = updateValue(setData);
@@ -116,7 +120,7 @@ const SettingsApplication: FC = () => {
<Box color="warning.main">
<Typography variant="body2">
Select a pre-configured interface board profile from the list below or choose "Custom" to configure your own
hardware settings.
hardware settings
</Typography>
</Box>
<ValidatedTextField
@@ -321,6 +325,26 @@ const SettingsApplication: FC = () => {
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
General Options
</Typography>
<Box
sx={{
'& .MuiTextField-root': { width: '25ch' }
}}
>
<ValidatedTextField
name="locale"
label="Language (for device entities)"
disabled={saving}
value={data.locale}
variant="outlined"
onChange={updateFormValue}
margin="normal"
size="small"
select
>
<MenuItem value="en">English</MenuItem>
<MenuItem value="de">Deutsch</MenuItem>
</ValidatedTextField>
</Box>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
@@ -350,7 +374,7 @@ const SettingsApplication: FC = () => {
/>
<BlockFormControlLabel
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
label="Enable Read only mode (blocks all outgoing EMS Tx write commands)"
label="Enable read-only mode (blocks all outgoing EMS Tx Write commands)"
disabled={saving}
/>
<BlockFormControlLabel
@@ -371,11 +395,14 @@ const SettingsApplication: FC = () => {
/>
{data.shower_alert && (
<>
<Grid item xs={2}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_trigger"
label="Trigger Time (minutes)"
label="Trigger Time"
InputProps={{
endAdornment: <InputAdornment position="end">minutes</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_trigger}
type="number"
@@ -383,11 +410,14 @@ const SettingsApplication: FC = () => {
disabled={!data.shower_timer}
/>
</Grid>
<Grid item xs={2}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_coldshot"
label="Cold Shot Time (seconds)"
label="Cold Shot Duration"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_coldshot}
type="number"
@@ -413,8 +443,8 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>on/off</MenuItem>
<MenuItem value={2}>ON/OFF</MenuItem>
<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>
</ValidatedTextField>
@@ -430,8 +460,8 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>"on"/"off"</MenuItem>
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
<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>
@@ -487,7 +517,7 @@ const SettingsApplication: FC = () => {
/>
{data.syslog_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_host"
@@ -500,7 +530,7 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_port"
@@ -514,7 +544,7 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
name="syslog_level"
label="Log Level"
@@ -534,11 +564,14 @@ const SettingsApplication: FC = () => {
<MenuItem value={9}>ALL</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_mark_interval"
label="Mark Interval (seconds, 0=off)"
label="Mark Interval"
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}
fullWidth
variant="outlined"
value={data.syslog_mark_interval}

View File

@@ -294,7 +294,7 @@ const SettingsCustomization: FC = () => {
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">Select a device and customize each of its entities using the options:</Typography>
<Typography variant="body2">Select a device and customize the entities using the options:</Typography>
<Typography variant="body2">
<OptionIcon type="favorite" isSet={true} />
=mark as favorite&nbsp;&nbsp;

View File

@@ -1,4 +1,5 @@
export interface Settings {
locale: string;
tx_mode: number;
ems_bus_id: number;
syslog_enabled: boolean;

View File

@@ -3,10 +3,8 @@ import Schema from 'async-validator';
export const SIGN_IN_REQUEST_VALIDATOR = new Schema({
username: {
required: true,
message: 'Please provide a username'
},
password: {
required: true,
message: 'Please provide a password'
}
});