mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
formatting
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 120,
|
"printWidth": 85,
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"importOrder": ["^react", "^@mui/(.*)$", "^api*/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
|
"importOrder": ["^react", "^@mui/(.*)$", "^api*/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
|
||||||
"importOrderSeparation": true,
|
"importOrderSeparation": true,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { createWriteStream, existsSync, readFileSync, readdirSync, unlinkSync } from 'fs';
|
import {
|
||||||
|
createWriteStream,
|
||||||
|
existsSync,
|
||||||
|
readFileSync,
|
||||||
|
readdirSync,
|
||||||
|
unlinkSync
|
||||||
|
} from 'fs';
|
||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
import { relative, resolve, sep } from 'path';
|
import { relative, resolve, sep } from 'path';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
@@ -18,12 +24,7 @@ const generateWWWClass = () =>
|
|||||||
class WWWData {
|
class WWWData {
|
||||||
${indent}public:
|
${indent}public:
|
||||||
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||||
${fileInfo
|
${fileInfo.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`).join('\n')}
|
||||||
.map(
|
|
||||||
(file) =>
|
|
||||||
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`
|
|
||||||
)
|
|
||||||
.join('\n')}
|
|
||||||
${indent.repeat(2)}}
|
${indent.repeat(2)}}
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
local('Roboto'),
|
local('Roboto'),
|
||||||
local('Roboto-Regular'),
|
local('Roboto-Regular'),
|
||||||
url(../fonts/re.woff2) format('woff2');
|
url(../fonts/re.woff2) format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0152-0153, U+015A-015B,
|
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131,
|
||||||
U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
U+0141-0144, U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC,
|
||||||
|
U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,14 @@ const AppRouting: FC = () => {
|
|||||||
<Authentication>
|
<Authentication>
|
||||||
<RemoveTrailingSlashes />
|
<RemoveTrailingSlashes />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
<Route
|
||||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />} />
|
path="/unauthorized"
|
||||||
|
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/fileUpdated"
|
||||||
|
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ const AuthenticatedRouting: FC = () => {
|
|||||||
<Route path="/settings/mqtt/*" element={<Mqtt />} />
|
<Route path="/settings/mqtt/*" element={<Mqtt />} />
|
||||||
<Route path="/settings/ota/*" element={<OTASettings />} />
|
<Route path="/settings/ota/*" element={<OTASettings />} />
|
||||||
<Route path="/settings/security/*" element={<Security />} />
|
<Route path="/settings/security/*" element={<Security />} />
|
||||||
<Route path="/settings/espsystemstatus/*" element={<ESPSystemStatus />} />
|
<Route
|
||||||
|
path="/settings/espsystemstatus/*"
|
||||||
|
element={<ESPSystemStatus />}
|
||||||
|
/>
|
||||||
<Route path="/settings/upload/*" element={<UploadDownload />} />
|
<Route path="/settings/upload/*" element={<UploadDownload />} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import { CssBaseline } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import { ThemeProvider, createTheme, responsiveFontSizes } from '@mui/material/styles';
|
import {
|
||||||
|
ThemeProvider,
|
||||||
|
createTheme,
|
||||||
|
responsiveFontSizes
|
||||||
|
} from '@mui/material/styles';
|
||||||
|
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import type { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,12 @@ const SignIn: FC = () => {
|
|||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), {
|
const { send: callSignIn, onSuccess } = useRequest(
|
||||||
immediate: false
|
(request: SignInRequest) => AuthenticationApi.signIn(request),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onSuccess((response) => {
|
onSuccess((response) => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
@@ -80,7 +83,9 @@ const SignIn: FC = () => {
|
|||||||
|
|
||||||
const submitOnEnter = onEnterCallback(signIn);
|
const submitOnEnter = onEnterCallback(signIn);
|
||||||
|
|
||||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
|
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
|
||||||
|
target
|
||||||
|
}) => {
|
||||||
const loc = target.value as Locales;
|
const loc = target.value as Locales;
|
||||||
localStorage.setItem('lang', loc);
|
localStorage.setItem('lang', loc);
|
||||||
await loadLocaleAsync(loc);
|
await loadLocaleAsync(loc);
|
||||||
@@ -110,7 +115,14 @@ const SignIn: FC = () => {
|
|||||||
>
|
>
|
||||||
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
<Typography variant="h4">{PROJECT_NAME}</Typography>
|
||||||
|
|
||||||
<TextField name="locale" variant="outlined" value={locale} onChange={onLocaleSelected} size="small" select>
|
<TextField
|
||||||
|
name="locale"
|
||||||
|
variant="outlined"
|
||||||
|
value={locale}
|
||||||
|
onChange={onLocaleSelected}
|
||||||
|
size="small"
|
||||||
|
select
|
||||||
|
>
|
||||||
<MenuItem key="de" value="de">
|
<MenuItem key="de" value="de">
|
||||||
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
|
||||||
DE
|
DE
|
||||||
@@ -182,7 +194,13 @@ const SignIn: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={validateAndSignIn}
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
<ForwardIcon sx={{ mr: 1 }} />
|
<ForwardIcon sx={{ mr: 1 }} />
|
||||||
{LL.SIGN_IN()}
|
{LL.SIGN_IN()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ import type { APSettingsType, APStatusType } from 'types';
|
|||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
|
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
|
||||||
export const readAPSettings = () => alovaInstance.Get<APSettingsType>('/rest/apSettings');
|
export const readAPSettings = () =>
|
||||||
export const updateAPSettings = (data: APSettingsType) => alovaInstance.Post<APSettingsType>('/rest/apSettings', data);
|
alovaInstance.Get<APSettingsType>('/rest/apSettings');
|
||||||
|
export const updateAPSettings = (data: APSettingsType) =>
|
||||||
|
alovaInstance.Post<APSettingsType>('/rest/apSettings', data);
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { ACCESS_TOKEN, alovaInstance } from './endpoints';
|
|||||||
export const SIGN_IN_PATHNAME = 'loginPathname';
|
export const SIGN_IN_PATHNAME = 'loginPathname';
|
||||||
export const SIGN_IN_SEARCH = 'loginSearch';
|
export const SIGN_IN_SEARCH = 'loginSearch';
|
||||||
|
|
||||||
export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization');
|
export const verifyAuthorization = () =>
|
||||||
export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request);
|
alovaInstance.Get('/rest/verifyAuthorization');
|
||||||
|
export const signIn = (request: SignInRequest) =>
|
||||||
|
alovaInstance.Post<SignInResponse>('/rest/signIn', request);
|
||||||
|
|
||||||
export function getStorage() {
|
export function getStorage() {
|
||||||
return localStorage || sessionStorage;
|
return localStorage || sessionStorage;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export const alovaInstance = createAlova({
|
|||||||
requestAdapter: xhrRequestAdapter(),
|
requestAdapter: xhrRequestAdapter(),
|
||||||
beforeRequest(method) {
|
beforeRequest(method) {
|
||||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
if (localStorage.getItem(ACCESS_TOKEN)) {
|
||||||
method.config.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
method.config.headers.Authorization =
|
||||||
|
'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import type { MqttSettingsType, MqttStatusType } from 'types';
|
|||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
export const readMqttStatus = () => alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
|
export const readMqttStatus = () =>
|
||||||
export const readMqttSettings = () => alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
|
alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
|
||||||
|
export const readMqttSettings = () =>
|
||||||
|
alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
|
||||||
export const updateMqttSettings = (data: MqttSettingsType) =>
|
export const updateMqttSettings = (data: MqttSettingsType) =>
|
||||||
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);
|
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { NetworkSettingsType, NetworkStatusType, WiFiNetworkList } from 'ty
|
|||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
|
export const readNetworkStatus = () =>
|
||||||
|
alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
|
||||||
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
|
||||||
export const listNetworks = () =>
|
export const listNetworks = () =>
|
||||||
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
|
||||||
@@ -10,6 +11,8 @@ export const listNetworks = () =>
|
|||||||
timeout: 20000 // timeout 20 seconds
|
timeout: 20000 // timeout 20 seconds
|
||||||
});
|
});
|
||||||
export const readNetworkSettings = () =>
|
export const readNetworkSettings = () =>
|
||||||
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings', { name: 'networkSettings' });
|
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings', {
|
||||||
|
name: 'networkSettings'
|
||||||
|
});
|
||||||
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
|
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
|
||||||
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);
|
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { NTPSettingsType, NTPStatusType, Time } from 'types';
|
|||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
export const readNTPStatus = () => alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
|
export const readNTPStatus = () =>
|
||||||
|
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
|
||||||
export const readNTPSettings = () =>
|
export const readNTPSettings = () =>
|
||||||
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {
|
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {
|
||||||
name: 'ntpSettings'
|
name: 'ntpSettings'
|
||||||
@@ -10,4 +11,5 @@ export const readNTPSettings = () =>
|
|||||||
export const updateNTPSettings = (data: NTPSettingsType) =>
|
export const updateNTPSettings = (data: NTPSettingsType) =>
|
||||||
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);
|
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);
|
||||||
|
|
||||||
export const updateTime = (data: Time) => alovaInstance.Post<Time>('/rest/time', data);
|
export const updateTime = (data: Time) =>
|
||||||
|
alovaInstance.Post<Time>('/rest/time', data);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { SecuritySettingsType, Token } from 'types';
|
|||||||
|
|
||||||
import { alovaInstance } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
|
export const readSecuritySettings = () =>
|
||||||
|
alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
|
||||||
|
|
||||||
export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
|
export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
|
||||||
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
alovaInstance.Post('/rest/securitySettings', securitySettings);
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import type { ESPSystemStatus, LogSettings, OTASettings, SystemStatus } from 'ty
|
|||||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
||||||
|
|
||||||
// ESPSystemStatus - also used to ping in Restart monitor for pinging
|
// ESPSystemStatus - also used to ping in Restart monitor for pinging
|
||||||
export const readESPSystemStatus = () => alovaInstance.Get<ESPSystemStatus>('/rest/ESPSystemStatus');
|
export const readESPSystemStatus = () =>
|
||||||
|
alovaInstance.Get<ESPSystemStatus>('/rest/ESPSystemStatus');
|
||||||
|
|
||||||
// SystemStatus
|
// SystemStatus
|
||||||
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
export const readSystemStatus = () =>
|
||||||
|
alovaInstance.Get<SystemStatus>('/rest/systemStatus');
|
||||||
|
|
||||||
// commands
|
// commands
|
||||||
export const restart = () => alovaInstance.Post('/rest/restart');
|
export const restart = () => alovaInstance.Post('/rest/restart');
|
||||||
@@ -19,12 +21,16 @@ export const partition = () => alovaInstance.Post('/rest/partition');
|
|||||||
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
|
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
|
||||||
|
|
||||||
// OTA
|
// OTA
|
||||||
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
|
export const readOTASettings = () =>
|
||||||
export const updateOTASettings = (data: OTASettings) => alovaInstance.Post('/rest/otaSettings', data);
|
alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
|
||||||
|
export const updateOTASettings = (data: OTASettings) =>
|
||||||
|
alovaInstance.Post('/rest/otaSettings', data);
|
||||||
|
|
||||||
// SystemLog
|
// SystemLog
|
||||||
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
export const readLogSettings = () =>
|
||||||
export const updateLogSettings = (data: LogSettings) => alovaInstance.Post('/rest/logSettings', data);
|
alovaInstance.Get<LogSettings>(`/rest/logSettings`);
|
||||||
|
export const updateLogSettings = (data: LogSettings) =>
|
||||||
|
alovaInstance.Post('/rest/logSettings', data);
|
||||||
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
|
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
|
||||||
export const fetchLogES = () => alovaInstance.Get('/es/log');
|
export const fetchLogES = () => alovaInstance.Get('/es/log');
|
||||||
|
|
||||||
@@ -47,6 +53,6 @@ export const uploadFile = (file: File) => {
|
|||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
return alovaInstance.Post('/rest/uploadFile', formData, {
|
return alovaInstance.Post('/rest/uploadFile', formData, {
|
||||||
timeout: 60000, // override timeout for uploading firmware - 1 minute
|
timeout: 60000, // override timeout for uploading firmware - 1 minute
|
||||||
enableUpload: true
|
enableUpload: true // can be removed with Alova 2.20+
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ try {
|
|||||||
export class Unpackr {
|
export class Unpackr {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.useRecords === false && options.mapsAsObjects === undefined) options.mapsAsObjects = true;
|
if (options.useRecords === false && options.mapsAsObjects === undefined)
|
||||||
|
options.mapsAsObjects = true;
|
||||||
if (options.sequential && options.trusted !== false) {
|
if (options.sequential && options.trusted !== false) {
|
||||||
options.trusted = true;
|
options.trusted = true;
|
||||||
if (!options.structures && options.useRecords != false) {
|
if (!options.structures && options.useRecords != false) {
|
||||||
@@ -46,7 +47,8 @@ export class Unpackr {
|
|||||||
if (!options.maxSharedStructures) options.maxSharedStructures = 0;
|
if (!options.maxSharedStructures) options.maxSharedStructures = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options.structures) options.structures.sharedLength = options.structures.length;
|
if (options.structures)
|
||||||
|
options.structures.sharedLength = options.structures.length;
|
||||||
else if (options.getStructures) {
|
else if (options.getStructures) {
|
||||||
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
|
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
|
||||||
options.structures.sharedLength = 0;
|
options.structures.sharedLength = 0;
|
||||||
@@ -63,11 +65,14 @@ export class Unpackr {
|
|||||||
// re-entrant execution, save the state and restore it after we do this unpack
|
// re-entrant execution, save the state and restore it after we do this unpack
|
||||||
return saveState(() => {
|
return saveState(() => {
|
||||||
clearSource();
|
clearSource();
|
||||||
return this ? this.unpack(source, options) : Unpackr.prototype.unpack.call(defaultOptions, source, options);
|
return this
|
||||||
|
? this.unpack(source, options)
|
||||||
|
: Unpackr.prototype.unpack.call(defaultOptions, source, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!source.buffer && source.constructor === ArrayBuffer)
|
if (!source.buffer && source.constructor === ArrayBuffer)
|
||||||
source = typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
|
source =
|
||||||
|
typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
|
||||||
if (typeof options === 'object') {
|
if (typeof options === 'object') {
|
||||||
srcEnd = options.end || source.length;
|
srcEnd = options.end || source.length;
|
||||||
position = options.start || 0;
|
position = options.start || 0;
|
||||||
@@ -86,14 +91,21 @@ export class Unpackr {
|
|||||||
// new ones
|
// new ones
|
||||||
try {
|
try {
|
||||||
dataView =
|
dataView =
|
||||||
source.dataView || (source.dataView = new DataView(source.buffer, source.byteOffset, source.byteLength));
|
source.dataView ||
|
||||||
|
(source.dataView = new DataView(
|
||||||
|
source.buffer,
|
||||||
|
source.byteOffset,
|
||||||
|
source.byteLength
|
||||||
|
));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// if it doesn't have a buffer, maybe it is the wrong type of object
|
// if it doesn't have a buffer, maybe it is the wrong type of object
|
||||||
src = null;
|
src = null;
|
||||||
if (source instanceof Uint8Array) throw error;
|
if (source instanceof Uint8Array) throw error;
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Source must be a Uint8Array or Buffer but was a ' +
|
'Source must be a Uint8Array or Buffer but was a ' +
|
||||||
(source && typeof source == 'object' ? source.constructor.name : typeof source)
|
(source && typeof source == 'object'
|
||||||
|
? source.constructor.name
|
||||||
|
: typeof source)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this instanceof Unpackr) {
|
if (this instanceof Unpackr) {
|
||||||
@@ -117,7 +129,9 @@ export class Unpackr {
|
|||||||
try {
|
try {
|
||||||
sequentialMode = true;
|
sequentialMode = true;
|
||||||
const size = source.length;
|
const size = source.length;
|
||||||
const value = this ? this.unpack(source, size) : defaultUnpackr.unpack(source, size);
|
const value = this
|
||||||
|
? this.unpack(source, size)
|
||||||
|
: defaultUnpackr.unpack(source, size);
|
||||||
if (forEach) {
|
if (forEach) {
|
||||||
if (forEach(value) === false) return;
|
if (forEach(value) === false) return;
|
||||||
while (position < size) {
|
while (position < size) {
|
||||||
@@ -145,9 +159,11 @@ export class Unpackr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_mergeStructures(loadedStructures, existingStructures) {
|
_mergeStructures(loadedStructures, existingStructures) {
|
||||||
if (onLoadedStructures) loadedStructures = onLoadedStructures.call(this, loadedStructures);
|
if (onLoadedStructures)
|
||||||
|
loadedStructures = onLoadedStructures.call(this, loadedStructures);
|
||||||
loadedStructures = loadedStructures || [];
|
loadedStructures = loadedStructures || [];
|
||||||
if (Object.isFrozen(loadedStructures)) loadedStructures = loadedStructures.map((structure) => structure.slice(0));
|
if (Object.isFrozen(loadedStructures))
|
||||||
|
loadedStructures = loadedStructures.map((structure) => structure.slice(0));
|
||||||
for (let i = 0, l = loadedStructures.length; i < l; i++) {
|
for (let i = 0, l = loadedStructures.length; i < l; i++) {
|
||||||
const structure = loadedStructures[i];
|
const structure = loadedStructures[i];
|
||||||
if (structure) {
|
if (structure) {
|
||||||
@@ -162,7 +178,8 @@ export class Unpackr {
|
|||||||
const existing = existingStructures[id];
|
const existing = existingStructures[id];
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (structure)
|
if (structure)
|
||||||
(loadedStructures.restoreStructures || (loadedStructures.restoreStructures = []))[id] = structure;
|
(loadedStructures.restoreStructures ||
|
||||||
|
(loadedStructures.restoreStructures = []))[id] = structure;
|
||||||
loadedStructures[id] = existing;
|
loadedStructures[id] = existing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +198,16 @@ export function checkedRead(options: any) {
|
|||||||
try {
|
try {
|
||||||
if (!currentUnpackr.trusted && !sequentialMode) {
|
if (!currentUnpackr.trusted && !sequentialMode) {
|
||||||
const sharedLength = currentStructures.sharedLength || 0;
|
const sharedLength = currentStructures.sharedLength || 0;
|
||||||
if (sharedLength < currentStructures.length) currentStructures.length = sharedLength;
|
if (sharedLength < currentStructures.length)
|
||||||
|
currentStructures.length = sharedLength;
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
if (currentUnpackr.randomAccessStructure && src[position] < 0x40 && src[position] >= 0x20 && readStruct) {
|
if (
|
||||||
|
currentUnpackr.randomAccessStructure &&
|
||||||
|
src[position] < 0x40 &&
|
||||||
|
src[position] >= 0x20 &&
|
||||||
|
readStruct
|
||||||
|
) {
|
||||||
result = readStruct(src, position, srcEnd, currentUnpackr);
|
result = readStruct(src, position, srcEnd, currentUnpackr);
|
||||||
src = null; // dispose of this so that recursive unpack calls don't save state
|
src = null; // dispose of this so that recursive unpack calls don't save state
|
||||||
if (!(options && options.lazy) && result) result = result.toJSON();
|
if (!(options && options.lazy) && result) result = result.toJSON();
|
||||||
@@ -198,7 +221,8 @@ export function checkedRead(options: any) {
|
|||||||
|
|
||||||
if (position == srcEnd) {
|
if (position == srcEnd) {
|
||||||
// finished reading this source, cleanup references
|
// finished reading this source, cleanup references
|
||||||
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
|
if (currentStructures && currentStructures.restoreStructures)
|
||||||
|
restoreStructures();
|
||||||
currentStructures = null;
|
currentStructures = null;
|
||||||
src = null;
|
src = null;
|
||||||
if (referenceMap) referenceMap = null;
|
if (referenceMap) referenceMap = null;
|
||||||
@@ -208,10 +232,9 @@ export function checkedRead(options: any) {
|
|||||||
} else if (!sequentialMode) {
|
} else if (!sequentialMode) {
|
||||||
let jsonView;
|
let jsonView;
|
||||||
try {
|
try {
|
||||||
jsonView = JSON.stringify(result, (_, value) => (typeof value === 'bigint' ? `${value}n` : value)).slice(
|
jsonView = JSON.stringify(result, (_, value) =>
|
||||||
0,
|
typeof value === 'bigint' ? `${value}n` : value
|
||||||
100
|
).slice(0, 100);
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
jsonView = '(JSON view not available ' + error + ')';
|
jsonView = '(JSON view not available ' + error + ')';
|
||||||
}
|
}
|
||||||
@@ -220,9 +243,14 @@ export function checkedRead(options: any) {
|
|||||||
// else more to read, but we are reading sequentially, so don't clear source yet
|
// else more to read, but we are reading sequentially, so don't clear source yet
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
|
if (currentStructures && currentStructures.restoreStructures)
|
||||||
|
restoreStructures();
|
||||||
clearSource();
|
clearSource();
|
||||||
if (error instanceof RangeError || error.message.startsWith('Unexpected end of buffer') || position > srcEnd) {
|
if (
|
||||||
|
error instanceof RangeError ||
|
||||||
|
error.message.startsWith('Unexpected end of buffer') ||
|
||||||
|
position > srcEnd
|
||||||
|
) {
|
||||||
error.incomplete = true;
|
error.incomplete = true;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -243,7 +271,8 @@ export function read() {
|
|||||||
if (token < 0x40) return token;
|
if (token < 0x40) return token;
|
||||||
else {
|
else {
|
||||||
const structure =
|
const structure =
|
||||||
currentStructures[token & 0x3f] || (currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
|
currentStructures[token & 0x3f] ||
|
||||||
|
(currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
|
||||||
if (structure) {
|
if (structure) {
|
||||||
if (!structure.read) {
|
if (!structure.read) {
|
||||||
structure.read = createStructureReader(structure, token & 0x3f);
|
structure.read = createStructureReader(structure, token & 0x3f);
|
||||||
@@ -282,7 +311,10 @@ export function read() {
|
|||||||
// fixstr
|
// fixstr
|
||||||
const length = token - 0xa0;
|
const length = token - 0xa0;
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += length) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (srcStringEnd == 0 && srcEnd < 140) {
|
if (srcStringEnd == 0 && srcEnd < 140) {
|
||||||
// for small blocks, avoiding the overhead of the extract call is helpful
|
// for small blocks, avoiding the overhead of the extract call is helpful
|
||||||
@@ -298,8 +330,16 @@ export function read() {
|
|||||||
case 0xc1:
|
case 0xc1:
|
||||||
if (bundledStrings) {
|
if (bundledStrings) {
|
||||||
value = read(); // followed by the length of the string in characters (not bytes!)
|
value = read(); // followed by the length of the string in characters (not bytes!)
|
||||||
if (value > 0) return bundledStrings[1].slice(bundledStrings.position1, (bundledStrings.position1 += value));
|
if (value > 0)
|
||||||
else return bundledStrings[0].slice(bundledStrings.position0, (bundledStrings.position0 -= value));
|
return bundledStrings[1].slice(
|
||||||
|
bundledStrings.position1,
|
||||||
|
(bundledStrings.position1 += value)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return bundledStrings[0].slice(
|
||||||
|
bundledStrings.position0,
|
||||||
|
(bundledStrings.position0 -= value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return C1; // "never-used", return special object to denote that
|
return C1; // "never-used", return special object to denote that
|
||||||
case 0xc2:
|
case 0xc2:
|
||||||
@@ -338,7 +378,8 @@ export function read() {
|
|||||||
value = dataView.getFloat32(position);
|
value = dataView.getFloat32(position);
|
||||||
if (currentUnpackr.useFloat32 > 2) {
|
if (currentUnpackr.useFloat32 > 2) {
|
||||||
// this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
|
// this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
|
||||||
const multiplier = mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
|
const multiplier =
|
||||||
|
mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
|
||||||
position += 4;
|
position += 4;
|
||||||
return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
||||||
}
|
}
|
||||||
@@ -391,7 +432,8 @@ export function read() {
|
|||||||
value = dataView.getBigInt64(position).toString();
|
value = dataView.getBigInt64(position).toString();
|
||||||
} else if (currentUnpackr.int64AsType === 'auto') {
|
} else if (currentUnpackr.int64AsType === 'auto') {
|
||||||
value = dataView.getBigInt64(position);
|
value = dataView.getBigInt64(position);
|
||||||
if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52)) value = Number(value);
|
if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52))
|
||||||
|
value = Number(value);
|
||||||
} else value = dataView.getBigInt64(position);
|
} else value = dataView.getBigInt64(position);
|
||||||
position += 8;
|
position += 8;
|
||||||
return value;
|
return value;
|
||||||
@@ -433,7 +475,10 @@ export function read() {
|
|||||||
// str 8
|
// str 8
|
||||||
value = src[position++];
|
value = src[position++];
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += value) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return readString8(value);
|
return readString8(value);
|
||||||
case 0xda:
|
case 0xda:
|
||||||
@@ -441,7 +486,10 @@ export function read() {
|
|||||||
value = dataView.getUint16(position);
|
value = dataView.getUint16(position);
|
||||||
position += 2;
|
position += 2;
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += value) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return readString16(value);
|
return readString16(value);
|
||||||
case 0xdb:
|
case 0xdb:
|
||||||
@@ -449,7 +497,10 @@ export function read() {
|
|||||||
value = dataView.getUint32(position);
|
value = dataView.getUint32(position);
|
||||||
position += 4;
|
position += 4;
|
||||||
if (srcStringEnd >= position) {
|
if (srcStringEnd >= position) {
|
||||||
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += value) - srcStringStart
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return readString32(value);
|
return readString32(value);
|
||||||
case 0xdc:
|
case 0xdc:
|
||||||
@@ -504,7 +555,8 @@ function createStructureReader(structure, firstId) {
|
|||||||
.join(',') +
|
.join(',') +
|
||||||
'})}'
|
'})}'
|
||||||
)(read));
|
)(read));
|
||||||
if (structure.highByte === 0) structure.read = createSecondByteReader(firstId, structure.read);
|
if (structure.highByte === 0)
|
||||||
|
structure.read = createSecondByteReader(firstId, structure.read);
|
||||||
return readObject(); // second byte is already read, if there is one so immediately read object
|
return readObject(); // second byte is already read, if there is one so immediately read object
|
||||||
}
|
}
|
||||||
const object = {};
|
const object = {};
|
||||||
@@ -527,7 +579,8 @@ const createSecondByteReader = (firstId, read0) =>
|
|||||||
function () {
|
function () {
|
||||||
const highByte = src[position++];
|
const highByte = src[position++];
|
||||||
if (highByte === 0) return read0();
|
if (highByte === 0) return read0();
|
||||||
const id = firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5);
|
const id =
|
||||||
|
firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5);
|
||||||
const structure = currentStructures[id] || loadStructures()[id];
|
const structure = currentStructures[id] || loadStructures()[id];
|
||||||
if (!structure) {
|
if (!structure) {
|
||||||
throw new Error('Record id is not defined for ' + id);
|
throw new Error('Record id is not defined for ' + id);
|
||||||
@@ -542,7 +595,10 @@ export function loadStructures() {
|
|||||||
src = null;
|
src = null;
|
||||||
return currentUnpackr.getStructures();
|
return currentUnpackr.getStructures();
|
||||||
});
|
});
|
||||||
return (currentStructures = currentUnpackr._mergeStructures(loadedStructures, currentStructures));
|
return (currentStructures = currentUnpackr._mergeStructures(
|
||||||
|
loadedStructures,
|
||||||
|
currentStructures
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
var readFixedString = readStringJS;
|
var readFixedString = readStringJS;
|
||||||
@@ -563,7 +619,11 @@ export function setExtractor(extractStrings) {
|
|||||||
if (string == null) {
|
if (string == null) {
|
||||||
if (bundledStrings) return readStringJS(length);
|
if (bundledStrings) return readStringJS(length);
|
||||||
const byteOffset = src.byteOffset;
|
const byteOffset = src.byteOffset;
|
||||||
const extraction = extractStrings(position - headerLength + byteOffset, srcEnd + byteOffset, src.buffer);
|
const extraction = extractStrings(
|
||||||
|
position - headerLength + byteOffset,
|
||||||
|
srcEnd + byteOffset,
|
||||||
|
src.buffer
|
||||||
|
);
|
||||||
if (typeof extraction == 'string') {
|
if (typeof extraction == 'string') {
|
||||||
string = extraction;
|
string = extraction;
|
||||||
strings = EMPTY_ARRAY;
|
strings = EMPTY_ARRAY;
|
||||||
@@ -593,7 +653,8 @@ function readStringJS(length) {
|
|||||||
if (length < 16) {
|
if (length < 16) {
|
||||||
if ((result = shortStringInJS(length))) return result;
|
if ((result = shortStringInJS(length))) return result;
|
||||||
}
|
}
|
||||||
if (length > 64 && decoder) return decoder.decode(src.subarray(position, (position += length)));
|
if (length > 64 && decoder)
|
||||||
|
return decoder.decode(src.subarray(position, (position += length)));
|
||||||
const end = position + length;
|
const end = position + length;
|
||||||
const units = [];
|
const units = [];
|
||||||
result = '';
|
result = '';
|
||||||
@@ -616,7 +677,8 @@ function readStringJS(length) {
|
|||||||
const byte2 = src[position++] & 0x3f;
|
const byte2 = src[position++] & 0x3f;
|
||||||
const byte3 = src[position++] & 0x3f;
|
const byte3 = src[position++] & 0x3f;
|
||||||
const byte4 = src[position++] & 0x3f;
|
const byte4 = src[position++] & 0x3f;
|
||||||
let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
let unit =
|
||||||
|
((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
||||||
if (unit > 0xffff) {
|
if (unit > 0xffff) {
|
||||||
unit -= 0x10000;
|
unit -= 0x10000;
|
||||||
units.push(((unit >>> 10) & 0x3ff) | 0xd800);
|
units.push(((unit >>> 10) & 0x3ff) | 0xd800);
|
||||||
@@ -810,7 +872,8 @@ function shortStringInJS(length) {
|
|||||||
position -= 14;
|
position -= 14;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (length < 15) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
|
if (length < 15)
|
||||||
|
return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
|
||||||
const o = src[position++];
|
const o = src[position++];
|
||||||
if ((o & 0x80) > 0) {
|
if ((o & 0x80) > 0) {
|
||||||
position -= 15;
|
position -= 15;
|
||||||
@@ -862,14 +925,17 @@ function readExt(length) {
|
|||||||
const type = src[position++];
|
const type = src[position++];
|
||||||
if (currentExtensions[type]) {
|
if (currentExtensions[type]) {
|
||||||
let end;
|
let end;
|
||||||
return currentExtensions[type](src.subarray(position, (end = position += length)), (readPosition) => {
|
return currentExtensions[type](
|
||||||
position = readPosition;
|
src.subarray(position, (end = position += length)),
|
||||||
try {
|
(readPosition) => {
|
||||||
return read();
|
position = readPosition;
|
||||||
} finally {
|
try {
|
||||||
position = end;
|
return read();
|
||||||
|
} finally {
|
||||||
|
position = end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
} else throw new Error('Unknown extension type ' + type);
|
} else throw new Error('Unknown extension type ' + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,14 +947,20 @@ function readKey() {
|
|||||||
length = length - 0xa0;
|
length = length - 0xa0;
|
||||||
if (srcStringEnd >= position)
|
if (srcStringEnd >= position)
|
||||||
// if it has been extracted, must use it (and faster anyway)
|
// if it has been extracted, must use it (and faster anyway)
|
||||||
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart);
|
return srcString.slice(
|
||||||
|
position - srcStringStart,
|
||||||
|
(position += length) - srcStringStart
|
||||||
|
);
|
||||||
else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length);
|
else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length);
|
||||||
} else {
|
} else {
|
||||||
// not cacheable, go back and do a standard read
|
// not cacheable, go back and do a standard read
|
||||||
position--;
|
position--;
|
||||||
return read().toString();
|
return read().toString();
|
||||||
}
|
}
|
||||||
const key = ((length << 5) ^ (length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) & 0xfff;
|
const key =
|
||||||
|
((length << 5) ^
|
||||||
|
(length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) &
|
||||||
|
0xfff;
|
||||||
let entry = keyCache[key];
|
let entry = keyCache[key];
|
||||||
let checkPosition = position;
|
let checkPosition = position;
|
||||||
let end = position + length - 3;
|
let end = position + length - 3;
|
||||||
@@ -947,7 +1019,8 @@ const recordDefinition = (id, highByte) => {
|
|||||||
}
|
}
|
||||||
const existingStructure = currentStructures[id];
|
const existingStructure = currentStructures[id];
|
||||||
if (existingStructure && existingStructure.isShared) {
|
if (existingStructure && existingStructure.isShared) {
|
||||||
(currentStructures.restoreStructures || (currentStructures.restoreStructures = []))[id] = existingStructure;
|
(currentStructures.restoreStructures ||
|
||||||
|
(currentStructures.restoreStructures = []))[id] = existingStructure;
|
||||||
}
|
}
|
||||||
currentStructures[id] = structure;
|
currentStructures[id] = structure;
|
||||||
structure.read = createStructureReader(structure, firstByte);
|
structure.read = createStructureReader(structure, firstByte);
|
||||||
@@ -1009,7 +1082,8 @@ export const typedArrays = [
|
|||||||
currentExtensions[0x74] = (data) => {
|
currentExtensions[0x74] = (data) => {
|
||||||
const typeCode = data[0];
|
const typeCode = data[0];
|
||||||
const typedArrayName = typedArrays[typeCode];
|
const typedArrayName = typedArrays[typeCode];
|
||||||
if (!typedArrayName) throw new Error('Could not find typed array for code ' + typeCode);
|
if (!typedArrayName)
|
||||||
|
throw new Error('Could not find typed array for code ' + typeCode);
|
||||||
// we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned
|
// we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned
|
||||||
return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer);
|
return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer);
|
||||||
};
|
};
|
||||||
@@ -1033,11 +1107,20 @@ currentExtensions[0x62] = (data) => {
|
|||||||
|
|
||||||
currentExtensions[0xff] = (data) => {
|
currentExtensions[0xff] = (data) => {
|
||||||
// 32-bit date extension
|
// 32-bit date extension
|
||||||
if (data.length == 4) return new Date((data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000);
|
if (data.length == 4)
|
||||||
|
return new Date(
|
||||||
|
(data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000
|
||||||
|
);
|
||||||
else if (data.length == 8)
|
else if (data.length == 8)
|
||||||
return new Date(
|
return new Date(
|
||||||
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) / 1000000 +
|
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) /
|
||||||
((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000
|
1000000 +
|
||||||
|
((data[3] & 0x3) * 0x100000000 +
|
||||||
|
data[4] * 0x1000000 +
|
||||||
|
(data[5] << 16) +
|
||||||
|
(data[6] << 8) +
|
||||||
|
data[7]) *
|
||||||
|
1000
|
||||||
);
|
);
|
||||||
else if (data.length == 12)
|
else if (data.length == 12)
|
||||||
return new Date(
|
return new Date(
|
||||||
@@ -1070,7 +1153,10 @@ function saveState(callback) {
|
|||||||
|
|
||||||
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
|
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
|
||||||
const savedStructures = currentStructures;
|
const savedStructures = currentStructures;
|
||||||
const savedStructuresContents = currentStructures.slice(0, currentStructures.length);
|
const savedStructuresContents = currentStructures.slice(
|
||||||
|
0,
|
||||||
|
currentStructures.length
|
||||||
|
);
|
||||||
const savedPackr = currentUnpackr;
|
const savedPackr = currentUnpackr;
|
||||||
const savedSequentialMode = sequentialMode;
|
const savedSequentialMode = sequentialMode;
|
||||||
const value = callback();
|
const value = callback();
|
||||||
@@ -1122,7 +1208,10 @@ const u8Array = new Uint8Array(f32Array.buffer, 0, 4);
|
|||||||
export function roundFloat32(float32Number) {
|
export function roundFloat32(float32Number) {
|
||||||
f32Array[0] = float32Number;
|
f32Array[0] = float32Number;
|
||||||
const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)];
|
const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)];
|
||||||
return ((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
|
return (
|
||||||
|
((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) /
|
||||||
|
multiplier
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export function setReadStruct(updatedReadStruct, loadedStructs, saveState) {
|
export function setReadStruct(updatedReadStruct, loadedStructs, saveState) {
|
||||||
readStruct = updatedReadStruct;
|
readStruct = updatedReadStruct;
|
||||||
|
|||||||
@@ -14,27 +14,44 @@ export interface MessageBoxProps extends BoxProps {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEVEL_ICONS: { [type in MessageBoxLevel]: React.ComponentType<SvgIconProps> } = {
|
const LEVEL_ICONS: {
|
||||||
|
[type in MessageBoxLevel]: React.ComponentType<SvgIconProps>;
|
||||||
|
} = {
|
||||||
success: CheckCircleOutlineOutlinedIcon,
|
success: CheckCircleOutlineOutlinedIcon,
|
||||||
info: InfoOutlinedIcon,
|
info: InfoOutlinedIcon,
|
||||||
warning: ReportProblemOutlinedIcon,
|
warning: ReportProblemOutlinedIcon,
|
||||||
error: ErrorIcon
|
error: ErrorIcon
|
||||||
};
|
};
|
||||||
|
|
||||||
const LEVEL_BACKGROUNDS: { [type in MessageBoxLevel]: (theme: Theme) => string } = {
|
const LEVEL_BACKGROUNDS: {
|
||||||
|
[type in MessageBoxLevel]: (theme: Theme) => string;
|
||||||
|
} = {
|
||||||
success: (theme: Theme) => theme.palette.success.dark,
|
success: (theme: Theme) => theme.palette.success.dark,
|
||||||
info: (theme: Theme) => theme.palette.info.main,
|
info: (theme: Theme) => theme.palette.info.main,
|
||||||
warning: (theme: Theme) => theme.palette.warning.dark,
|
warning: (theme: Theme) => theme.palette.warning.dark,
|
||||||
error: (theme: Theme) => theme.palette.error.dark
|
error: (theme: Theme) => theme.palette.error.dark
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageBox: FC<MessageBoxProps> = ({ level, message, sx, children, ...rest }) => {
|
const MessageBox: FC<MessageBoxProps> = ({
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
sx,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const Icon = LEVEL_ICONS[level];
|
const Icon = LEVEL_ICONS[level];
|
||||||
const backgroundColor = LEVEL_BACKGROUNDS[level](theme);
|
const backgroundColor = LEVEL_BACKGROUNDS[level](theme);
|
||||||
const color = 'white';
|
const color = 'white';
|
||||||
return (
|
return (
|
||||||
<Box p={2} display="flex" alignItems="center" borderRadius={1} sx={{ backgroundColor, color, ...sx }} {...rest}>
|
<Box
|
||||||
|
p={2}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
borderRadius={1}
|
||||||
|
sx={{ backgroundColor, color, ...sx }}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
<Icon />
|
<Icon />
|
||||||
<Typography sx={{ ml: 2, flexGrow: 1 }} variant="body1">
|
<Typography sx={{ ml: 2, flexGrow: 1 }} variant="body1">
|
||||||
{message}
|
{message}
|
||||||
|
|||||||
@@ -14,7 +14,16 @@ const SectionContent: FC<SectionContentProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Paper id={id} sx={{ p: 2, m: 2 }}>
|
<Paper id={id} sx={{ p: 2, m: 2 }}>
|
||||||
{title && (
|
{title && (
|
||||||
<Divider sx={{ pb: 2, borderColor: 'primary.main', fontSize: 20, color: 'primary.main' }}>{title}</Divider>
|
<Divider
|
||||||
|
sx={{
|
||||||
|
pb: 2,
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
fontSize: 20,
|
||||||
|
color: 'primary.main'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Divider>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import type { ValidatedTextFieldProps } from './ValidatedTextField';
|
|||||||
|
|
||||||
type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
|
type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
|
||||||
|
|
||||||
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ InputProps, ...props }) => {
|
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({
|
||||||
|
InputProps,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ interface ValidatedFieldProps {
|
|||||||
|
|
||||||
export type ValidatedTextFieldProps = ValidatedFieldProps & TextFieldProps;
|
export type ValidatedTextFieldProps = ValidatedFieldProps & TextFieldProps;
|
||||||
|
|
||||||
const ValidatedTextField: FC<ValidatedTextFieldProps> = ({ fieldErrors, ...rest }) => {
|
const ValidatedTextField: FC<ValidatedTextFieldProps> = ({
|
||||||
|
fieldErrors,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
const errors = fieldErrors && fieldErrors[rest.name];
|
const errors = fieldErrors && fieldErrors[rest.name];
|
||||||
const renderErrors = () => errors && errors.map((e, i) => <FormHelperText key={i}>{e.message}</FormHelperText>);
|
const renderErrors = () =>
|
||||||
|
errors &&
|
||||||
|
errors.map((e, i) => <FormHelperText key={i}>{e.message}</FormHelperText>);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextField error={!!errors} {...rest} />
|
<TextField error={!!errors} {...rest} />
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => (
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton color="inherit" edge="start" onClick={onToggleDrawer} sx={{ mr: 2, display: { md: 'none' } }}>
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
edge="start"
|
||||||
|
onClick={onToggleDrawer}
|
||||||
|
sx={{ mr: 2, display: { md: 'none' } }}
|
||||||
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6" noWrap component="div">
|
<Typography variant="h6" noWrap component="div">
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ const LayoutMenu: FC = () => {
|
|||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(true);
|
const [menuOpen, setMenuOpen] = useState(true);
|
||||||
|
|
||||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
|
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
|
||||||
|
target
|
||||||
|
}) => {
|
||||||
const loc = target.value as Locales;
|
const loc = target.value as Locales;
|
||||||
localStorage.setItem('lang', loc);
|
localStorage.setItem('lang', loc);
|
||||||
await loadLocaleAsync(loc);
|
await loadLocaleAsync(loc);
|
||||||
@@ -98,7 +100,14 @@ const LayoutMenu: FC = () => {
|
|||||||
mb: '2px',
|
mb: '2px',
|
||||||
color: 'lightblue'
|
color: 'lightblue'
|
||||||
}}
|
}}
|
||||||
secondary={LL.CUSTOMIZATIONS() + ', ' + LL.SCHEDULER() + ', ' + LL.CUSTOM_ENTITIES(0) + '...'}
|
secondary={
|
||||||
|
LL.CUSTOMIZATIONS() +
|
||||||
|
', ' +
|
||||||
|
LL.SCHEDULER() +
|
||||||
|
', ' +
|
||||||
|
LL.CUSTOM_ENTITIES(0) +
|
||||||
|
'...'
|
||||||
|
}
|
||||||
secondaryTypographyProps={{
|
secondaryTypographyProps={{
|
||||||
noWrap: true,
|
noWrap: true,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -123,7 +132,12 @@ const LayoutMenu: FC = () => {
|
|||||||
disabled={!me.admin}
|
disabled={!me.admin}
|
||||||
to={`/customizations`}
|
to={`/customizations`}
|
||||||
/>
|
/>
|
||||||
<LayoutMenuItem icon={MoreTimeIcon} label={LL.SCHEDULER()} disabled={!me.admin} to={`/scheduler`} />
|
<LayoutMenuItem
|
||||||
|
icon={MoreTimeIcon}
|
||||||
|
label={LL.SCHEDULER()}
|
||||||
|
disabled={!me.admin}
|
||||||
|
to={`/scheduler`}
|
||||||
|
/>
|
||||||
<LayoutMenuItem
|
<LayoutMenuItem
|
||||||
icon={PlaylistAddIcon}
|
icon={PlaylistAddIcon}
|
||||||
label={LL.CUSTOM_ENTITIES(0)}
|
label={LL.CUSTOM_ENTITIES(0)}
|
||||||
@@ -137,7 +151,12 @@ const LayoutMenu: FC = () => {
|
|||||||
|
|
||||||
<List style={{ marginTop: `auto` }}>
|
<List style={{ marginTop: `auto` }}>
|
||||||
<LayoutMenuItem icon={AssessmentIcon} label={LL.SYSTEM(0)} to="/system" />
|
<LayoutMenuItem icon={AssessmentIcon} label={LL.SYSTEM(0)} to="/system" />
|
||||||
<LayoutMenuItem icon={SettingsIcon} label={LL.SETTINGS(0)} disabled={!me.admin} to="/settings" />
|
<LayoutMenuItem
|
||||||
|
icon={SettingsIcon}
|
||||||
|
label={LL.SETTINGS(0)}
|
||||||
|
disabled={!me.admin}
|
||||||
|
to="/settings"
|
||||||
|
/>
|
||||||
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP_OF('')} to={`/help`} />
|
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP_OF('')} to={`/help`} />
|
||||||
</List>
|
</List>
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -239,7 +258,12 @@ const LayoutMenu: FC = () => {
|
|||||||
</TextField>
|
</TextField>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Button variant="outlined" fullWidth color="primary" onClick={() => signOut(true)}>
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
color="primary"
|
||||||
|
onClick={() => signOut(true)}
|
||||||
|
>
|
||||||
{LL.SIGN_OUT()}
|
{LL.SIGN_OUT()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ interface LayoutMenuItemProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
|
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
to,
|
||||||
|
disabled
|
||||||
|
}) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const selected = routeMatches(to, pathname);
|
const selected = routeMatches(to, pathname);
|
||||||
@@ -23,7 +28,9 @@ const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabl
|
|||||||
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
|
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
|
||||||
<Icon />
|
<Icon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>{label}</ListItemText>
|
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>
|
||||||
|
{label}
|
||||||
|
</ListItemText>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ import type { FC } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||||
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText
|
||||||
|
} from '@mui/material';
|
||||||
import type { SvgIconProps } from '@mui/material';
|
import type { SvgIconProps } from '@mui/material';
|
||||||
|
|
||||||
interface ListMenuItemProps {
|
interface ListMenuItemProps {
|
||||||
@@ -27,19 +34,38 @@ function RenderIcon({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LayoutMenuItem: FC<ListMenuItemProps> = ({ icon, bgcolor, label, text, to, disabled }) => (
|
const LayoutMenuItem: FC<ListMenuItemProps> = ({
|
||||||
|
icon,
|
||||||
|
bgcolor,
|
||||||
|
label,
|
||||||
|
text,
|
||||||
|
to,
|
||||||
|
disabled
|
||||||
|
}) => (
|
||||||
<>
|
<>
|
||||||
{to && !disabled ? (
|
{to && !disabled ? (
|
||||||
<ListItem
|
<ListItem
|
||||||
disablePadding
|
disablePadding
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
<ListItemIcon style={{ justifyContent: 'right', color: 'lightblue', verticalAlign: 'middle' }}>
|
<ListItemIcon
|
||||||
|
style={{
|
||||||
|
justifyContent: 'right',
|
||||||
|
color: 'lightblue',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<NavigateNextIcon />
|
<NavigateNextIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItemButton component={Link} to={to}>
|
<ListItemButton component={Link} to={to}>
|
||||||
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} to="" />
|
<RenderIcon
|
||||||
|
icon={icon}
|
||||||
|
bgcolor={bgcolor}
|
||||||
|
label={label}
|
||||||
|
text={text}
|
||||||
|
to=""
|
||||||
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ message }) => (
|
|||||||
borderRadius: 0
|
borderRadius: 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
<WarningIcon fontSize="large" color="error" />
|
<WarningIcon fontSize="large" color="error" />
|
||||||
<Box ml={2}>
|
<Box ml={2}>
|
||||||
<Typography variant="h4">Application Error</Typography>
|
<Typography variant="h4">Application Error</Typography>
|
||||||
|
|||||||
@@ -12,14 +12,23 @@ interface FormLoaderProps {
|
|||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, onRetry, message = 'Loading…' }) => {
|
const FormLoader: FC<FormLoaderProps> = ({
|
||||||
|
errorMessage,
|
||||||
|
onRetry,
|
||||||
|
message = 'Loading…'
|
||||||
|
}) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
return (
|
return (
|
||||||
<MessageBox my={2} level="error" message={errorMessage}>
|
<MessageBox my={2} level="error" message={errorMessage}>
|
||||||
{onRetry && (
|
{onRetry && (
|
||||||
<Button startIcon={<RefreshIcon />} variant="contained" color="error" onClick={onRetry}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={onRetry}
|
||||||
|
>
|
||||||
{LL.RETRY()}
|
{LL.RETRY()}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column" padding={2} height={height}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
flexDirection="column"
|
||||||
|
padding={2}
|
||||||
|
height={height}
|
||||||
|
>
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
sx={(theme: Theme) => ({
|
sx={(theme: Theme) => ({
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { Blocker } from 'react-router-dom';
|
import type { Blocker } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
import { dialogStyle } from 'CustomTheme';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
@@ -18,10 +24,18 @@ const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
|
|||||||
<DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle>
|
<DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle>
|
||||||
<DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent>
|
<DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="outlined" onClick={() => blocker.reset?.()} color="secondary">
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => blocker.reset?.()}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.STAY()}
|
{LL.STAY()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" onClick={() => blocker.proceed?.()} color="primary">
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => blocker.proceed?.()}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{LL.LEAVE()}
|
{LL.LEAVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import type { RequiredChildrenProps } from 'utils';
|
|||||||
|
|
||||||
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
return authenticatedContext.me.admin ? <>{children}</> : <Navigate replace to="/" />;
|
return authenticatedContext.me.admin ? (
|
||||||
|
<>{children}</>
|
||||||
|
) : (
|
||||||
|
<Navigate replace to="/" />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireAdmin;
|
export default RequireAdmin;
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { Navigate, useLocation } from 'react-router-dom';
|
|||||||
import { storeLoginRedirect } from 'api/authentication';
|
import { storeLoginRedirect } from 'api/authentication';
|
||||||
|
|
||||||
import type { AuthenticatedContextValue } from 'contexts/authentication/context';
|
import type { AuthenticatedContextValue } from 'contexts/authentication/context';
|
||||||
import { AuthenticatedContext, AuthenticationContext } from 'contexts/authentication/context';
|
import {
|
||||||
|
AuthenticatedContext,
|
||||||
|
AuthenticationContext
|
||||||
|
} from 'contexts/authentication/context';
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import type { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
@@ -19,7 +22,9 @@ const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return authenticationContext.me ? (
|
return authenticationContext.me ? (
|
||||||
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
|
<AuthenticatedContext.Provider
|
||||||
|
value={authenticationContext as AuthenticatedContextValue}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthenticatedContext.Provider>
|
</AuthenticatedContext.Provider>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import type { RequiredChildrenProps } from 'utils';
|
|||||||
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
|
||||||
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect()} /> : <>{children}</>;
|
return authenticationContext.me ? (
|
||||||
|
<Navigate to={AuthenticationApi.fetchLoginRedirect()} />
|
||||||
|
) : (
|
||||||
|
<>{children}</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireUnauthenticated;
|
export default RequireUnauthenticated;
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs value={value} onChange={handleTabChange} variant={smallDown ? 'scrollable' : 'fullWidth'}>
|
<Tabs
|
||||||
|
value={value}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
variant={smallDown ? 'scrollable' : 'fullWidth'}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ export interface SingleUploadProps {
|
|||||||
progress: Progress;
|
progress: Progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, progress }) => {
|
const SingleUpload: FC<SingleUploadProps> = ({
|
||||||
|
onDrop,
|
||||||
|
onCancel,
|
||||||
|
isUploading,
|
||||||
|
progress
|
||||||
|
}) => {
|
||||||
const uploading = isUploading && progress.total > 0;
|
const uploading = isUploading && progress.total > 0;
|
||||||
|
|
||||||
const dropzoneState = useDropzone({
|
const dropzoneState = useDropzone({
|
||||||
@@ -53,8 +58,14 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, pr
|
|||||||
if (uploading) {
|
if (uploading) {
|
||||||
if (progress.total && progress.loaded) {
|
if (progress.total && progress.loaded) {
|
||||||
return progress.loaded <= progress.total
|
return progress.loaded <= progress.total
|
||||||
? LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%'
|
? LL.UPLOADING() +
|
||||||
: LL.UPLOADING() + ': ' + Math.round((progress.total * 100) / progress.loaded) + '%';
|
': ' +
|
||||||
|
Math.round((progress.loaded * 100) / progress.total) +
|
||||||
|
'%'
|
||||||
|
: LL.UPLOADING() +
|
||||||
|
': ' +
|
||||||
|
Math.round((progress.total * 100) / progress.loaded) +
|
||||||
|
'%';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return LL.UPLOAD_DROP_TEXT();
|
return LL.UPLOAD_DROP_TEXT();
|
||||||
@@ -95,7 +106,12 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, pr
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
const [initialized, setInitialized] = useState<boolean>(false);
|
const [initialized, setInitialized] = useState<boolean>(false);
|
||||||
const [me, setMe] = useState<Me>();
|
const [me, setMe] = useState<Me>();
|
||||||
|
|
||||||
const { send: verifyAuthorization } = useRequest(AuthenticationApi.verifyAuthorization(), {
|
const { send: verifyAuthorization } = useRequest(
|
||||||
immediate: false
|
AuthenticationApi.verifyAuthorization(),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const signIn = (accessToken: string) => {
|
const signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ export interface AuthenticationContextValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
||||||
export const AuthenticationContext = createContext(AuthenticationContextDefaultValue);
|
export const AuthenticationContext = createContext(
|
||||||
|
AuthenticationContextDefaultValue
|
||||||
|
);
|
||||||
|
|
||||||
export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
||||||
me: Me;
|
me: Me;
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore
|
|||||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||||
import TuneIcon from '@mui/icons-material/Tune';
|
import TuneIcon from '@mui/icons-material/Tune';
|
||||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, List } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
List
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import * as SystemApi from 'api/system';
|
import * as SystemApi from 'api/system';
|
||||||
|
|
||||||
@@ -92,7 +100,11 @@ const Settings: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderRestartDialog = () => (
|
const renderRestartDialog = () => (
|
||||||
<Dialog sx={dialogStyle} open={confirmRestart} onClose={() => setConfirmRestart(false)}>
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmRestart}
|
||||||
|
onClose={() => setConfirmRestart(false)}
|
||||||
|
>
|
||||||
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
||||||
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -128,7 +140,11 @@ const Settings: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renderFactoryResetDialog = () => (
|
const renderFactoryResetDialog = () => (
|
||||||
<Dialog sx={dialogStyle} open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmFactoryReset}
|
||||||
|
onClose={() => setConfirmFactoryReset(false)}
|
||||||
|
>
|
||||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -189,9 +205,26 @@ const Settings: FC = () => {
|
|||||||
to="ntp"
|
to="ntp"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListMenuItem icon={DeviceHubIcon} bgcolor="#68374d" label="MQTT" text={LL.CONFIGURE('MQTT')} to="mqtt" />
|
<ListMenuItem
|
||||||
<ListMenuItem icon={CastIcon} bgcolor="#efc34b" label="OTA" text={LL.CONFIGURE('OTA')} to="ota" />
|
icon={DeviceHubIcon}
|
||||||
<ListMenuItem icon={LockIcon} label={LL.SECURITY(0)} text={LL.SECURITY_1()} to="security" />
|
bgcolor="#68374d"
|
||||||
|
label="MQTT"
|
||||||
|
text={LL.CONFIGURE('MQTT')}
|
||||||
|
to="mqtt"
|
||||||
|
/>
|
||||||
|
<ListMenuItem
|
||||||
|
icon={CastIcon}
|
||||||
|
bgcolor="#efc34b"
|
||||||
|
label="OTA"
|
||||||
|
text={LL.CONFIGURE('OTA')}
|
||||||
|
to="ota"
|
||||||
|
/>
|
||||||
|
<ListMenuItem
|
||||||
|
icon={LockIcon}
|
||||||
|
label={LL.SECURITY(0)}
|
||||||
|
text={LL.SECURITY_1()}
|
||||||
|
to="security"
|
||||||
|
/>
|
||||||
|
|
||||||
<ListMenuItem
|
<ListMenuItem
|
||||||
icon={MemoryIcon}
|
icon={MemoryIcon}
|
||||||
@@ -242,7 +275,9 @@ const Settings: FC = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>;
|
return (
|
||||||
|
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
|
|||||||
import { createAPSettingsValidator, validate } from 'validators';
|
import { createAPSettingsValidator, validate } from 'validators';
|
||||||
|
|
||||||
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
|
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
|
||||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||||
|
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||||
|
|
||||||
const APSettings: FC = () => {
|
const APSettings: FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -48,7 +49,12 @@ const APSettings: FC = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -78,9 +84,15 @@ const APSettings: FC = () => {
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>{LL.AP_PROVIDE_TEXT_1()}</MenuItem>
|
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>{LL.AP_PROVIDE_TEXT_2()}</MenuItem>
|
{LL.AP_PROVIDE_TEXT_1()}
|
||||||
<MenuItem value={APProvisionMode.AP_NEVER}>{LL.AP_PROVIDE_TEXT_3()}</MenuItem>
|
</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>
|
</ValidatedTextField>
|
||||||
{isAPEnabled(data) && (
|
{isAPEnabled(data) && (
|
||||||
<>
|
<>
|
||||||
@@ -123,7 +135,13 @@ const APSettings: FC = () => {
|
|||||||
))}
|
))}
|
||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="ssid_hidden" checked={data.ssid_hidden} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="ssid_hidden"
|
||||||
|
checked={data.ssid_hidden}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.AP_HIDE_SSID()}
|
label={LL.AP_HIDE_SSID()}
|
||||||
/>
|
/>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import ComputerIcon from '@mui/icons-material/Computer';
|
|||||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
import type { Theme } from '@mui/material';
|
import type { Theme } from '@mui/material';
|
||||||
|
|
||||||
import * as APApi from 'api/ap';
|
import * as APApi from 'api/ap';
|
||||||
@@ -69,7 +78,10 @@ const APStatus: FC = () => {
|
|||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>IP</Avatar>
|
<Avatar>IP</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={data.ip_address} />
|
<ListItemText
|
||||||
|
primary={LL.ADDRESS_OF('IP')}
|
||||||
|
secondary={data.ip_address}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -78,7 +90,10 @@ const APStatus: FC = () => {
|
|||||||
<DeviceHubIcon />
|
<DeviceHubIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} />
|
<ListItemText
|
||||||
|
primary={LL.ADDRESS_OF('MAC')}
|
||||||
|
secondary={data.mac_address}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -92,7 +107,12 @@ const APStatus: FC = () => {
|
|||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ import type { FC } from 'react';
|
|||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { Button, Checkbox, Grid, InputAdornment, MenuItem, TextField, Typography } from '@mui/material';
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Grid,
|
||||||
|
InputAdornment,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import * as MqttApi from 'api/mqtt';
|
import * as MqttApi from 'api/mqtt';
|
||||||
|
|
||||||
@@ -43,7 +51,12 @@ const MqttSettings: FC = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -63,10 +76,22 @@ const MqttSettings: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enabled"
|
||||||
|
checked={data.enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_MQTT()}
|
label={LL.ENABLE_MQTT()}
|
||||||
/>
|
/>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
@@ -144,7 +169,9 @@ const MqttSettings: FC = () => {
|
|||||||
name="keep_alive"
|
name="keep_alive"
|
||||||
label="Keep Alive"
|
label="Keep Alive"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -173,7 +200,13 @@ const MqttSettings: FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
{data.enableTLS !== undefined && (
|
{data.enableTLS !== undefined && (
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enableTLS" checked={data.enableTLS} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enableTLS"
|
||||||
|
checked={data.enableTLS}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_TLS()}
|
label={LL.ENABLE_TLS()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -190,11 +223,23 @@ const MqttSettings: FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="clean_session"
|
||||||
|
checked={data.clean_session}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.MQTT_CLEAN_SESSION()}
|
label={LL.MQTT_CLEAN_SESSION()}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="mqtt_retain" checked={data.mqtt_retain} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="mqtt_retain"
|
||||||
|
checked={data.mqtt_retain}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.MQTT_RETAIN_FLAG()}
|
label={LL.MQTT_RETAIN_FLAG()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -215,7 +260,13 @@ const MqttSettings: FC = () => {
|
|||||||
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="send_response"
|
||||||
|
checked={data.send_response}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.MQTT_RESPONSE()}
|
label={LL.MQTT_RESPONSE()}
|
||||||
/>
|
/>
|
||||||
{!data.ha_enabled && (
|
{!data.ha_enabled && (
|
||||||
@@ -229,7 +280,13 @@ const MqttSettings: FC = () => {
|
|||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="publish_single"
|
||||||
|
checked={data.publish_single}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.MQTT_PUBLISH_TEXT_1()}
|
label={LL.MQTT_PUBLISH_TEXT_1()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -237,7 +294,11 @@ const MqttSettings: FC = () => {
|
|||||||
<Grid item>
|
<Grid item>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} />
|
<Checkbox
|
||||||
|
name="publish_single2cmd"
|
||||||
|
checked={data.publish_single2cmd}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
label={LL.MQTT_PUBLISH_TEXT_2()}
|
label={LL.MQTT_PUBLISH_TEXT_2()}
|
||||||
/>
|
/>
|
||||||
@@ -246,10 +307,22 @@ const MqttSettings: FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{!data.publish_single && (
|
{!data.publish_single && (
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="ha_enabled"
|
||||||
|
checked={data.ha_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.MQTT_PUBLISH_TEXT_3()}
|
label={LL.MQTT_PUBLISH_TEXT_3()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -312,14 +385,22 @@ const MqttSettings: FC = () => {
|
|||||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_heartbeat"
|
name="publish_time_heartbeat"
|
||||||
label="Heartbeat"
|
label="Heartbeat"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -334,7 +415,9 @@ const MqttSettings: FC = () => {
|
|||||||
name="publish_time_boiler"
|
name="publish_time_boiler"
|
||||||
label={LL.MQTT_INT_BOILER()}
|
label={LL.MQTT_INT_BOILER()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -349,7 +432,9 @@ const MqttSettings: FC = () => {
|
|||||||
name="publish_time_thermostat"
|
name="publish_time_thermostat"
|
||||||
label={LL.MQTT_INT_THERMOSTATS()}
|
label={LL.MQTT_INT_THERMOSTATS()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -364,7 +449,9 @@ const MqttSettings: FC = () => {
|
|||||||
name="publish_time_solar"
|
name="publish_time_solar"
|
||||||
label={LL.MQTT_INT_SOLAR()}
|
label={LL.MQTT_INT_SOLAR()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -379,7 +466,9 @@ const MqttSettings: FC = () => {
|
|||||||
name="publish_time_mixer"
|
name="publish_time_mixer"
|
||||||
label={LL.MQTT_INT_MIXER()}
|
label={LL.MQTT_INT_MIXER()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -394,7 +483,9 @@ const MqttSettings: FC = () => {
|
|||||||
name="publish_time_water"
|
name="publish_time_water"
|
||||||
label={LL.MQTT_INT_WATER()}
|
label={LL.MQTT_INT_WATER()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -409,7 +500,9 @@ const MqttSettings: FC = () => {
|
|||||||
name="publish_time_sensor"
|
name="publish_time_sensor"
|
||||||
label={LL.TEMP_SENSORS()}
|
label={LL.TEMP_SENSORS()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -423,7 +516,9 @@ const MqttSettings: FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
name="publish_time_other"
|
name="publish_time_other"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
label={LL.DEFAULT(0)}
|
label={LL.DEFAULT(0)}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -5,7 +5,16 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
|||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import ReportIcon from '@mui/icons-material/Report';
|
import ReportIcon from '@mui/icons-material/Report';
|
||||||
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
||||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
import type { Theme } from '@mui/material';
|
import type { Theme } from '@mui/material';
|
||||||
|
|
||||||
import * as MqttApi from 'api/mqtt';
|
import * as MqttApi from 'api/mqtt';
|
||||||
@@ -16,7 +25,10 @@ import { useI18nContext } from 'i18n/i18n-react';
|
|||||||
import type { MqttStatusType } from 'types';
|
import type { MqttStatusType } from 'types';
|
||||||
import { MqttDisconnectReason } from 'types';
|
import { MqttDisconnectReason } from 'types';
|
||||||
|
|
||||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatusType, theme: Theme) => {
|
export const mqttStatusHighlight = (
|
||||||
|
{ enabled, connected }: MqttStatusType,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
}
|
}
|
||||||
@@ -26,14 +38,20 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatusType, them
|
|||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatusType, theme: Theme) => {
|
export const mqttPublishHighlight = (
|
||||||
|
{ mqtt_fails }: MqttStatusType,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
if (mqtt_fails === 0) return theme.palette.success.main;
|
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||||
if (mqtt_fails < 10) return theme.palette.warning.main;
|
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||||
|
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatusType, theme: Theme) => {
|
export const mqttQueueHighlight = (
|
||||||
|
{ mqtt_queued }: MqttStatusType,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
if (mqtt_queued <= 1) return theme.palette.success.main;
|
if (mqtt_queued <= 1) return theme.palette.success.main;
|
||||||
|
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
@@ -92,7 +110,10 @@ const MqttStatus: FC = () => {
|
|||||||
<ReportIcon />
|
<ReportIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.DISCONNECT_REASON()} secondary={disconnectReason(data)} />
|
<ListItemText
|
||||||
|
primary={LL.DISCONNECT_REASON()}
|
||||||
|
secondary={disconnectReason(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</>
|
</>
|
||||||
@@ -140,7 +161,12 @@ const MqttStatus: FC = () => {
|
|||||||
{data.enabled && renderConnectionStatus()}
|
{data.enabled && renderConnectionStatus()}
|
||||||
</List>
|
</List>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -99,7 +99,12 @@ const NetworkSettings: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [initialized, setInitialized, data, selectedNetwork]);
|
}, [initialized, setInitialized, data, selectedNetwork]);
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -142,7 +147,9 @@ const NetworkSettings: FC = () => {
|
|||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
<Avatar>
|
||||||
|
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
||||||
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={selectedNetwork.ssid}
|
primary={selectedNetwork.ssid}
|
||||||
@@ -220,11 +227,23 @@ const NetworkSettings: FC = () => {
|
|||||||
<MenuItem value={8}>2 dBm</MenuItem>
|
<MenuItem value={8}>2 dBm</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="nosleep"
|
||||||
|
checked={data.nosleep}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.NETWORK_DISABLE_SLEEP()}
|
label={LL.NETWORK_DISABLE_SLEEP()}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="bandwidth20"
|
||||||
|
checked={data.bandwidth20}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.NETWORK_LOW_BAND()}
|
label={LL.NETWORK_LOW_BAND()}
|
||||||
/>
|
/>
|
||||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
@@ -241,11 +260,23 @@ const NetworkSettings: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enableMDNS"
|
||||||
|
checked={data.enableMDNS}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.NETWORK_USE_DNS()}
|
label={LL.NETWORK_USE_DNS()}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enableCORS"
|
||||||
|
checked={data.enableCORS}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.NETWORK_ENABLE_CORS()}
|
label={LL.NETWORK_ENABLE_CORS()}
|
||||||
/>
|
/>
|
||||||
{data.enableCORS && (
|
{data.enableCORS && (
|
||||||
@@ -261,12 +292,24 @@ const NetworkSettings: FC = () => {
|
|||||||
)}
|
)}
|
||||||
{data.enableIPv6 !== undefined && (
|
{data.enableIPv6 !== undefined && (
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enableIPv6"
|
||||||
|
checked={data.enableIPv6}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.NETWORK_ENABLE_IPV6()}
|
label={LL.NETWORK_ENABLE_IPV6()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="static_ip_config"
|
||||||
|
checked={data.static_ip_config}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.NETWORK_FIXED_IP()}
|
label={LL.NETWORK_FIXED_IP()}
|
||||||
/>
|
/>
|
||||||
{data.static_ip_config && (
|
{data.static_ip_config && (
|
||||||
@@ -325,36 +368,42 @@ const NetworkSettings: FC = () => {
|
|||||||
)}
|
)}
|
||||||
{restartNeeded && (
|
{restartNeeded && (
|
||||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={restart}
|
||||||
|
>
|
||||||
{LL.RESTART()}
|
{LL.RESTART()}
|
||||||
</Button>
|
</Button>
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!restartNeeded && (selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
|
{!restartNeeded &&
|
||||||
<ButtonRow>
|
(selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
|
||||||
<Button
|
<ButtonRow>
|
||||||
startIcon={<CancelIcon />}
|
<Button
|
||||||
disabled={saving}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
disabled={saving}
|
||||||
color="primary"
|
variant="outlined"
|
||||||
type="submit"
|
color="primary"
|
||||||
onClick={loadData}
|
type="submit"
|
||||||
>
|
onClick={loadData}
|
||||||
{LL.CANCEL()}
|
>
|
||||||
</Button>
|
{LL.CANCEL()}
|
||||||
<Button
|
</Button>
|
||||||
startIcon={<WarningIcon color="warning" />}
|
<Button
|
||||||
disabled={saving}
|
startIcon={<WarningIcon color="warning" />}
|
||||||
variant="contained"
|
disabled={saving}
|
||||||
color="info"
|
variant="contained"
|
||||||
type="submit"
|
color="info"
|
||||||
onClick={validateAndSubmit}
|
type="submit"
|
||||||
>
|
onClick={validateAndSubmit}
|
||||||
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
>
|
||||||
</Button>
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
</ButtonRow>
|
</Button>
|
||||||
)}
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ import RouterIcon from '@mui/icons-material/Router';
|
|||||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||||
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
|
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
|
||||||
import WifiIcon from '@mui/icons-material/Wifi';
|
import WifiIcon from '@mui/icons-material/Wifi';
|
||||||
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
import type { Theme } from '@mui/material';
|
import type { Theme } from '@mui/material';
|
||||||
|
|
||||||
import * as NetworkApi from 'api/network';
|
import * as NetworkApi from 'api/network';
|
||||||
@@ -49,7 +58,8 @@ const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => {
|
|||||||
return theme.palette.success.main;
|
return theme.palette.success.main;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isWiFi = ({ status }: NetworkStatusType) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
export const isWiFi = ({ status }: NetworkStatusType) =>
|
||||||
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||||
export const isEthernet = ({ status }: NetworkStatusType) =>
|
export const isEthernet = ({ status }: NetworkStatusType) =>
|
||||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||||
|
|
||||||
@@ -61,7 +71,10 @@ const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatusType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const IPs = (status: NetworkStatusType) => {
|
const IPs = (status: NetworkStatusType) => {
|
||||||
if (!status.local_ipv6 || status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000') {
|
if (
|
||||||
|
!status.local_ipv6 ||
|
||||||
|
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000'
|
||||||
|
) {
|
||||||
return status.local_ip;
|
return status.local_ip;
|
||||||
}
|
}
|
||||||
if (!status.local_ip || status.local_ip === '0.0.0.0') {
|
if (!status.local_ip || status.local_ip === '0.0.0.0') {
|
||||||
@@ -71,7 +84,11 @@ const IPs = (status: NetworkStatusType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NetworkStatus: FC = () => {
|
const NetworkStatus: FC = () => {
|
||||||
const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus);
|
const {
|
||||||
|
data: data,
|
||||||
|
send: loadData,
|
||||||
|
error
|
||||||
|
} = useRequest(NetworkApi.readNetworkStatus);
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
@@ -135,7 +152,10 @@ const NetworkStatus: FC = () => {
|
|||||||
<SettingsInputAntennaIcon />
|
<SettingsInputAntennaIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="SSID (RSSI)" secondary={data.ssid + ' (' + data.rssi + ' dBm)'} />
|
<ListItemText
|
||||||
|
primary="SSID (RSSI)"
|
||||||
|
secondary={data.ssid + ' (' + data.rssi + ' dBm)'}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</>
|
</>
|
||||||
@@ -155,14 +175,20 @@ const NetworkStatus: FC = () => {
|
|||||||
<DeviceHubIcon />
|
<DeviceHubIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} />
|
<ListItemText
|
||||||
|
primary={LL.ADDRESS_OF('MAC')}
|
||||||
|
secondary={data.mac_address}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>#</Avatar>
|
<Avatar>#</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.NETWORK_SUBNET()} secondary={data.subnet_mask} />
|
<ListItemText
|
||||||
|
primary={LL.NETWORK_SUBNET()}
|
||||||
|
secondary={data.subnet_mask}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -171,7 +197,10 @@ const NetworkStatus: FC = () => {
|
|||||||
<SettingsInputComponentIcon />
|
<SettingsInputComponentIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.NETWORK_GATEWAY()} secondary={data.gateway_ip || 'none'} />
|
<ListItemText
|
||||||
|
primary={LL.NETWORK_GATEWAY()}
|
||||||
|
secondary={data.gateway_ip || 'none'}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -180,14 +209,22 @@ const NetworkStatus: FC = () => {
|
|||||||
<DnsIcon />
|
<DnsIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.NETWORK_DNS()} secondary={dnsServers(data)} />
|
<ListItemText
|
||||||
|
primary={LL.NETWORK_DNS()}
|
||||||
|
secondary={dnsServers(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ export interface WiFiConnectionContextValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
|
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
|
||||||
export const WiFiConnectionContext = createContext(WiFiConnectionContextDefaultValue);
|
export const WiFiConnectionContext = createContext(
|
||||||
|
WiFiConnectionContextDefaultValue
|
||||||
|
);
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ const WiFiNetworkScanner: FC = () => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
|
||||||
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(NetworkApi.scanNetworks); // is called on page load to start network scan
|
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(
|
||||||
|
NetworkApi.scanNetworks
|
||||||
|
); // is called on page load to start network scan
|
||||||
const {
|
const {
|
||||||
data: networkList,
|
data: networkList,
|
||||||
send: getNetworkList,
|
send: getNetworkList,
|
||||||
@@ -51,7 +53,9 @@ const WiFiNetworkScanner: FC = () => {
|
|||||||
|
|
||||||
const renderNetworkScanner = () => {
|
const renderNetworkScanner = () => {
|
||||||
if (!networkList) {
|
if (!networkList) {
|
||||||
return <FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />;
|
return (
|
||||||
|
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return <WiFiNetworkSelector networkList={networkList} />;
|
return <WiFiNetworkSelector networkList={networkList} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import type { FC } from 'react';
|
|||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||||
import WifiIcon from '@mui/icons-material/Wifi';
|
import WifiIcon from '@mui/icons-material/Wifi';
|
||||||
import { Avatar, Badge, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText, useTheme } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
import type { Theme } from '@mui/material';
|
import type { Theme } from '@mui/material';
|
||||||
|
|
||||||
import { MessageBox } from 'components';
|
import { MessageBox } from 'components';
|
||||||
@@ -60,14 +69,22 @@ const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
|
|||||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||||
|
|
||||||
const renderNetwork = (network: WiFiNetwork) => (
|
const renderNetwork = (network: WiFiNetwork) => (
|
||||||
<ListItem key={network.bssid} onClick={() => wifiConnectionContext.selectNetwork(network)}>
|
<ListItem
|
||||||
|
key={network.bssid}
|
||||||
|
onClick={() => wifiConnectionContext.selectNetwork(network)}
|
||||||
|
>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={network.ssid}
|
primary={network.ssid}
|
||||||
secondary={
|
secondary={
|
||||||
'Security: ' + networkSecurityMode(network) + ', Ch: ' + network.channel + ', bssid: ' + network.bssid
|
'Security: ' +
|
||||||
|
networkSecurityMode(network) +
|
||||||
|
', Ch: ' +
|
||||||
|
network.channel +
|
||||||
|
', bssid: ' +
|
||||||
|
network.bssid
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ const NTPSettings: FC = () => {
|
|||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -76,7 +81,13 @@ const NTPSettings: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enabled"
|
||||||
|
checked={data.enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_NTP()}
|
label={LL.ENABLE_NTP()}
|
||||||
/>
|
/>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
|
|||||||
@@ -46,14 +46,19 @@ const NTPStatus: FC = () => {
|
|||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const { send: updateTime } = useRequest((local_time: Time) => NTPApi.updateTime(local_time), {
|
const { send: updateTime } = useRequest(
|
||||||
immediate: false
|
(local_time: Time) => NTPApi.updateTime(local_time),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
NTPApi.updateTime;
|
NTPApi.updateTime;
|
||||||
|
|
||||||
const isNtpActive = ({ status }: NTPStatusType) => status === NTPSyncStatus.NTP_ACTIVE;
|
const isNtpActive = ({ status }: NTPStatusType) =>
|
||||||
const isNtpEnabled = ({ status }: NTPStatusType) => status !== NTPSyncStatus.NTP_DISABLED;
|
status === NTPSyncStatus.NTP_ACTIVE;
|
||||||
|
const isNtpEnabled = ({ status }: NTPStatusType) =>
|
||||||
|
status !== NTPSyncStatus.NTP_DISABLED;
|
||||||
|
|
||||||
const ntpStatusHighlight = ({ status }: NTPStatusType, theme: Theme) => {
|
const ntpStatusHighlight = ({ status }: NTPStatusType, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -68,7 +73,8 @@ const NTPStatus: FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
|
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setLocalTime(event.target.value);
|
||||||
|
|
||||||
const openSetTime = () => {
|
const openSetTime = () => {
|
||||||
setLocalTime(formatLocalDateTime(new Date()));
|
setLocalTime(formatLocalDateTime(new Date()));
|
||||||
@@ -108,7 +114,11 @@ const NTPStatus: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderSetTimeDialog = () => (
|
const renderSetTimeDialog = () => (
|
||||||
<Dialog sx={dialogStyle} open={settingTime} onClose={() => setSettingTime(false)}>
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={settingTime}
|
||||||
|
onClose={() => setSettingTime(false)}
|
||||||
|
>
|
||||||
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||||
@@ -127,7 +137,12 @@ const NTPStatus: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setSettingTime(false)}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -179,7 +194,10 @@ const NTPStatus: FC = () => {
|
|||||||
<AccessTimeIcon />
|
<AccessTimeIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.LOCAL_TIME()} secondary={formatDateTime(data.local_time)} />
|
<ListItemText
|
||||||
|
primary={LL.LOCAL_TIME()}
|
||||||
|
secondary={formatDateTime(data.local_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -188,14 +206,22 @@ const NTPStatus: FC = () => {
|
|||||||
<SwapVerticalCircleIcon />
|
<SwapVerticalCircleIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={LL.UTC_TIME()} secondary={formatDateTime(data.utc_time)} />
|
<ListItemText
|
||||||
|
primary={LL.UTC_TIME()}
|
||||||
|
secondary={formatDateTime(data.utc_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
@@ -203,7 +229,12 @@ const NTPStatus: FC = () => {
|
|||||||
{data && !isNtpActive(data) && (
|
{data && !isNtpActive(data) && (
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}>
|
<Button
|
||||||
|
onClick={openSetTime}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<AccessTimeIcon />}
|
||||||
|
>
|
||||||
{LL.SET_TIME(0)}
|
{LL.SET_TIME(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -43,7 +43,12 @@ const OTASettings: FC = () => {
|
|||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -67,7 +72,13 @@ const OTASettings: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="enabled"
|
||||||
|
checked={data.enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_OTA()}
|
label={LL.ENABLE_OTA()}
|
||||||
/>
|
/>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
|
|||||||
@@ -30,9 +30,12 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const open = !!username;
|
const open = !!username;
|
||||||
|
|
||||||
const { data: token, send: generateToken } = useRequest(SecurityApi.generateToken(username), {
|
const { data: token, send: generateToken } = useRequest(
|
||||||
immediate: false
|
SecurityApi.generateToken(username),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -41,14 +44,26 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
|||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} onClose={onClose} open={!!username} fullWidth maxWidth="sm">
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
onClose={onClose}
|
||||||
|
open={!!username}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
|
>
|
||||||
<DialogTitle>{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
|
<DialogTitle>{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
{token ? (
|
{token ? (
|
||||||
<>
|
<>
|
||||||
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
|
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
|
||||||
<Box mt={2} mb={2}>
|
<Box mt={2} mb={2}>
|
||||||
<TextField label="Token" multiline value={token.token} fullWidth contentEditable={false} />
|
<TextField
|
||||||
|
label="Token"
|
||||||
|
multiline
|
||||||
|
value={token.token}
|
||||||
|
fullWidth
|
||||||
|
contentEditable={false}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -59,7 +74,12 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
|
|||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CloseIcon />} variant="outlined" onClick={onClose} color="secondary">
|
<Button
|
||||||
|
startIcon={<CloseIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CLOSE()}
|
{LL.CLOSE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -14,9 +14,23 @@ import { Box, Button, IconButton } from '@mui/material';
|
|||||||
|
|
||||||
import * as SecurityApi from 'api/security';
|
import * as SecurityApi from 'api/security';
|
||||||
|
|
||||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
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 { useTheme } from '@table-library/react-table-library/theme';
|
||||||
import { BlockNavigation, ButtonRow, FormLoader, MessageBox, SectionContent } from 'components';
|
import {
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
MessageBox,
|
||||||
|
SectionContent
|
||||||
|
} from 'components';
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { SecuritySettingsType, UserType } from 'types';
|
import type { SecuritySettingsType, UserType } from 'types';
|
||||||
@@ -27,10 +41,11 @@ import GenerateToken from './GenerateToken';
|
|||||||
import User from './User';
|
import User from './User';
|
||||||
|
|
||||||
const ManageUsers: FC = () => {
|
const ManageUsers: FC = () => {
|
||||||
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettingsType>({
|
const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
|
||||||
read: SecurityApi.readSecuritySettings,
|
useRest<SecuritySettingsType>({
|
||||||
update: SecurityApi.updateSecuritySettings
|
read: SecurityApi.readSecuritySettings,
|
||||||
});
|
update: SecurityApi.updateSecuritySettings
|
||||||
|
});
|
||||||
|
|
||||||
const [user, setUser] = useState<UserType>();
|
const [user, setUser] = useState<UserType>();
|
||||||
const [creating, setCreating] = useState<boolean>(false);
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
@@ -114,7 +129,12 @@ const ManageUsers: FC = () => {
|
|||||||
|
|
||||||
const doneEditingUser = () => {
|
const doneEditingUser = () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
const users = [...data.users.filter((u: { username: string }) => u.username !== user.username), user];
|
const users = [
|
||||||
|
...data.users.filter(
|
||||||
|
(u: { username: string }) => u.username !== user.username
|
||||||
|
),
|
||||||
|
user
|
||||||
|
];
|
||||||
updateDataValue({ ...data, users });
|
updateDataValue({ ...data, users });
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
setChanged(changed + 1);
|
setChanged(changed + 1);
|
||||||
@@ -148,11 +168,18 @@ const ManageUsers: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add id to the type, needed for the table
|
// add id to the type, needed for the table
|
||||||
const user_table = data.users.map((u) => ({ ...u, id: u.username })) as UserType2[];
|
const user_table = data.users.map((u) => ({
|
||||||
|
...u,
|
||||||
|
id: u.username
|
||||||
|
})) as UserType2[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table data={{ nodes: user_table }} theme={table_theme} layout={{ custom: true }}>
|
<Table
|
||||||
|
data={{ nodes: user_table }}
|
||||||
|
theme={table_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
{(tableList: UserType2[]) => (
|
{(tableList: UserType2[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -189,7 +216,9 @@ const ManageUsers: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{noAdminConfigured() && <MessageBox level="warning" message={LL.USER_WARNING()} my={2} />}
|
{noAdminConfigured() && (
|
||||||
|
<MessageBox level="warning" message={LL.USER_WARNING()} my={2} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||||
@@ -221,7 +250,12 @@ const ManageUsers: FC = () => {
|
|||||||
|
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<PersonAddIcon />} variant="outlined" color="secondary" onClick={createUser}>
|
<Button
|
||||||
|
startIcon={<PersonAddIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={createUser}
|
||||||
|
>
|
||||||
{LL.ADD(0)}
|
{LL.ADD(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ import { Button } from '@mui/material';
|
|||||||
import * as SecurityApi from 'api/security';
|
import * as SecurityApi from 'api/security';
|
||||||
|
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
import { BlockNavigation, ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField } from 'components';
|
import {
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
MessageBox,
|
||||||
|
SectionContent,
|
||||||
|
ValidatedPasswordField
|
||||||
|
} from 'components';
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { SecuritySettingsType } from 'types';
|
import type { SecuritySettingsType } from 'types';
|
||||||
@@ -37,7 +44,12 @@ const SecuritySettings: FC = () => {
|
|||||||
|
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -4,12 +4,23 @@ import type { FC } from 'react';
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
import { dialogStyle } from 'CustomTheme';
|
||||||
import type Schema from 'async-validator';
|
import type Schema from 'async-validator';
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components';
|
import {
|
||||||
|
BlockFormControlLabel,
|
||||||
|
ValidatedPasswordField,
|
||||||
|
ValidatedTextField
|
||||||
|
} from 'components';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { UserType } from 'types';
|
import type { UserType } from 'types';
|
||||||
import { updateValue } from 'utils';
|
import { updateValue } from 'utils';
|
||||||
@@ -26,7 +37,14 @@ interface UserFormProps {
|
|||||||
onCancelEditing: () => void;
|
onCancelEditing: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
|
const User: FC<UserFormProps> = ({
|
||||||
|
creating,
|
||||||
|
validator,
|
||||||
|
user,
|
||||||
|
setUser,
|
||||||
|
onDoneEditing,
|
||||||
|
onCancelEditing
|
||||||
|
}) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const updateFormValue = updateValue(setUser);
|
const updateFormValue = updateValue(setUser);
|
||||||
@@ -52,7 +70,13 @@ const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEdi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} onClose={onCancelEditing} open={!!user} fullWidth maxWidth="sm">
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
onClose={onCancelEditing}
|
||||||
|
open={!!user}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
|
>
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<DialogTitle id="user-form-dialog-title">
|
<DialogTitle id="user-form-dialog-title">
|
||||||
@@ -81,12 +105,23 @@ const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEdi
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox name="admin" checked={user.admin} onChange={updateFormValue} />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
name="admin"
|
||||||
|
checked={user.admin}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.IS_ADMIN(1)}
|
label={LL.IS_ADMIN(1)}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onCancelEditing} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onCancelEditing}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ import MemoryIcon from '@mui/icons-material/Memory';
|
|||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
|
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
|
||||||
import SdStorageIcon from '@mui/icons-material/SdStorage';
|
import SdStorageIcon from '@mui/icons-material/SdStorage';
|
||||||
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import * as SystemApi from 'api/system';
|
import * as SystemApi from 'api/system';
|
||||||
|
|
||||||
@@ -25,7 +34,11 @@ const ESPSystemStatus: FC = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.STATUS_OF('ESP32'));
|
useLayoutTitle(LL.STATUS_OF('ESP32'));
|
||||||
|
|
||||||
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true });
|
const {
|
||||||
|
data: data,
|
||||||
|
send: loadData,
|
||||||
|
error
|
||||||
|
} = useRequest(SystemApi.readESPSystemStatus, { force: true });
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -41,7 +54,10 @@ const ESPSystemStatus: FC = () => {
|
|||||||
<DevicesIcon />
|
<DevicesIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="SDK" secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version} />
|
<ListItemText
|
||||||
|
primary="SDK"
|
||||||
|
secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -75,7 +91,12 @@ const ESPSystemStatus: FC = () => {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={LL.HEAP()}
|
primary={LL.HEAP()}
|
||||||
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '}
|
secondary={
|
||||||
|
formatNumber(data.free_heap) +
|
||||||
|
' KB / ' +
|
||||||
|
formatNumber(data.max_alloc_heap) +
|
||||||
|
' KB '
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{data.psram_size !== undefined && data.free_psram !== undefined && (
|
{data.psram_size !== undefined && data.free_psram !== undefined && (
|
||||||
@@ -89,7 +110,12 @@ const ESPSystemStatus: FC = () => {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={LL.PSRAM()}
|
primary={LL.PSRAM()}
|
||||||
secondary={formatNumber(data.psram_size) + ' KB / ' + formatNumber(data.free_psram) + ' KB'}
|
secondary={
|
||||||
|
formatNumber(data.psram_size) +
|
||||||
|
' KB / ' +
|
||||||
|
formatNumber(data.free_psram) +
|
||||||
|
' KB'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
@@ -104,7 +130,10 @@ const ESPSystemStatus: FC = () => {
|
|||||||
<ListItemText
|
<ListItemText
|
||||||
primary={LL.FLASH()}
|
primary={LL.FLASH()}
|
||||||
secondary={
|
secondary={
|
||||||
formatNumber(data.flash_chip_size) + ' KB / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'
|
formatNumber(data.flash_chip_size) +
|
||||||
|
' KB / ' +
|
||||||
|
(data.flash_chip_speed / 1000000).toFixed(0) +
|
||||||
|
' MHz'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -118,7 +147,12 @@ const ESPSystemStatus: FC = () => {
|
|||||||
<ListItemText
|
<ListItemText
|
||||||
primary={LL.APPSIZE()}
|
primary={LL.APPSIZE()}
|
||||||
secondary={
|
secondary={
|
||||||
data.partition + ': ' + formatNumber(data.app_used) + ' KB / ' + formatNumber(data.app_free) + ' KB'
|
data.partition +
|
||||||
|
': ' +
|
||||||
|
formatNumber(data.app_used) +
|
||||||
|
' KB / ' +
|
||||||
|
formatNumber(data.app_free) +
|
||||||
|
' KB'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -131,7 +165,12 @@ const ESPSystemStatus: FC = () => {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={LL.FILESYSTEM()}
|
primary={LL.FILESYSTEM()}
|
||||||
secondary={formatNumber(data.fs_used) + ' KB / ' + formatNumber(data.fs_free) + ' KB'}
|
secondary={
|
||||||
|
formatNumber(data.fs_used) +
|
||||||
|
' KB / ' +
|
||||||
|
formatNumber(data.fs_free) +
|
||||||
|
' KB'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
@@ -139,7 +178,12 @@ const ESPSystemStatus: FC = () => {
|
|||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -36,7 +36,12 @@ const RestartMonitor: FC = () => {
|
|||||||
|
|
||||||
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
|
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
|
||||||
|
|
||||||
return <FormLoader message={LL.APPLICATION_RESTARTING() + '...'} errorMessage={failed ? 'Timed out' : undefined} />;
|
return (
|
||||||
|
<FormLoader
|
||||||
|
message={LL.APPLICATION_RESTARTING() + '...'}
|
||||||
|
errorMessage={failed ? 'Timed out' : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RestartMonitor;
|
export default RestartMonitor;
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ const System: FC = () => {
|
|||||||
<RouterTabs value={routerTab}>
|
<RouterTabs value={routerTab}>
|
||||||
<Tab value="status" label={LL.STATUS_OF('')} />
|
<Tab value="status" label={LL.STATUS_OF('')} />
|
||||||
<Tab value="activity" label={LL.ACTIVITY()} />
|
<Tab value="activity" label={LL.ACTIVITY()} />
|
||||||
<Tab disabled={!me.admin} value="log" label={me.admin ? LL.LOG_OF('') : ''} />
|
<Tab
|
||||||
|
disabled={!me.admin}
|
||||||
|
value="log"
|
||||||
|
label={me.admin ? LL.LOG_OF('') : ''}
|
||||||
|
/>
|
||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<SystemStatus />} />
|
<Route path="status" element={<SystemStatus />} />
|
||||||
|
|||||||
@@ -4,14 +4,28 @@ import { toast } from 'react-toastify';
|
|||||||
|
|
||||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { Box, Button, Checkbox, Grid, MenuItem, TextField, styled } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Grid,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
styled
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import * as SystemApi from 'api/system';
|
import * as SystemApi from 'api/system';
|
||||||
import { fetchLogES } from 'api/system';
|
import { fetchLogES } from 'api/system';
|
||||||
|
|
||||||
import { useSSE } from '@alova/scene-react';
|
import { useSSE } from '@alova/scene-react';
|
||||||
import { useRequest } from 'alova';
|
import { useRequest } from 'alova';
|
||||||
import { BlockFormControlLabel, BlockNavigation, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
import {
|
||||||
|
BlockFormControlLabel,
|
||||||
|
BlockNavigation,
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { LogEntry, LogSettings } from 'types';
|
import type { LogEntry, LogSettings } from 'types';
|
||||||
import { LogLevel } from 'types';
|
import { LogLevel } from 'types';
|
||||||
@@ -25,8 +39,10 @@ const LogEntryLine = styled('div')(() => ({
|
|||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const topOffset = () => document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
|
const topOffset = () =>
|
||||||
const leftOffset = () => document.getElementById('log-window')?.getBoundingClientRect().left || 0;
|
document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
|
||||||
|
const leftOffset = () =>
|
||||||
|
document.getElementById('log-window')?.getBoundingClientRect().left || 0;
|
||||||
|
|
||||||
const levelLabel = (level: LogLevel) => {
|
const levelLabel = (level: LogLevel) => {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
@@ -50,16 +66,30 @@ const SystemLog: FC = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.LOG_OF(''));
|
useLayoutTitle(LL.LOG_OF(''));
|
||||||
|
|
||||||
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
const {
|
||||||
useRest<LogSettings>({
|
loadData,
|
||||||
read: SystemApi.readLogSettings,
|
data,
|
||||||
update: SystemApi.updateLogSettings
|
updateDataValue,
|
||||||
});
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
saveData,
|
||||||
|
errorMessage
|
||||||
|
} = useRest<LogSettings>({
|
||||||
|
read: SystemApi.readLogSettings,
|
||||||
|
update: SystemApi.updateLogSettings
|
||||||
|
});
|
||||||
|
|
||||||
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
|
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
|
||||||
const [lastIndex, setLastIndex] = useState<number>(0);
|
const [lastIndex, setLastIndex] = useState<number>(0);
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
const { onMessage, onError } = useSSE(fetchLogES, {
|
const { onMessage, onError } = useSSE(fetchLogES, {
|
||||||
@@ -102,10 +132,14 @@ const SystemLog: FC = () => {
|
|||||||
const onDownload = () => {
|
const onDownload = () => {
|
||||||
let result = '';
|
let result = '';
|
||||||
for (const i of logEntries) {
|
for (const i of logEntries) {
|
||||||
result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
|
result +=
|
||||||
|
i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
|
||||||
}
|
}
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(result));
|
a.setAttribute(
|
||||||
|
'href',
|
||||||
|
'data:text/plain;charset=utf-8,' + encodeURIComponent(result)
|
||||||
|
);
|
||||||
a.setAttribute('download', 'log.txt');
|
a.setAttribute('download', 'log.txt');
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
@@ -134,7 +168,13 @@ const SystemLog: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid container spacing={3} direction="row" justifyContent="flex-start" alignItems="center">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<Grid item xs={2}>
|
<Grid item xs={2}>
|
||||||
<TextField
|
<TextField
|
||||||
name="level"
|
name="level"
|
||||||
@@ -173,7 +213,13 @@ const SystemLog: FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.compact}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="compact"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.COMPACT()}
|
label={LL.COMPACT()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -185,7 +231,12 @@ const SystemLog: FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button startIcon={<DownloadIcon />} variant="outlined" color="secondary" onClick={onDownload}>
|
<Button
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onDownload}
|
||||||
|
>
|
||||||
{LL.EXPORT()}
|
{LL.EXPORT()}
|
||||||
</Button>
|
</Button>
|
||||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ const SystemStatus: FC = () => {
|
|||||||
|
|
||||||
const [confirmScan, setConfirmScan] = useState<boolean>(false);
|
const [confirmScan, setConfirmScan] = useState<boolean>(false);
|
||||||
|
|
||||||
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
|
const {
|
||||||
|
data: data,
|
||||||
|
send: loadData,
|
||||||
|
error
|
||||||
|
} = useRequest(SystemApi.readSystemStatus, { force: true });
|
||||||
|
|
||||||
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
|
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
|
||||||
immediate: false
|
immediate: false
|
||||||
@@ -134,7 +138,8 @@ const SystemStatus: FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeHighlight = (value: boolean) => (value ? theme.palette.success.main : theme.palette.info.main);
|
const activeHighlight = (value: boolean) =>
|
||||||
|
value ? theme.palette.success.main : theme.palette.info.main;
|
||||||
|
|
||||||
const scan = async () => {
|
const scan = async () => {
|
||||||
await scanDevices()
|
await scanDevices()
|
||||||
@@ -148,14 +153,28 @@ const SystemStatus: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderScanDialog = () => (
|
const renderScanDialog = () => (
|
||||||
<Dialog sx={dialogStyle} open={confirmScan} onClose={() => setConfirmScan(false)}>
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmScan}
|
||||||
|
onClose={() => setConfirmScan(false)}
|
||||||
|
>
|
||||||
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
|
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
|
||||||
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
|
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setConfirmScan(false)}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
|
<Button
|
||||||
|
startIcon={<PermScanWifiIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={scan}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{LL.SCAN()}
|
{LL.SCAN()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -282,7 +301,12 @@ const SystemStatus: FC = () => {
|
|||||||
{renderScanDialog()}
|
{renderScanDialog()}
|
||||||
|
|
||||||
<Box mt={2} display="flex" flexWrap="wrap">
|
<Box mt={2} display="flex" flexWrap="wrap">
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import * as SystemApi from 'api/system';
|
|||||||
|
|
||||||
import * as EMSESP from 'project/api';
|
import * as EMSESP from 'project/api';
|
||||||
import { useRequest } from 'alova';
|
import { useRequest } from 'alova';
|
||||||
import { FormLoader, SectionContent, SingleUpload, useLayoutTitle } from 'components';
|
import {
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
SingleUpload,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { APIcall } from 'project/types';
|
import type { APIcall } from 'project/types';
|
||||||
|
|
||||||
@@ -19,23 +24,40 @@ const UploadDownload: FC = () => {
|
|||||||
const [restarting, setRestarting] = useState<boolean>();
|
const [restarting, setRestarting] = useState<boolean>();
|
||||||
const [md5, setMd5] = useState<string>();
|
const [md5, setMd5] = useState<string>();
|
||||||
|
|
||||||
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
|
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(
|
||||||
immediate: false
|
EMSESP.getSettings(),
|
||||||
});
|
{
|
||||||
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), {
|
immediate: false
|
||||||
immediate: false
|
}
|
||||||
});
|
);
|
||||||
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
|
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } =
|
||||||
immediate: false
|
useRequest(EMSESP.getCustomizations(), {
|
||||||
});
|
immediate: false
|
||||||
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
|
});
|
||||||
immediate: false
|
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(
|
||||||
});
|
EMSESP.getEntities(),
|
||||||
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data: APIcall) => EMSESP.API(data), {
|
{
|
||||||
immediate: false
|
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: data,
|
||||||
|
send: loadData,
|
||||||
|
error
|
||||||
|
} = useRequest(SystemApi.readESPSystemStatus, { force: true });
|
||||||
|
|
||||||
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
|
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@@ -50,11 +72,17 @@ const UploadDownload: FC = () => {
|
|||||||
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
||||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
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 STABLE_RELNOTES_URL =
|
||||||
const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
'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) =>
|
const getBinURL = (v: string) =>
|
||||||
'EMS-ESP-' + v.replaceAll('.', '_') + '-' + data.esp_platform.replaceAll('-', '_') + '.bin';
|
'EMS-ESP-' +
|
||||||
|
v.replaceAll('.', '_') +
|
||||||
|
'-' +
|
||||||
|
data.esp_platform.replaceAll('-', '_') +
|
||||||
|
'.bin';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: isUploading,
|
loading: isUploading,
|
||||||
@@ -115,8 +143,11 @@ const UploadDownload: FC = () => {
|
|||||||
saveFile(event.data, 'schedule.json');
|
saveFile(event.data, 'schedule.json');
|
||||||
});
|
});
|
||||||
onGetAPI((event) => {
|
onGetAPI((event) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
saveFile(
|
||||||
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt');
|
event.data,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadSettings = async () => {
|
const downloadSettings = async () => {
|
||||||
@@ -170,7 +201,8 @@ const UploadDownload: FC = () => {
|
|||||||
<b>{data.emsesp_version}</b> ({data.esp_platform})
|
<b>{data.emsesp_version}</b> ({data.esp_platform})
|
||||||
{latestVersion && (
|
{latestVersion && (
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
{LL.THE_LATEST()} {LL.OFFICIAL()} {LL.RELEASE_IS()} <b>{latestVersion}</b>
|
{LL.THE_LATEST()} {LL.OFFICIAL()} {LL.RELEASE_IS()}
|
||||||
|
<b>{latestVersion}</b>
|
||||||
(
|
(
|
||||||
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
||||||
{LL.RELEASE_NOTES()}
|
{LL.RELEASE_NOTES()}
|
||||||
@@ -178,7 +210,13 @@ const UploadDownload: FC = () => {
|
|||||||
) (
|
) (
|
||||||
<Link
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion as string)}
|
href={
|
||||||
|
STABLE_URL +
|
||||||
|
'v' +
|
||||||
|
latestVersion +
|
||||||
|
'/' +
|
||||||
|
getBinURL(latestVersion as string)
|
||||||
|
}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{LL.DOWNLOAD(1)}
|
{LL.DOWNLOAD(1)}
|
||||||
@@ -188,14 +226,19 @@ const UploadDownload: FC = () => {
|
|||||||
)}
|
)}
|
||||||
{latestDevVersion && (
|
{latestDevVersion && (
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
{LL.THE_LATEST()} {LL.DEVELOPMENT()} {LL.RELEASE_IS()}
|
{LL.THE_LATEST()} {LL.DEVELOPMENT()} {LL.RELEASE_IS()}
|
||||||
|
|
||||||
<b>{latestDevVersion}</b>
|
<b>{latestDevVersion}</b>
|
||||||
(
|
(
|
||||||
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
||||||
{LL.RELEASE_NOTES()}
|
{LL.RELEASE_NOTES()}
|
||||||
</Link>
|
</Link>
|
||||||
) (
|
) (
|
||||||
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion as string)} color="primary">
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={DEV_URL + getBinURL(latestDevVersion as string)}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{LL.DOWNLOAD(1)}
|
{LL.DOWNLOAD(1)}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
@@ -219,7 +262,12 @@ const UploadDownload: FC = () => {
|
|||||||
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
<Typography variant="body2">{'MD5: ' + md5}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
|
<SingleUpload
|
||||||
|
onDrop={startUpload}
|
||||||
|
onCancel={cancelUpload}
|
||||||
|
isUploading={isUploading}
|
||||||
|
progress={progress}
|
||||||
|
/>
|
||||||
{!isUploading && (
|
{!isUploading && (
|
||||||
<>
|
<>
|
||||||
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
|
||||||
@@ -307,7 +355,9 @@ const UploadDownload: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>;
|
return (
|
||||||
|
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UploadDownload;
|
export default UploadDownload;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const de: Translation = {
|
const de: Translation = {
|
||||||
LANGUAGE: 'Sprache',
|
LANGUAGE: 'Sprache',
|
||||||
RETRY: 'Neuer Versuch',
|
RETRY: 'Neuer Versuch',
|
||||||
@@ -208,7 +206,8 @@ const de: Translation = {
|
|||||||
USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren',
|
USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren',
|
||||||
ADD: 'Hinzufügen',
|
ADD: 'Hinzufügen',
|
||||||
ACCESS_TOKEN_FOR: 'Zugangs-Token für',
|
ACCESS_TOKEN_FOR: 'Zugangs-Token für',
|
||||||
ACCESS_TOKEN_TEXT: 'Dieses Token ist für REST API Aufrufe bestimmt, die eine Authentifizierung benötigen. Es kann entweder als Bearer Token im `Authorization-Header` oder in der Access_Token URL verwendet werden.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'Dieses Token ist für REST API Aufrufe bestimmt, die eine Authentifizierung benötigen. Es kann entweder als Bearer Token im `Authorization-Header` oder in der Access_Token URL verwendet werden.',
|
||||||
GENERATING_TOKEN: 'Erzeuge Token',
|
GENERATING_TOKEN: 'Erzeuge Token',
|
||||||
USER: 'Nutzer',
|
USER: 'Nutzer',
|
||||||
MODIFY: 'Ändern',
|
MODIFY: 'Ändern',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const en: Translation = {
|
const en: Translation = {
|
||||||
LANGUAGE: 'Language',
|
LANGUAGE: 'Language',
|
||||||
RETRY: 'Retry',
|
RETRY: 'Retry',
|
||||||
@@ -208,7 +206,8 @@ const en: Translation = {
|
|||||||
USER_WARNING: 'You must have at least one admin user configured',
|
USER_WARNING: 'You must have at least one admin user configured',
|
||||||
ADD: 'Add',
|
ADD: 'Add',
|
||||||
ACCESS_TOKEN_FOR: 'Access Token for',
|
ACCESS_TOKEN_FOR: 'Access Token for',
|
||||||
ACCESS_TOKEN_TEXT: 'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
|
||||||
GENERATING_TOKEN: 'Generating token',
|
GENERATING_TOKEN: 'Generating token',
|
||||||
USER: 'User',
|
USER: 'User',
|
||||||
MODIFY: 'Modify',
|
MODIFY: 'Modify',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const fr: Translation = {
|
const fr: Translation = {
|
||||||
LANGUAGE: 'Langue',
|
LANGUAGE: 'Langue',
|
||||||
RETRY: 'Réessayer',
|
RETRY: 'Réessayer',
|
||||||
@@ -9,7 +7,7 @@ const fr: Translation = {
|
|||||||
IS_REQUIRED: '{0} est requis',
|
IS_REQUIRED: '{0} est requis',
|
||||||
SIGN_IN: 'Se connecter',
|
SIGN_IN: 'Se connecter',
|
||||||
SIGN_OUT: 'Se déconnecter',
|
SIGN_OUT: 'Se déconnecter',
|
||||||
USERNAME: 'Nom d\'utilisateur',
|
USERNAME: "Nom d'utilisateur",
|
||||||
PASSWORD: 'Mot de passe',
|
PASSWORD: 'Mot de passe',
|
||||||
SU_PASSWORD: 'Mot de passe su',
|
SU_PASSWORD: 'Mot de passe su',
|
||||||
SETTINGS_OF: 'Paramètres {0}',
|
SETTINGS_OF: 'Paramètres {0}',
|
||||||
@@ -28,13 +26,13 @@ const fr: Translation = {
|
|||||||
ENTITIES: 'Entités',
|
ENTITIES: 'Entités',
|
||||||
REFRESH: 'Rafraîchir',
|
REFRESH: 'Rafraîchir',
|
||||||
EXPORT: 'Exporter',
|
EXPORT: 'Exporter',
|
||||||
DEVICE_DETAILS: 'Détails de l\'appareil',
|
DEVICE_DETAILS: "Détails de l'appareil",
|
||||||
ID_OF: 'ID {0}',
|
ID_OF: 'ID {0}',
|
||||||
DEVICE: 'Appareil',
|
DEVICE: 'Appareil',
|
||||||
PRODUCT: 'Produit',
|
PRODUCT: 'Produit',
|
||||||
VERSION: 'Version',
|
VERSION: 'Version',
|
||||||
BRAND: 'Marque',
|
BRAND: 'Marque',
|
||||||
ENTITY_NAME: 'Nom de l\'entité',
|
ENTITY_NAME: "Nom de l'entité",
|
||||||
VALUE: 'Valeur',
|
VALUE: 'Valeur',
|
||||||
DEVICES: 'Appareils',
|
DEVICES: 'Appareils',
|
||||||
SENSORS: 'Capteurs',
|
SENSORS: 'Capteurs',
|
||||||
@@ -88,7 +86,7 @@ const fr: Translation = {
|
|||||||
'Lectures capteurs de température',
|
'Lectures capteurs de température',
|
||||||
'Lectures capteurs analogiques',
|
'Lectures capteurs analogiques',
|
||||||
'Publications MQTT',
|
'Publications MQTT',
|
||||||
'Appels à l\'API',
|
"Appels à l'API",
|
||||||
'Messages Syslog'
|
'Messages Syslog'
|
||||||
],
|
],
|
||||||
NUM_DEVICES: '{num} Appareil{{s}}',
|
NUM_DEVICES: '{num} Appareil{{s}}',
|
||||||
@@ -98,11 +96,11 @@ const fr: Translation = {
|
|||||||
NUM_SECONDS: '{num} seconde{{s}}',
|
NUM_SECONDS: '{num} seconde{{s}}',
|
||||||
NUM_HOURS: '{num} heure{{s}}',
|
NUM_HOURS: '{num} heure{{s}}',
|
||||||
NUM_MINUTES: '{num} minute{{s}}',
|
NUM_MINUTES: '{num} minute{{s}}',
|
||||||
APPLICATION_SETTINGS: 'Paramètres de l\'application',
|
APPLICATION_SETTINGS: "Paramètres de l'application",
|
||||||
CUSTOMIZATIONS: 'Personnalisation',
|
CUSTOMIZATIONS: 'Personnalisation',
|
||||||
APPLICATION_RESTARTING: 'EMS-ESP redémarre',
|
APPLICATION_RESTARTING: 'EMS-ESP redémarre',
|
||||||
INTERFACE_BOARD_PROFILE: 'Profile de carte d\'interface',
|
INTERFACE_BOARD_PROFILE: "Profile de carte d'interface",
|
||||||
BOARD_PROFILE_TEXT: 'Sélectionnez un profil de carte d\'interface préconfiguré dans la liste ci-dessous ou choisissez Personnalisé pour configurer vos propres paramètres matériels',
|
BOARD_PROFILE_TEXT: "Sélectionnez un profil de carte d'interface préconfiguré dans la liste ci-dessous ou choisissez Personnalisé pour configurer vos propres paramètres matériels",
|
||||||
BOARD_PROFILE: 'Profil de carte',
|
BOARD_PROFILE: 'Profil de carte',
|
||||||
CUSTOM: 'Personnalisé',
|
CUSTOM: 'Personnalisé',
|
||||||
GPIO_OF: 'GPIO {0}',
|
GPIO_OF: 'GPIO {0}',
|
||||||
@@ -119,14 +117,14 @@ const fr: Translation = {
|
|||||||
ENABLE_TELNET: 'Activer la console Telnet',
|
ENABLE_TELNET: 'Activer la console Telnet',
|
||||||
ENABLE_ANALOG: 'Activer les capteurs analogiques',
|
ENABLE_ANALOG: 'Activer les capteurs analogiques',
|
||||||
CONVERT_FAHRENHEIT: 'Convertir les températures en Fahrenheit',
|
CONVERT_FAHRENHEIT: 'Convertir les températures en Fahrenheit',
|
||||||
BYPASS_TOKEN: 'Contourner l\'autorisation du jeton d\'accès sur les appels API',
|
BYPASS_TOKEN: "Contourner l'autorisation du jeton d'accès sur les appels API",
|
||||||
READONLY: 'Activer le mode lecture uniquement (bloque toutes les commandes EMS sortantes en écriture Tx)',
|
READONLY: 'Activer le mode lecture uniquement (bloque toutes les commandes EMS sortantes en écriture Tx)',
|
||||||
UNDERCLOCK_CPU: 'Underclock du CPU',
|
UNDERCLOCK_CPU: 'Underclock du CPU',
|
||||||
HEATINGOFF: 'Start boiler with forced heating off', // TODO translate
|
HEATINGOFF: 'Start boiler with forced heating off', // TODO translate
|
||||||
ENABLE_SHOWER_TIMER: 'Activer la minuterie de la douche',
|
ENABLE_SHOWER_TIMER: 'Activer la minuterie de la douche',
|
||||||
ENABLE_SHOWER_ALERT: 'Activer les alertes de durée de douche',
|
ENABLE_SHOWER_ALERT: 'Activer les alertes de durée de douche',
|
||||||
TRIGGER_TIME: 'Durée avant déclenchement',
|
TRIGGER_TIME: 'Durée avant déclenchement',
|
||||||
COLD_SHOT_DURATION: 'Durée du coup d\'eau froide',
|
COLD_SHOT_DURATION: "Durée du coup d'eau froide",
|
||||||
FORMATTING_OPTIONS: 'Options de mise en forme',
|
FORMATTING_OPTIONS: 'Options de mise en forme',
|
||||||
BOOLEAN_FORMAT_DASHBOARD: 'Tableau de bord du format booléen',
|
BOOLEAN_FORMAT_DASHBOARD: 'Tableau de bord du format booléen',
|
||||||
BOOLEAN_FORMAT_API: 'Format booléen API/MQTT',
|
BOOLEAN_FORMAT_API: 'Format booléen API/MQTT',
|
||||||
@@ -150,8 +148,8 @@ const fr: Translation = {
|
|||||||
CUSTOMIZATIONS_SAVED: 'Personnalisations enregistrées',
|
CUSTOMIZATIONS_SAVED: 'Personnalisations enregistrées',
|
||||||
CUSTOMIZATIONS_HELP_1: 'Sélectionnez un appareil et personnalisez les options des entités ou cliquez pour renommer',
|
CUSTOMIZATIONS_HELP_1: 'Sélectionnez un appareil et personnalisez les options des entités ou cliquez pour renommer',
|
||||||
CUSTOMIZATIONS_HELP_2: 'marquer comme favori',
|
CUSTOMIZATIONS_HELP_2: 'marquer comme favori',
|
||||||
CUSTOMIZATIONS_HELP_3: 'désactiver l\'action d\'écriture',
|
CUSTOMIZATIONS_HELP_3: "désactiver l'action d'écriture",
|
||||||
CUSTOMIZATIONS_HELP_4: 'exclure de MQTT et de l\'API',
|
CUSTOMIZATIONS_HELP_4: "exclure de MQTT et de l'API",
|
||||||
CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord',
|
CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord',
|
||||||
CUSTOMIZATIONS_HELP_6: 'remove from memory', // TODO translate
|
CUSTOMIZATIONS_HELP_6: 'remove from memory', // TODO translate
|
||||||
SELECT_DEVICE: 'Sélectionnez un appareil',
|
SELECT_DEVICE: 'Sélectionnez un appareil',
|
||||||
@@ -163,7 +161,7 @@ const fr: Translation = {
|
|||||||
HELP_INFORMATION_1: 'Visitez le wiki en ligne pour obtenir des instructions sur la façon de configurer EMS-ESP.',
|
HELP_INFORMATION_1: 'Visitez le wiki en ligne pour obtenir des instructions sur la façon de configurer EMS-ESP.',
|
||||||
HELP_INFORMATION_2: 'Pour une discussion en direct avec la communauté, rejoignez notre serveur Discord',
|
HELP_INFORMATION_2: 'Pour une discussion en direct avec la communauté, rejoignez notre serveur Discord',
|
||||||
HELP_INFORMATION_3: 'Pour demander une fonctionnalité ou signaler un problème',
|
HELP_INFORMATION_3: 'Pour demander une fonctionnalité ou signaler un problème',
|
||||||
HELP_INFORMATION_4: 'N\'oubliez pas de télécharger et de joindre les informations relatives à votre système pour obtenir une réponse plus rapide lorsque vous signalez un problème',
|
HELP_INFORMATION_4: "N'oubliez pas de télécharger et de joindre les informations relatives à votre système pour obtenir une réponse plus rapide lorsque vous signalez un problème",
|
||||||
HELP_INFORMATION_5: 'EMS-ESP est un projet libre et open-source. Merci de soutenir son développement futur en lui donnant une étoile sur Github !',
|
HELP_INFORMATION_5: 'EMS-ESP est un projet libre et open-source. Merci de soutenir son développement futur en lui donnant une étoile sur Github !',
|
||||||
UPLOAD: 'Upload',
|
UPLOAD: 'Upload',
|
||||||
DOWNLOAD: '{{D|d|d}}ownload',
|
DOWNLOAD: '{{D|d|d}}ownload',
|
||||||
@@ -178,8 +176,8 @@ const fr: Translation = {
|
|||||||
CLOSE: 'Fermer',
|
CLOSE: 'Fermer',
|
||||||
USE: 'Utiliser',
|
USE: 'Utiliser',
|
||||||
FACTORY_RESET: 'Réinitialisation',
|
FACTORY_RESET: 'Réinitialisation',
|
||||||
SYSTEM_FACTORY_TEXT: 'L\'appareil a été réinitialisé et va maintenant redémarrer',
|
SYSTEM_FACTORY_TEXT: "L'appareil a été réinitialisé et va maintenant redémarrer",
|
||||||
SYSTEM_FACTORY_TEXT_DIALOG: 'Êtes-vous sûr de vouloir réinitialiser l\'appareil à ses paramètres d\'usine ?',
|
SYSTEM_FACTORY_TEXT_DIALOG: "Êtes-vous sûr de vouloir réinitialiser l'appareil à ses paramètres d'usine ?",
|
||||||
THE_LATEST: 'La dernière',
|
THE_LATEST: 'La dernière',
|
||||||
OFFICIAL: 'officielle',
|
OFFICIAL: 'officielle',
|
||||||
DEVELOPMENT: 'développement',
|
DEVELOPMENT: 'développement',
|
||||||
@@ -195,10 +193,12 @@ const fr: Translation = {
|
|||||||
BUFFER_SIZE: 'Max taille du buffer',
|
BUFFER_SIZE: 'Max taille du buffer',
|
||||||
COMPACT: 'Compact',
|
COMPACT: 'Compact',
|
||||||
ENABLE_OTA: 'Activer les updates OTA',
|
ENABLE_OTA: 'Activer les updates OTA',
|
||||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Télécharger les personnalisations d\'entités',
|
DOWNLOAD_CUSTOMIZATION_TEXT: "Télécharger les personnalisations d'entités",
|
||||||
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', // TODO translate
|
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', // TODO translate
|
||||||
DOWNLOAD_SETTINGS_TEXT: 'Téléchargez les paramètres de l\'application. Soyez prudent lorsque vous partagez vos paramètres car ce fichier contient des mots de passe et d\'autres informations système sensibles.',
|
DOWNLOAD_SETTINGS_TEXT:
|
||||||
UPLOAD_TEXT: 'Téléchargez un nouveau fichier de firmware (.bin), un fichier de paramètres ou de personnalisations (.json) ci-dessous, pour une validation optionnelle téléchargez d\'abord un fichier (.md5)',
|
"Téléchargez les paramètres de l'application. Soyez prudent lorsque vous partagez vos paramètres car ce fichier contient des mots de passe et d'autres informations système sensibles.",
|
||||||
|
UPLOAD_TEXT:
|
||||||
|
"Téléchargez un nouveau fichier de firmware (.bin), un fichier de paramètres ou de personnalisations (.json) ci-dessous, pour une validation optionnelle téléchargez d'abord un fichier (.md5)",
|
||||||
UPLOADING: 'Téléchargement',
|
UPLOADING: 'Téléchargement',
|
||||||
UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici',
|
UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici',
|
||||||
ERROR: 'Erreur inattendue, veuillez réessayer',
|
ERROR: 'Erreur inattendue, veuillez réessayer',
|
||||||
@@ -207,12 +207,13 @@ const fr: Translation = {
|
|||||||
IS_ADMIN: 'admin',
|
IS_ADMIN: 'admin',
|
||||||
USER_WARNING: 'Vous devez avoir au moins un utilisateur admin configuré',
|
USER_WARNING: 'Vous devez avoir au moins un utilisateur admin configuré',
|
||||||
ADD: 'Ajouter',
|
ADD: 'Ajouter',
|
||||||
ACCESS_TOKEN_FOR: 'Jeton d\'accès pour',
|
ACCESS_TOKEN_FOR: "Jeton d'accès pour",
|
||||||
ACCESS_TOKEN_TEXT: 'Le jeton ci-dessous est utilisé avec les appels d\'API REST qui nécessitent une autorisation. Il peut être passé soit en tant que jeton Bearer dans l\'en-tête Authorization, soit dans le paramètre de requête URL access_token.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
"Le jeton ci-dessous est utilisé avec les appels d'API REST qui nécessitent une autorisation. Il peut être passé soit en tant que jeton Bearer dans l'en-tête Authorization, soit dans le paramètre de requête URL access_token.",
|
||||||
GENERATING_TOKEN: 'Génération de jeton',
|
GENERATING_TOKEN: 'Génération de jeton',
|
||||||
USER: 'Utilisateur',
|
USER: 'Utilisateur',
|
||||||
MODIFY: 'Modifier',
|
MODIFY: 'Modifier',
|
||||||
SU_TEXT: 'Le mot de passe su (super utilisateur) est utilisé pour signer les jetons d\'authentification et activer les privilèges d\'administrateur dans la console.',
|
SU_TEXT: "Le mot de passe su (super utilisateur) est utilisé pour signer les jetons d'authentification et activer les privilèges d'administrateur dans la console.",
|
||||||
NOT_ENABLED: 'Non activé',
|
NOT_ENABLED: 'Non activé',
|
||||||
ERRORS_OF: 'Erreurs {0}',
|
ERRORS_OF: 'Erreurs {0}',
|
||||||
DISCONNECT_REASON: 'Raison de la déconnexion',
|
DISCONNECT_REASON: 'Raison de la déconnexion',
|
||||||
@@ -240,7 +241,7 @@ const fr: Translation = {
|
|||||||
MQTT_QUEUE: 'Queue MQTT',
|
MQTT_QUEUE: 'Queue MQTT',
|
||||||
DEFAULT: 'Défaut',
|
DEFAULT: 'Défaut',
|
||||||
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
|
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
|
||||||
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)',// TODO translate
|
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)', // TODO translate
|
||||||
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
|
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
|
||||||
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
|
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
|
||||||
MQTT_CLEAN_SESSION: 'Flag Clean Session',
|
MQTT_CLEAN_SESSION: 'Flag Clean Session',
|
||||||
@@ -248,15 +249,15 @@ const fr: Translation = {
|
|||||||
INACTIVE: 'Inactif',
|
INACTIVE: 'Inactif',
|
||||||
ACTIVE: 'Actif',
|
ACTIVE: 'Actif',
|
||||||
UNKNOWN: 'Inconnu',
|
UNKNOWN: 'Inconnu',
|
||||||
SET_TIME: 'Définir l\'heure',
|
SET_TIME: "Définir l'heure",
|
||||||
SET_TIME_TEXT: 'Entrer la date et l\'heure locale ci-dessous pour régler l\'heure',
|
SET_TIME_TEXT: "Entrer la date et l'heure locale ci-dessous pour régler l'heure",
|
||||||
LOCAL_TIME: 'Heure locale',
|
LOCAL_TIME: 'Heure locale',
|
||||||
UTC_TIME: 'Heure UTC',
|
UTC_TIME: 'Heure UTC',
|
||||||
ENABLE_NTP: 'Activer le NTP',
|
ENABLE_NTP: 'Activer le NTP',
|
||||||
NTP_SERVER: 'Serveur NTP',
|
NTP_SERVER: 'Serveur NTP',
|
||||||
TIME_ZONE: 'Fuseau horaire',
|
TIME_ZONE: 'Fuseau horaire',
|
||||||
ACCESS_POINT: 'Point d\'accès',
|
ACCESS_POINT: "Point d'accès",
|
||||||
AP_PROVIDE: 'Activer le Point d\'Accès',
|
AP_PROVIDE: "Activer le Point d'Accès",
|
||||||
AP_PROVIDE_TEXT_1: 'toujours',
|
AP_PROVIDE_TEXT_1: 'toujours',
|
||||||
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
|
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
|
||||||
AP_PROVIDE_TEXT_3: 'jamais',
|
AP_PROVIDE_TEXT_3: 'jamais',
|
||||||
@@ -275,13 +276,13 @@ const fr: Translation = {
|
|||||||
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi', // and enable ETH // TODO translate
|
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi', // and enable ETH // TODO translate
|
||||||
NETWORK_BLANK_BSSID: 'leave blank to use only SSID', // TODO translate
|
NETWORK_BLANK_BSSID: 'leave blank to use only SSID', // TODO translate
|
||||||
TX_POWER: 'Puissance Tx',
|
TX_POWER: 'Puissance Tx',
|
||||||
HOSTNAME: 'Nom d\'hôte',
|
HOSTNAME: "Nom d'hôte",
|
||||||
NETWORK_DISABLE_SLEEP: 'Désactiver le mode veille du WiFi',
|
NETWORK_DISABLE_SLEEP: 'Désactiver le mode veille du WiFi',
|
||||||
NETWORK_LOW_BAND: 'Utiliser une bande passante WiFi plus faible',
|
NETWORK_LOW_BAND: 'Utiliser une bande passante WiFi plus faible',
|
||||||
NETWORK_USE_DNS: 'Activer le service mDNS',
|
NETWORK_USE_DNS: 'Activer le service mDNS',
|
||||||
NETWORK_ENABLE_CORS: 'Activer CORS',
|
NETWORK_ENABLE_CORS: 'Activer CORS',
|
||||||
NETWORK_CORS_ORIGIN: 'Origine CORS',
|
NETWORK_CORS_ORIGIN: 'Origine CORS',
|
||||||
NETWORK_ENABLE_IPV6: 'Activer le support de l\'IPv6',
|
NETWORK_ENABLE_IPV6: "Activer le support de l'IPv6",
|
||||||
NETWORK_FIXED_IP: 'Utiliser une adresse IP fixe',
|
NETWORK_FIXED_IP: 'Utiliser une adresse IP fixe',
|
||||||
NETWORK_GATEWAY: 'Passerelle',
|
NETWORK_GATEWAY: 'Passerelle',
|
||||||
NETWORK_SUBNET: 'Masque de sous-réseau',
|
NETWORK_SUBNET: 'Masque de sous-réseau',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const it: Translation = {
|
const it: Translation = {
|
||||||
LANGUAGE: 'Lingua',
|
LANGUAGE: 'Lingua',
|
||||||
RETRY: 'Riprovare',
|
RETRY: 'Riprovare',
|
||||||
@@ -102,7 +100,8 @@ const it: Translation = {
|
|||||||
CUSTOMIZATIONS: 'Personalizzazione',
|
CUSTOMIZATIONS: 'Personalizzazione',
|
||||||
APPLICATION_RESTARTING: 'EMS-ESP sta riavviando',
|
APPLICATION_RESTARTING: 'EMS-ESP sta riavviando',
|
||||||
INTERFACE_BOARD_PROFILE: 'Profilo scheda di interfaccia',
|
INTERFACE_BOARD_PROFILE: 'Profilo scheda di interfaccia',
|
||||||
BOARD_PROFILE_TEXT: 'Selezionare un profilo di interfaccia pre-configurato dalla lista sottostante o scegliere un profilo personalizzato per configurare le impostazioni del tuo hardware',
|
BOARD_PROFILE_TEXT:
|
||||||
|
'Selezionare un profilo di interfaccia pre-configurato dalla lista sottostante o scegliere un profilo personalizzato per configurare le impostazioni del tuo hardware',
|
||||||
BOARD_PROFILE: 'Profilo Scheda',
|
BOARD_PROFILE: 'Profilo Scheda',
|
||||||
CUSTOM: 'Personalizzazione',
|
CUSTOM: 'Personalizzazione',
|
||||||
GPIO_OF: 'GPIO {0}',
|
GPIO_OF: 'GPIO {0}',
|
||||||
@@ -197,8 +196,10 @@ const it: Translation = {
|
|||||||
ENABLE_OTA: 'Abilita aggiornamenti OTA',
|
ENABLE_OTA: 'Abilita aggiornamenti OTA',
|
||||||
DOWNLOAD_CUSTOMIZATION_TEXT: 'Scarica personalizzazioni entità',
|
DOWNLOAD_CUSTOMIZATION_TEXT: 'Scarica personalizzazioni entità',
|
||||||
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events',
|
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events',
|
||||||
DOWNLOAD_SETTINGS_TEXT: 'Scarica le impostazioni dell applicazione. Fai attenzione quando condividi le tue impostazioni poiché questo file contiene password e altre informazioni di sistema riservate',
|
DOWNLOAD_SETTINGS_TEXT:
|
||||||
UPLOAD_TEXT: 'Carica un nuovo file firmware (.bin) , file delle impostazioni o delle personalizzazioni (.json) di seguito, per un opzione di convalida scaricare dapprima un file "*.MD5" ',
|
'Scarica le impostazioni dell applicazione. Fai attenzione quando condividi le tue impostazioni poiché questo file contiene password e altre informazioni di sistema riservate',
|
||||||
|
UPLOAD_TEXT:
|
||||||
|
'Carica un nuovo file firmware (.bin) , file delle impostazioni o delle personalizzazioni (.json) di seguito, per un opzione di convalida scaricare dapprima un file "*.MD5" ',
|
||||||
UPLOADING: 'Caricamento',
|
UPLOADING: 'Caricamento',
|
||||||
UPLOAD_DROP_TEXT: 'Trascina il file o clicca qui',
|
UPLOAD_DROP_TEXT: 'Trascina il file o clicca qui',
|
||||||
ERROR: 'Errore Inaspettato, prego tenta ancora',
|
ERROR: 'Errore Inaspettato, prego tenta ancora',
|
||||||
@@ -208,7 +209,8 @@ const it: Translation = {
|
|||||||
USER_WARNING: 'Devi avere configurato almeno un utente amministratore',
|
USER_WARNING: 'Devi avere configurato almeno un utente amministratore',
|
||||||
ADD: 'Aggiungi',
|
ADD: 'Aggiungi',
|
||||||
ACCESS_TOKEN_FOR: 'Token di accesso per',
|
ACCESS_TOKEN_FOR: 'Token di accesso per',
|
||||||
ACCESS_TOKEN_TEXT: 'Il token seguente viene utilizzato con le chiamate API REST che richiedono l autorizzazione. Può essere passato come token Bearer nell intestazione di autorizzazione o nel parametro di query URL access_token.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'Il token seguente viene utilizzato con le chiamate API REST che richiedono l autorizzazione. Può essere passato come token Bearer nell intestazione di autorizzazione o nel parametro di query URL access_token.',
|
||||||
GENERATING_TOKEN: 'Generazione token',
|
GENERATING_TOKEN: 'Generazione token',
|
||||||
USER: 'Utente',
|
USER: 'Utente',
|
||||||
MODIFY: 'Modifica',
|
MODIFY: 'Modifica',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const nl: Translation = {
|
const nl: Translation = {
|
||||||
LANGUAGE: 'Taal',
|
LANGUAGE: 'Taal',
|
||||||
RETRY: 'Opnieuw proberen',
|
RETRY: 'Opnieuw proberen',
|
||||||
@@ -208,7 +206,8 @@ const nl: Translation = {
|
|||||||
USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren',
|
USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren',
|
||||||
ADD: 'Toevoegen',
|
ADD: 'Toevoegen',
|
||||||
ACCESS_TOKEN_FOR: 'Access Token voor',
|
ACCESS_TOKEN_FOR: 'Access Token voor',
|
||||||
ACCESS_TOKEN_TEXT: 'Het token hieronder wordt gebruikt voor de REST API calls die authorisatie nodig hebben. Het kan zowel als Bearer token in de Authorization header of in acccess_token URL query parameter gebruikt worden',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'Het token hieronder wordt gebruikt voor de REST API calls die authorisatie nodig hebben. Het kan zowel als Bearer token in de Authorization header of in acccess_token URL query parameter gebruikt worden',
|
||||||
GENERATING_TOKEN: 'Token aan het genereren',
|
GENERATING_TOKEN: 'Token aan het genereren',
|
||||||
USER: 'Gebruiker',
|
USER: 'Gebruiker',
|
||||||
MODIFY: 'Aanpassen',
|
MODIFY: 'Aanpassen',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const no: Translation = {
|
const no: Translation = {
|
||||||
LANGUAGE: 'Språk',
|
LANGUAGE: 'Språk',
|
||||||
RETRY: 'Forsøk igjen',
|
RETRY: 'Forsøk igjen',
|
||||||
@@ -208,7 +206,8 @@ const no: Translation = {
|
|||||||
USER_WARNING: 'Du må ha minst en admin bruker konfigurert',
|
USER_WARNING: 'Du må ha minst en admin bruker konfigurert',
|
||||||
ADD: 'Legg til',
|
ADD: 'Legg til',
|
||||||
ACCESS_TOKEN_FOR: 'Aksess Token for',
|
ACCESS_TOKEN_FOR: 'Aksess Token for',
|
||||||
ACCESS_TOKEN_TEXT: 'Token nedenfor benyttes med REST API-kall som krever autorisering. Den kan sendes med enten som en Bearer token i Authorization-headern eller i access_token URL query-parameter.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'Token nedenfor benyttes med REST API-kall som krever autorisering. Den kan sendes med enten som en Bearer token i Authorization-headern eller i access_token URL query-parameter.',
|
||||||
GENERATING_TOKEN: 'Generer token',
|
GENERATING_TOKEN: 'Generer token',
|
||||||
USER: 'Bruker',
|
USER: 'Bruker',
|
||||||
MODIFY: 'Endre',
|
MODIFY: 'Endre',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { BaseTranslation } from '../i18n-types';
|
import type { BaseTranslation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const pl: BaseTranslation = {
|
const pl: BaseTranslation = {
|
||||||
LANGUAGE: 'Język',
|
LANGUAGE: 'Język',
|
||||||
RETRY: 'Ponów',
|
RETRY: 'Ponów',
|
||||||
@@ -158,7 +156,7 @@ const pl: BaseTranslation = {
|
|||||||
SET_ALL: 'Ustaw wszystko jako',
|
SET_ALL: 'Ustaw wszystko jako',
|
||||||
OPTIONS: 'Opcje',
|
OPTIONS: 'Opcje',
|
||||||
NAME: '{{Nazwa|nazwa|}}',
|
NAME: '{{Nazwa|nazwa|}}',
|
||||||
CUSTOMIZATIONS_RESET: 'Na pewno chcesz usunąć wszystkie personalizacje łącznie z ustawieniami dla czujników temperatury 1-Wire® i urządzeń podłączonych do EMS-ESP?',
|
CUSTOMIZATIONS_RESET: 'Na pewno chcesz usunąć wszystkie personalizacje łącznie z ustawieniami dla czujników temperatury 1-Wire® i urządzeń podłączonych do EMS-ESP?',
|
||||||
SUPPORT_INFORMATION: '{{I|i|}}nformacj{{e|i|}} o systemie',
|
SUPPORT_INFORMATION: '{{I|i|}}nformacj{{e|i|}} o systemie',
|
||||||
HELP_INFORMATION_1: 'Aby uzyskać instrukcje dotyczące konfiguracji EMS-ESP, skorzystaj z wiki w internecie',
|
HELP_INFORMATION_1: 'Aby uzyskać instrukcje dotyczące konfiguracji EMS-ESP, skorzystaj z wiki w internecie',
|
||||||
HELP_INFORMATION_2: 'Aby dołączyć do naszego serwera Discord i komunikować się na żywo ze społecznością',
|
HELP_INFORMATION_2: 'Aby dołączyć do naszego serwera Discord i komunikować się na żywo ze społecznością',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const sk: Translation = {
|
const sk: Translation = {
|
||||||
LANGUAGE: 'Jazyk',
|
LANGUAGE: 'Jazyk',
|
||||||
RETRY: 'Opakovať',
|
RETRY: 'Opakovať',
|
||||||
@@ -209,7 +207,8 @@ const sk: Translation = {
|
|||||||
USER_WARNING: 'Musíte mať nakonfigurovaného aspoň jedného používateľa administrátora',
|
USER_WARNING: 'Musíte mať nakonfigurovaného aspoň jedného používateľa administrátora',
|
||||||
ADD: 'Pridať',
|
ADD: 'Pridať',
|
||||||
ACCESS_TOKEN_FOR: 'Prístupový token pre',
|
ACCESS_TOKEN_FOR: 'Prístupový token pre',
|
||||||
ACCESS_TOKEN_TEXT: 'Nižšie uvedený token sa používa pri volaniach REST API, ktoré vyžadujú autorizáciu. Môže byť odovzdaný buď ako token Bearer v hlavičke Authorization (Autorizácia), alebo v parametri dotazu URL access_token.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'Nižšie uvedený token sa používa pri volaniach REST API, ktoré vyžadujú autorizáciu. Môže byť odovzdaný buď ako token Bearer v hlavičke Authorization (Autorizácia), alebo v parametri dotazu URL access_token.',
|
||||||
GENERATING_TOKEN: 'Generovanie tokenu',
|
GENERATING_TOKEN: 'Generovanie tokenu',
|
||||||
USER: 'Užívateľ',
|
USER: 'Užívateľ',
|
||||||
MODIFY: 'Upraviť',
|
MODIFY: 'Upraviť',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const sv: Translation = {
|
const sv: Translation = {
|
||||||
LANGUAGE: 'Språk',
|
LANGUAGE: 'Språk',
|
||||||
RETRY: 'Försök igen',
|
RETRY: 'Försök igen',
|
||||||
@@ -208,7 +206,8 @@ const sv: Translation = {
|
|||||||
USER_WARNING: 'Du måste ha minst en admin konfigurerad',
|
USER_WARNING: 'Du måste ha minst en admin konfigurerad',
|
||||||
ADD: 'Lägg till',
|
ADD: 'Lägg till',
|
||||||
ACCESS_TOKEN_FOR: 'Access Token för',
|
ACCESS_TOKEN_FOR: 'Access Token för',
|
||||||
ACCESS_TOKEN_TEXT: 'Nedan Token används med REST API-anrop som kräver auktorisering. Den kan skickas med antingen som en Bearer token i Authorization-headern eller i access_token URL query-parametern.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'Nedan Token används med REST API-anrop som kräver auktorisering. Den kan skickas med antingen som en Bearer token i Authorization-headern eller i access_token URL query-parametern.',
|
||||||
GENERATING_TOKEN: 'Genererar token',
|
GENERATING_TOKEN: 'Genererar token',
|
||||||
USER: 'Användare',
|
USER: 'Användare',
|
||||||
MODIFY: 'Ändra',
|
MODIFY: 'Ändra',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { Translation } from '../i18n-types';
|
import type { Translation } from '../i18n-types';
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
|
|
||||||
const tr: Translation = {
|
const tr: Translation = {
|
||||||
LANGUAGE: 'Dil',
|
LANGUAGE: 'Dil',
|
||||||
RETRY: 'Tekrar Dene',
|
RETRY: 'Tekrar Dene',
|
||||||
@@ -208,7 +206,8 @@ const tr: Translation = {
|
|||||||
USER_WARNING: 'En az bir yönetici kullanıcısı ayarlamanız gerekmektedir',
|
USER_WARNING: 'En az bir yönetici kullanıcısı ayarlamanız gerekmektedir',
|
||||||
ADD: 'Ekle',
|
ADD: 'Ekle',
|
||||||
ACCESS_TOKEN_FOR: 'Erişim Jetonunun sahibi',
|
ACCESS_TOKEN_FOR: 'Erişim Jetonunun sahibi',
|
||||||
ACCESS_TOKEN_TEXT: 'Aşağıdaki Jeton yetki gerektiren REST API çağrıları ile kullanılmaktadır. Taşıyıcı Jeton olarak yetkilendirme başlığında yada erişim jetonu olarak URL sorgu parametresinde kullanılabilir.',
|
ACCESS_TOKEN_TEXT:
|
||||||
|
'Aşağıdaki Jeton yetki gerektiren REST API çağrıları ile kullanılmaktadır. Taşıyıcı Jeton olarak yetkilendirme başlığında yada erişim jetonu olarak URL sorgu parametresinde kullanılabilir.',
|
||||||
GENERATING_TOKEN: 'Jeton oluşturuluyor',
|
GENERATING_TOKEN: 'Jeton oluşturuluyor',
|
||||||
USER: 'Kullanıcı',
|
USER: 'Kullanıcı',
|
||||||
MODIFY: 'Düzenle',
|
MODIFY: 'Düzenle',
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom';
|
import {
|
||||||
|
Route,
|
||||||
|
RouterProvider,
|
||||||
|
createBrowserRouter,
|
||||||
|
createRoutesFromElements
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import App from 'App';
|
import App from 'App';
|
||||||
|
|
||||||
const router = createBrowserRouter(createRoutesFromElements(<Route path="/*" element={<App />} />));
|
const router = createBrowserRouter(
|
||||||
|
createRoutesFromElements(<Route path="/*" element={<App />} />)
|
||||||
|
);
|
||||||
|
|
||||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ import { toast } from 'react-toastify';
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { Box, Button, Checkbox, Divider, Grid, InputAdornment, MenuItem, TextField, Typography } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
InputAdornment,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import * as SystemApi from 'api/system';
|
import * as SystemApi from 'api/system';
|
||||||
|
|
||||||
@@ -61,7 +71,12 @@ const ApplicationSettings: FC = () => {
|
|||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
|
const updateFormValue = updateValueDirty(
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
updateDataValue
|
||||||
|
);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -220,7 +235,9 @@ const ApplicationSettings: FC = () => {
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="dallas_gpio"
|
name="dallas_gpio"
|
||||||
label={LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'}
|
label={
|
||||||
|
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
|
||||||
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={numberValue(data.dallas_gpio)}
|
value={numberValue(data.dallas_gpio)}
|
||||||
@@ -322,7 +339,13 @@ const ApplicationSettings: FC = () => {
|
|||||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
|
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<TextField
|
<TextField
|
||||||
name="tx_mode"
|
name="tx_mode"
|
||||||
@@ -396,54 +419,120 @@ const ApplicationSettings: FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
{data.led_gpio !== 0 && (
|
{data.led_gpio !== 0 && (
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.hide_led}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="hide_led"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.HIDE_LED()}
|
label={LL.HIDE_LED()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.telnet_enabled} onChange={updateFormValue} name="telnet_enabled" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.telnet_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="telnet_enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_TELNET()}
|
label={LL.ENABLE_TELNET()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.analog_enabled} onChange={updateFormValue} name="analog_enabled" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.analog_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="analog_enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_ANALOG()}
|
label={LL.ENABLE_ANALOG()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.fahrenheit}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="fahrenheit"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.CONVERT_FAHRENHEIT()}
|
label={LL.CONVERT_FAHRENHEIT()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.notoken_api} onChange={updateFormValue} name="notoken_api" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.notoken_api}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="notoken_api"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.BYPASS_TOKEN()}
|
label={LL.BYPASS_TOKEN()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.readonly_mode}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="readonly_mode"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.READONLY()}
|
label={LL.READONLY()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.low_clock} onChange={updateFormValue} name="low_clock" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.low_clock}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="low_clock"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.UNDERCLOCK_CPU()}
|
label={LL.UNDERCLOCK_CPU()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.boiler_heatingoff} onChange={updateFormValue} name="boiler_heatingoff" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.boiler_heatingoff}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="boiler_heatingoff"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.HEATINGOFF()}
|
label={LL.HEATINGOFF()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<Grid container spacing={0} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.shower_timer} onChange={updateFormValue} name="shower_timer" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.shower_timer}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="shower_timer"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_SHOWER_TIMER()}
|
label={LL.ENABLE_SHOWER_TIMER()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.shower_alert} onChange={updateFormValue} name="shower_alert" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.shower_alert}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="shower_alert"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_SHOWER_ALERT()}
|
label={LL.ENABLE_SHOWER_ALERT()}
|
||||||
disabled={!data.shower_timer}
|
disabled={!data.shower_timer}
|
||||||
/>
|
/>
|
||||||
@@ -465,7 +554,9 @@ const ApplicationSettings: FC = () => {
|
|||||||
name="shower_alert_trigger"
|
name="shower_alert_trigger"
|
||||||
label={LL.TRIGGER_TIME()}
|
label={LL.TRIGGER_TIME()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={numberValue(data.shower_alert_trigger)}
|
value={numberValue(data.shower_alert_trigger)}
|
||||||
@@ -481,7 +572,9 @@ const ApplicationSettings: FC = () => {
|
|||||||
name="shower_alert_coldshot"
|
name="shower_alert_coldshot"
|
||||||
label={LL.COLD_SHOT_DURATION()}
|
label={LL.COLD_SHOT_DURATION()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={numberValue(data.shower_alert_coldshot)}
|
value={numberValue(data.shower_alert_coldshot)}
|
||||||
@@ -497,7 +590,13 @@ const ApplicationSettings: FC = () => {
|
|||||||
<Typography sx={{ pt: 3 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 3 }} variant="h6" color="primary">
|
||||||
{LL.FORMATTING_OPTIONS()}
|
{LL.FORMATTING_OPTIONS()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<TextField
|
<TextField
|
||||||
name="bool_dashboard"
|
name="bool_dashboard"
|
||||||
@@ -556,7 +655,13 @@ const ApplicationSettings: FC = () => {
|
|||||||
{LL.TEMP_SENSORS()}
|
{LL.TEMP_SENSORS()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.dallas_parasite} onChange={updateFormValue} name="dallas_parasite" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.dallas_parasite}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="dallas_parasite"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ENABLE_PARASITE()}
|
label={LL.ENABLE_PARASITE()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
@@ -566,7 +671,13 @@ const ApplicationSettings: FC = () => {
|
|||||||
{LL.LOGGING()}
|
{LL.LOGGING()}
|
||||||
</Typography>
|
</Typography>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={data.trace_raw} onChange={updateFormValue} name="trace_raw" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.trace_raw}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="trace_raw"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.LOG_HEX()}
|
label={LL.LOG_HEX()}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
@@ -582,7 +693,13 @@ const ApplicationSettings: FC = () => {
|
|||||||
label={LL.ENABLE_SYSLOG()}
|
label={LL.ENABLE_SYSLOG()}
|
||||||
/>
|
/>
|
||||||
{data.syslog_enabled && (
|
{data.syslog_enabled && (
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
@@ -636,7 +753,9 @@ const ApplicationSettings: FC = () => {
|
|||||||
name="syslog_mark_interval"
|
name="syslog_mark_interval"
|
||||||
label={LL.MARK_INTERVAL()}
|
label={LL.MARK_INTERVAL()}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -651,7 +770,12 @@ const ApplicationSettings: FC = () => {
|
|||||||
)}
|
)}
|
||||||
{restartNeeded && (
|
{restartNeeded && (
|
||||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={restart}
|
||||||
|
>
|
||||||
{LL.RESTART()}
|
{LL.RESTART()}
|
||||||
</Button>
|
</Button>
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
|
|||||||
@@ -10,10 +10,24 @@ import RefreshIcon from '@mui/icons-material/Refresh';
|
|||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { Box, Button, Typography } from '@mui/material';
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
|
|
||||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
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 { useTheme } from '@table-library/react-table-library/theme';
|
||||||
import { updateState, useRequest } from 'alova';
|
import { updateState, useRequest } from 'alova';
|
||||||
import { BlockNavigation, ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
import {
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
import * as EMSESP from './api';
|
import * as EMSESP from './api';
|
||||||
@@ -171,8 +185,13 @@ const CustomEntities: FC = () => {
|
|||||||
|
|
||||||
updateState('entities', (data: EntityItem[]) => {
|
updateState('entities', (data: EntityItem[]) => {
|
||||||
const new_data = creating
|
const new_data = creating
|
||||||
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
|
? [
|
||||||
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
|
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
||||||
|
updatedItem
|
||||||
|
]
|
||||||
|
: data.map((ei) =>
|
||||||
|
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
|
||||||
|
);
|
||||||
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
@@ -201,7 +220,8 @@ const CustomEntities: FC = () => {
|
|||||||
return value === undefined
|
return value === undefined
|
||||||
? ''
|
? ''
|
||||||
: typeof value === 'number'
|
: typeof value === 'number'
|
||||||
? new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
? new Intl.NumberFormat().format(value) +
|
||||||
|
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
|
||||||
: (value as string);
|
: (value as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +235,11 @@ const CustomEntities: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table data={{ nodes: entities.filter((ei) => !ei.deleted) }} theme={entity_theme} layout={{ custom: true }}>
|
<Table
|
||||||
|
data={{ nodes: entities.filter((ei) => !ei.deleted) }}
|
||||||
|
theme={entity_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
{(tableList: EntityItem[]) => (
|
{(tableList: EntityItem[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -233,12 +257,18 @@ const CustomEntities: FC = () => {
|
|||||||
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
|
||||||
<Cell>
|
<Cell>
|
||||||
{ei.name}
|
{ei.name}
|
||||||
{ei.writeable && <EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
|
{ei.writeable && (
|
||||||
|
<EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
|
)}
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}</Cell>
|
|
||||||
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
||||||
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
|
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
|
||||||
<Cell>{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}</Cell>
|
<Cell>
|
||||||
|
{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}
|
||||||
|
</Cell>
|
||||||
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
@@ -273,7 +303,12 @@ const CustomEntities: FC = () => {
|
|||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
{numChanges > 0 && (
|
{numChanges > 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDialogCancel}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -289,10 +324,20 @@ const CustomEntities: FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchEntities}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={fetchEntities}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<AddIcon />} variant="outlined" color="primary" onClick={addEntityItem}>
|
<Button
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={addEntityItem}
|
||||||
|
>
|
||||||
{LL.ADD(0)}
|
{LL.ADD(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -142,7 +142,13 @@ const CustomEntitiesDialog = ({
|
|||||||
<>
|
<>
|
||||||
<Grid item xs={4} mt={3}>
|
<Grid item xs={4} mt={3}>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={editItem.writeable} onChange={updateFormValue} name="writeable" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={editItem.writeable}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="writeable"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.WRITEABLE()}
|
label={LL.WRITEABLE()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -157,7 +163,11 @@ const CustomEntitiesDialog = ({
|
|||||||
value={editItem.device_id as string}
|
value={editItem.device_id as string}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
inputProps={{ style: { textTransform: 'uppercase' } }}
|
inputProps={{ style: { textTransform: 'uppercase' } }}
|
||||||
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">0x</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
@@ -170,7 +180,11 @@ const CustomEntitiesDialog = ({
|
|||||||
value={editItem.type_id}
|
value={editItem.type_id}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
inputProps={{ style: { textTransform: 'uppercase' } }}
|
inputProps={{ style: { textTransform: 'uppercase' } }}
|
||||||
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">0x</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
@@ -207,55 +221,57 @@ const CustomEntitiesDialog = ({
|
|||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{editItem.value_type !== DeviceValueType.BOOL && editItem.value_type !== DeviceValueType.STRING && (
|
{editItem.value_type !== DeviceValueType.BOOL &&
|
||||||
<>
|
editItem.value_type !== DeviceValueType.STRING && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<TextField
|
||||||
|
name="factor"
|
||||||
|
label={LL.FACTOR()}
|
||||||
|
value={numberValue(editItem.factor)}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
type="number"
|
||||||
|
inputProps={{ step: '0.001' }}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<TextField
|
||||||
|
name="uom"
|
||||||
|
label={LL.UNIT()}
|
||||||
|
value={editItem.uom}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
onChange={updateFormValue}
|
||||||
|
select
|
||||||
|
>
|
||||||
|
{DeviceValueUOM_s.map((val, i) => (
|
||||||
|
<MenuItem key={i} value={i}>
|
||||||
|
{val}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{editItem.value_type === DeviceValueType.STRING &&
|
||||||
|
editItem.device_id !== '0' && (
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextField
|
<TextField
|
||||||
name="factor"
|
name="factor"
|
||||||
label={LL.FACTOR()}
|
label="Bytes"
|
||||||
value={numberValue(editItem.factor)}
|
value={editItem.factor}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
type="number"
|
type="number"
|
||||||
inputProps={{ step: '0.001' }}
|
inputProps={{ min: '1', max: '27', step: '1' }}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
)}
|
||||||
<TextField
|
|
||||||
name="uom"
|
|
||||||
label={LL.UNIT()}
|
|
||||||
value={editItem.uom}
|
|
||||||
margin="normal"
|
|
||||||
fullWidth
|
|
||||||
onChange={updateFormValue}
|
|
||||||
select
|
|
||||||
>
|
|
||||||
{DeviceValueUOM_s.map((val, i) => (
|
|
||||||
<MenuItem key={i} value={i}>
|
|
||||||
{val}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{editItem.value_type === DeviceValueType.STRING && editItem.device_id !== '0' && (
|
|
||||||
<Grid item xs={4}>
|
|
||||||
<TextField
|
|
||||||
name="factor"
|
|
||||||
label="Bytes"
|
|
||||||
value={editItem.factor}
|
|
||||||
variant="outlined"
|
|
||||||
onChange={updateFormValue}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
type="number"
|
|
||||||
inputProps={{ min: '1', max: '27', step: '1' }}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -264,15 +280,30 @@ const CustomEntitiesDialog = ({
|
|||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!creating && (
|
{!creating && (
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
|
<Button
|
||||||
|
startIcon={<RemoveIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="warning"
|
||||||
|
onClick={remove}
|
||||||
|
>
|
||||||
{LL.REMOVE()}
|
{LL.REMOVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary">
|
<Button
|
||||||
|
startIcon={creating ? <AddIcon /> : <DoneIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -27,11 +27,25 @@ import {
|
|||||||
|
|
||||||
import * as SystemApi from 'api/system';
|
import * as SystemApi from 'api/system';
|
||||||
|
|
||||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
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 { useTheme } from '@table-library/react-table-library/theme';
|
||||||
import { dialogStyle } from 'CustomTheme';
|
import { dialogStyle } from 'CustomTheme';
|
||||||
import { useRequest } from 'alova';
|
import { useRequest } from 'alova';
|
||||||
import { BlockNavigation, ButtonRow, MessageBox, SectionContent, useLayoutTitle } from 'components';
|
import {
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
MessageBox,
|
||||||
|
SectionContent,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
import RestartMonitor from 'framework/system/RestartMonitor';
|
import RestartMonitor from 'framework/system/RestartMonitor';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
@@ -63,7 +77,9 @@ const Customization: FC = () => {
|
|||||||
// fetch devices first
|
// fetch devices first
|
||||||
const { data: devices } = useRequest(EMSESP.readDevices);
|
const { data: devices } = useRequest(EMSESP.readDevices);
|
||||||
|
|
||||||
const [selectedDevice, setSelectedDevice] = useState<number>(Number(useLocation().state) || -1);
|
const [selectedDevice, setSelectedDevice] = useState<number>(
|
||||||
|
Number(useLocation().state) || -1
|
||||||
|
);
|
||||||
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
|
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
|
||||||
|
|
||||||
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
|
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
|
||||||
@@ -71,7 +87,8 @@ const Customization: FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { send: writeCustomizationEntities } = useRequest(
|
const { send: writeCustomizationEntities } = useRequest(
|
||||||
(data: { id: number; entity_ids: string[] }) => EMSESP.writeCustomizationEntities(data),
|
(data: { id: number; entity_ids: string[] }) =>
|
||||||
|
EMSESP.writeCustomizationEntities(data),
|
||||||
{
|
{
|
||||||
immediate: false
|
immediate: false
|
||||||
}
|
}
|
||||||
@@ -86,7 +103,15 @@ const Customization: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setOriginalSettings = (data: DeviceEntity[]) => {
|
const setOriginalSettings = (data: DeviceEntity[]) => {
|
||||||
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
|
setDeviceEntities(
|
||||||
|
data.map((de) => ({
|
||||||
|
...de,
|
||||||
|
o_m: de.m,
|
||||||
|
o_cn: de.cn,
|
||||||
|
o_mi: de.mi,
|
||||||
|
o_ma: de.ma
|
||||||
|
}))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSuccess((event) => {
|
onSuccess((event) => {
|
||||||
@@ -166,7 +191,12 @@ const Customization: FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function hasEntityChanged(de: DeviceEntity) {
|
function hasEntityChanged(de: DeviceEntity) {
|
||||||
return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi;
|
return (
|
||||||
|
(de?.cn || '') !== (de?.o_cn || '') ||
|
||||||
|
de.m !== de.o_m ||
|
||||||
|
de.ma !== de.o_ma ||
|
||||||
|
de.mi !== de.o_mi
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -221,8 +251,11 @@ const Customization: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatName = (de: DeviceEntity, withShortname: boolean) =>
|
const formatName = (de: DeviceEntity, withShortname: boolean) =>
|
||||||
(de.n && de.n[0] === '!' ? LL.COMMAND(1) + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) +
|
(de.n && de.n[0] === '!'
|
||||||
(withShortname ? ' ' + de.id : '');
|
? LL.COMMAND(1) + ': ' + de.n.slice(1)
|
||||||
|
: de.cn && de.cn !== ''
|
||||||
|
? de.cn
|
||||||
|
: de.n) + (withShortname ? ' ' + de.id : '');
|
||||||
|
|
||||||
const getMaskNumber = (newMask: string[]) => {
|
const getMaskNumber = (newMask: string[]) => {
|
||||||
let new_mask = 0;
|
let new_mask = 0;
|
||||||
@@ -253,7 +286,8 @@ const Customization: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filter_entity = (de: DeviceEntity) =>
|
const filter_entity = (de: DeviceEntity) =>
|
||||||
(de.m & selectedFilters || !selectedFilters) && formatName(de, true).includes(search.toLocaleLowerCase());
|
(de.m & selectedFilters || !selectedFilters) &&
|
||||||
|
formatName(de, true).includes(search.toLocaleLowerCase());
|
||||||
|
|
||||||
const maskDisabled = (set: boolean) => {
|
const maskDisabled = (set: boolean) => {
|
||||||
setDeviceEntities(
|
setDeviceEntities(
|
||||||
@@ -262,8 +296,14 @@ const Customization: FC = () => {
|
|||||||
return {
|
return {
|
||||||
...de,
|
...de,
|
||||||
m: set
|
m: set
|
||||||
? de.m | (DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
|
? de.m |
|
||||||
: de.m & ~(DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
|
(DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE)
|
||||||
|
: de.m &
|
||||||
|
~(
|
||||||
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE |
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE
|
||||||
|
)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return de;
|
return de;
|
||||||
@@ -288,7 +328,11 @@ const Customization: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de)));
|
setDeviceEntities(
|
||||||
|
deviceEntities?.map((de) =>
|
||||||
|
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDialogSave = (updatedItem: DeviceEntity) => {
|
const onDialogSave = (updatedItem: DeviceEntity) => {
|
||||||
@@ -330,7 +374,10 @@ const Customization: FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeCustomizationEntities({ id: selectedDevice, entity_ids: masked_entities }).catch((error: Error) => {
|
await writeCustomizationEntities({
|
||||||
|
id: selectedDevice,
|
||||||
|
entity_ids: masked_entities
|
||||||
|
}).catch((error: Error) => {
|
||||||
if (error.message === 'Reboot required') {
|
if (error.message === 'Reboot required') {
|
||||||
setRestartNeeded(true);
|
setRestartNeeded(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -376,14 +423,26 @@ const Customization: FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Box color="warning.main">
|
<Box color="warning.main">
|
||||||
<Typography variant="body2" mt={1}>
|
<Typography variant="body2" mt={1}>
|
||||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
|
||||||
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
|
||||||
|
{LL.CUSTOMIZATIONS_HELP_4()}
|
||||||
|
<OptionIcon type="web_exclude" isSet={true} />=
|
||||||
|
{LL.CUSTOMIZATIONS_HELP_5()}
|
||||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
|
<Grid
|
||||||
|
container
|
||||||
|
mb={1}
|
||||||
|
mt={0}
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<Grid item xs={2}>
|
<Grid item xs={2}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -455,11 +514,16 @@ const Customization: FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography variant="subtitle2" color="primary">
|
<Typography variant="subtitle2" color="primary">
|
||||||
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length} {LL.ENTITIES(deviceEntities.length)}
|
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length}
|
||||||
|
{LL.ENTITIES(deviceEntities.length)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
<Table
|
||||||
|
data={{ nodes: shown_data }}
|
||||||
|
theme={entities_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
{(tableList: DeviceEntity[]) => (
|
{(tableList: DeviceEntity[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -479,13 +543,20 @@ const Customization: FC = () => {
|
|||||||
</Cell>
|
</Cell>
|
||||||
<Cell>
|
<Cell>
|
||||||
{formatName(de, false)} (
|
{formatName(de, false)} (
|
||||||
<Link target="_blank" href={APIURL + selectedDeviceName + '/' + de.id}>
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={APIURL + selectedDeviceName + '/' + de.id}
|
||||||
|
>
|
||||||
{de.id}
|
{de.id}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
|
<Cell>
|
||||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
|
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}
|
||||||
|
</Cell>
|
||||||
<Cell>{formatValue(de.v)}</Cell>
|
<Cell>{formatValue(de.v)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
@@ -498,14 +569,28 @@ const Customization: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderResetDialog = () => (
|
const renderResetDialog = () => (
|
||||||
<Dialog sx={dialogStyle} open={confirmReset} onClose={() => setConfirmReset(false)}>
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmReset}
|
||||||
|
onClose={() => setConfirmReset(false)}
|
||||||
|
>
|
||||||
<DialogTitle>{LL.RESET(1)}</DialogTitle>
|
<DialogTitle>{LL.RESET(1)}</DialogTitle>
|
||||||
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setConfirmReset(false)}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<SettingsBackupRestoreIcon />} variant="outlined" onClick={resetCustomization} color="error">
|
<Button
|
||||||
|
startIcon={<SettingsBackupRestoreIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={resetCustomization}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
{LL.RESET(0)}
|
{LL.RESET(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -518,7 +603,12 @@ const Customization: FC = () => {
|
|||||||
{deviceEntities && renderDeviceData()}
|
{deviceEntities && renderDeviceData()}
|
||||||
{restartNeeded && (
|
{restartNeeded && (
|
||||||
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
|
||||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={restart}
|
||||||
|
>
|
||||||
{LL.RESTART()}
|
{LL.RESTART()}
|
||||||
</Button>
|
</Button>
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ interface SettingsCustomizationDialogProps {
|
|||||||
selectedItem: DeviceEntity;
|
selectedItem: DeviceEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
|
const CustomizationDialog = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedItem
|
||||||
|
}: SettingsCustomizationDialogProps) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
@@ -38,7 +43,9 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
|
|||||||
const updateFormValue = updateValue(setEditItem);
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
const isWriteableNumber =
|
const isWriteableNumber =
|
||||||
typeof editItem.v === 'number' && editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY);
|
typeof editItem.v === 'number' &&
|
||||||
|
editItem.w &&
|
||||||
|
!(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -52,7 +59,12 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
|
|||||||
};
|
};
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
if (isWriteableNumber && editItem.mi && editItem.ma && editItem.mi > editItem?.ma) {
|
if (
|
||||||
|
isWriteableNumber &&
|
||||||
|
editItem.mi &&
|
||||||
|
editItem.ma &&
|
||||||
|
editItem.mi > editItem?.ma
|
||||||
|
) {
|
||||||
setError(true);
|
setError(true);
|
||||||
} else {
|
} else {
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
@@ -140,10 +152,20 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
|
|||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
|
<Button
|
||||||
|
startIcon={<DoneIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{LL.UPDATE()}
|
{LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/
|
|||||||
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
||||||
import { FaSolarPanel } from 'react-icons/fa';
|
import { FaSolarPanel } from 'react-icons/fa';
|
||||||
import { GiHeatHaze, GiTap } from 'react-icons/gi';
|
import { GiHeatHaze, GiTap } from 'react-icons/gi';
|
||||||
import { MdOutlineDevices, MdOutlinePool, MdOutlineSensors, MdThermostatAuto } from 'react-icons/md';
|
import {
|
||||||
|
MdOutlineDevices,
|
||||||
|
MdOutlinePool,
|
||||||
|
MdOutlineSensors,
|
||||||
|
MdThermostatAuto
|
||||||
|
} from 'react-icons/md';
|
||||||
import { TiFlowSwitch } from 'react-icons/ti';
|
import { TiFlowSwitch } from 'react-icons/ti';
|
||||||
import { VscVmConnect } from 'react-icons/vsc';
|
import { VscVmConnect } from 'react-icons/vsc';
|
||||||
|
|
||||||
@@ -47,7 +52,11 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
|
|||||||
case DeviceType.POOL:
|
case DeviceType.POOL:
|
||||||
return <MdOutlinePool />;
|
return <MdOutlinePool />;
|
||||||
case DeviceType.CUSTOM:
|
case DeviceType.CUSTOM:
|
||||||
return <PlaylistAddIcon sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }} />;
|
return (
|
||||||
|
<PlaylistAddIcon
|
||||||
|
sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { IconContext } from 'react-icons';
|
import { IconContext } from 'react-icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -35,7 +41,15 @@ import {
|
|||||||
|
|
||||||
import { useRowSelect } from '@table-library/react-table-library/select';
|
import { useRowSelect } from '@table-library/react-table-library/select';
|
||||||
import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
|
import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
|
||||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
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 { useTheme } from '@table-library/react-table-library/theme';
|
||||||
import type { Action, State } from '@table-library/react-table-library/types/common';
|
import type { Action, State } from '@table-library/react-table-library/types/common';
|
||||||
import { dialogStyle } from 'CustomTheme';
|
import { dialogStyle } from 'CustomTheme';
|
||||||
@@ -67,19 +81,25 @@ const Devices: FC = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.DEVICES());
|
useLayoutTitle(LL.DEVICES());
|
||||||
|
|
||||||
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
|
const { data: coreData, send: readCoreData } = useRequest(
|
||||||
initialData: {
|
() => EMSESP.readCoreData(),
|
||||||
connected: true,
|
{
|
||||||
devices: []
|
initialData: {
|
||||||
|
connected: true,
|
||||||
|
devices: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const { data: deviceData, send: readDeviceData } = useRequest((id: number) => EMSESP.readDeviceData(id), {
|
const { data: deviceData, send: readDeviceData } = useRequest(
|
||||||
initialData: {
|
(id: number) => EMSESP.readDeviceData(id),
|
||||||
data: []
|
{
|
||||||
},
|
initialData: {
|
||||||
immediate: false
|
data: []
|
||||||
});
|
},
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { loading: submitting, send: writeDeviceValue } = useRequest(
|
const { loading: submitting, send: writeDeviceValue } = useRequest(
|
||||||
(data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data),
|
(data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data),
|
||||||
@@ -235,9 +255,14 @@ const Devices: FC = () => {
|
|||||||
},
|
},
|
||||||
sortToggleType: SortToggleType.AlternateWithReset,
|
sortToggleType: SortToggleType.AlternateWithReset,
|
||||||
sortFns: {
|
sortFns: {
|
||||||
NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))),
|
NAME: (array) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
array.sort((a, b) =>
|
||||||
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
|
a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))
|
||||||
|
),
|
||||||
|
|
||||||
|
VALUE: (array) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||||
|
array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -300,35 +325,59 @@ const Devices: FC = () => {
|
|||||||
if (sc === '' || sc === '""') {
|
if (sc === '' || sc === '""') {
|
||||||
return sc;
|
return sc;
|
||||||
}
|
}
|
||||||
if (sc.includes('"') || sc.includes(';') || sc.includes('\n') || sc.includes('\r')) {
|
if (
|
||||||
|
sc.includes('"') ||
|
||||||
|
sc.includes(';') ||
|
||||||
|
sc.includes('\n') ||
|
||||||
|
sc.includes('\r')
|
||||||
|
) {
|
||||||
return '"' + sc.replace(/"/g, '""') + '"';
|
return '"' + sc.replace(/"/g, '""') + '"';
|
||||||
}
|
}
|
||||||
return sc;
|
return sc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask;
|
const hasMask = (id: string, mask: number) =>
|
||||||
|
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||||
|
|
||||||
const handleDownloadCsv = () => {
|
const handleDownloadCsv = () => {
|
||||||
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id);
|
const deviceIndex = coreData.devices.findIndex(
|
||||||
|
(d) => d.id === device_select.state.id
|
||||||
|
);
|
||||||
if (deviceIndex === -1) {
|
if (deviceIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filename = coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n;
|
const filename =
|
||||||
|
coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ accessor: (dv: DeviceValue) => dv.id.slice(2), name: LL.ENTITY_NAME(0) },
|
|
||||||
{
|
{
|
||||||
accessor: (dv: DeviceValue) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v),
|
accessor: (dv: DeviceValue) => dv.id.slice(2),
|
||||||
|
name: LL.ENTITY_NAME(0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: (dv: DeviceValue) =>
|
||||||
|
typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v,
|
||||||
name: LL.VALUE(1)
|
name: LL.VALUE(1)
|
||||||
},
|
},
|
||||||
{ accessor: (dv: DeviceValue) => DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, ''), name: 'UoM' },
|
|
||||||
{
|
{
|
||||||
accessor: (dv: DeviceValue) => (dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) ? 'yes' : 'no'),
|
accessor: (dv: DeviceValue) =>
|
||||||
|
DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, ''),
|
||||||
|
name: 'UoM'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: (dv: DeviceValue) =>
|
||||||
|
dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) ? 'yes' : 'no',
|
||||||
name: LL.WRITEABLE()
|
name: LL.WRITEABLE()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: (dv: DeviceValue) =>
|
accessor: (dv: DeviceValue) =>
|
||||||
dv.h ? dv.h : dv.l ? dv.l.join(' | ') : dv.m !== undefined && dv.x !== undefined ? dv.m + ', ' + dv.x : '',
|
dv.h
|
||||||
|
? dv.h
|
||||||
|
: dv.l
|
||||||
|
? dv.l.join(' | ')
|
||||||
|
: dv.m !== undefined && dv.x !== undefined
|
||||||
|
? dv.m + ', ' + dv.x
|
||||||
|
: '',
|
||||||
name: 'Range'
|
name: 'Range'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -341,10 +390,13 @@ const Devices: FC = () => {
|
|||||||
(csvString: string, rowItem: DeviceValue) =>
|
(csvString: string, rowItem: DeviceValue) =>
|
||||||
csvString +
|
csvString +
|
||||||
columns
|
columns
|
||||||
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) => escapeCsvCell(accessor(rowItem) as string))
|
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) =>
|
||||||
|
escapeCsvCell(accessor(rowItem) as string)
|
||||||
|
)
|
||||||
.join(';') +
|
.join(';') +
|
||||||
'\r\n',
|
'\r\n',
|
||||||
columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') + '\r\n'
|
columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') +
|
||||||
|
'\r\n'
|
||||||
);
|
);
|
||||||
|
|
||||||
const csvFile = new Blob([csvData], { type: 'text/csv;charset:utf-8' });
|
const csvFile = new Blob([csvData], { type: 'text/csv;charset:utf-8' });
|
||||||
@@ -381,45 +433,76 @@ const Devices: FC = () => {
|
|||||||
|
|
||||||
const renderDeviceDetails = () => {
|
const renderDeviceDetails = () => {
|
||||||
if (showDeviceInfo) {
|
if (showDeviceInfo) {
|
||||||
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id);
|
const deviceIndex = coreData.devices.findIndex(
|
||||||
|
(d) => d.id === device_select.state.id
|
||||||
|
);
|
||||||
if (deviceIndex === -1) {
|
if (deviceIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={showDeviceInfo} onClose={() => setShowDeviceInfo(false)}>
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={showDeviceInfo}
|
||||||
|
onClose={() => setShowDeviceInfo(false)}
|
||||||
|
>
|
||||||
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
|
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<List dense={true}>
|
<List dense={true}>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={LL.TYPE(0)} secondary={coreData.devices[deviceIndex].tn} />
|
<ListItemText
|
||||||
|
primary={LL.TYPE(0)}
|
||||||
|
secondary={coreData.devices[deviceIndex].tn}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={LL.NAME(0)} secondary={coreData.devices[deviceIndex].n} />
|
<ListItemText
|
||||||
|
primary={LL.NAME(0)}
|
||||||
|
secondary={coreData.devices[deviceIndex].n}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && (
|
{coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && (
|
||||||
<>
|
<>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={LL.BRAND()} secondary={coreData.devices[deviceIndex].b} />
|
<ListItemText
|
||||||
|
primary={LL.BRAND()}
|
||||||
|
secondary={coreData.devices[deviceIndex].b}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={LL.ID_OF(LL.DEVICE())}
|
primary={LL.ID_OF(LL.DEVICE())}
|
||||||
secondary={'0x' + ('00' + coreData.devices[deviceIndex].d.toString(16).toUpperCase()).slice(-2)}
|
secondary={
|
||||||
|
'0x' +
|
||||||
|
(
|
||||||
|
'00' +
|
||||||
|
coreData.devices[deviceIndex].d.toString(16).toUpperCase()
|
||||||
|
).slice(-2)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={LL.ID_OF(LL.PRODUCT())} secondary={coreData.devices[deviceIndex].p} />
|
<ListItemText
|
||||||
|
primary={LL.ID_OF(LL.PRODUCT())}
|
||||||
|
secondary={coreData.devices[deviceIndex].p}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary={LL.VERSION()} secondary={coreData.devices[deviceIndex].v} />
|
<ListItemText
|
||||||
|
primary={LL.VERSION()}
|
||||||
|
secondary={coreData.devices[deviceIndex].v}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="outlined" onClick={() => setShowDeviceInfo(false)} color="secondary">
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setShowDeviceInfo(false)}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CLOSE()}
|
{LL.CLOSE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -429,11 +512,24 @@ const Devices: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderCoreData = () => (
|
const renderCoreData = () => (
|
||||||
<IconContext.Provider value={{ color: 'lightblue', size: '18', style: { verticalAlign: 'middle' } }}>
|
<IconContext.Provider
|
||||||
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
|
value={{
|
||||||
|
color: 'lightblue',
|
||||||
|
size: '18',
|
||||||
|
style: { verticalAlign: 'middle' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!coreData.connected && (
|
||||||
|
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
|
||||||
|
)}
|
||||||
|
|
||||||
{coreData.connected && (
|
{coreData.connected && (
|
||||||
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
|
<Table
|
||||||
|
data={{ nodes: coreData.devices }}
|
||||||
|
select={device_select}
|
||||||
|
theme={device_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
{(tableList: Device[]) => (
|
{(tableList: Device[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -451,7 +547,9 @@ const Devices: FC = () => {
|
|||||||
</Cell>
|
</Cell>
|
||||||
<Cell>
|
<Cell>
|
||||||
{device.n}
|
{device.n}
|
||||||
<span style={{ color: 'lightblue' }}> ({device.e})</span>
|
<span style={{ color: 'lightblue' }}>
|
||||||
|
({device.e})
|
||||||
|
</span>
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell stiff>{device.tn}</Cell>
|
<Cell stiff>{device.tn}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -481,8 +579,12 @@ const Devices: FC = () => {
|
|||||||
const renderNameCell = (dv: DeviceValue) => (
|
const renderNameCell = (dv: DeviceValue) => (
|
||||||
<>
|
<>
|
||||||
{dv.id.slice(2)}
|
{dv.id.slice(2)}
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && <StarIcon color="primary" sx={{ fontSize: 12 }} />}
|
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && <EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
|
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
|
)}
|
||||||
|
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||||
|
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
|
)}
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
||||||
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
)}
|
)}
|
||||||
@@ -493,7 +595,9 @@ const Devices: FC = () => {
|
|||||||
? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
||||||
: deviceData.data;
|
: deviceData.data;
|
||||||
|
|
||||||
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id);
|
const deviceIndex = coreData.devices.findIndex(
|
||||||
|
(d) => d.id === device_select.state.id
|
||||||
|
);
|
||||||
if (deviceIndex === -1) {
|
if (deviceIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -514,7 +618,8 @@ const Devices: FC = () => {
|
|||||||
>
|
>
|
||||||
<Box sx={{ border: '1px solid #177ac9' }}>
|
<Box sx={{ border: '1px solid #177ac9' }}>
|
||||||
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ ml: 1 }}>
|
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ ml: 1 }}>
|
||||||
{coreData.devices[deviceIndex].tn} | {coreData.devices[deviceIndex].n}
|
{coreData.devices[deviceIndex].tn} |
|
||||||
|
{coreData.devices[deviceIndex].n}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid container justifyContent="space-between">
|
<Grid container justifyContent="space-between">
|
||||||
@@ -527,30 +632,50 @@ const Devices: FC = () => {
|
|||||||
' ' +
|
' ' +
|
||||||
LL.ENTITIES(shown_data.length)}
|
LL.ENTITIES(shown_data.length)}
|
||||||
<IconButton onClick={() => setShowDeviceInfo(true)}>
|
<IconButton onClick={() => setShowDeviceInfo(true)}>
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
<InfoOutlinedIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{me.admin && (
|
{me.admin && (
|
||||||
<IconButton onClick={customize}>
|
<IconButton onClick={customize}>
|
||||||
<FormatListNumberedIcon sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
<FormatListNumberedIcon
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<IconButton onClick={handleDownloadCsv}>
|
<IconButton onClick={handleDownloadCsv}>
|
||||||
<DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
<DownloadIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => setOnlyFav(!onlyFav)}>
|
<IconButton onClick={() => setOnlyFav(!onlyFav)}>
|
||||||
{onlyFav ? (
|
{onlyFav ? (
|
||||||
<StarIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
<StarIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
<StarBorderOutlinedIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={refreshData}>
|
<IconButton onClick={refreshData}>
|
||||||
<RefreshIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
<RefreshIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid item zeroMinWidth justifyContent="flex-end">
|
<Grid item zeroMinWidth justifyContent="flex-end">
|
||||||
<IconButton onClick={resetDeviceSelect}>
|
<IconButton onClick={resetDeviceSelect}>
|
||||||
<HighlightOffIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
|
<HighlightOffIcon
|
||||||
|
color="primary"
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -595,15 +720,20 @@ const Devices: FC = () => {
|
|||||||
<Cell>{renderNameCell(dv)}</Cell>
|
<Cell>{renderNameCell(dv)}</Cell>
|
||||||
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
|
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
{me.admin && dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
{me.admin &&
|
||||||
<IconButton size="small" onClick={() => showDeviceValue(dv)}>
|
dv.c &&
|
||||||
{dv.v === '' && dv.c ? (
|
!hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||||
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
<IconButton
|
||||||
) : (
|
size="small"
|
||||||
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
onClick={() => showDeviceValue(dv)}
|
||||||
)}
|
>
|
||||||
</IconButton>
|
{dv.v === '' && dv.c ? (
|
||||||
)}
|
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
|
) : (
|
||||||
|
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
</Cell>
|
</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
@@ -627,14 +757,20 @@ const Devices: FC = () => {
|
|||||||
onSave={deviceValueDialogSave}
|
onSave={deviceValueDialogSave}
|
||||||
selectedItem={selectedDeviceValue}
|
selectedItem={selectedDeviceValue}
|
||||||
writeable={
|
writeable={
|
||||||
selectedDeviceValue.c !== undefined && !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
selectedDeviceValue.c !== undefined &&
|
||||||
|
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
|
||||||
}
|
}
|
||||||
validator={deviceValueItemValidation(selectedDeviceValue)}
|
validator={deviceValueItemValidation(selectedDeviceValue)}
|
||||||
progress={submitting}
|
progress={submitting}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ButtonRow mt={1}>
|
<ButtonRow mt={1}>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={refreshData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -102,7 +102,11 @@ const DevicesDialog = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : writeable ? LL.CHANGE_VALUE() : LL.VALUE(1)}
|
{selectedItem.v === '' && selectedItem.c
|
||||||
|
? LL.RUN_COMMAND()
|
||||||
|
: writeable
|
||||||
|
? LL.CHANGE_VALUE()
|
||||||
|
: LL.VALUE(1)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||||
@@ -138,9 +142,17 @@ const DevicesDialog = ({
|
|||||||
type="number"
|
type="number"
|
||||||
sx={{ width: '30ch' }}
|
sx={{ width: '30ch' }}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
inputProps={editItem.s ? { min: editItem.m, max: editItem.x, step: editItem.s } : {}}
|
inputProps={
|
||||||
|
editItem.s
|
||||||
|
? { min: editItem.m, max: editItem.x, step: editItem.s }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment>
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
{setUom(editItem.u)}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -175,10 +187,20 @@ const DevicesDialog = ({
|
|||||||
position: 'relative'
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={save}
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
{progress && (
|
{progress && (
|
||||||
|
|||||||
@@ -55,25 +55,46 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
||||||
<OptionIcon type="favorite" isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE} />
|
<OptionIcon
|
||||||
|
type="favorite"
|
||||||
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
|
||||||
<OptionIcon type="readonly" isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY} />
|
<OptionIcon
|
||||||
|
type="readonly"
|
||||||
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
|
||||||
<OptionIcon
|
<OptionIcon
|
||||||
type="api_mqtt_exclude"
|
type="api_mqtt_exclude"
|
||||||
isSet={(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE}
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) ===
|
||||||
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
|
||||||
<OptionIcon
|
<OptionIcon
|
||||||
type="web_exclude"
|
type="web_exclude"
|
||||||
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) ===
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="128">
|
<ToggleButton value="128">
|
||||||
<OptionIcon type="deleted" isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED} />
|
<OptionIcon
|
||||||
|
type="deleted"
|
||||||
|
isSet={
|
||||||
|
(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,9 +29,12 @@ const Help: FC = () => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
useLayoutTitle(LL.HELP_OF(''));
|
useLayoutTitle(LL.HELP_OF(''));
|
||||||
|
|
||||||
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data: APIcall) => EMSESP.API(data), {
|
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
|
||||||
immediate: false
|
(data: APIcall) => EMSESP.API(data),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onGetAPI((event) => {
|
onGetAPI((event) => {
|
||||||
const anchor = document.createElement('a');
|
const anchor = document.createElement('a');
|
||||||
@@ -40,8 +43,10 @@ const Help: FC = () => {
|
|||||||
type: 'text/plain'
|
type: 'text/plain'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
anchor.download = 'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
|
anchor.download =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
|
||||||
anchor.click();
|
anchor.click();
|
||||||
URL.revokeObjectURL(anchor.href);
|
URL.revokeObjectURL(anchor.href);
|
||||||
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
toast.info(LL.DOWNLOAD_SUCCESSFUL());
|
||||||
@@ -79,7 +84,10 @@ const Help: FC = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemButton component="a" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose">
|
<ListItemButton
|
||||||
|
component="a"
|
||||||
|
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
|
||||||
|
>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
<Avatar sx={{ bgcolor: '#72caf9' }}>
|
||||||
<GitHubIcon />
|
<GitHubIcon />
|
||||||
@@ -119,7 +127,11 @@ const Help: FC = () => {
|
|||||||
<b>{LL.HELP_INFORMATION_5()}</b>
|
<b>{LL.HELP_INFORMATION_5()}</b>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography align="center">
|
<Typography align="center">
|
||||||
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32" color="primary">
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/emsesp/EMS-ESP32"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{'github.com/emsesp/EMS-ESP32'}
|
{'github.com/emsesp/EMS-ESP32'}
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -12,9 +12,19 @@ import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined
|
|||||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||||
import type { SvgIconProps } from '@mui/material';
|
import type { SvgIconProps } from '@mui/material';
|
||||||
|
|
||||||
type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
|
type OptionType =
|
||||||
|
| 'deleted'
|
||||||
|
| 'readonly'
|
||||||
|
| 'web_exclude'
|
||||||
|
| 'api_mqtt_exclude'
|
||||||
|
| 'favorite';
|
||||||
|
|
||||||
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = {
|
const OPTION_ICONS: {
|
||||||
|
[type in OptionType]: [
|
||||||
|
React.ComponentType<SvgIconProps>,
|
||||||
|
React.ComponentType<SvgIconProps>
|
||||||
|
];
|
||||||
|
} = {
|
||||||
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
|
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
|
||||||
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
|
||||||
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],
|
||||||
|
|||||||
@@ -9,10 +9,24 @@ import CircleIcon from '@mui/icons-material/Circle';
|
|||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { Box, Button, Divider, Stack, Typography } from '@mui/material';
|
import { Box, Button, Divider, Stack, Typography } from '@mui/material';
|
||||||
|
|
||||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
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 { useTheme } from '@table-library/react-table-library/theme';
|
||||||
import { updateState, useRequest } from 'alova';
|
import { updateState, useRequest } from 'alova';
|
||||||
import { BlockNavigation, ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
import {
|
||||||
|
BlockNavigation,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
import * as EMSESP from './api';
|
import * as EMSESP from './api';
|
||||||
@@ -39,9 +53,12 @@ const Scheduler: FC = () => {
|
|||||||
force: true
|
force: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const { send: writeSchedule } = useRequest((data: Schedule) => EMSESP.writeSchedule(data), {
|
const { send: writeSchedule } = useRequest(
|
||||||
immediate: false
|
(data: Schedule) => EMSESP.writeSchedule(data),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function hasScheduleChanged(si: ScheduleItem) {
|
function hasScheduleChanged(si: ScheduleItem) {
|
||||||
return (
|
return (
|
||||||
@@ -57,7 +74,10 @@ const Scheduler: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
|
const formatter = new Intl.DateTimeFormat(locale, {
|
||||||
|
weekday: 'short',
|
||||||
|
timeZone: 'UTC'
|
||||||
|
});
|
||||||
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
|
||||||
const dd = day < 10 ? `0${day}` : day;
|
const dd = day < 10 ? `0${day}` : day;
|
||||||
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
return new Date(`2017-01-${dd}T00:00:00+00:00`);
|
||||||
@@ -157,8 +177,13 @@ const Scheduler: FC = () => {
|
|||||||
|
|
||||||
updateState('schedule', (data: ScheduleItem[]) => {
|
updateState('schedule', (data: ScheduleItem[]) => {
|
||||||
const new_data = creating
|
const new_data = creating
|
||||||
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]
|
? [
|
||||||
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si));
|
...data.filter((si) => creating || si.o_id !== updatedItem.o_id),
|
||||||
|
updatedItem
|
||||||
|
]
|
||||||
|
: data.map((si) =>
|
||||||
|
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
|
||||||
|
);
|
||||||
|
|
||||||
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
||||||
|
|
||||||
@@ -189,8 +214,13 @@ const Scheduler: FC = () => {
|
|||||||
const dayBox = (si: ScheduleItem, flag: number) => (
|
const dayBox = (si: ScheduleItem, flag: number) => (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
<Typography
|
||||||
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
|
sx={{ fontSize: 11 }}
|
||||||
|
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
|
||||||
|
>
|
||||||
|
{flag === ScheduleFlag.SCHEDULE_TIMER
|
||||||
|
? LL.TIMER(0)
|
||||||
|
: dow[Math.log(flag) / Math.log(2)]}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
@@ -201,7 +231,11 @@ const Scheduler: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
|
data={{
|
||||||
|
nodes: schedule
|
||||||
|
.filter((si) => !si.deleted)
|
||||||
|
.sort((a, b) => a.time.localeCompare(b.time))
|
||||||
|
}}
|
||||||
theme={schedule_theme}
|
theme={schedule_theme}
|
||||||
layout={{ custom: true }}
|
layout={{ custom: true }}
|
||||||
>
|
>
|
||||||
@@ -222,9 +256,15 @@ const Scheduler: FC = () => {
|
|||||||
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
{si.active ? (
|
{si.active ? (
|
||||||
<CircleIcon color="success" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
<CircleIcon
|
||||||
|
color="success"
|
||||||
|
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CircleIcon color="error" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
|
<CircleIcon
|
||||||
|
color="error"
|
||||||
|
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
@@ -277,7 +317,12 @@ const Scheduler: FC = () => {
|
|||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
{numChanges !== 0 && (
|
{numChanges !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDialogCancel}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -293,7 +338,12 @@ const Scheduler: FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addScheduleItem}>
|
<Button
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={addScheduleItem}
|
||||||
|
>
|
||||||
{LL.ADD(0)}
|
{LL.ADD(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -40,7 +40,15 @@ interface SchedulerDialogProps {
|
|||||||
dow: string[];
|
dow: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, validator, dow }: SchedulerDialogProps) => {
|
const SchedulerDialog = ({
|
||||||
|
open,
|
||||||
|
creating,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
selectedItem,
|
||||||
|
validator,
|
||||||
|
dow
|
||||||
|
}: SchedulerDialogProps) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
|
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
@@ -111,8 +119,14 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showFlag = (si: ScheduleItem, flag: number) => (
|
const showFlag = (si: ScheduleItem, flag: number) => (
|
||||||
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
|
<Typography
|
||||||
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
|
variant="button"
|
||||||
|
sx={{ fontSize: 10 }}
|
||||||
|
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
|
||||||
|
>
|
||||||
|
{flag === ScheduleFlag.SCHEDULE_TIMER
|
||||||
|
? LL.TIMER(0)
|
||||||
|
: dow[Math.log(flag) / Math.log(2)]}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -121,7 +135,8 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
|
|||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} {LL.SCHEDULE(1)}
|
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}
|
||||||
|
{LL.SCHEDULE(1)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box display="flex" flexWrap="wrap" mb={1}>
|
<Box display="flex" flexWrap="wrap" mb={1}>
|
||||||
@@ -134,13 +149,27 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
|
|||||||
setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 });
|
setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleButton value="2">{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
|
<ToggleButton value="2">
|
||||||
<ToggleButton value="4">{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton>
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}
|
||||||
<ToggleButton value="8">{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="16">{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}</ToggleButton>
|
<ToggleButton value="4">
|
||||||
<ToggleButton value="32">{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton>
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}
|
||||||
<ToggleButton value="64">{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="1">{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton>
|
<ToggleButton value="8">
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="16">
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="32">
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="64">
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="1">
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}
|
||||||
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||||
@@ -160,7 +189,10 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
|
|||||||
size="large"
|
size="large"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER });
|
setEditItem({
|
||||||
|
...editItem,
|
||||||
|
flags: ScheduleFlag.SCHEDULE_TIMER
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
|
||||||
@@ -170,7 +202,13 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
|
|||||||
</Box>
|
</Box>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={<Checkbox checked={editItem.active} onChange={updateFormValue} name="active" />}
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={editItem.active}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="active"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={LL.ACTIVE()}
|
label={LL.ACTIVE()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -220,15 +258,30 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
|
|||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!creating && (
|
{!creating && (
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
|
<Button
|
||||||
|
startIcon={<RemoveIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="warning"
|
||||||
|
onClick={remove}
|
||||||
|
>
|
||||||
{LL.REMOVE()}
|
{LL.REMOVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary">
|
<Button
|
||||||
|
startIcon={creating ? <AddIcon /> : <DoneIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={save}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
|
|||||||
import { Box, Button, Typography } from '@mui/material';
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
|
|
||||||
import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
|
import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
|
||||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
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 { useTheme } from '@table-library/react-table-library/theme';
|
||||||
import type { State } from '@table-library/react-table-library/types/common';
|
import type { State } from '@table-library/react-table-library/types/common';
|
||||||
import { useRequest } from 'alova';
|
import { useRequest } from 'alova';
|
||||||
@@ -21,28 +29,45 @@ import { useI18nContext } from 'i18n/i18n-react';
|
|||||||
import * as EMSESP from './api';
|
import * as EMSESP from './api';
|
||||||
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
|
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
|
||||||
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
|
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
|
||||||
import { AnalogType, AnalogTypeNames, DeviceValueUOM, DeviceValueUOM_s } from './types';
|
import {
|
||||||
import type { AnalogSensor, TemperatureSensor, WriteAnalogSensor, WriteTemperatureSensor } from './types';
|
AnalogType,
|
||||||
import { analogSensorItemValidation, temperatureSensorItemValidation } from './validators';
|
AnalogTypeNames,
|
||||||
|
DeviceValueUOM,
|
||||||
|
DeviceValueUOM_s
|
||||||
|
} from './types';
|
||||||
|
import type {
|
||||||
|
AnalogSensor,
|
||||||
|
TemperatureSensor,
|
||||||
|
WriteAnalogSensor,
|
||||||
|
WriteTemperatureSensor
|
||||||
|
} from './types';
|
||||||
|
import {
|
||||||
|
analogSensorItemValidation,
|
||||||
|
temperatureSensorItemValidation
|
||||||
|
} from './validators';
|
||||||
|
|
||||||
const Sensors: FC = () => {
|
const Sensors: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const { me } = useContext(AuthenticatedContext);
|
||||||
|
|
||||||
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
|
const [selectedTemperatureSensor, setSelectedTemperatureSensor] =
|
||||||
|
useState<TemperatureSensor>();
|
||||||
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
|
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
|
||||||
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
|
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
|
||||||
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
|
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
|
||||||
const [creating, setCreating] = useState<boolean>(false);
|
const [creating, setCreating] = useState<boolean>(false);
|
||||||
|
|
||||||
const { data: sensorData, send: fetchSensorData } = useRequest(() => EMSESP.readSensorData(), {
|
const { data: sensorData, send: fetchSensorData } = useRequest(
|
||||||
initialData: {
|
() => EMSESP.readSensorData(),
|
||||||
ts: [],
|
{
|
||||||
as: [],
|
initialData: {
|
||||||
analog_enabled: false,
|
ts: [],
|
||||||
platform: 'ESP32'
|
as: [],
|
||||||
|
analog_enabled: false,
|
||||||
|
platform: 'ESP32'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
const { send: writeTemperatureSensor } = useRequest(
|
const { send: writeTemperatureSensor } = useRequest(
|
||||||
(data: WriteTemperatureSensor) => EMSESP.writeTemperatureSensor(data),
|
(data: WriteTemperatureSensor) => EMSESP.writeTemperatureSensor(data),
|
||||||
@@ -51,9 +76,12 @@ const Sensors: FC = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { send: writeAnalogSensor } = useRequest((data: WriteAnalogSensor) => EMSESP.writeAnalogSensor(data), {
|
const { send: writeAnalogSensor } = useRequest(
|
||||||
immediate: false
|
(data: WriteAnalogSensor) => EMSESP.writeAnalogSensor(data),
|
||||||
});
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const common_theme = useTheme({
|
const common_theme = useTheme({
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
@@ -304,7 +332,12 @@ const Sensors: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RenderTemperatureSensors = () => (
|
const RenderTemperatureSensors = () => (
|
||||||
<Table data={{ nodes: sensorData.ts }} theme={temperature_theme} sort={temperature_sort} layout={{ custom: true }}>
|
<Table
|
||||||
|
data={{ nodes: sensorData.ts }}
|
||||||
|
theme={temperature_theme}
|
||||||
|
sort={temperature_sort}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
{(tableList: TemperatureSensor[]) => (
|
{(tableList: TemperatureSensor[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -314,7 +347,9 @@ const Sensors: FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
|
||||||
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
|
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
|
||||||
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
onClick={() =>
|
||||||
|
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{LL.NAME(0)}
|
{LL.NAME(0)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -324,7 +359,9 @@ const Sensors: FC = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
|
||||||
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
|
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
|
||||||
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
onClick={() =>
|
||||||
|
temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{LL.VALUE(0)}
|
{LL.VALUE(0)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -345,7 +382,12 @@ const Sensors: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const RenderAnalogSensors = () => (
|
const RenderAnalogSensors = () => (
|
||||||
<Table data={{ nodes: sensorData.as }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
|
<Table
|
||||||
|
data={{ nodes: sensorData.as }}
|
||||||
|
theme={analog_theme}
|
||||||
|
sort={analog_sort}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
{(tableList: AnalogSensor[]) => (
|
{(tableList: AnalogSensor[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -439,7 +481,11 @@ const Sensors: FC = () => {
|
|||||||
onSave={onAnalogDialogSave}
|
onSave={onAnalogDialogSave}
|
||||||
creating={creating}
|
creating={creating}
|
||||||
selectedItem={selectedAnalogSensor}
|
selectedItem={selectedAnalogSensor}
|
||||||
validator={analogSensorItemValidation(sensorData.as, creating, sensorData.platform)}
|
validator={analogSensorItemValidation(
|
||||||
|
sensorData.as,
|
||||||
|
creating,
|
||||||
|
sensorData.platform
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -447,7 +493,12 @@ const Sensors: FC = () => {
|
|||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Box mt={1} display="flex" flexWrap="wrap">
|
<Box mt={1} display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={fetchSensorData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ const SensorsAnalogDialog = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} {LL.ANALOG_SENSOR(0)}
|
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}
|
||||||
|
{LL.ANALOG_SENSOR(0)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
@@ -113,7 +114,14 @@ const SensorsAnalogDialog = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={8}>
|
<Grid item xs={8}>
|
||||||
<TextField name="t" label={LL.TYPE(0)} value={editItem.t} fullWidth select onChange={updateFormValue}>
|
<TextField
|
||||||
|
name="t"
|
||||||
|
label={LL.TYPE(0)}
|
||||||
|
value={editItem.t}
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
onChange={updateFormValue}
|
||||||
|
>
|
||||||
{AnalogTypeNames.map((val, i) => (
|
{AnalogTypeNames.map((val, i) => (
|
||||||
<MenuItem key={i} value={i}>
|
<MenuItem key={i} value={i}>
|
||||||
{val}
|
{val}
|
||||||
@@ -123,7 +131,14 @@ const SensorsAnalogDialog = ({
|
|||||||
</Grid>
|
</Grid>
|
||||||
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
|
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextField name="u" label={LL.UNIT()} value={editItem.u} fullWidth select onChange={updateFormValue}>
|
<TextField
|
||||||
|
name="u"
|
||||||
|
label={LL.UNIT()}
|
||||||
|
value={editItem.u}
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
onChange={updateFormValue}
|
||||||
|
>
|
||||||
{DeviceValueUOM_s.map((val, i) => (
|
{DeviceValueUOM_s.map((val, i) => (
|
||||||
<MenuItem key={i} value={i}>
|
<MenuItem key={i} value={i}>
|
||||||
{val}
|
{val}
|
||||||
@@ -144,7 +159,9 @@ const SensorsAnalogDialog = ({
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
inputProps={{ min: '0', max: '3300', step: '1' }}
|
inputProps={{ min: '0', max: '3300', step: '1' }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: <InputAdornment position="start">mV</InputAdornment>
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">mV</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -177,70 +194,75 @@ const SensorsAnalogDialog = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26) && (
|
{editItem.t === AnalogType.DIGITAL_OUT &&
|
||||||
<Grid item xs={4}>
|
(editItem.g === 25 || editItem.g === 26) && (
|
||||||
<TextField
|
|
||||||
name="o"
|
|
||||||
label={LL.VALUE(1)}
|
|
||||||
value={numberValue(editItem.o)}
|
|
||||||
fullWidth
|
|
||||||
type="number"
|
|
||||||
variant="outlined"
|
|
||||||
onChange={updateFormValue}
|
|
||||||
inputProps={{ min: '0', max: '255', step: '1' }}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
{editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26 && (
|
|
||||||
<>
|
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextField
|
<TextField
|
||||||
name="o"
|
name="o"
|
||||||
label={LL.VALUE(1)}
|
label={LL.VALUE(1)}
|
||||||
value={numberValue(editItem.o)}
|
value={numberValue(editItem.o)}
|
||||||
fullWidth
|
fullWidth
|
||||||
select
|
type="number"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
>
|
inputProps={{ min: '0', max: '255', step: '1' }}
|
||||||
<MenuItem value={0}>{LL.OFF()}</MenuItem>
|
/>
|
||||||
<MenuItem value={1}>{LL.ON()}</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
)}
|
||||||
<TextField
|
{editItem.t === AnalogType.DIGITAL_OUT &&
|
||||||
name="f"
|
editItem.g !== 25 &&
|
||||||
label={LL.POLARITY()}
|
editItem.g !== 26 && (
|
||||||
value={editItem.f}
|
<>
|
||||||
fullWidth
|
<Grid item xs={4}>
|
||||||
select
|
<TextField
|
||||||
onChange={updateFormValue}
|
name="o"
|
||||||
>
|
label={LL.VALUE(1)}
|
||||||
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
|
value={numberValue(editItem.o)}
|
||||||
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
|
fullWidth
|
||||||
</TextField>
|
select
|
||||||
</Grid>
|
variant="outlined"
|
||||||
<Grid item xs={4}>
|
onChange={updateFormValue}
|
||||||
<TextField
|
>
|
||||||
name="u"
|
<MenuItem value={0}>{LL.OFF()}</MenuItem>
|
||||||
label={LL.STARTVALUE()}
|
<MenuItem value={1}>{LL.ON()}</MenuItem>
|
||||||
value={editItem.u}
|
</TextField>
|
||||||
fullWidth
|
</Grid>
|
||||||
select
|
<Grid item xs={4}>
|
||||||
onChange={updateFormValue}
|
<TextField
|
||||||
>
|
name="f"
|
||||||
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
|
label={LL.POLARITY()}
|
||||||
<MenuItem value={1}>
|
value={editItem.f}
|
||||||
{LL.ALWAYS()} {LL.OFF()}
|
fullWidth
|
||||||
</MenuItem>
|
select
|
||||||
<MenuItem value={2}>
|
onChange={updateFormValue}
|
||||||
{LL.ALWAYS()} {LL.ON()}
|
>
|
||||||
</MenuItem>
|
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
|
||||||
</TextField>
|
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
|
||||||
</Grid>
|
</TextField>
|
||||||
</>
|
</Grid>
|
||||||
)}
|
<Grid item xs={4}>
|
||||||
{(editItem.t === AnalogType.PWM_0 || editItem.t === AnalogType.PWM_1 || editItem.t === AnalogType.PWM_2) && (
|
<TextField
|
||||||
|
name="u"
|
||||||
|
label={LL.STARTVALUE()}
|
||||||
|
value={editItem.u}
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
onChange={updateFormValue}
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
|
||||||
|
<MenuItem value={1}>
|
||||||
|
{LL.ALWAYS()} {LL.OFF()}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={2}>
|
||||||
|
{LL.ALWAYS()} {LL.ON()}
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(editItem.t === AnalogType.PWM_0 ||
|
||||||
|
editItem.t === AnalogType.PWM_1 ||
|
||||||
|
editItem.t === AnalogType.PWM_2) && (
|
||||||
<>
|
<>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -253,7 +275,9 @@ const SensorsAnalogDialog = ({
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
inputProps={{ min: '1', max: '5000', step: '1' }}
|
inputProps={{ min: '1', max: '5000', step: '1' }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: <InputAdornment position="start">Hz</InputAdornment>
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">Hz</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -268,7 +292,9 @@ const SensorsAnalogDialog = ({
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
inputProps={{ min: '0', max: '100', step: '0.1' }}
|
inputProps={{ min: '0', max: '100', step: '0.1' }}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: <InputAdornment position="start">%</InputAdornment>
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">%</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -279,15 +305,30 @@ const SensorsAnalogDialog = ({
|
|||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!creating && (
|
{!creating && (
|
||||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={remove}>
|
<Button
|
||||||
|
startIcon={<RemoveIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={remove}
|
||||||
|
>
|
||||||
{LL.REMOVE()}
|
{LL.REMOVE()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={save}
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -107,10 +107,20 @@ const SensorsTemperatureDialog = ({
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={close}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={save}
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
{LL.UPDATE()}
|
{LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ import type { FC } from 'react';
|
|||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
|
|
||||||
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
|
import {
|
||||||
|
Body,
|
||||||
|
Cell,
|
||||||
|
Header,
|
||||||
|
HeaderCell,
|
||||||
|
HeaderRow,
|
||||||
|
Row,
|
||||||
|
Table
|
||||||
|
} from '@table-library/react-table-library/table';
|
||||||
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
|
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
|
||||||
import { useRequest } from 'alova';
|
import { useRequest } from 'alova';
|
||||||
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||||
@@ -93,7 +101,11 @@ const SystemActivity: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
|
<Table
|
||||||
|
data={{ nodes: data.stats }}
|
||||||
|
theme={stats_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
{(tableList: Stat[]) => (
|
{(tableList: Stat[]) => (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
@@ -118,7 +130,12 @@ const SystemActivity: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
<ButtonRow mt={1}>
|
<ButtonRow mt={1}>
|
||||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
{LL.REFRESH()}
|
{LL.REFRESH()}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|||||||
@@ -30,17 +30,20 @@ export const writeDeviceValue = (data: { id: number; c: string; v: unknown }) =>
|
|||||||
|
|
||||||
// Application Settings
|
// Application Settings
|
||||||
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
|
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
|
||||||
export const writeSettings = (data: Settings) => alovaInstance.Post('/rest/settings', data);
|
export const writeSettings = (data: Settings) =>
|
||||||
|
alovaInstance.Post('/rest/settings', data);
|
||||||
export const getBoardProfile = (boardProfile: string) =>
|
export const getBoardProfile = (boardProfile: string) =>
|
||||||
alovaInstance.Get('/rest/boardProfile', {
|
alovaInstance.Get('/rest/boardProfile', {
|
||||||
params: { boardProfile }
|
params: { boardProfile }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sensors
|
// Sensors
|
||||||
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
|
export const readSensorData = () =>
|
||||||
|
alovaInstance.Get<SensorData>('/rest/sensorData');
|
||||||
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
|
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
|
||||||
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
|
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
|
||||||
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as);
|
export const writeAnalogSensor = (as: WriteAnalogSensor) =>
|
||||||
|
alovaInstance.Post('/rest/writeAnalogSensor', as);
|
||||||
|
|
||||||
// Activity
|
// Activity
|
||||||
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
|
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
|
||||||
@@ -73,9 +76,12 @@ export const readDeviceEntities = (id: number) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices');
|
export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices');
|
||||||
export const resetCustomizations = () => alovaInstance.Post('/rest/resetCustomizations');
|
export const resetCustomizations = () =>
|
||||||
export const writeCustomizationEntities = (data: { id: number; entity_ids: string[] }) =>
|
alovaInstance.Post('/rest/resetCustomizations');
|
||||||
alovaInstance.Post('/rest/customizationEntities', data);
|
export const writeCustomizationEntities = (data: {
|
||||||
|
id: number;
|
||||||
|
entity_ids: string[];
|
||||||
|
}) => alovaInstance.Post('/rest/customizationEntities', data);
|
||||||
|
|
||||||
// SettingsScheduler
|
// SettingsScheduler
|
||||||
export const readSchedule = () =>
|
export const readSchedule = () =>
|
||||||
@@ -95,7 +101,8 @@ export const readSchedule = () =>
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
export const writeSchedule = (data: Schedule) => alovaInstance.Post('/rest/schedule', data);
|
export const writeSchedule = (data: Schedule) =>
|
||||||
|
alovaInstance.Post('/rest/schedule', data);
|
||||||
|
|
||||||
// SettingsEntities
|
// SettingsEntities
|
||||||
export const readCustomEntities = () =>
|
export const readCustomEntities = () =>
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => {
|
|||||||
return formatted;
|
return formatted;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatValue(LL: TranslationFunctions, value: unknown, uom: DeviceValueUOM) {
|
export function formatValue(
|
||||||
|
LL: TranslationFunctions,
|
||||||
|
value: unknown,
|
||||||
|
uom: DeviceValueUOM
|
||||||
|
) {
|
||||||
if (typeof value !== 'number') {
|
if (typeof value !== 'number') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
|||||||
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
|
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
|
||||||
|
|
||||||
export const GPIO_VALIDATOR = {
|
export const GPIO_VALIDATOR = {
|
||||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: number,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
value &&
|
value &&
|
||||||
(value === 1 ||
|
(value === 1 ||
|
||||||
@@ -24,7 +28,11 @@ export const GPIO_VALIDATOR = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const GPIO_VALIDATORR = {
|
export const GPIO_VALIDATORR = {
|
||||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: number,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
value &&
|
value &&
|
||||||
(value === 1 ||
|
(value === 1 ||
|
||||||
@@ -44,7 +52,11 @@ export const GPIO_VALIDATORR = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const GPIO_VALIDATORC3 = {
|
export const GPIO_VALIDATORC3 = {
|
||||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: number,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
|
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
|
||||||
callback('Must be an valid GPIO port');
|
callback('Must be an valid GPIO port');
|
||||||
} else {
|
} else {
|
||||||
@@ -54,8 +66,18 @@ export const GPIO_VALIDATORC3 = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const GPIO_VALIDATORS2 = {
|
export const GPIO_VALIDATORS2 = {
|
||||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
validator(
|
||||||
if (value && ((value >= 19 && value <= 20) || (value >= 22 && value <= 32) || value > 40 || value < 0)) {
|
rule: InternalRuleItem,
|
||||||
|
value: number,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
((value >= 19 && value <= 20) ||
|
||||||
|
(value >= 22 && value <= 32) ||
|
||||||
|
value > 40 ||
|
||||||
|
value < 0)
|
||||||
|
) {
|
||||||
callback('Must be an valid GPIO port');
|
callback('Must be an valid GPIO port');
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
@@ -64,7 +86,11 @@ export const GPIO_VALIDATORS2 = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const GPIO_VALIDATORS3 = {
|
export const GPIO_VALIDATORS3 = {
|
||||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: number,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
value &&
|
value &&
|
||||||
((value >= 19 && value <= 20) ||
|
((value >= 19 && value <= 20) ||
|
||||||
@@ -84,46 +110,121 @@ export const createSettingsValidator = (settings: Settings) =>
|
|||||||
new Schema({
|
new Schema({
|
||||||
...(settings.board_profile === 'CUSTOM' &&
|
...(settings.board_profile === 'CUSTOM' &&
|
||||||
settings.platform === 'ESP32' && {
|
settings.platform === 'ESP32' && {
|
||||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATOR],
|
led_gpio: [
|
||||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATOR],
|
{ required: true, message: 'LED GPIO is required' },
|
||||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATOR],
|
GPIO_VALIDATOR
|
||||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATOR],
|
],
|
||||||
|
dallas_gpio: [
|
||||||
|
{ required: true, message: 'GPIO is required' },
|
||||||
|
GPIO_VALIDATOR
|
||||||
|
],
|
||||||
|
pbutton_gpio: [
|
||||||
|
{ required: true, message: 'Button GPIO is required' },
|
||||||
|
GPIO_VALIDATOR
|
||||||
|
],
|
||||||
|
tx_gpio: [
|
||||||
|
{ required: true, message: 'Tx GPIO is required' },
|
||||||
|
GPIO_VALIDATOR
|
||||||
|
],
|
||||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
|
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
|
||||||
}),
|
}),
|
||||||
...(settings.board_profile === 'CUSTOM' &&
|
...(settings.board_profile === 'CUSTOM' &&
|
||||||
settings.platform === 'ESP32R' && {
|
settings.platform === 'ESP32R' && {
|
||||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORR],
|
led_gpio: [
|
||||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORR],
|
{ required: true, message: 'LED GPIO is required' },
|
||||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORR],
|
GPIO_VALIDATORR
|
||||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORR],
|
],
|
||||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORR]
|
dallas_gpio: [
|
||||||
|
{ required: true, message: 'GPIO is required' },
|
||||||
|
GPIO_VALIDATORR
|
||||||
|
],
|
||||||
|
pbutton_gpio: [
|
||||||
|
{ required: true, message: 'Button GPIO is required' },
|
||||||
|
GPIO_VALIDATORR
|
||||||
|
],
|
||||||
|
tx_gpio: [
|
||||||
|
{ required: true, message: 'Tx GPIO is required' },
|
||||||
|
GPIO_VALIDATORR
|
||||||
|
],
|
||||||
|
rx_gpio: [
|
||||||
|
{ required: true, message: 'Rx GPIO is required' },
|
||||||
|
GPIO_VALIDATORR
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
...(settings.board_profile === 'CUSTOM' &&
|
...(settings.board_profile === 'CUSTOM' &&
|
||||||
settings.platform === 'ESP32-C3' && {
|
settings.platform === 'ESP32-C3' && {
|
||||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORC3],
|
led_gpio: [
|
||||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORC3],
|
{ required: true, message: 'LED GPIO is required' },
|
||||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORC3],
|
GPIO_VALIDATORC3
|
||||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORC3],
|
],
|
||||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORC3]
|
dallas_gpio: [
|
||||||
|
{ required: true, message: 'GPIO is required' },
|
||||||
|
GPIO_VALIDATORC3
|
||||||
|
],
|
||||||
|
pbutton_gpio: [
|
||||||
|
{ required: true, message: 'Button GPIO is required' },
|
||||||
|
GPIO_VALIDATORC3
|
||||||
|
],
|
||||||
|
tx_gpio: [
|
||||||
|
{ required: true, message: 'Tx GPIO is required' },
|
||||||
|
GPIO_VALIDATORC3
|
||||||
|
],
|
||||||
|
rx_gpio: [
|
||||||
|
{ required: true, message: 'Rx GPIO is required' },
|
||||||
|
GPIO_VALIDATORC3
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
...(settings.board_profile === 'CUSTOM' &&
|
...(settings.board_profile === 'CUSTOM' &&
|
||||||
settings.platform === 'ESP32-S2' && {
|
settings.platform === 'ESP32-S2' && {
|
||||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS2],
|
led_gpio: [
|
||||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS2],
|
{ required: true, message: 'LED GPIO is required' },
|
||||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS2],
|
GPIO_VALIDATORS2
|
||||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS2],
|
],
|
||||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS2]
|
dallas_gpio: [
|
||||||
|
{ required: true, message: 'GPIO is required' },
|
||||||
|
GPIO_VALIDATORS2
|
||||||
|
],
|
||||||
|
pbutton_gpio: [
|
||||||
|
{ required: true, message: 'Button GPIO is required' },
|
||||||
|
GPIO_VALIDATORS2
|
||||||
|
],
|
||||||
|
tx_gpio: [
|
||||||
|
{ required: true, message: 'Tx GPIO is required' },
|
||||||
|
GPIO_VALIDATORS2
|
||||||
|
],
|
||||||
|
rx_gpio: [
|
||||||
|
{ required: true, message: 'Rx GPIO is required' },
|
||||||
|
GPIO_VALIDATORS2
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
...(settings.board_profile === 'CUSTOM' &&
|
...(settings.board_profile === 'CUSTOM' &&
|
||||||
settings.platform === 'ESP32-S3' && {
|
settings.platform === 'ESP32-S3' && {
|
||||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS3],
|
led_gpio: [
|
||||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS3],
|
{ required: true, message: 'LED GPIO is required' },
|
||||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS3],
|
GPIO_VALIDATORS3
|
||||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS3],
|
],
|
||||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS3]
|
dallas_gpio: [
|
||||||
|
{ required: true, message: 'GPIO is required' },
|
||||||
|
GPIO_VALIDATORS3
|
||||||
|
],
|
||||||
|
pbutton_gpio: [
|
||||||
|
{ required: true, message: 'Button GPIO is required' },
|
||||||
|
GPIO_VALIDATORS3
|
||||||
|
],
|
||||||
|
tx_gpio: [
|
||||||
|
{ required: true, message: 'Tx GPIO is required' },
|
||||||
|
GPIO_VALIDATORS3
|
||||||
|
],
|
||||||
|
rx_gpio: [
|
||||||
|
{ required: true, message: 'Rx GPIO is required' },
|
||||||
|
GPIO_VALIDATORS3
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
...(settings.syslog_enabled && {
|
...(settings.syslog_enabled && {
|
||||||
syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
syslog_host: [
|
||||||
|
{ required: true, message: 'Host is required' },
|
||||||
|
IP_OR_HOSTNAME_VALIDATOR
|
||||||
|
],
|
||||||
syslog_port: [
|
syslog_port: [
|
||||||
{ required: true, message: 'Port is required' },
|
{ required: true, message: 'Port is required' },
|
||||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||||
@@ -134,14 +235,35 @@ export const createSettingsValidator = (settings: Settings) =>
|
|||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
...(settings.shower_alert && {
|
...(settings.shower_alert && {
|
||||||
shower_alert_trigger: [{ type: 'number', min: 1, max: 20, message: 'Time must be between 1 and 20 minutes' }],
|
shower_alert_trigger: [
|
||||||
shower_alert_coldshot: [{ type: 'number', min: 1, max: 10, message: 'Time must be between 1 and 10 seconds' }]
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
message: 'Time must be between 1 and 20 minutes'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shower_alert_coldshot: [
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
message: 'Time must be between 1 and 10 seconds'
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
|
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
|
||||||
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
|
validator(
|
||||||
if ((o_name === undefined || o_name !== name) && schedule.find((si) => si.name === name)) {
|
rule: InternalRuleItem,
|
||||||
|
name: string,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
(o_name === undefined || o_name !== name) &&
|
||||||
|
schedule.find((si) => si.name === name)
|
||||||
|
) {
|
||||||
callback('Name already in use');
|
callback('Name already in use');
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
@@ -149,7 +271,10 @@ export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
|
export const schedulerItemValidation = (
|
||||||
|
schedule: ScheduleItem[],
|
||||||
|
scheduleItem: ScheduleItem
|
||||||
|
) =>
|
||||||
new Schema({
|
new Schema({
|
||||||
name: [
|
name: [
|
||||||
{
|
{
|
||||||
@@ -162,7 +287,12 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
|
|||||||
],
|
],
|
||||||
cmd: [
|
cmd: [
|
||||||
{ required: true, message: 'Command is required' },
|
{ required: true, message: 'Command is required' },
|
||||||
{ type: 'string', min: 1, max: 64, message: 'Command must be 1-64 characters' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
min: 1,
|
||||||
|
max: 64,
|
||||||
|
message: 'Command must be 1-64 characters'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,7 +308,11 @@ export const entityItemValidation = () =>
|
|||||||
],
|
],
|
||||||
device_id: [
|
device_id: [
|
||||||
{
|
{
|
||||||
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: string,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (isNaN(parseInt(value, 16))) {
|
if (isNaN(parseInt(value, 16))) {
|
||||||
callback('Is required and must be in hex format');
|
callback('Is required and must be in hex format');
|
||||||
}
|
}
|
||||||
@@ -188,7 +322,11 @@ export const entityItemValidation = () =>
|
|||||||
],
|
],
|
||||||
type_id: [
|
type_id: [
|
||||||
{
|
{
|
||||||
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: string,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (isNaN(parseInt(value, 16))) {
|
if (isNaN(parseInt(value, 16))) {
|
||||||
callback('Is required and must be in hex format');
|
callback('Is required and must be in hex format');
|
||||||
}
|
}
|
||||||
@@ -208,7 +346,11 @@ export const temperatureSensorItemValidation = () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
|
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
|
||||||
validator(rule: InternalRuleItem, gpio: number, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
gpio: number,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (sensors.find((as) => as.g === gpio)) {
|
if (sensors.find((as) => as.g === gpio)) {
|
||||||
callback('GPIO already in use');
|
callback('GPIO already in use');
|
||||||
} else {
|
} else {
|
||||||
@@ -217,7 +359,11 @@ export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: boolean, platform: string) =>
|
export const analogSensorItemValidation = (
|
||||||
|
sensors: AnalogSensor[],
|
||||||
|
creating: boolean,
|
||||||
|
platform: string
|
||||||
|
) =>
|
||||||
new Schema({
|
new Schema({
|
||||||
n: [{ required: true, message: 'Name is required' }],
|
n: [{ required: true, message: 'Name is required' }],
|
||||||
g: [
|
g: [
|
||||||
@@ -240,8 +386,17 @@ export const deviceValueItemValidation = (dv: DeviceValue) =>
|
|||||||
v: [
|
v: [
|
||||||
{ required: true, message: 'Value is required' },
|
{ required: true, message: 'Value is required' },
|
||||||
{
|
{
|
||||||
validator(rule: InternalRuleItem, value: unknown, callback: (error?: string) => void) {
|
validator(
|
||||||
if (typeof value === 'number' && dv.m && dv.x && (value < dv.m || value > dv.x)) {
|
rule: InternalRuleItem,
|
||||||
|
value: unknown,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
typeof value === 'number' &&
|
||||||
|
dv.m &&
|
||||||
|
dv.x &&
|
||||||
|
(value < dv.m || value > dv.x)
|
||||||
|
) {
|
||||||
callback('Value out of range');
|
callback('Value out of range');
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const routeMatches = (route: string, pathname: string) => pathname.startsWith(route + '/') || pathname === route;
|
export const routeMatches = (route: string, pathname: string) =>
|
||||||
|
pathname.startsWith(route + '/') || pathname === route;
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
|||||||
hour12: false
|
hour12: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const formatDateTime = (dateTime: string) => LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19)));
|
export const formatDateTime = (dateTime: string) =>
|
||||||
|
LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19)));
|
||||||
|
|
||||||
export const formatLocalDateTime = (date: Date) =>
|
export const formatLocalDateTime = (date: Date) =>
|
||||||
new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, -1).substring(0, 19);
|
new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, -1)
|
||||||
|
.substring(0, 19);
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
|||||||
const [dirtyFlags, setDirtyFlags] = useState<string[]>([]);
|
const [dirtyFlags, setDirtyFlags] = useState<string[]>([]);
|
||||||
const blocker = useBlocker(dirtyFlags.length !== 0);
|
const blocker = useBlocker(dirtyFlags.length !== 0);
|
||||||
|
|
||||||
const { data, send: readData, update: updateData, onComplete: onReadComplete } = useRequest(read());
|
const {
|
||||||
|
data,
|
||||||
|
send: readData,
|
||||||
|
update: updateData,
|
||||||
|
onComplete: onReadComplete
|
||||||
|
} = useRequest(read());
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: saving,
|
loading: saving,
|
||||||
|
|||||||
@@ -6,15 +6,27 @@ import { IP_ADDRESS_VALIDATOR } from './shared';
|
|||||||
|
|
||||||
export const createAPSettingsValidator = (apSettings: APSettingsType) =>
|
export const createAPSettingsValidator = (apSettings: APSettingsType) =>
|
||||||
new Schema({
|
new Schema({
|
||||||
provision_mode: { required: true, message: 'Please provide a provision mode' },
|
provision_mode: {
|
||||||
|
required: true,
|
||||||
|
message: 'Please provide a provision mode'
|
||||||
|
},
|
||||||
...(isAPEnabled(apSettings) && {
|
...(isAPEnabled(apSettings) && {
|
||||||
ssid: [
|
ssid: [
|
||||||
{ required: true, message: 'Please provide an SSID' },
|
{ required: true, message: 'Please provide an SSID' },
|
||||||
{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
max: 32,
|
||||||
|
message: 'SSID must be 32 characters or less'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: 'Please provide an access point password' },
|
{ required: true, message: 'Please provide an access point password' },
|
||||||
{ type: 'string', min: 8, max: 64, message: 'Password must be 8-64 characters' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
min: 8,
|
||||||
|
max: 64,
|
||||||
|
message: 'Password must be 8-64 characters'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
channel: [
|
channel: [
|
||||||
{ required: true, message: 'Please provide a network channel' },
|
{ required: true, message: 'Please provide a network channel' },
|
||||||
@@ -22,10 +34,24 @@ export const createAPSettingsValidator = (apSettings: APSettingsType) =>
|
|||||||
],
|
],
|
||||||
max_clients: [
|
max_clients: [
|
||||||
{ required: true, message: 'Please specify a value for max clients' },
|
{ required: true, message: 'Please specify a value for max clients' },
|
||||||
{ type: 'number', min: 1, max: 9, message: 'Max clients must be between 1 and 9' }
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 9,
|
||||||
|
message: 'Max clients must be between 1 and 9'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
local_ip: [{ required: true, message: 'Local IP address is required' }, IP_ADDRESS_VALIDATOR],
|
local_ip: [
|
||||||
gateway_ip: [{ required: true, message: 'Gateway IP address is required' }, IP_ADDRESS_VALIDATOR],
|
{ required: true, message: 'Local IP address is required' },
|
||||||
subnet_mask: [{ required: true, message: 'Subnet mask is required' }, IP_ADDRESS_VALIDATOR]
|
IP_ADDRESS_VALIDATOR
|
||||||
|
],
|
||||||
|
gateway_ip: [
|
||||||
|
{ required: true, message: 'Gateway IP address is required' },
|
||||||
|
IP_ADDRESS_VALIDATOR
|
||||||
|
],
|
||||||
|
subnet_mask: [
|
||||||
|
{ required: true, message: 'Subnet mask is required' },
|
||||||
|
IP_ADDRESS_VALIDATOR
|
||||||
|
]
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
|||||||
export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) =>
|
export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) =>
|
||||||
new Schema({
|
new Schema({
|
||||||
...(mqttSettings.enabled && {
|
...(mqttSettings.enabled && {
|
||||||
host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
host: [
|
||||||
|
{ required: true, message: 'Host is required' },
|
||||||
|
IP_OR_HOSTNAME_VALIDATOR
|
||||||
|
],
|
||||||
base: { required: true, message: 'Base is required' },
|
base: { required: true, message: 'Base is required' },
|
||||||
port: [
|
port: [
|
||||||
{ required: true, message: 'Port is required' },
|
{ required: true, message: 'Port is required' },
|
||||||
@@ -14,11 +17,21 @@ export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) =>
|
|||||||
],
|
],
|
||||||
keep_alive: [
|
keep_alive: [
|
||||||
{ required: true, message: 'Keep alive is required' },
|
{ required: true, message: 'Keep alive is required' },
|
||||||
{ type: 'number', min: 1, max: 86400, message: 'Keep alive must be between 1 and 86400' }
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 86400,
|
||||||
|
message: 'Keep alive must be between 1 and 86400'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
publish_time_heartbeat: [
|
publish_time_heartbeat: [
|
||||||
{ required: true, message: 'Heartbeat is required' },
|
{ required: true, message: 'Heartbeat is required' },
|
||||||
{ type: 'number', min: 10, max: 86400, message: 'Heartbeat must be between 10 and 86400' }
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 10,
|
||||||
|
max: 86400,
|
||||||
|
message: 'Heartbeat must be between 10 and 86400'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,16 +3,42 @@ import type { NetworkSettingsType } from 'types';
|
|||||||
|
|
||||||
import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared';
|
import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared';
|
||||||
|
|
||||||
export const createNetworkSettingsValidator = (networkSettings: NetworkSettingsType) =>
|
export const createNetworkSettingsValidator = (
|
||||||
|
networkSettings: NetworkSettingsType
|
||||||
|
) =>
|
||||||
new Schema({
|
new Schema({
|
||||||
ssid: [{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }],
|
ssid: [
|
||||||
bssid: [{ type: 'string', max: 17, message: 'BSSID must be 17 characters or empty' }],
|
{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }
|
||||||
password: { type: 'string', max: 64, message: 'Password must be 64 characters or less' },
|
],
|
||||||
hostname: [{ required: true, message: 'Hostname is required' }, HOSTNAME_VALIDATOR],
|
bssid: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
max: 17,
|
||||||
|
message: 'BSSID must be 17 characters or empty'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
max: 64,
|
||||||
|
message: 'Password must be 64 characters or less'
|
||||||
|
},
|
||||||
|
hostname: [
|
||||||
|
{ required: true, message: 'Hostname is required' },
|
||||||
|
HOSTNAME_VALIDATOR
|
||||||
|
],
|
||||||
...(networkSettings.static_ip_config && {
|
...(networkSettings.static_ip_config && {
|
||||||
local_ip: [{ required: true, message: 'Local IP is required' }, IP_ADDRESS_VALIDATOR],
|
local_ip: [
|
||||||
gateway_ip: [{ required: true, message: 'Gateway IP is required' }, IP_ADDRESS_VALIDATOR],
|
{ required: true, message: 'Local IP is required' },
|
||||||
subnet_mask: [{ required: true, message: 'Subnet mask is required' }, IP_ADDRESS_VALIDATOR],
|
IP_ADDRESS_VALIDATOR
|
||||||
|
],
|
||||||
|
gateway_ip: [
|
||||||
|
{ required: true, message: 'Gateway IP is required' },
|
||||||
|
IP_ADDRESS_VALIDATOR
|
||||||
|
],
|
||||||
|
subnet_mask: [
|
||||||
|
{ required: true, message: 'Subnet mask is required' },
|
||||||
|
IP_ADDRESS_VALIDATOR
|
||||||
|
],
|
||||||
dns_ip_1: IP_ADDRESS_VALIDATOR,
|
dns_ip_1: IP_ADDRESS_VALIDATOR,
|
||||||
dns_ip_2: IP_ADDRESS_VALIDATOR
|
dns_ip_2: IP_ADDRESS_VALIDATOR
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import Schema from 'async-validator';
|
|||||||
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
|
||||||
|
|
||||||
export const NTP_SETTINGS_VALIDATOR = new Schema({
|
export const NTP_SETTINGS_VALIDATOR = new Schema({
|
||||||
server: [{ required: true, message: 'Server is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
server: [
|
||||||
|
{ required: true, message: 'Server is required' },
|
||||||
|
IP_OR_HOSTNAME_VALIDATOR
|
||||||
|
],
|
||||||
tz_label: {
|
tz_label: {
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Time zone is required'
|
message: 'Time zone is required'
|
||||||
|
|||||||
@@ -5,12 +5,21 @@ import type { UserType } from 'types';
|
|||||||
export const SECURITY_SETTINGS_VALIDATOR = new Schema({
|
export const SECURITY_SETTINGS_VALIDATOR = new Schema({
|
||||||
jwt_secret: [
|
jwt_secret: [
|
||||||
{ required: true, message: 'JWT secret is required' },
|
{ required: true, message: 'JWT secret is required' },
|
||||||
{ type: 'string', min: 1, max: 64, message: 'JWT secret must be between 1 and 64 characters' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
min: 1,
|
||||||
|
max: 64,
|
||||||
|
message: 'JWT secret must be between 1 and 64 characters'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createUniqueUsernameValidator = (users: UserType[]) => ({
|
export const createUniqueUsernameValidator = (users: UserType[]) => ({
|
||||||
validator(rule: InternalRuleItem, username: string, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
username: string,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (username && users.find((u) => u.username === username)) {
|
if (username && users.find((u) => u.username === username)) {
|
||||||
callback('Username already in use');
|
callback('Username already in use');
|
||||||
} else {
|
} else {
|
||||||
@@ -32,6 +41,11 @@ export const createUserValidator = (users: UserType[], creating: boolean) =>
|
|||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: 'Please provide a password' },
|
{ required: true, message: 'Please provide a password' },
|
||||||
{ type: 'string', min: 1, max: 64, message: 'Password must be 1-64 characters' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
min: 1,
|
||||||
|
max: 64,
|
||||||
|
message: 'Password must be 1-64 characters'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ export const validate = <T extends object>(
|
|||||||
options?: ValidateOption
|
options?: ValidateOption
|
||||||
): Promise<T> =>
|
): Promise<T> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
void validator.validate(source, options ? options : {}, (errors, fieldErrors) => {
|
void validator.validate(
|
||||||
if (errors) {
|
source,
|
||||||
reject(fieldErrors);
|
options ? options : {},
|
||||||
} else {
|
(errors, fieldErrors) => {
|
||||||
resolve(source as T);
|
if (errors) {
|
||||||
|
reject(fieldErrors);
|
||||||
|
} else {
|
||||||
|
resolve(source as T);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// updated to support both IPv4 and IPv6
|
// updated to support both IPv4 and IPv6
|
||||||
@@ -23,7 +27,11 @@ const IP_ADDRESS_REGEXP =
|
|||||||
const isValidIpAddress = (value: string) => IP_ADDRESS_REGEXP.test(value);
|
const isValidIpAddress = (value: string) => IP_ADDRESS_REGEXP.test(value);
|
||||||
|
|
||||||
export const IP_ADDRESS_VALIDATOR = {
|
export const IP_ADDRESS_VALIDATOR = {
|
||||||
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: string,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (value && !isValidIpAddress(value)) {
|
if (value && !isValidIpAddress(value)) {
|
||||||
callback('Must be an IP address');
|
callback('Must be an IP address');
|
||||||
} else {
|
} else {
|
||||||
@@ -36,10 +44,15 @@ const HOSTNAME_LENGTH_REGEXP = /^.{0,200}$/;
|
|||||||
const HOSTNAME_PATTERN_REGEXP =
|
const HOSTNAME_PATTERN_REGEXP =
|
||||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
|
||||||
|
|
||||||
const isValidHostname = (value: string) => HOSTNAME_LENGTH_REGEXP.test(value) && HOSTNAME_PATTERN_REGEXP.test(value);
|
const isValidHostname = (value: string) =>
|
||||||
|
HOSTNAME_LENGTH_REGEXP.test(value) && HOSTNAME_PATTERN_REGEXP.test(value);
|
||||||
|
|
||||||
export const HOSTNAME_VALIDATOR = {
|
export const HOSTNAME_VALIDATOR = {
|
||||||
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: string,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (value && !isValidHostname(value)) {
|
if (value && !isValidHostname(value)) {
|
||||||
callback('Must be a valid hostname');
|
callback('Must be a valid hostname');
|
||||||
} else {
|
} else {
|
||||||
@@ -49,7 +62,11 @@ export const HOSTNAME_VALIDATOR = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IP_OR_HOSTNAME_VALIDATOR = {
|
export const IP_OR_HOSTNAME_VALIDATOR = {
|
||||||
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
|
validator(
|
||||||
|
rule: InternalRuleItem,
|
||||||
|
value: string,
|
||||||
|
callback: (error?: string) => void
|
||||||
|
) {
|
||||||
if (value && !(isValidIpAddress(value) || isValidHostname(value))) {
|
if (value && !(isValidIpAddress(value) || isValidHostname(value))) {
|
||||||
callback('Must be a valid IP address or hostname');
|
callback('Must be a valid IP address or hostname');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,10 +3,20 @@ import Schema from 'async-validator';
|
|||||||
export const OTA_SETTINGS_VALIDATOR = new Schema({
|
export const OTA_SETTINGS_VALIDATOR = new Schema({
|
||||||
port: [
|
port: [
|
||||||
{ required: true, message: 'Port is required' },
|
{ required: true, message: 'Port is required' },
|
||||||
{ type: 'number', min: 1025, max: 65535, message: 'Port must be between 1025 and 65535' }
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1025,
|
||||||
|
max: 65535,
|
||||||
|
message: 'Port must be between 1025 and 65535'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: 'Password is required' },
|
{ required: true, message: 'Password is required' },
|
||||||
{ type: 'string', min: 1, max: 64, message: 'Password must be between 1 and 64 characters' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
min: 1,
|
||||||
|
max: 64,
|
||||||
|
message: 'Password must be between 1 and 64 characters'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,7 +121,11 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
manualChunks(id: string) {
|
manualChunks(id: string) {
|
||||||
if (id.includes('node_modules')) {
|
if (id.includes('node_modules')) {
|
||||||
// creating a chunk to react routes deps. Reducing the vendor chunk size
|
// creating a chunk to react routes deps. Reducing the vendor chunk size
|
||||||
if (id.includes('react-router-dom') || id.includes('@remix-run') || id.includes('react-router')) {
|
if (
|
||||||
|
id.includes('react-router-dom') ||
|
||||||
|
id.includes('@remix-run') ||
|
||||||
|
id.includes('react-router')
|
||||||
|
) {
|
||||||
return '@react-router';
|
return '@react-router';
|
||||||
}
|
}
|
||||||
return 'vendor';
|
return 'vendor';
|
||||||
|
|||||||
Reference in New Issue
Block a user