This commit is contained in:
MichaelDvP
2024-04-22 13:14:24 +02:00
132 changed files with 3840 additions and 3059 deletions

1
.gitignore vendored
View File

@@ -63,3 +63,4 @@ bw-output/
# standalone executable for testing # standalone executable for testing
emsesp emsesp
interface/tsconfig.tsbuildinfo

View File

@@ -1,8 +1,13 @@
{ {
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"trailingComma": "none", "trailingComma": "none",
"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>", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderGroupNamespaceSpecifiers": true
} }

View File

@@ -6,6 +6,10 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll": "explicit"
}, },
"eslint.validate": [
"typescript"
],
"eslint.codeActionsOnSave.rules": null,
"eslint.nodePath": "interface/.yarn/sdks", "eslint.nodePath": "interface/.yarn/sdks",
"eslint.workingDirectories": ["interface"], "eslint.workingDirectories": ["interface"],
"prettier.prettierPath": "", "prettier.prettierPath": "",
@@ -87,5 +91,6 @@
"cSpell.enableFiletypes": [ "cSpell.enableFiletypes": [
"!cpp", "!cpp",
"!typescript" "!typescript"
] ],
"typescript.preferences.preferTypeOnlyAutoImports": true
} }

View File

@@ -1,12 +0,0 @@
node_modules/
build/
dist/
.yarn/
.prettierrc
.eslintrc*
env.d.ts
progmem-generator.js
unpack.ts
vite.config.ts
package.json

View File

@@ -1,108 +0,0 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
// "airbnb/hooks",
// "airbnb-typescript",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:prettier/recommended",
"plugin:import/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module",
"tsconfigRootDir": ".",
"project": ["tsconfig.json"]
},
"plugins": ["react", "@typescript-eslint", "autofix", "react-hooks"],
"settings": {
"import/resolver": {
"typescript": {
"project": "./tsconfig.json"
}
},
"react": {
"version": "18.x"
}
},
"rules": {
"object-shorthand": "error",
"no-console": "warn",
"@typescript-eslint/consistent-type-definitions": ["off", "type"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-enum-comparison": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-implied-eval": "off",
"@typescript-eslint/no-misused-promises": "off",
"arrow-body-style": ["error", "as-needed"],
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/consistent-type-imports": [
"error",
{
"prefer": "type-imports"
}
],
"import/order": [
"warn",
{
"groups": ["builtin", "external", "parent", "sibling", "index", "object", "type"],
"pathGroups": [
{
"pattern": "@/**/**",
"group": "parent",
"position": "before"
}
],
"alphabetize": { "order": "asc" }
}
],
// "autofix/no-unused-vars": [
// "error",
// {
// "argsIgnorePattern": "^_",
// "ignoreRestSiblings": true,
// "destructuredArrayIgnorePattern": "^_"
// }
// ],
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"@typescript-eslint/ban-types": [
"error",
{
"extendDefaults": true,
"types": {
"{}": false
}
}
],
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
}

View File

@@ -1,6 +1,8 @@
node_modules/ node_modules/
build/ build/
dist/ dist/
src/i18n/*
.prettierrc .prettierrc
.yarn/ .yarn/
.typesafe-i18n.json .typesafe-i18n.json

View File

@@ -0,0 +1,33 @@
// @ts-check
import eslint from '@eslint/js';
import prettierConfig from 'eslint-config-prettier';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
prettierConfig,
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
}
},
{
ignores: ['dist/*', '*.js', '**/*.cjs', '**/unpack.ts']
},
{
rules: {
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false
}
]
}
}
);

View File

@@ -18,18 +18,18 @@
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-api\" \"npm:mock-es\" \"npm:mock-upload\" \"vite\"", "standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-api\" \"npm:mock-es\" \"npm:mock-upload\" \"vite\"",
"typesafe-i18n": "typesafe-i18n --no-watch", "typesafe-i18n": "typesafe-i18n --no-watch",
"webUI": "node progmem-generator.js", "webUI": "node progmem-generator.js",
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'", "format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
"lint": "eslint . --cache --fix" "lint": "eslint . --fix"
}, },
"dependencies": { "dependencies": {
"@alova/adapter-xhr": "^1.0.3", "@alova/adapter-xhr": "^1.0.3",
"@alova/scene-react": "^1.5.0",
"@babel/core": "^7.24.4", "@babel/core": "^7.24.4",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.15", "@mui/icons-material": "^5.15.15",
"@mui/material": "^5.15.15", "@mui/material": "^5.15.15",
"@table-library/react-table-library": "4.1.7", "@table-library/react-table-library": "4.1.7",
"@types/imagemin": "^8.0.5",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.12.7", "@types/node": "^20.12.7",
"@types/react": "^18.2.79", "@types/react": "^18.2.79",
@@ -37,7 +37,6 @@
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"alova": "2.19.2", "alova": "2.19.2",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"eslint-plugin-prettier": "^5.1.3",
"history": "^5.3.0", "history": "^5.3.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -52,22 +51,18 @@
"typescript": "^5.4.5" "typescript": "^5.4.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.1.1",
"@preact/compat": "^17.1.2", "@preact/compat": "^17.1.2",
"@preact/preset-vite": "^2.8.2", "@preact/preset-vite": "^2.8.2",
"@typescript-eslint/eslint-plugin": "^7.7.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/parser": "^7.7.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"eslint": "9.1.0", "eslint": "^9.1.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-autofix": "^1.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"preact": "^10.20.2", "preact": "^10.20.2",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"terser": "^5.30.3", "terser": "^5.30.3",
"typescript-eslint": "^7.7.0",
"vite": "^5.2.10", "vite": "^5.2.10",
"vite-plugin-imagemin": "^0.6.1", "vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^4.3.2" "vite-tsconfig-paths": "^4.3.2"

View File

@@ -1,8 +1,14 @@
import { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } from 'fs';
import { resolve, relative, sep } from 'path';
import zlib from 'zlib';
import mime from 'mime-types';
import crypto from 'crypto'; import crypto from 'crypto';
import {
createWriteStream,
existsSync,
readFileSync,
readdirSync,
unlinkSync
} from 'fs';
import mime from 'mime-types';
import { relative, resolve, sep } from 'path';
import zlib from 'zlib';
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n'; const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
const INDENT = ' '; const INDENT = ' ';
@@ -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)}}
}; };
`; `;

View File

@@ -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;
} }

View File

@@ -1,16 +1,14 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ToastContainer, Slide } from 'react-toastify'; import type { FC } from 'react';
import { Slide, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css'; import 'react-toastify/dist/ReactToastify.min.css';
import { localStorageDetector } from 'typesafe-i18n/detectors';
import type { FC } from 'react';
import AppRouting from 'AppRouting'; import AppRouting from 'AppRouting';
import CustomTheme from 'CustomTheme'; import CustomTheme from 'CustomTheme';
import TypesafeI18n from 'i18n/i18n-react'; import TypesafeI18n from 'i18n/i18n-react';
import { detectLocale } from 'i18n/i18n-util'; import { detectLocale } from 'i18n/i18n-util';
import { loadLocaleAsync } from 'i18n/i18n-util.async'; import { loadLocaleAsync } from 'i18n/i18n-util.async';
import { localStorageDetector } from 'typesafe-i18n/detectors';
const detectedLocale = detectLocale(localStorageDetector); const detectedLocale = detectLocale(localStorageDetector);

View File

@@ -1,14 +1,11 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { Route, Routes, Navigate, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify';
import type { FC } from 'react'; import type { FC } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify';
import AuthenticatedRouting from 'AuthenticatedRouting'; import AuthenticatedRouting from 'AuthenticatedRouting';
import SignIn from 'SignIn'; import SignIn from 'SignIn';
import { RequireAuthenticated, RequireUnauthenticated } from 'components'; import { RequireAuthenticated, RequireUnauthenticated } from 'components';
import { Authentication, AuthenticationContext } from 'contexts/authentication'; import { Authentication, AuthenticationContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -47,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={

View File

@@ -1,7 +1,6 @@
import { useContext, type FC } from 'react'; import { type FC, useContext } from 'react';
import { Navigate, Routes, Route } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import Help from './project/Help';
import { Layout } from 'components'; import { Layout } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import Settings from 'framework/Settings'; import Settings from 'framework/Settings';
@@ -21,6 +20,8 @@ import Devices from 'project/Devices';
import Scheduler from 'project/Scheduler'; import Scheduler from 'project/Scheduler';
import Sensors from 'project/Sensors'; import Sensors from 'project/Sensors';
import Help from './project/Help';
const AuthenticatedRouting: FC = () => { const AuthenticatedRouting: FC = () => {
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
return ( return (
@@ -44,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 />} />
</> </>
)} )}

View File

@@ -1,7 +1,12 @@
import { CssBaseline } from '@mui/material';
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
import type { FC } from 'react'; import type { FC } from 'react';
import { CssBaseline } from '@mui/material';
import {
ThemeProvider,
createTheme,
responsiveFontSizes
} from '@mui/material/styles';
import type { RequiredChildrenProps } from 'utils'; import type { RequiredChildrenProps } from 'utils';
export const dialogStyle = { export const dialogStyle = {

View File

@@ -1,19 +1,17 @@
import ForwardIcon from '@mui/icons-material/Forward';
import { Box, Paper, Typography, MenuItem, TextField, Button } from '@mui/material';
import { useRequest } from 'alova';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import type { ValidateFieldsError } from 'async-validator';
import type { Locales } from 'i18n/i18n-types';
import type { ChangeEventHandler, FC } from 'react'; import type { ChangeEventHandler, FC } from 'react';
import type { SignInRequest } from 'types'; import { toast } from 'react-toastify';
import ForwardIcon from '@mui/icons-material/Forward';
import { Box, Button, MenuItem, Paper, TextField, Typography } from '@mui/material';
import * as AuthenticationApi from 'api/authentication'; import * as AuthenticationApi from 'api/authentication';
import { PROJECT_NAME } from 'api/env'; import { PROJECT_NAME } from 'api/env';
import { useRequest } from 'alova';
import type { ValidateFieldsError } from 'async-validator';
import { ValidatedPasswordField, ValidatedTextField } from 'components'; import { ValidatedPasswordField, ValidatedTextField } from 'components';
import { AuthenticationContext } from 'contexts/authentication'; import { AuthenticationContext } from 'contexts/authentication';
import DEflag from 'i18n/DE.svg'; import DEflag from 'i18n/DE.svg';
import FRflag from 'i18n/FR.svg'; import FRflag from 'i18n/FR.svg';
import GBflag from 'i18n/GB.svg'; import GBflag from 'i18n/GB.svg';
@@ -25,7 +23,9 @@ import SKflag from 'i18n/SK.svg';
import SVflag from 'i18n/SV.svg'; import SVflag from 'i18n/SV.svg';
import TRflag from 'i18n/TR.svg'; import TRflag from 'i18n/TR.svg';
import { I18nContext } from 'i18n/i18n-react'; import { I18nContext } from 'i18n/i18n-react';
import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async'; import { loadLocaleAsync } from 'i18n/i18n-util.async';
import type { SignInRequest } from 'types';
import { onEnterCallback, updateValue } from 'utils'; import { onEnterCallback, updateValue } from 'utils';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators'; import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
@@ -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) {
@@ -54,7 +57,7 @@ const SignIn: FC = () => {
const updateLoginRequestValue = updateValue(setSignInRequest); const updateLoginRequestValue = updateValue(setSignInRequest);
const signIn = async () => { const signIn = async () => {
await callSignIn(signInRequest).catch((event) => { await callSignIn(signInRequest).catch((event: Error) => {
if (event.message === 'Unauthorized') { if (event.message === 'Unauthorized') {
toast.warning(LL.INVALID_LOGIN()); toast.warning(LL.INVALID_LOGIN());
} else { } else {
@@ -72,15 +75,17 @@ const SignIn: FC = () => {
try { try {
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest); await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
await signIn(); await signIn();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
setProcessing(false); setProcessing(false);
} }
}; };
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' }} />
&nbsp;DE &nbsp;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>

View File

@@ -1,7 +1,9 @@
import type { APSettingsType, APStatusType } from 'types';
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
import type { APSettings, APStatus } from 'types'; export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
export const readAPSettings = () =>
export const readAPStatus = () => alovaInstance.Get<APStatus>('/rest/apStatus'); alovaInstance.Get<APSettingsType>('/rest/apSettings');
export const readAPSettings = () => alovaInstance.Get<APSettings>('/rest/apSettings'); export const updateAPSettings = (data: APSettingsType) =>
export const updateAPSettings = (data: APSettings) => alovaInstance.Post<APSettings>('/rest/apSettings', data); alovaInstance.Post<APSettingsType>('/rest/apSettings', data);

View File

@@ -1,15 +1,18 @@
import { jwtDecode } from 'jwt-decode';
import { ACCESS_TOKEN, alovaInstance } from './endpoints';
import type * as H from 'history';
import type { Path } from 'react-router-dom'; import type { Path } from 'react-router-dom';
import type * as H from 'history';
import { jwtDecode } from 'jwt-decode';
import type { Me, SignInRequest, SignInResponse } from 'types'; import type { Me, SignInRequest, SignInResponse } from 'types';
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;

View File

@@ -1,13 +1,11 @@
import { xhrRequestAdapter } from '@alova/adapter-xhr'; import { xhrRequestAdapter } from '@alova/adapter-xhr';
import { createAlova } from 'alova'; import { createAlova } from 'alova';
import ReactHook from 'alova/react'; import ReactHook from 'alova/react';
import { unpack } from '../api/unpack'; import { unpack } from '../api/unpack';
export const ACCESS_TOKEN = 'access_token'; export const ACCESS_TOKEN = 'access_token';
const host = window.location.host;
export const EVENT_SOURCE_ROOT = 'http://' + host + '/es/';
export const alovaInstance = createAlova({ export const alovaInstance = createAlova({
statesHook: ReactHook, statesHook: ReactHook,
timeout: 3000, // 3 seconds but throwing a timeout error timeout: 3000, // 3 seconds but throwing a timeout error
@@ -21,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);
} }
}, },
@@ -37,9 +36,9 @@ export const alovaInstance = createAlova({
} else if (response.status >= 400) { } else if (response.status >= 400) {
throw new Error(response.statusText); throw new Error(response.statusText);
} }
const data = await response.data; const data: ArrayBuffer = (await response.data) as ArrayBuffer;
if (response.data instanceof ArrayBuffer) { if (response.data instanceof ArrayBuffer) {
return unpack(data); return unpack(data) as ArrayBuffer;
} }
return data; return data;
} }

View File

@@ -1,7 +1,10 @@
import { alovaInstance } from './endpoints';
import type { MqttSettingsType, MqttStatusType } from 'types'; import type { MqttSettingsType, MqttStatusType } from 'types';
export const readMqttStatus = () => alovaInstance.Get<MqttStatusType>('/rest/mqttStatus'); import { alovaInstance } from './endpoints';
export const readMqttSettings = () => alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
export const readMqttStatus = () =>
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);

View File

@@ -1,8 +1,9 @@
import type { NetworkSettingsType, NetworkStatusType, WiFiNetworkList } from 'types';
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types'; export const readNetworkStatus = () =>
alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatus>('/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<NetworkSettings>('/rest/networkSettings', { name: 'networkSettings' }); alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings', {
export const updateNetworkSettings = (wifiSettings: NetworkSettings) => name: 'networkSettings'
alovaInstance.Post<NetworkSettings>('/rest/networkSettings', wifiSettings); });
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);

View File

@@ -1,11 +1,15 @@
import { alovaInstance } from './endpoints'; import type { NTPSettingsType, NTPStatusType, Time } from 'types';
import type { NTPSettings, NTPStatus, Time } from 'types';
export const readNTPStatus = () => alovaInstance.Get<NTPStatus>('/rest/ntpStatus'); import { alovaInstance } from './endpoints';
export const readNTPStatus = () =>
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
export const readNTPSettings = () => export const readNTPSettings = () =>
alovaInstance.Get<NTPSettings>('/rest/ntpSettings', { alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {
name: 'ntpSettings' name: 'ntpSettings'
}); });
export const updateNTPSettings = (data: NTPSettings) => alovaInstance.Post<NTPSettings>('/rest/ntpSettings', data); export const updateNTPSettings = (data: NTPSettingsType) =>
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);

View File

@@ -1,10 +1,11 @@
import type { SecuritySettingsType, Token } from 'types';
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
import type { SecuritySettings, Token } from 'types'; export const readSecuritySettings = () =>
alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings'); export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
alovaInstance.Post('/rest/securitySettings', securitySettings); alovaInstance.Post('/rest/securitySettings', securitySettings);
export const generateToken = (username?: string) => export const generateToken = (username?: string) =>

View File

@@ -1,11 +1,19 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import type { ESPSystemStatus, LogSettings, OTASettings, SystemStatus } from 'types';
import { alovaInstance, alovaInstanceGH } from './endpoints'; import { alovaInstance, alovaInstanceGH } from './endpoints';
import type { OTASettings, SystemStatus, LogSettings, ESPSystemStatus } from 'types';
// 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');
@@ -13,24 +21,29 @@ 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: any) => 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: any) => 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');
// Get versions from github // Get versions from github
export const getStableVersion = () => export const getStableVersion = () =>
alovaInstanceGH.Get('latest', { alovaInstanceGH.Get('latest', {
transformData(response: any) { transformData(response) {
return response.data.name.substring(1); return response.data.name.substring(1);
} }
}); });
export const getDevVersion = () => export const getDevVersion = () =>
alovaInstanceGH.Get('tags/latest', { alovaInstanceGH.Get('tags/latest', {
transformData(response: any) { transformData(response) {
return response.data.name.split(/\s+/).splice(-1)[0].substring(1); return response.data.name.split(/\s+/).splice(-1)[0].substring(1);
} }
}); });
@@ -40,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+
}); });
}; };

View File

@@ -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;

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import type { BoxProps } from '@mui/material'; import type { BoxProps } from '@mui/material';
import type { FC } from 'react';
const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => ( const ButtonRow: FC<BoxProps> = ({ children, ...rest }) => (
<Box <Box

View File

@@ -1,10 +1,11 @@
import type { FC } from 'react';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined'; import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';
import { Box, Typography, useTheme } from '@mui/material'; import { Box, Typography, useTheme } from '@mui/material';
import type { BoxProps, SvgIconProps, Theme } from '@mui/material'; import type { BoxProps, SvgIconProps, Theme } from '@mui/material';
import type { FC } from 'react';
type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error'; type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error';
@@ -13,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}

View File

@@ -1,6 +1,7 @@
import { Paper, Divider } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import { Divider, Paper } from '@mui/material';
import type { RequiredChildrenProps } from 'utils'; import type { RequiredChildrenProps } from 'utils';
interface SectionContentProps extends RequiredChildrenProps { interface SectionContentProps extends RequiredChildrenProps {
@@ -13,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>

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react';
import { FormControlLabel } from '@mui/material'; import { FormControlLabel } from '@mui/material';
import type { FormControlLabelProps } from '@mui/material'; import type { FormControlLabelProps } from '@mui/material';
import type { FC } from 'react';
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => ( const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
<div> <div>

View File

@@ -1,15 +1,19 @@
import { useState } from 'react';
import type { FC } from 'react';
import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { IconButton, InputAdornment } from '@mui/material'; import { IconButton, InputAdornment } from '@mui/material';
import { useState } from 'react';
import ValidatedTextField from './ValidatedTextField'; import ValidatedTextField from './ValidatedTextField';
import type { ValidatedTextFieldProps } from './ValidatedTextField'; import type { ValidatedTextFieldProps } from './ValidatedTextField';
import type { FC } from 'react';
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 (

View File

@@ -1,7 +1,9 @@
import type { FC } from 'react';
import { FormHelperText, TextField } from '@mui/material'; import { FormHelperText, TextField } from '@mui/material';
import type { TextFieldProps } from '@mui/material'; import type { TextFieldProps } from '@mui/material';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
interface ValidatedFieldProps { interface ValidatedFieldProps {
fieldErrors?: ValidateFieldsError; fieldErrors?: ValidateFieldsError;
@@ -10,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} />

View File

@@ -1,13 +1,16 @@
import { Box, Toolbar } from '@mui/material'; import { useEffect, useState } from 'react';
import { useState, useEffect } from 'react'; import type { FC } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Box, Toolbar } from '@mui/material';
import { PROJECT_NAME } from 'api/env';
import type { RequiredChildrenProps } from 'utils';
import LayoutAppBar from './LayoutAppBar'; import LayoutAppBar from './LayoutAppBar';
import LayoutDrawer from './LayoutDrawer'; import LayoutDrawer from './LayoutDrawer';
import { LayoutContext } from './context'; import { LayoutContext } from './context';
import type { FC } from 'react';
import type { RequiredChildrenProps } from 'utils';
import { PROJECT_NAME } from 'api/env';
export const DRAWER_WIDTH = 210; export const DRAWER_WIDTH = 210;

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import { AppBar, IconButton, Toolbar, Typography } from '@mui/material'; import { AppBar, IconButton, Toolbar, Typography } from '@mui/material';
import type { FC } from 'react';
export const DRAWER_WIDTH = 210; export const DRAWER_WIDTH = 210;
@@ -20,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">

View File

@@ -1,12 +1,12 @@
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
import { DRAWER_WIDTH } from './Layout';
import LayoutMenu from './LayoutMenu';
import type { FC } from 'react'; import type { FC } from 'react';
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
import { PROJECT_NAME } from 'api/env'; import { PROJECT_NAME } from 'api/env';
import { DRAWER_WIDTH } from './Layout';
import LayoutMenu from './LayoutMenu';
const LayoutDrawerLogo = styled('img')(({ theme }) => ({ const LayoutDrawerLogo = styled('img')(({ theme }) => ({
[theme.breakpoints.down('sm')]: { [theme.breakpoints.down('sm')]: {
height: 24, height: 24,

View File

@@ -1,3 +1,6 @@
import { useContext, useState } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import AssessmentIcon from '@mui/icons-material/Assessment'; import AssessmentIcon from '@mui/icons-material/Assessment';
import CategoryIcon from '@mui/icons-material/Category'; import CategoryIcon from '@mui/icons-material/Category';
@@ -9,28 +12,23 @@ import PersonIcon from '@mui/icons-material/Person';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import SensorsIcon from '@mui/icons-material/Sensors'; import SensorsIcon from '@mui/icons-material/Sensors';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import { import {
Divider, Avatar,
List,
Box, Box,
Button, Button,
Popover, Divider,
Avatar, List,
MenuItem,
TextField,
ListItem, ListItem,
ListItemButton, ListItemButton,
ListItemIcon, ListItemIcon,
ListItemText ListItemText,
MenuItem,
Popover,
TextField
} from '@mui/material'; } from '@mui/material';
import { useContext, useState } from 'react';
import type { Locales } from 'i18n/i18n-types';
import type { FC, ChangeEventHandler } from 'react';
import LayoutMenuItem from 'components/layout/LayoutMenuItem'; import LayoutMenuItem from 'components/layout/LayoutMenuItem';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import DEflag from 'i18n/DE.svg'; import DEflag from 'i18n/DE.svg';
import FRflag from 'i18n/FR.svg'; import FRflag from 'i18n/FR.svg';
import GBflag from 'i18n/GB.svg'; import GBflag from 'i18n/GB.svg';
@@ -41,8 +39,8 @@ import PLflag from 'i18n/PL.svg';
import SKflag from 'i18n/SK.svg'; import SKflag from 'i18n/SK.svg';
import SVflag from 'i18n/SV.svg'; import SVflag from 'i18n/SV.svg';
import TRflag from 'i18n/TR.svg'; import TRflag from 'i18n/TR.svg';
import { I18nContext } from 'i18n/i18n-react'; import { I18nContext } from 'i18n/i18n-react';
import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async'; import { loadLocaleAsync } from 'i18n/i18n-util.async';
const LayoutMenu: FC = () => { const LayoutMenu: FC = () => {
@@ -56,14 +54,16 @@ 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);
setLocale(loc); setLocale(loc);
}; };
const handleClick = (event: any) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
@@ -94,13 +94,20 @@ const LayoutMenu: FC = () => {
}} }}
> >
<ListItemText <ListItemText
primary={LL.CUSTOMIZE()} primary={LL.MODULE()}
primaryTypographyProps={{ primaryTypographyProps={{
fontWeight: '600', fontWeight: '600',
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,
@@ -125,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)}
@@ -139,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 />
@@ -241,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>

View File

@@ -1,7 +1,8 @@
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Link, useLocation } from 'react-router-dom';
import type { SvgIconProps } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import type { SvgIconProps } from '@mui/material';
import { routeMatches } from 'utils'; import { routeMatches } from 'utils';
@@ -12,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);
@@ -22,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>
); );
}; };

View File

@@ -1,8 +1,16 @@
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Link } from 'react-router-dom';
import type { SvgIconProps } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import { Link } from 'react-router-dom';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import {
Avatar,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemIcon,
ListItemText
} from '@mui/material';
import type { SvgIconProps } from '@mui/material';
interface ListMenuItemProps { interface ListMenuItemProps {
icon: React.ComponentType<SvgIconProps>; icon: React.ComponentType<SvgIconProps>;
@@ -26,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>
) : ( ) : (

View File

@@ -1,4 +1,4 @@
import { useRef, useEffect, createContext, useContext } from 'react'; import { createContext, useContext, useEffect, useRef } from 'react';
export interface LayoutContextValue { export interface LayoutContextValue {
title: string; title: string;

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Box, Paper, Typography } from '@mui/material'; import { Box, Paper, Typography } from '@mui/material';
import type { FC } from 'react';
interface ApplicationErrorProps { interface ApplicationErrorProps {
message?: string; message?: string;
@@ -21,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>

View File

@@ -1,9 +1,9 @@
import RefreshIcon from '@mui/icons-material/Refresh';
import { Box, Button, CircularProgress, Typography } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import { MessageBox } from 'components'; import RefreshIcon from '@mui/icons-material/Refresh';
import { Box, Button, CircularProgress, Typography } from '@mui/material';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
interface FormLoaderProps { interface FormLoaderProps {
@@ -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>
)} )}

View File

@@ -1,7 +1,8 @@
import { CircularProgress, Box, Typography } from '@mui/material';
import type { Theme } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import { Box, CircularProgress, Typography } from '@mui/material';
import type { Theme } from '@mui/material';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
interface LoadingSpinnerProps { interface LoadingSpinnerProps {
@@ -12,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),

View File

@@ -1,7 +1,13 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
import type { FC } from 'react'; import type { FC } from 'react';
import type { Blocker } from 'react-router-dom';
import type { unstable_Blocker as Blocker } from 'react-router-dom'; 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>

View File

@@ -1,13 +1,17 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import type { FC } from 'react'; import type { FC } from 'react';
import { Navigate } from 'react-router-dom';
import type { RequiredChildrenProps } from 'utils';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
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;

View File

@@ -1,12 +1,15 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import type { FC } from 'react';
import { Navigate, useLocation } from 'react-router-dom'; import { Navigate, useLocation } from 'react-router-dom';
import type { AuthenticatedContextValue } from 'contexts/authentication/context';
import type { FC } from 'react';
import type { RequiredChildrenProps } from 'utils';
import { storeLoginRedirect } from 'api/authentication'; import { storeLoginRedirect } from 'api/authentication';
import { AuthenticatedContext, AuthenticationContext } from 'contexts/authentication/context';
import type { AuthenticatedContextValue } from 'contexts/authentication/context';
import {
AuthenticatedContext,
AuthenticationContext
} from 'contexts/authentication/context';
import type { RequiredChildrenProps } from 'utils';
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => { const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
const authenticationContext = useContext(AuthenticationContext); const authenticationContext = useContext(AuthenticationContext);
@@ -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>
) : ( ) : (

View File

@@ -1,15 +1,20 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import type { FC } from 'react'; import type { FC } from 'react';
import { Navigate } from 'react-router-dom';
import type { RequiredChildrenProps } from 'utils';
import * as AuthenticationApi from 'api/authentication'; import * as AuthenticationApi from 'api/authentication';
import { AuthenticationContext } from 'contexts/authentication'; import { AuthenticationContext } from 'contexts/authentication';
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;

View File

@@ -1,6 +1,7 @@
import { Tabs, useMediaQuery, useTheme } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import type { FC } from 'react'; import type { FC } from 'react';
import { useNavigate } from 'react-router-dom';
import { Tabs, useMediaQuery, useTheme } from '@mui/material';
import type { RequiredChildrenProps } from 'utils'; import type { RequiredChildrenProps } from 'utils';
@@ -14,12 +15,16 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
const theme = useTheme(); const theme = useTheme();
const smallDown = useMediaQuery(theme.breakpoints.down('sm')); const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
const handleTabChange = (_event: any, path: string) => { const handleTabChange = (_event: unknown, path: string) => {
navigate(path); navigate(path);
}; };
return ( return (
<Tabs value={value} onChange={handleTabChange} variant={smallDown ? 'scrollable' : 'fullWidth'}> <Tabs
value={value}
onChange={handleTabChange}
variant={smallDown ? 'scrollable' : 'fullWidth'}
>
{children} {children}
</Tabs> </Tabs>
); );

View File

@@ -1,13 +1,14 @@
import { Fragment } from 'react';
import type { FC } from 'react';
import { useDropzone } from 'react-dropzone';
import type { DropzoneState } from 'react-dropzone';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CloudUploadIcon from '@mui/icons-material/CloudUpload'; import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material'; import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material';
import { Fragment } from 'react';
import { useDropzone } from 'react-dropzone';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { Progress } from 'alova';
import type { FC } from 'react';
import type { DropzoneState } from 'react-dropzone';
import type { Progress } from 'alova';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const getBorderColor = (theme: Theme, props: DropzoneState) => { const getBorderColor = (theme: Theme, props: DropzoneState) => {
@@ -30,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({
@@ -52,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();
@@ -94,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>

View File

@@ -1,16 +1,18 @@
import { useRequest } from 'alova';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { redirect } from 'react-router-dom'; import { redirect } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { AuthenticationContext } from './context';
import type { FC } from 'react';
import type { Me } from 'types';
import type { RequiredChildrenProps } from 'utils';
import * as AuthenticationApi from 'api/authentication'; import * as AuthenticationApi from 'api/authentication';
import { ACCESS_TOKEN } from 'api/endpoints'; import { ACCESS_TOKEN } from 'api/endpoints';
import { useRequest } from 'alova';
import { LoadingSpinner } from 'components'; import { LoadingSpinner } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { Me } from 'types';
import type { RequiredChildrenProps } from 'utils';
import { AuthenticationContext } from './context';
const Authentication: FC<RequiredChildrenProps> = ({ children }) => { const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -18,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 {
@@ -59,7 +64,6 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
setMe(undefined); setMe(undefined);
setInitialized(true); setInitialized(true);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@@ -1,4 +1,5 @@
import { createContext } from 'react'; import { createContext } from 'react';
import type { Me } from 'types'; import type { Me } from 'types';
export interface AuthenticationContextValue { export interface AuthenticationContextValue {
@@ -9,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;

View File

@@ -1,3 +1,6 @@
import { type FC, useState } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CastIcon from '@mui/icons-material/Cast'; import CastIcon from '@mui/icons-material/Cast';
@@ -10,18 +13,26 @@ 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 { List, Button, Dialog, DialogActions, DialogContent, DialogTitle, Box } from '@mui/material';
import { useRequest } from 'alova';
import { useState, type FC } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from './system/RestartMonitor';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { ButtonRow, SectionContent, useLayoutTitle } from 'components'; import { ButtonRow, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import RestartMonitor from './system/RestartMonitor';
const Settings: FC = () => { const Settings: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS(0)); useLayoutTitle(LL.SETTINGS(0));
@@ -49,8 +60,8 @@ const Settings: FC = () => {
.then(() => { .then(() => {
setRestarting(true); setRestarting(true);
}) })
.catch((err) => { .catch((error: Error) => {
toast.error(err.message); toast.error(error.message);
}) })
.finally(() => { .finally(() => {
setConfirmRestart(false); setConfirmRestart(false);
@@ -64,8 +75,8 @@ const Settings: FC = () => {
.then(() => { .then(() => {
setRestarting(true); setRestarting(true);
}) })
.catch((err) => { .catch((error: Error) => {
toast.error(err.message); toast.error(error.message);
}) })
.finally(() => { .finally(() => {
setConfirmFactoryReset(false); setConfirmFactoryReset(false);
@@ -79,8 +90,8 @@ const Settings: FC = () => {
.then(() => { .then(() => {
setRestarting(true); setRestarting(true);
}) })
.catch((err) => { .catch((error: Error) => {
toast.error(err.message); toast.error(error.message);
}) })
.finally(() => { .finally(() => {
setConfirmRestart(false); setConfirmRestart(false);
@@ -89,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>
@@ -125,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>
@@ -186,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}
@@ -239,7 +275,9 @@ const Settings: FC = () => {
</> </>
); );
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>; return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
}; };
export default Settings; export default Settings;

View File

@@ -1,31 +1,32 @@
import { useState } from 'react';
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, MenuItem } from '@mui/material'; import { Button, Checkbox, MenuItem } from '@mui/material';
import { range } from 'lodash-es';
import { useState } from 'react';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import type { APSettingsType } from 'types';
import * as APApi from 'api/ap'; import * as APApi from 'api/ap';
import type { ValidateFieldsError } from 'async-validator';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
BlockNavigation,
ButtonRow, ButtonRow,
FormLoader, FormLoader,
SectionContent, SectionContent,
ValidatedPasswordField, ValidatedPasswordField,
ValidatedTextField, ValidatedTextField
BlockNavigation
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { range } from 'lodash-es';
import type { APSettingsType } from 'types';
import { APProvisionMode } from 'types'; import { APProvisionMode } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils'; 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) {
@@ -60,8 +66,8 @@ const APSettings: FC = () => {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(createAPSettingsValidator(data), data); await validate(createAPSettingsValidator(data), data);
await saveData(); await saveData();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
}; };
@@ -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

View File

@@ -1,17 +1,27 @@
import type { FC } from 'react';
import ComputerIcon from '@mui/icons-material/Computer'; 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 {
import { useRequest } from 'alova'; Avatar,
Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react';
import type { APStatusType } from 'types';
import * as APApi from 'api/ap'; import * as APApi from 'api/ap';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { APStatusType } from 'types';
import { APNetworkStatus } from 'types'; import { APNetworkStatus } from 'types';
export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => { export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
@@ -68,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>
@@ -77,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>
@@ -91,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>

View File

@@ -1,12 +1,13 @@
import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { Navigate, Routes, Route } from 'react-router-dom';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import APSettings from './APSettings'; import APSettings from './APSettings';
import APStatus from './APStatus'; import APStatus from './APStatus';
import type { FC } from 'react';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const AccessPoint: FC = () => { const AccessPoint: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,13 +1,14 @@
import { Tab } from '@mui/material';
import { Navigate, Route, Routes } from 'react-router-dom';
import MqttSettings from './MqttSettings';
import MqttStatus from './MqttStatus';
import type { FC } from 'react'; import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components'; import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import MqttSettings from './MqttSettings';
import MqttStatus from './MqttStatus';
const Mqtt: FC = () => { const Mqtt: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,24 +1,33 @@
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment, TextField } from '@mui/material';
import { useState } from 'react'; import { useState } from 'react';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react'; import type { FC } from 'react';
import type { MqttSettingsType } from 'types'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
import {
Button,
Checkbox,
Grid,
InputAdornment,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
import type { ValidateFieldsError } from 'async-validator';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
BlockNavigation,
ButtonRow, ButtonRow,
FormLoader, FormLoader,
SectionContent, SectionContent,
ValidatedPasswordField, ValidatedPasswordField,
ValidatedTextField, ValidatedTextField
BlockNavigation
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { MqttSettingsType } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators'; import { createMqttSettingsValidator, validate } from 'validators';
const MqttSettings: FC = () => { const MqttSettings: FC = () => {
@@ -42,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) {
@@ -54,18 +68,30 @@ const MqttSettings: FC = () => {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(createMqttSettingsValidator(data), data); await validate(createMqttSettingsValidator(data), data);
await saveData(); await saveData();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
}; };
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}
@@ -143,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"
@@ -172,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()}
/> />
)} )}
@@ -189,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()}
/> />
@@ -214,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 && (
@@ -228,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>
@@ -236,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()}
/> />
@@ -245,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>
@@ -311,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()}&nbsp;(0=auto) {LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(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"
@@ -333,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"
@@ -348,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"
@@ -363,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"
@@ -378,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"
@@ -393,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"
@@ -408,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"
@@ -422,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

View File

@@ -1,20 +1,34 @@
import type { FC } from 'react';
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion'; import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
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 {
import { useRequest } from 'alova'; Avatar,
Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react';
import type { MqttStatusType } from 'types';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent } from 'components'; import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
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;
} }
@@ -24,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;
@@ -90,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" />
</> </>
@@ -138,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>

View File

@@ -1,15 +1,17 @@
import { Tab } from '@mui/material';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom'; import type { FC } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
import { Tab } from '@mui/material';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { WiFiNetwork } from 'types';
import NetworkSettings from './NetworkSettings'; import NetworkSettings from './NetworkSettings';
import NetworkStatus from './NetworkStatus'; import NetworkStatus from './NetworkStatus';
import { WiFiConnectionContext } from './WiFiConnectionContext'; import { WiFiConnectionContext } from './WiFiConnectionContext';
import WiFiNetworkScanner from './WiFiNetworkScanner'; import WiFiNetworkScanner from './WiFiNetworkScanner';
import type { FC } from 'react';
import type { WiFiNetwork } from 'types';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Network: FC = () => { const Network: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,3 +1,7 @@
import { useContext, useEffect, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
@@ -14,39 +18,35 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
Typography, MenuItem,
TextField, TextField,
MenuItem Typography
} from '@mui/material'; } from '@mui/material';
// eslint-disable-next-line import/named
import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system';
import { updateState, useRequest } from 'alova'; import { updateState, useRequest } from 'alova';
import { useContext, useEffect, useState } from 'react'; import type { ValidateFieldsError } from 'async-validator';
import { toast } from 'react-toastify'; import {
BlockFormControlLabel,
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent,
ValidatedPasswordField,
ValidatedTextField
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { NetworkSettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network';
import RestartMonitor from '../system/RestartMonitor'; import RestartMonitor from '../system/RestartMonitor';
import { WiFiConnectionContext } from './WiFiConnectionContext'; import { WiFiConnectionContext } from './WiFiConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector'; import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import type { NetworkSettingsType } from 'types';
import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system';
import {
BlockFormControlLabel,
ButtonRow,
FormLoader,
SectionContent,
ValidatedPasswordField,
ValidatedTextField,
MessageBox,
BlockNavigation
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network';
const NetworkSettings: FC = () => { const NetworkSettings: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -80,7 +80,7 @@ const NetworkSettings: FC = () => {
useEffect(() => { useEffect(() => {
if (!initialized && data) { if (!initialized && data) {
if (selectedNetwork) { if (selectedNetwork) {
updateState('networkSettings', (current_data) => ({ updateState('networkSettings', (current_data: NetworkSettingsType) => ({
ssid: selectedNetwork.ssid, ssid: selectedNetwork.ssid,
bssid: selectedNetwork.bssid, bssid: selectedNetwork.bssid,
password: current_data ? current_data.password : '', password: current_data ? current_data.password : '',
@@ -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>();
@@ -115,8 +120,8 @@ const NetworkSettings: FC = () => {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(createNetworkSettingsValidator(data), data); await validate(createNetworkSettingsValidator(data), data);
await saveData(); await saveData();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
deselectNetwork(); deselectNetwork();
}; };
@@ -127,7 +132,7 @@ const NetworkSettings: FC = () => {
}; };
const restart = async () => { const restart = async () => {
await restartCommand().catch((error) => { await restartCommand().catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
setRestarting(true); setRestarting(true);
@@ -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>
)}
</> </>
); );
}; };

View File

@@ -1,3 +1,5 @@
import type { FC } from 'react';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DnsIcon from '@mui/icons-material/Dns'; import DnsIcon from '@mui/icons-material/Dns';
import GiteIcon from '@mui/icons-material/Gite'; import GiteIcon from '@mui/icons-material/Gite';
@@ -6,16 +8,24 @@ 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 {
import { useRequest } from 'alova'; Avatar,
Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react';
import type { NetworkStatusType } from 'types';
import * as NetworkApi from 'api/network'; import * as NetworkApi from 'api/network';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NetworkStatusType } from 'types';
import { NetworkConnectionStatus } from 'types'; import { NetworkConnectionStatus } from 'types';
const isConnected = ({ status }: NetworkStatusType) => const isConnected = ({ status }: NetworkStatusType) =>
@@ -48,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;
@@ -60,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') {
@@ -70,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();
@@ -134,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" />
</> </>
@@ -154,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>
@@ -170,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>
@@ -179,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>

View File

@@ -1,4 +1,5 @@
import { createContext } from 'react'; import { createContext } from 'react';
import type { WiFiNetwork } from 'types'; import type { WiFiNetwork } from 'types';
export interface WiFiConnectionContextValue { export interface WiFiConnectionContextValue {
@@ -8,4 +9,6 @@ export interface WiFiConnectionContextValue {
} }
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue; const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
export const WiFiConnectionContext = createContext(WiFiConnectionContextDefaultValue); export const WiFiConnectionContext = createContext(
WiFiConnectionContextDefaultValue
);

View File

@@ -1,15 +1,16 @@
import { useRef, useState } from 'react';
import type { FC } from 'react';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi'; import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
// eslint-disable-next-line import/named
import * as NetworkApi from 'api/network';
import { updateState, useRequest } from 'alova'; import { updateState, useRequest } from 'alova';
import { useState, useRef } from 'react'; import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import WiFiNetworkSelector from './WiFiNetworkSelector'; import WiFiNetworkSelector from './WiFiNetworkSelector';
import type { FC } from 'react';
import * as NetworkApi from 'api/network';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const NUM_POLLS = 10; const NUM_POLLS = 10;
const POLLING_FREQUENCY = 1000; const POLLING_FREQUENCY = 1000;
@@ -19,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,
@@ -50,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} />;
}; };

View File

@@ -1,17 +1,27 @@
import { useContext } from 'react';
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 {
import { useContext } from 'react'; Avatar,
Badge,
List,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { WiFiNetwork, WiFiNetworkList } from 'types';
import { WiFiEncryptionType } from 'types';
import { WiFiConnectionContext } from './WiFiConnectionContext'; import { WiFiConnectionContext } from './WiFiConnectionContext';
import type { Theme } from '@mui/material';
import type { FC } from 'react';
import type { WiFiNetwork, WiFiNetworkList } from 'types';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { WiFiEncryptionType } from 'types';
interface WiFiNetworkSelectorProps { interface WiFiNetworkSelectorProps {
networkList: WiFiNetworkList; networkList: WiFiNetworkList;
@@ -39,7 +49,7 @@ export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
case WiFiEncryptionType.WIFI_AUTH_WPA2_WPA3_PSK: case WiFiEncryptionType.WIFI_AUTH_WPA2_WPA3_PSK:
return 'WPA2/WPA3'; return 'WPA2/WPA3';
default: default:
return 'Unknown: ' + encryption_type; return 'Unknown: ' + String(encryption_type);
} }
}; };
@@ -59,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>

View File

@@ -1,28 +1,30 @@
import { useState } from 'react';
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, MenuItem } from '@mui/material'; import { Button, Checkbox, MenuItem } from '@mui/material';
// eslint-disable-next-line import/named
import { updateState } from 'alova';
import { useState } from 'react';
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import type { NTPSettingsType } from 'types';
import * as NTPApi from 'api/ntp'; import * as NTPApi from 'api/ntp';
import { updateState } from 'alova';
import type { ValidateFieldsError } from 'async-validator';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
BlockNavigation,
ButtonRow, ButtonRow,
FormLoader, FormLoader,
SectionContent, SectionContent,
ValidatedTextField, ValidatedTextField
BlockNavigation
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NTPSettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ';
const NTPSettings: FC = () => { const NTPSettings: FC = () => {
const { const {
loadData, loadData,
@@ -42,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>();
@@ -56,15 +63,15 @@ const NTPSettings: FC = () => {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(NTP_SETTINGS_VALIDATOR, data); await validate(NTP_SETTINGS_VALIDATOR, data);
await saveData(); await saveData();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
}; };
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => { const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFormValue(event); updateFormValue(event);
updateState('ntpSettings', (settings) => ({ updateState('ntpSettings', (settings: NTPSettingsType) => ({
...settings, ...settings,
tz_label: event.target.value, tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value] tz_format: TIME_ZONES[event.target.value]
@@ -74,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

View File

@@ -1,3 +1,7 @@
import { useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DnsIcon from '@mui/icons-material/Dns'; import DnsIcon from '@mui/icons-material/Dns';
@@ -18,21 +22,18 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
TextField, TextField,
useTheme, Typography,
Typography useTheme
} from '@mui/material'; } from '@mui/material';
import { useRequest } from 'alova';
import { useState } from 'react';
import { toast } from 'react-toastify';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import type { FC } from 'react';
import type { NTPStatusType } from 'types';
import { dialogStyle } from 'CustomTheme';
import * as NTPApi from 'api/ntp'; import * as NTPApi from 'api/ntp';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NTPStatusType, Time } from 'types';
import { NTPSyncStatus } from 'types'; import { NTPSyncStatus } from 'types';
import { formatDateTime, formatLocalDateTime } from 'utils'; import { formatDateTime, formatLocalDateTime } from 'utils';
@@ -45,14 +46,19 @@ const NTPStatus: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { send: updateTime } = useRequest((local_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) {
@@ -67,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()));
@@ -107,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}>
@@ -126,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
@@ -178,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>
@@ -187,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>
@@ -202,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>

View File

@@ -1,13 +1,14 @@
import { Tab } from '@mui/material';
import { Navigate, Route, Routes } from 'react-router-dom';
import NTPSettings from './NTPSettings';
import NTPStatus from './NTPStatus';
import type { FC } from 'react'; import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components'; import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import NTPSettings from './NTPSettings';
import NTPStatus from './NTPStatus';
const NetworkTime: FC = () => { const NetworkTime: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle('NTP'); useLayoutTitle('NTP');

View File

@@ -1,8 +1,6 @@
import { MenuItem } from '@mui/material'; import { MenuItem } from '@mui/material';
type TimeZones = { type TimeZones = Record<string, string>;
[name: string]: string;
};
export const TIME_ZONES: TimeZones = { export const TIME_ZONES: TimeZones = {
'Africa/Abidjan': 'GMT0', 'Africa/Abidjan': 'GMT0',

View File

@@ -1,26 +1,26 @@
import { useState } from 'react';
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 } from '@mui/material'; import { Button, Checkbox } from '@mui/material';
import { useState } from 'react';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import type { OTASettingsType } from 'types';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import type { ValidateFieldsError } from 'async-validator';
import { import {
BlockFormControlLabel, BlockFormControlLabel,
BlockNavigation,
ButtonRow, ButtonRow,
FormLoader, FormLoader,
SectionContent, SectionContent,
ValidatedPasswordField, ValidatedPasswordField,
ValidatedTextField, ValidatedTextField,
BlockNavigation,
useLayoutTitle useLayoutTitle
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { OTASettingsType } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import { OTA_SETTINGS_VALIDATOR } from 'validators/system'; import { OTA_SETTINGS_VALIDATOR } from 'validators/system';
@@ -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>();
@@ -57,8 +62,8 @@ const OTASettings: FC = () => {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(OTA_SETTINGS_VALIDATOR, data); await validate(OTA_SETTINGS_VALIDATOR, data);
await saveData(); await saveData();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as 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

View File

@@ -1,23 +1,24 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box, Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
LinearProgress, LinearProgress,
Typography,
TextField, TextField,
Button Typography
} from '@mui/material'; } from '@mui/material';
import { useRequest } from 'alova';
import { useEffect } from 'react';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import { MessageBox } from 'components';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
interface GenerateTokenProps { interface GenerateTokenProps {
@@ -29,26 +30,40 @@ 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) {
void generateToken(); void generateToken();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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>

View File

@@ -1,3 +1,7 @@
import { useContext, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
@@ -6,29 +10,42 @@ import EditIcon from '@mui/icons-material/Edit';
import PersonAddIcon from '@mui/icons-material/PersonAdd'; import PersonAddIcon from '@mui/icons-material/PersonAdd';
import VpnKeyIcon from '@mui/icons-material/VpnKey'; import VpnKeyIcon from '@mui/icons-material/VpnKey';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Button, IconButton, Box } from '@mui/material'; import { Box, Button, IconButton } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useContext, useState } from 'react';
import { useBlocker } from 'react-router-dom';
import GenerateToken from './GenerateToken';
import User from './User';
import type { FC } from 'react';
import type { SecuritySettingsType, UserType } from 'types';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import { ButtonRow, FormLoader, MessageBox, SectionContent, BlockNavigation } from 'components';
import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import {
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType, UserType } from 'types';
import { useRest } from 'utils'; import { useRest } from 'utils';
import { createUserValidator } from 'validators'; import { createUserValidator } from 'validators';
import GenerateToken from './GenerateToken';
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);
@@ -112,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);
@@ -138,12 +160,27 @@ const ManageUsers: FC = () => {
setChanged(0); setChanged(0);
}; };
const user_table = data.users.map((u) => ({ ...u, id: u.username })); interface UserType2 {
id: string;
username: string;
password: string;
admin: boolean;
}
// add id to the type, needed for the table
const user_table = data.users.map((u) => ({
...u,
id: u.username
})) as UserType2[];
return ( return (
<> <>
<Table data={{ nodes: user_table }} theme={table_theme} layout={{ custom: true }}> <Table
{(tableList: any) => ( data={{ nodes: user_table }}
theme={table_theme}
layout={{ custom: true }}
>
{(tableList: UserType2[]) => (
<> <>
<Header> <Header>
<HeaderRow> <HeaderRow>
@@ -153,7 +190,7 @@ const ManageUsers: FC = () => {
</HeaderRow> </HeaderRow>
</Header> </Header>
<Body> <Body>
{tableList.map((u: any) => ( {tableList.map((u: UserType2) => (
<Row key={u.id} item={u}> <Row key={u.id} item={u}>
<Cell>{u.username}</Cell> <Cell>{u.username}</Cell>
<Cell stiff>{u.admin ? <CheckIcon /> : <CloseIcon />}</Cell> <Cell stiff>{u.admin ? <CheckIcon /> : <CloseIcon />}</Cell>
@@ -179,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 } }}>
@@ -211,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>

View File

@@ -1,12 +1,13 @@
import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { Navigate, Routes, Route } from 'react-router-dom';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import ManageUsers from './ManageUsers'; import ManageUsers from './ManageUsers';
import SecuritySettings from './SecuritySettings'; import SecuritySettings from './SecuritySettings';
import type { FC } from 'react';
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Security: FC = () => { const Security: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();

View File

@@ -1,16 +1,24 @@
import { useContext, useState } from 'react';
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 } from '@mui/material'; import { Button } from '@mui/material';
import { useContext, useState } from 'react';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import type { SecuritySettingsType } from 'types';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField, BlockNavigation } from 'components';
import type { ValidateFieldsError } from 'async-validator';
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 { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators'; import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
@@ -36,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) {
@@ -49,8 +62,8 @@ const SecuritySettings: FC = () => {
await validate(SECURITY_SETTINGS_VALIDATOR, data); await validate(SECURITY_SETTINGS_VALIDATOR, data);
await saveData(); await saveData();
await authenticatedContext.refresh(); await authenticatedContext.refresh();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
}; };

View File

@@ -1,17 +1,28 @@
import { useEffect, useState } from 'react';
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 { useState, useEffect } from 'react';
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 type { FC } from 'react'; import {
BlockFormControlLabel,
import type { UserType } from 'types'; ValidatedPasswordField,
import { dialogStyle } from 'CustomTheme'; ValidatedTextField
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { UserType } from 'types';
import { updateValue } from 'utils'; import { updateValue } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
@@ -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);
@@ -45,14 +63,20 @@ const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEdi
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, user); await validate(validator, user);
onDoneEditing(); onDoneEditing();
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
} }
}; };
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

View File

@@ -1,3 +1,5 @@
import type { FC } from 'react';
import AppsIcon from '@mui/icons-material/Apps'; import AppsIcon from '@mui/icons-material/Apps';
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard'; import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
import DevicesIcon from '@mui/icons-material/Devices'; import DevicesIcon from '@mui/icons-material/Devices';
@@ -6,12 +8,20 @@ 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,
import { useRequest } from 'alova'; Box,
import type { FC } from 'react'; Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components'; import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -24,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) {
@@ -40,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>
@@ -74,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 && (
@@ -88,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>
</> </>
@@ -103,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>
@@ -117,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>
@@ -130,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" />
@@ -138,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>

View File

@@ -1,10 +1,10 @@
import { useRequest } from 'alova'; import { useEffect, useRef, useState } from 'react';
import { useRef, useState, useEffect } from 'react';
import type { FC } from 'react'; import type { FC } from 'react';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { FormLoader } from 'components';
import { useRequest } from 'alova';
import { FormLoader } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000; const RESTART_TIMEOUT = 2 * 60 * 1000;
@@ -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;

View File

@@ -1,15 +1,16 @@
import { type FC, useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
import { useContext, type FC } from 'react';
import { Navigate, Routes, Route } from 'react-router-dom';
import SystemLog from './SystemLog';
import SystemStatus from './SystemStatus';
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from 'components';
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } 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 SystemActivity from 'project/SystemActivity'; import SystemActivity from 'project/SystemActivity';
import SystemLog from './SystemLog';
import SystemStatus from './SystemStatus';
const System: FC = () => { const System: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -23,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 />} />

View File

@@ -1,24 +1,36 @@
import { useEffect, useRef, useState } from 'react';
import type { FC } from 'react';
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, styled, Button, Checkbox, MenuItem, Grid, TextField } from '@mui/material'; import {
import { useRequest } from 'alova'; Box,
import { useState, useEffect, useRef } from 'react'; Button,
import { toast } from 'react-toastify'; Checkbox,
import type { FC } from 'react'; Grid,
MenuItem,
TextField,
styled
} from '@mui/material';
import type { LogSettings, LogEntry } from 'types';
import { addAccessTokenParameter } from 'api/authentication';
import { EVENT_SOURCE_ROOT } from 'api/endpoints';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { fetchLogES } from 'api/system';
import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation, useLayoutTitle } from 'components'; import { useSSE } from '@alova/scene-react';
import { useRequest } from 'alova';
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 { LogLevel } from 'types'; import { LogLevel } from 'types';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
const LogEntryLine = styled('div')(() => ({ const LogEntryLine = styled('div')(() => ({
color: '#bbbbbb', color: '#bbbbbb',
fontFamily: 'monospace', fontFamily: 'monospace',
@@ -27,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) {
@@ -52,18 +66,53 @@ 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
});
// called on page load to reset pointer and fetch all log entries
useRequest(SystemApi.fetchLog());
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
const { onMessage, onError } = useSSE(fetchLogES, {
immediate: true,
// withCredentials: true,
interceptByGlobalResponded: false
});
onMessage((message: { id: number; data: string }) => {
const rawData = message.data;
const logentry = JSON.parse(rawData) as LogEntry;
if (logentry.i > lastIndex) {
setLastIndex(logentry.i);
setLogEntries((log) => [...log, logentry]);
}
});
onError(() => {
toast.error('No connection to Log server');
});
// called on page load to reset pointer and fetch all log entries
useRequest(SystemApi.fetchLog());
const paddedLevelLabel = (level: LogLevel) => { const paddedLevelLabel = (level: LogLevel) => {
const label = levelLabel(level); const label = levelLabel(level);
@@ -83,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();
@@ -97,8 +150,8 @@ const SystemLog: FC = () => {
await saveData(); await saveData();
}; };
// handle scrolling
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (logEntries.length) { if (logEntries.length) {
ref.current?.scrollIntoView({ ref.current?.scrollIntoView({
@@ -108,29 +161,6 @@ const SystemLog: FC = () => {
} }
}, [logEntries.length]); }, [logEntries.length]);
useEffect(() => {
const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL));
es.onmessage = (event: MessageEvent) => {
const rawData = event.data;
if (typeof rawData === 'string' || rawData instanceof String) {
const logentry = JSON.parse(rawData as string) as LogEntry;
if (logentry.i > lastIndex) {
setLastIndex(logentry.i);
setLogEntries((log) => [...log, logentry]);
}
}
};
es.onerror = () => {
es.close();
toast.error('No connection to Log server');
};
return () => {
es.close();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const content = () => { const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -138,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"
@@ -177,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>
@@ -189,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 && (

View File

@@ -1,3 +1,6 @@
import { type FC, useContext, useState } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
import BuildIcon from '@mui/icons-material/Build'; import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -9,7 +12,6 @@ import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
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 TimerIcon from '@mui/icons-material/Timer'; import TimerIcon from '@mui/icons-material/Timer';
import { import {
Avatar, Avatar,
Box, Box,
@@ -26,17 +28,15 @@ import {
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { useRequest } from 'alova';
import { useContext, type FC, useState } from 'react';
import { toast } from 'react-toastify';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import * as EMSESP from 'project/api';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
import { busConnectionStatus } from 'project/types'; import { busConnectionStatus } from 'project/types';
import { NTPSyncStatus } from 'types'; import { NTPSyncStatus } from 'types';
@@ -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,28 +138,43 @@ 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()
.then(() => { .then(() => {
toast.info(LL.SCANNING() + '...'); toast.info(LL.SCANNING() + '...');
}) })
.catch((err) => { .catch((error: Error) => {
toast.error(err.message); toast.error(error.message);
}); });
setConfirmScan(false); setConfirmScan(false);
}; };
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>

View File

@@ -1,38 +1,63 @@
import DownloadIcon from '@mui/icons-material/GetApp'; import { type FC, useState } from 'react';
import { Typography, Button, Box, Link } from '@mui/material';
import { useRequest } from 'alova';
import { useState, type FC } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import RestartMonitor from './RestartMonitor';
import DownloadIcon from '@mui/icons-material/GetApp';
import { Box, Button, Link, Typography } from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { FormLoader, SectionContent, SingleUpload, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api'; import * as EMSESP from 'project/api';
import { useRequest } from 'alova';
import {
FormLoader,
SectionContent,
SingleUpload,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { APIcall } from 'project/types';
import RestartMonitor from './RestartMonitor';
const UploadDownload: FC = () => { const UploadDownload: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
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) => 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,
@@ -47,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,
@@ -64,8 +95,9 @@ const UploadDownload: FC = () => {
force: true force: true
}); });
onSuccessUpload(({ data }: any) => { onSuccessUpload(({ data }) => {
if (data) { if (data) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setMd5(data.md5); setMd5(data.md5);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL()); toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
} else { } else {
@@ -74,18 +106,18 @@ const UploadDownload: FC = () => {
}); });
const startUpload = async (files: File[]) => { const startUpload = async (files: File[]) => {
await sendUpload(files[0]).catch((err) => { await sendUpload(files[0]).catch((error: Error) => {
if (err.message === 'The user aborted a request') { if (error.message === 'The user aborted a request') {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED()); toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else if (err.message === 'Network Error') { } else if (error.message === 'Network Error') {
toast.warning('Invalid file extension or incompatible bin file'); toast.warning('Invalid file extension or incompatible bin file');
} else { } else {
toast.error(err.message); toast.error(error.message);
} }
}); });
}; };
const saveFile = (json: any, endpoint: string) => { const saveFile = (json: unknown, endpoint: string) => {
const anchor = document.createElement('a'); const anchor = document.createElement('a');
anchor.href = URL.createObjectURL( anchor.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], { new Blob([JSON.stringify(json, null, 2)], {
@@ -111,30 +143,34 @@ const UploadDownload: FC = () => {
saveFile(event.data, 'schedule.json'); saveFile(event.data, 'schedule.json');
}); });
onGetAPI((event) => { onGetAPI((event) => {
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'); saveFile(
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 () => {
await getSettings().catch((error) => { await getSettings().catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
const downloadCustomizations = async () => { const downloadCustomizations = async () => {
await getCustomizations().catch((error) => { await getCustomizations().catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
const downloadEntities = async () => { const downloadEntities = async () => {
await getEntities().catch((error) => { await getEntities().catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
const downloadSchedule = async () => { const downloadSchedule = async () => {
await getSchedule() await getSchedule()
.catch((error) => { .catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}) })
.finally(() => { .finally(() => {
@@ -143,7 +179,7 @@ const UploadDownload: FC = () => {
}; };
const callAPI = async (device: string, entity: string) => { const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error) => { await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
@@ -165,7 +201,8 @@ const UploadDownload: FC = () => {
<b>{data.emsesp_version}</b>&nbsp;({data.esp_platform}) <b>{data.emsesp_version}</b>&nbsp;({data.esp_platform})
{latestVersion && ( {latestVersion && (
<Box mt={2}> <Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.OFFICIAL()}&nbsp;{LL.RELEASE_IS()}&nbsp;<b>{latestVersion}</b> {LL.THE_LATEST()}&nbsp;{LL.OFFICIAL()}&nbsp;{LL.RELEASE_IS()}
&nbsp;<b>{latestVersion}</b>
&nbsp;( &nbsp;(
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary"> <Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()} {LL.RELEASE_NOTES()}
@@ -173,7 +210,13 @@ const UploadDownload: FC = () => {
)&nbsp;( )&nbsp;(
<Link <Link
target="_blank" target="_blank"
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion)} href={
STABLE_URL +
'v' +
latestVersion +
'/' +
getBinURL(latestVersion as string)
}
color="primary" color="primary"
> >
{LL.DOWNLOAD(1)} {LL.DOWNLOAD(1)}
@@ -183,14 +226,19 @@ const UploadDownload: FC = () => {
)} )}
{latestDevVersion && ( {latestDevVersion && (
<Box mt={2}> <Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.DEVELOPMENT()}&nbsp;{LL.RELEASE_IS()}&nbsp; {LL.THE_LATEST()}&nbsp;{LL.DEVELOPMENT()}&nbsp;{LL.RELEASE_IS()}
&nbsp;
<b>{latestDevVersion}</b> <b>{latestDevVersion}</b>
&nbsp;( &nbsp;(
<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>
)&nbsp;( )&nbsp;(
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion)} color="primary"> <Link
target="_blank"
href={DEV_URL + getBinURL(latestDevVersion as string)}
color="primary"
>
{LL.DOWNLOAD(1)} {LL.DOWNLOAD(1)}
</Link> </Link>
) )
@@ -214,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">
@@ -302,7 +355,9 @@ const UploadDownload: FC = () => {
); );
}; };
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>; return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
}; };
export default UploadDownload; export default UploadDownload;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const de: Translation = { const de: Translation = {
LANGUAGE: 'Sprache', LANGUAGE: 'Sprache',
@@ -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',
@@ -329,7 +328,7 @@ const de: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize' // TODO translate MODULE: 'Module' // TODO translate
}; };
export default de; export default de;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const en: Translation = { const en: Translation = {
LANGUAGE: 'Language', LANGUAGE: 'Language',
@@ -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',
@@ -329,7 +328,7 @@ const en: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings',
SECURITY_1: 'Add or remove users', SECURITY_1: 'Add or remove users',
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware',
CUSTOMIZE: 'Customize' MODULE: 'Module' // TODO translate
}; };
export default en; export default en;

View File

@@ -1,6 +1,7 @@
import type { Locales, Formatters } from './i18n-types';
import type { FormattersInitializer } from 'typesafe-i18n'; import type { FormattersInitializer } from 'typesafe-i18n';
import type { Formatters, Locales } from './i18n-types';
export const initFormatters: FormattersInitializer<Locales, Formatters> = () => { export const initFormatters: FormattersInitializer<Locales, Formatters> = () => {
const formatters: Formatters = { const formatters: Formatters = {
// add your formatter functions here // add your formatter functions here

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const fr: Translation = { const fr: Translation = {
LANGUAGE: 'Langue', LANGUAGE: 'Langue',
@@ -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',
@@ -329,7 +330,7 @@ const fr: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize' // TODO translate MODULE: 'Module' // TODO translate
}; };
export default fr; export default fr;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const it: Translation = { const it: Translation = {
LANGUAGE: 'Lingua', LANGUAGE: 'Lingua',
@@ -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',
@@ -329,7 +331,7 @@ const it: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize' // TODO translate MODULE: 'Module' // TODO translate
}; };
export default it; export default it;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const nl: Translation = { const nl: Translation = {
LANGUAGE: 'Taal', LANGUAGE: 'Taal',
@@ -99,7 +97,7 @@ const nl: Translation = {
NUM_HOURS: '{num} {{uur|uren}}', NUM_HOURS: '{num} {{uur|uren}}',
NUM_MINUTES: '{num} {{minuut|minuten}}', NUM_MINUTES: '{num} {{minuut|minuten}}',
APPLICATION_SETTINGS: 'Applicatieinstellingen', APPLICATION_SETTINGS: 'Applicatieinstellingen',
CUSTOMIZATIONS: 'Custom aanpassingen', CUSTOMIZATIONS: 'User Entities',
APPLICATION_RESTARTING: 'EMS-ESP herstarten', APPLICATION_RESTARTING: 'EMS-ESP herstarten',
INTERFACE_BOARD_PROFILE: 'Interface Apparaatprofiel', INTERFACE_BOARD_PROFILE: 'Interface Apparaatprofiel',
BOARD_PROFILE_TEXT: 'Selecteer een vooraf ingesteld apparaat profiel uit de lijst of kies Eigen om zelf uw hardware te configureren', BOARD_PROFILE_TEXT: 'Selecteer een vooraf ingesteld apparaat profiel uit de lijst of kies Eigen om zelf uw hardware te configureren',
@@ -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',
@@ -329,7 +328,7 @@ const nl: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize' // TODO translate MODULE: 'Module' // TODO translate
}; };
export default nl; export default nl;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const no: Translation = { const no: Translation = {
LANGUAGE: 'Språk', LANGUAGE: 'Språk',
@@ -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',
@@ -324,7 +323,12 @@ const no: Translation = {
UNCHANGED: 'Unchanged', // TODO translate UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always', // TODO translate ALWAYS: 'Always', // TODO translate
ACTIVITY: 'Activity', // TODO translate ACTIVITY: 'Activity', // TODO translate
CONFIGURE: 'Configure {0}' // TODO translate CONFIGURE: 'Configure {0}', // TODO translate
SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
MODULE: 'Module' // TODO translate
}; };
export default no; export default no;

View File

@@ -1,6 +1,4 @@
import type { BaseTranslation } from '../i18n-types'; import type { BaseTranslation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const pl: BaseTranslation = { const pl: BaseTranslation = {
LANGUAGE: 'Język', LANGUAGE: 'Język',
@@ -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ą',
@@ -287,8 +285,8 @@ const pl: BaseTranslation = {
NETWORK_SUBNET: 'Maska podsieci', NETWORK_SUBNET: 'Maska podsieci',
NETWORK_DNS: 'Serwery DNS', NETWORK_DNS: 'Serwery DNS',
ADDRESS_OF: 'Adres {0}', ADDRESS_OF: 'Adres {0}',
ADMIN: 'Użytkownik "administrator".', ADMIN: 'Administrator',
GUEST: 'Użytkownik "gość".', GUEST: 'Gość',
NEW: 'nowe{{go|j|}}', NEW: 'nowe{{go|j|}}',
NEW_NAME_OF: 'Nowa nazwa {0}', NEW_NAME_OF: 'Nowa nazwa {0}',
ENTITY: 'encji', ENTITY: 'encji',
@@ -329,7 +327,7 @@ const pl: BaseTranslation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize', // TODO translate MODULE: 'Module' // TODO translate
}; };
export default pl; export default pl;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const sk: Translation = { const sk: Translation = {
LANGUAGE: 'Jazyk', LANGUAGE: 'Jazyk',
@@ -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ť',
@@ -330,7 +329,7 @@ const sk: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize' // TODO translate MODULE: 'Module' // TODO translate
}; };
export default sk; export default sk;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const sv: Translation = { const sv: Translation = {
LANGUAGE: 'Språk', LANGUAGE: 'Språk',
@@ -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',
@@ -329,7 +328,7 @@ const sv: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize' // TODO translate MODULE: 'Module' // TODO translate
}; };
export default sv; export default sv;

View File

@@ -1,6 +1,4 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
/* eslint-disable */
const tr: Translation = { const tr: Translation = {
LANGUAGE: 'Dil', LANGUAGE: 'Dil',
@@ -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',
@@ -329,7 +328,7 @@ const tr: Translation = {
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
CUSTOMIZE: 'Customize' // TODO translate MODULE: 'Module' // TODO translate
}; };
export default tr; export default tr;

View File

@@ -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 { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } 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>

View File

@@ -1,34 +1,46 @@
import { useState } from 'react';
import type { FC } from 'react';
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, MenuItem, Grid, Typography, Divider, InputAdornment, TextField } from '@mui/material';
import { useRequest } from 'alova';
import { useState } from 'react';
import { toast } from 'react-toastify';
import * as EMSESP from './api';
import { BOARD_PROFILES } from './types';
import { createSettingsValidator } from './validators';
import type { Settings } from './types';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
import { import {
SectionContent, Box,
FormLoader, Button,
Checkbox,
Divider,
Grid,
InputAdornment,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
import { useRequest } from 'alova';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel, BlockFormControlLabel,
ValidatedTextField,
ButtonRow,
MessageBox,
BlockNavigation, BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent,
ValidatedTextField,
useLayoutTitle useLayoutTitle
} from 'components'; } 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';
import { numberValue, updateValueDirty, useRest } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
import * as EMSESP from './api';
import { BOARD_PROFILES } from './types';
import type { Settings } from './types';
import { createSettingsValidator } from './validators';
export function boardProfileSelectItems() { export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map((code) => ( return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}> <MenuItem key={code} value={code}>
@@ -59,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>();
@@ -67,7 +84,7 @@ const ApplicationSettings: FC = () => {
loading: processingBoard, loading: processingBoard,
send: readBoardProfile, send: readBoardProfile,
onSuccess: onSuccessBoardProfile onSuccess: onSuccessBoardProfile
} = useRequest((boardProfile) => EMSESP.getBoardProfile(boardProfile), { } = useRequest((boardProfile: string) => EMSESP.getBoardProfile(boardProfile), {
immediate: false immediate: false
}); });
@@ -93,7 +110,7 @@ const ApplicationSettings: FC = () => {
}); });
const updateBoardProfile = async (board_profile: string) => { const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error) => { await readBoardProfile(board_profile).catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
@@ -109,8 +126,8 @@ const ApplicationSettings: FC = () => {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(createSettingsValidator(data), data); await validate(createSettingsValidator(data), data);
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} finally { } finally {
await saveData(); await saveData();
} }
@@ -131,7 +148,7 @@ const ApplicationSettings: FC = () => {
const restart = async () => { const restart = async () => {
await validateAndSubmit(); await validateAndSubmit();
await restartCommand().catch((error) => { await restartCommand().catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
setRestarting(true); setRestarting(true);
@@ -218,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)}
@@ -320,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"
@@ -394,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}
/> />
@@ -463,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)}
@@ -479,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)}
@@ -495,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"
@@ -554,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}
/> />
@@ -564,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}
/> />
@@ -580,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}
@@ -634,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"
@@ -649,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>

View File

@@ -1,28 +1,41 @@
import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Button, Typography, Box } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } 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';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova'; import { updateState, useRequest } from 'alova';
import { useState, useCallback } from 'react'; import {
import { useBlocker } from 'react-router-dom'; BlockNavigation,
ButtonRow,
import { toast } from 'react-toastify'; FormLoader,
SectionContent,
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog'; useLayoutTitle
import * as EMSESP from './api'; } from 'components';
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import { entityItemValidation } from './validators';
import type { EntityItem } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import type { EntityItem } from './types';
import { entityItemValidation } from './validators';
const CustomEntities: FC = () => { const CustomEntities: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0); const [numChanges, setNumChanges] = useState<number>(0);
@@ -42,7 +55,10 @@ const CustomEntities: FC = () => {
force: true force: true
}); });
const { send: writeEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false }); const { send: writeEntities } = useRequest(
(data: { id: number; entity_ids: string[] }) => EMSESP.writeCustomEntities(data),
{ immediate: false }
);
function hasEntityChanged(ei: EntityItem) { function hasEntityChanged(ei: EntityItem) {
return ( return (
@@ -139,8 +155,8 @@ const CustomEntities: FC = () => {
.then(() => { .then(() => {
toast.success(LL.ENTITIES_UPDATED()); toast.success(LL.ENTITIES_UPDATED());
}) })
.catch((err) => { .catch((error: Error) => {
toast.error(err.message); toast.error(error.message);
}) })
.finally(async () => { .finally(async () => {
await fetchEntities(); await fetchEntities();
@@ -167,10 +183,15 @@ const CustomEntities: FC = () => {
const onDialogSave = (updatedItem: EntityItem) => { const onDialogSave = (updatedItem: EntityItem) => {
setDialogOpen(false); setDialogOpen(false);
updateState('entities', (data) => { 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;
}); });
@@ -195,12 +216,13 @@ const CustomEntities: FC = () => {
setDialogOpen(true); setDialogOpen(true);
}; };
function formatValue(value: any, uom: number) { function formatValue(value: unknown, uom: number) {
return value === undefined || uom === 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) +
: value; (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
: (value as string);
} }
function showHex(value: number, digit: number) { function showHex(value: number, digit: number) {
@@ -213,8 +235,12 @@ const CustomEntities: FC = () => {
} }
return ( return (
<Table data={{ nodes: entities.filter((ei) => !ei.deleted) }} theme={entity_theme} layout={{ custom: true }}> <Table
{(tableList: any) => ( data={{ nodes: entities.filter((ei) => !ei.deleted) }}
theme={entity_theme}
layout={{ custom: true }}
>
{(tableList: EntityItem[]) => (
<> <>
<Header> <Header>
<HeaderRow> <HeaderRow>
@@ -231,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}&nbsp; {ei.name}&nbsp;
{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>
))} ))}
@@ -271,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
@@ -287,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>

View File

@@ -1,3 +1,5 @@
import { useEffect, useState } from 'react';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
@@ -15,29 +17,26 @@ import {
MenuItem, MenuItem,
TextField TextField
} from '@mui/material'; } from '@mui/material';
import { useEffect, useState } from 'react';
import { DeviceValueUOM_s, DeviceValueType, DeviceValueTypeNames } from './types';
import type { EntityItem } from './types';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator';
import { BlockFormControlLabel, ValidatedTextField } from 'components'; import { BlockFormControlLabel, ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils'; import { numberValue, updateValue } from 'utils';
import { validate } from 'validators'; import { validate } from 'validators';
type CustomEntitiesDialogProps = { import { DeviceValueType, DeviceValueUOM_s, DeviceValueTypeNames } from './types';
import type { EntityItem } from './types';
interface CustomEntitiesDialogProps {
open: boolean; open: boolean;
creating: boolean; creating: boolean;
onClose: () => void; onClose: () => void;
onSave: (ei: EntityItem) => void; onSave: (ei: EntityItem) => void;
selectedItem: EntityItem; selectedItem: EntityItem;
validator: Schema; validator: Schema;
}; }
const CustomEntitiesDialog = ({ const CustomEntitiesDialog = ({
open, open,
@@ -80,8 +79,8 @@ const CustomEntitiesDialog = ({
editItem.type_id = parseInt(editItem.type_id, 16); editItem.type_id = parseInt(editItem.type_id, 16);
} }
onSave(editItem); onSave(editItem);
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
}; };
@@ -143,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>
@@ -158,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}>
@@ -171,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}>
@@ -209,55 +222,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>
@@ -266,15 +281,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>

View File

@@ -1,46 +1,60 @@
import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { useBlocker, useLocation } from 'react-router-dom';
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 SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { import {
Button,
Typography,
Box, Box,
MenuItem, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
InputAdornment,
Link,
MenuItem,
TextField,
ToggleButton, ToggleButton,
ToggleButtonGroup, ToggleButtonGroup,
Grid, Typography
TextField,
Link,
InputAdornment
} from '@mui/material'; } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useState, useEffect, useCallback } from 'react';
import { useBlocker, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify';
import * as SystemApi from 'api/system';
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 { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import {
BlockNavigation,
ButtonRow,
MessageBox,
SectionContent,
useLayoutTitle
} from 'components';
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import SettingsCustomizationDialog from './CustomizationDialog'; import SettingsCustomizationDialog from './CustomizationDialog';
import EntityMaskToggle from './EntityMaskToggle'; import EntityMaskToggle from './EntityMaskToggle';
import OptionIcon from './OptionIcon'; import OptionIcon from './OptionIcon';
import * as EMSESP from './api';
import { DeviceEntityMask } from './types'; import { DeviceEntityMask } from './types';
import type { DeviceShort, DeviceEntity } from './types'; import type { DeviceEntity, DeviceShort } from './types';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system';
import { ButtonRow, SectionContent, MessageBox, BlockNavigation, useLayoutTitle } from 'components';
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
export const APIURL = window.location.origin + '/api/'; export const APIURL = window.location.origin + '/api/';
@@ -63,25 +77,41 @@ const Customization: FC = () => {
// fetch devices first // fetch devices first
const { data: devices } = useRequest(EMSESP.readDevices); const { data: devices } = useRequest(EMSESP.readDevices);
// const { state } = useLocation(); const [selectedDevice, setSelectedDevice] = useState<number>(
const [selectedDevice, setSelectedDevice] = useState<number>(useLocation().state || -1); 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(), {
immediate: false immediate: false
}); });
const { send: writeCustomizationEntities } = useRequest((data) => EMSESP.writeCustomizationEntities(data), { const { send: writeCustomizationEntities } = useRequest(
immediate: false (data: { id: number; entity_ids: string[] }) =>
}); EMSESP.writeCustomizationEntities(data),
{
immediate: false
}
);
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), { const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest(
initialData: [], (data: number) => EMSESP.readDeviceEntities(data),
immediate: false {
}); initialData: [],
immediate: false
}
);
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) => {
@@ -161,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(() => {
@@ -195,17 +230,16 @@ const Customization: FC = () => {
setRestartNeeded(false); setRestartNeeded(false);
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [devices, selectedDevice]); }, [devices, selectedDevice]);
const restart = async () => { const restart = async () => {
await restartCommand().catch((error) => { await restartCommand().catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
setRestarting(true); setRestarting(true);
}; };
function formatValue(value: any) { function formatValue(value: unknown) {
if (typeof value === 'number') { if (typeof value === 'number') {
return new Intl.NumberFormat().format(value); return new Intl.NumberFormat().format(value);
} else if (value === undefined) { } else if (value === undefined) {
@@ -213,12 +247,15 @@ const Customization: FC = () => {
} else if (typeof value === 'boolean') { } else if (typeof value === 'boolean') {
return value ? 'true' : 'false'; return value ? 'true' : 'false';
} }
return value; return value as string;
} }
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;
@@ -249,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(
@@ -258,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;
@@ -273,7 +317,7 @@ const Customization: FC = () => {
await resetCustomizations(); await resetCustomizations();
toast.info(LL.CUSTOMIZATIONS_RESTART()); toast.info(LL.CUSTOMIZATIONS_RESTART());
} catch (error) { } catch (error) {
toast.error(error.message); toast.error((error as Error).message);
} finally { } finally {
setConfirmReset(false); setConfirmReset(false);
} }
@@ -284,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) => {
@@ -326,7 +374,10 @@ const Customization: FC = () => {
return; return;
} }
await writeCustomizationEntities({ id: selectedDevice, entity_ids: masked_entities }).catch((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 {
@@ -372,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()}&nbsp;&nbsp; <OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp; &nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp; <OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp; &nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
{LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />=
{LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<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"
@@ -402,7 +465,7 @@ const Customization: FC = () => {
size="small" size="small"
color="secondary" color="secondary"
value={getMaskString(selectedFilters)} value={getMaskString(selectedFilters)}
onChange={(event, mask) => { onChange={(event, mask: string[]) => {
setSelectedFilters(getMaskNumber(mask)); setSelectedFilters(getMaskNumber(mask));
}} }}
> >
@@ -451,12 +514,17 @@ const Customization: FC = () => {
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle2" color="primary"> <Typography variant="subtitle2" color="primary">
{LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length}&nbsp;{LL.ENTITIES(deviceEntities.length)} {LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length}
&nbsp;{LL.ENTITIES(deviceEntities.length)}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}> <Table
{(tableList: any) => ( data={{ nodes: shown_data }}
theme={entities_theme}
layout={{ custom: true }}
>
{(tableList: DeviceEntity[]) => (
<> <>
<Header> <Header>
<HeaderRow> <HeaderRow>
@@ -475,13 +543,20 @@ const Customization: FC = () => {
</Cell> </Cell>
<Cell> <Cell>
{formatName(de, false)}&nbsp;( {formatName(de, false)}&nbsp;(
<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>
))} ))}
@@ -494,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>
@@ -514,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>

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
import { import {
Box, Box,
Button, Button,
@@ -13,25 +14,28 @@ import {
TextField, TextField,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import { useEffect, useState } from 'react';
import { dialogStyle } from 'CustomTheme';
import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils';
import EntityMaskToggle from './EntityMaskToggle'; import EntityMaskToggle from './EntityMaskToggle';
import { DeviceEntityMask } from './types'; import { DeviceEntityMask } from './types';
import type { DeviceEntity } from './types'; import type { DeviceEntity } from './types';
import { dialogStyle } from 'CustomTheme'; interface SettingsCustomizationDialogProps {
import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils';
type SettingsCustomizationDialogProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSave: (di: DeviceEntity) => void; onSave: (di: DeviceEntity) => void;
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);
@@ -39,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) {
@@ -53,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);
@@ -141,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>

View File

@@ -1,21 +1,27 @@
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import type { FC } from 'react';
import { AiOutlineControl, AiOutlineGateway, AiOutlineAlert } from 'react-icons/ai'; import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
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 { MdThermostatAuto, MdOutlineSensors, MdOutlineDevices, MdOutlinePool } 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';
import { DeviceType } from './types';
import type { FC } from 'react'; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import { DeviceType } from './types';
interface DeviceIconProps { interface DeviceIconProps {
type_id: number; type_id: number;
} }
const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => { const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
switch (type_id) { switch (type_id as DeviceType) {
case DeviceType.TEMPERATURESENSOR: case DeviceType.TEMPERATURESENSOR:
case DeviceType.ANALOGSENSOR: case DeviceType.ANALOGSENSOR:
return <MdOutlineSensors />; return <MdOutlineSensors />;
@@ -46,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;
} }

View File

@@ -1,3 +1,15 @@
import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useState
} from 'react';
import type { FC } from 'react';
import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
@@ -12,48 +24,48 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import StarIcon from '@mui/icons-material/Star'; import StarIcon from '@mui/icons-material/Star';
import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined'; import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined';
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
import { import {
Box,
Button, Button,
Dialog, Dialog,
DialogTitle,
DialogContent,
DialogActions, DialogActions,
DialogContent,
DialogTitle,
Grid,
IconButton, IconButton,
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
Box,
Grid,
Typography Typography
} from '@mui/material'; } from '@mui/material';
import { useRowSelect } from '@table-library/react-table-library/select'; import { useRowSelect } from '@table-library/react-table-library/select';
import { useSort, SortToggleType } from '@table-library/react-table-library/sort'; import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } 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 { useRequest } from 'alova'; import type { Action, State } from '@table-library/react-table-library/types/common';
import { useState, useEffect, useCallback, useLayoutEffect, useContext } from 'react';
import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import DeviceIcon from './DeviceIcon';
import DashboardDevicesDialog from './DevicesDialog';
import * as EMSESP from './api';
import { formatValue } from './deviceValue';
import { DeviceValueUOM_s, DeviceEntityMask, DeviceType } from './types';
import { deviceValueItemValidation } from './validators';
import type { Device, DeviceValue } from './types';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { ButtonRow, SectionContent, MessageBox, useLayoutTitle } from 'components'; import { useRequest } from 'alova';
import { ButtonRow, MessageBox, SectionContent, useLayoutTitle } 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 * as EMSESP from './api';
import DeviceIcon from './DeviceIcon';
import DashboardDevicesDialog from './DevicesDialog';
import { formatValue } from './deviceValue';
import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
import type { Device, DeviceValue } from './types';
import { deviceValueItemValidation } from './validators';
const Devices: FC = () => { const Devices: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
@@ -69,23 +81,32 @@ 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) => 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((data) => EMSESP.writeDeviceValue(data), { const { loading: submitting, send: writeDeviceValue } = useRequest(
immediate: false (data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data),
}); {
immediate: false
}
);
useLayoutEffect(() => { useLayoutEffect(() => {
function updateSize() { function updateSize() {
@@ -213,7 +234,7 @@ const Devices: FC = () => {
} }
]); ]);
const getSortIcon = (state: any, sortKey: any) => { const getSortIcon = (state: State, sortKey: unknown) => {
if (state.sortKey === sortKey && state.reverse) { if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />; return <KeyboardArrowDownOutlinedIcon />;
} }
@@ -234,14 +255,20 @@ 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) =>
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) array.sort((a, b) =>
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()))
} }
} }
); );
async function onSelectChange(action: any, state: any) { async function onSelectChange(action: Action, state: State) {
setSelectedDevice(state.id); setSelectedDevice(state.id as number);
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
await readDeviceData(state.id); await readDeviceData(state.id);
} }
@@ -259,8 +286,8 @@ const Devices: FC = () => {
}; };
const escFunction = useCallback( const escFunction = useCallback(
(event: any) => { (event: KeyboardEvent) => {
if (event.keyCode === 27) { if (event.key === 'Escape') {
if (device_select) { if (device_select) {
device_select.fns.onRemoveAll(); device_select.fns.onRemoveAll();
} }
@@ -290,7 +317,7 @@ const Devices: FC = () => {
} }
}; };
const escapeCsvCell = (cell: any) => { const escapeCsvCell = (cell: string) => {
if (cell == null) { if (cell == null) {
return ''; return '';
} }
@@ -298,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'
} }
]; ];
@@ -336,9 +387,16 @@ const Devices: FC = () => {
: deviceData.data; : deviceData.data;
const csvData = data.reduce( const csvData = data.reduce(
(csvString: any, rowItem: any) => (csvString: string, rowItem: DeviceValue) =>
csvString + columns.map(({ accessor }: any) => escapeCsvCell(accessor(rowItem))).join(';') + '\r\n', csvString +
columns.map(({ name }: any) => escapeCsvCell(name)).join(';') + '\r\n' columns
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) =>
escapeCsvCell(accessor(rowItem) as string)
)
.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' });
@@ -363,7 +421,7 @@ const Devices: FC = () => {
.then(() => { .then(() => {
toast.success(LL.WRITE_CMD_SENT()); toast.success(LL.WRITE_CMD_SENT());
}) })
.catch((error) => { .catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}) })
.finally(async () => { .finally(async () => {
@@ -375,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>
@@ -423,12 +512,25 @@ 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
{(tableList: any) => ( data={{ nodes: coreData.devices }}
select={device_select}
theme={device_theme}
layout={{ custom: true }}
>
{(tableList: Device[]) => (
<> <>
<Header> <Header>
<HeaderRow> <HeaderRow>
@@ -445,7 +547,9 @@ const Devices: FC = () => {
</Cell> </Cell>
<Cell> <Cell>
{device.n} {device.n}
<span style={{ color: 'lightblue' }}>&nbsp;&nbsp;({device.e})</span> <span style={{ color: 'lightblue' }}>
&nbsp;&nbsp;({device.e})
</span>
</Cell> </Cell>
<Cell stiff>{device.tn}</Cell> <Cell stiff>{device.tn}</Cell>
</Row> </Row>
@@ -475,8 +579,12 @@ const Devices: FC = () => {
const renderNameCell = (dv: DeviceValue) => ( const renderNameCell = (dv: DeviceValue) => (
<> <>
{dv.id.slice(2)}&nbsp; {dv.id.slice(2)}&nbsp;
{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 }} />
)} )}
@@ -487,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;
} }
@@ -508,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}&nbsp;&#124;&nbsp;{coreData.devices[deviceIndex].n} {coreData.devices[deviceIndex].tn}&nbsp;&#124;&nbsp;
{coreData.devices[deviceIndex].n}
</Typography> </Typography>
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
@@ -521,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>
@@ -556,7 +687,7 @@ const Devices: FC = () => {
sort={dv_sort} sort={dv_sort}
layout={{ custom: true, fixedHeader: true }} layout={{ custom: true, fixedHeader: true }}
> >
{(tableList: any) => ( {(tableList: DeviceValue[]) => (
<> <>
<Header> <Header>
<HeaderRow> <HeaderRow>
@@ -589,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>
))} ))}
@@ -621,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>

View File

@@ -1,36 +1,35 @@
import { useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { import {
Box,
Button, Button,
CircularProgress,
Dialog, Dialog,
DialogTitle,
DialogContent,
DialogActions, DialogActions,
DialogContent,
DialogTitle,
FormHelperText,
Grid,
InputAdornment, InputAdornment,
MenuItem, MenuItem,
TextField, TextField,
FormHelperText, Typography
Grid,
Box,
Typography,
CircularProgress
} from '@mui/material'; } from '@mui/material';
import { useState, useEffect } from 'react';
import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils';
import { validate } from 'validators';
import { DeviceValueUOM, DeviceValueUOM_s } from './types'; import { DeviceValueUOM, DeviceValueUOM_s } from './types';
import type { DeviceValue } from './types'; import type { DeviceValue } from './types';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator'; interface DashboardDevicesDialogProps {
import { dialogStyle } from 'CustomTheme';
import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { updateValue, numberValue } from 'utils';
import { validate } from 'validators';
type DashboardDevicesDialogProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSave: (as: DeviceValue) => void; onSave: (as: DeviceValue) => void;
@@ -38,7 +37,7 @@ type DashboardDevicesDialogProps = {
writeable: boolean; writeable: boolean;
validator: Schema; validator: Schema;
progress: boolean; progress: boolean;
}; }
const DevicesDialog = ({ const DevicesDialog = ({
open, open,
@@ -71,12 +70,12 @@ const DevicesDialog = ({
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, editItem); await validate(validator, editItem);
onSave(editItem); onSave(editItem);
} catch (errors: any) { } catch (error) {
setFieldErrors(errors); setFieldErrors(error as ValidateFieldsError);
} }
}; };
const setUom = (uom: number) => { const setUom = (uom: DeviceValueUOM) => {
switch (uom) { switch (uom) {
case DeviceValueUOM.HOURS: case DeviceValueUOM.HOURS:
return LL.HOURS(); return LL.HOURS();
@@ -103,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}>
@@ -133,15 +136,23 @@ const DevicesDialog = ({
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
name="v" name="v"
label={LL.VALUE(1)} label={LL.VALUE(1)}
value={numberValue(Math.round(editItem.v * 10) / 10)} value={numberValue(Math.round((editItem.v as number) * 10) / 10)}
autoFocus autoFocus
disabled={!writeable} disabled={!writeable}
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>
)
}} }}
/> />
) : ( ) : (
@@ -176,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 && (

View File

@@ -1,12 +1,13 @@
import { ToggleButton, ToggleButtonGroup } from '@mui/material'; import { ToggleButton, ToggleButtonGroup } from '@mui/material';
import OptionIcon from './OptionIcon'; import OptionIcon from './OptionIcon';
import { DeviceEntityMask } from './types'; import { DeviceEntityMask } from './types';
import type { DeviceEntity } from './types'; import type { DeviceEntity } from './types';
type EntityMaskToggleProps = { interface EntityMaskToggleProps {
onUpdate: (de: DeviceEntity) => void; onUpdate: (de: DeviceEntity) => void;
de: DeviceEntity; de: DeviceEntity;
}; }
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => { const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
const getMaskNumber = (newMask: string[]) => { const getMaskNumber = (newMask: string[]) => {
@@ -42,7 +43,7 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
size="small" size="small"
color="secondary" color="secondary"
value={getMaskString(de.m)} value={getMaskString(de.m)}
onChange={(event, mask) => { onChange={(event, mask: string[]) => {
de.m = getMaskNumber(mask); de.m = getMaskNumber(mask);
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
@@ -54,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>
); );

View File

@@ -1,33 +1,40 @@
import type { FC } from 'react';
import { toast } from 'react-toastify';
import CommentIcon from '@mui/icons-material/CommentTwoTone'; import CommentIcon from '@mui/icons-material/CommentTwoTone';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
import GitHubIcon from '@mui/icons-material/GitHub'; import GitHubIcon from '@mui/icons-material/GitHub';
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone'; import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
import { import {
Avatar,
Box, Box,
Button,
Link,
List, List,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
ListItemText,
Link,
Typography,
Button,
ListItemButton, ListItemButton,
Avatar ListItemText,
Typography
} from '@mui/material'; } from '@mui/material';
import * as EMSESP from 'project/api';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { toast } from 'react-toastify';
import type { FC } from 'react';
import { SectionContent, useLayoutTitle } from 'components'; import { SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
import type { APIcall } from './types';
const Help: FC = () => { const Help: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.HELP_OF('')); useLayoutTitle(LL.HELP_OF(''));
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data) => 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');
@@ -36,14 +43,17 @@ const Help: FC = () => {
type: 'text/plain' type: 'text/plain'
}) })
); );
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());
}); });
const callAPI = async (device: string, entity: string) => { const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error) => { await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
toast.error(error.message); toast.error(error.message);
}); });
}; };
@@ -74,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 />
@@ -114,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>

View File

@@ -1,22 +1,30 @@
import type { FC } from 'react';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined'; import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
import StarIcon from '@mui/icons-material/Star'; import StarIcon from '@mui/icons-material/Star';
import StarOutlineIcon from '@mui/icons-material/StarOutline'; import StarOutlineIcon from '@mui/icons-material/StarOutline';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; 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';
import type { FC } from 'react';
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],

Some files were not shown because too many files have changed in this diff Show More