mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -63,3 +63,4 @@ bw-output/
|
|||||||
|
|
||||||
# standalone executable for testing
|
# standalone executable for testing
|
||||||
emsesp
|
emsesp
|
||||||
|
interface/tsconfig.tsbuildinfo
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -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
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.yarn/
|
|
||||||
|
|
||||||
.prettierrc
|
|
||||||
.eslintrc*
|
|
||||||
env.d.ts
|
|
||||||
progmem-generator.js
|
|
||||||
unpack.ts
|
|
||||||
vite.config.ts
|
|
||||||
package.json
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
src/i18n/*
|
||||||
|
|
||||||
.prettierrc
|
.prettierrc
|
||||||
.yarn/
|
.yarn/
|
||||||
.typesafe-i18n.json
|
.typesafe-i18n.json
|
||||||
33
interface/eslint.config.js
Normal file
33
interface/eslint.config.js
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)}}
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
(request: SignInRequest) => AuthenticationApi.signIn(request),
|
||||||
|
{
|
||||||
immediate: false
|
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' }} />
|
||||||
DE
|
DE
|
||||||
@@ -182,7 +194,13 @@ const SignIn: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={validateAndSignIn}
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
<ForwardIcon sx={{ mr: 1 }} />
|
<ForwardIcon sx={{ mr: 1 }} />
|
||||||
{LL.SIGN_IN()}
|
{LL.SIGN_IN()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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+
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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](
|
||||||
|
src.subarray(position, (end = position += length)),
|
||||||
|
(readPosition) => {
|
||||||
position = readPosition;
|
position = readPosition;
|
||||||
try {
|
try {
|
||||||
return read();
|
return read();
|
||||||
} finally {
|
} finally {
|
||||||
position = end;
|
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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
AuthenticationApi.verifyAuthorization(),
|
||||||
|
{
|
||||||
immediate: false
|
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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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()} (0=auto)
|
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="publish_time_heartbeat"
|
name="publish_time_heartbeat"
|
||||||
label="Heartbeat"
|
label="Heartbeat"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,13 +368,19 @@ 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 &&
|
||||||
|
(selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<CancelIcon />}
|
startIcon={<CancelIcon />}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
|||||||
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
(local_time: Time) => NTPApi.updateTime(local_time),
|
||||||
|
{
|
||||||
immediate: false
|
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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
SecurityApi.generateToken(username),
|
||||||
|
{
|
||||||
immediate: false
|
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>
|
||||||
|
|||||||
@@ -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,26 +10,39 @@ 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 } =
|
||||||
|
useRest<SecuritySettingsType>({
|
||||||
read: SecurityApi.readSecuritySettings,
|
read: SecurityApi.readSecuritySettings,
|
||||||
update: SecurityApi.updateSecuritySettings
|
update: SecurityApi.updateSecuritySettings
|
||||||
});
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
data,
|
||||||
|
updateDataValue,
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
saveData,
|
||||||
|
errorMessage
|
||||||
|
} = useRest<LogSettings>({
|
||||||
read: SystemApi.readLogSettings,
|
read: SystemApi.readLogSettings,
|
||||||
update: SystemApi.updateLogSettings
|
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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
EMSESP.getSettings(),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } =
|
||||||
|
useRequest(EMSESP.getCustomizations(), {
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
});
|
||||||
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), {
|
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(
|
||||||
|
EMSESP.getEntities(),
|
||||||
|
{
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
}
|
||||||
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
|
);
|
||||||
|
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(
|
||||||
|
EMSESP.getSchedule(),
|
||||||
|
{
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
}
|
||||||
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
|
);
|
||||||
|
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
|
||||||
|
(data: APIcall) => EMSESP.API(data),
|
||||||
|
{
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
}
|
||||||
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data) => 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> ({data.esp_platform})
|
<b>{data.emsesp_version}</b> ({data.esp_platform})
|
||||||
{latestVersion && (
|
{latestVersion && (
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
{LL.THE_LATEST()} {LL.OFFICIAL()} {LL.RELEASE_IS()} <b>{latestVersion}</b>
|
{LL.THE_LATEST()} {LL.OFFICIAL()} {LL.RELEASE_IS()}
|
||||||
|
<b>{latestVersion}</b>
|
||||||
(
|
(
|
||||||
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
||||||
{LL.RELEASE_NOTES()}
|
{LL.RELEASE_NOTES()}
|
||||||
@@ -173,7 +210,13 @@ const UploadDownload: FC = () => {
|
|||||||
) (
|
) (
|
||||||
<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()} {LL.DEVELOPMENT()} {LL.RELEASE_IS()}
|
{LL.THE_LATEST()} {LL.DEVELOPMENT()} {LL.RELEASE_IS()}
|
||||||
|
|
||||||
<b>{latestDevVersion}</b>
|
<b>{latestDevVersion}</b>
|
||||||
(
|
(
|
||||||
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
||||||
{LL.RELEASE_NOTES()}
|
{LL.RELEASE_NOTES()}
|
||||||
</Link>
|
</Link>
|
||||||
) (
|
) (
|
||||||
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion)} 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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
{ei.name}
|
||||||
{ei.writeable && <EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
|
{ei.writeable && (
|
||||||
|
<EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
|
)}
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}</Cell>
|
|
||||||
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
||||||
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
|
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
|
||||||
<Cell>{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}</Cell>
|
<Cell>
|
||||||
|
{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}
|
||||||
|
</Cell>
|
||||||
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,7 +222,8 @@ 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}>
|
<Grid item xs={4}>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -243,7 +257,8 @@ const CustomEntitiesDialog = ({
|
|||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{editItem.value_type === DeviceValueType.STRING && editItem.device_id !== '0' && (
|
{editItem.value_type === DeviceValueType.STRING &&
|
||||||
|
editItem.device_id !== '0' && (
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextField
|
<TextField
|
||||||
name="factor"
|
name="factor"
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
(data: { id: number; entity_ids: string[] }) =>
|
||||||
|
EMSESP.writeCustomizationEntities(data),
|
||||||
|
{
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), {
|
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest(
|
||||||
|
(data: number) => EMSESP.readDeviceEntities(data),
|
||||||
|
{
|
||||||
initialData: [],
|
initialData: [],
|
||||||
immediate: false
|
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()}
|
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
|
||||||
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
|
||||||
|
{LL.CUSTOMIZATIONS_HELP_4()}
|
||||||
|
<OptionIcon type="web_exclude" isSet={true} />=
|
||||||
|
{LL.CUSTOMIZATIONS_HELP_5()}
|
||||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
|
<Grid
|
||||||
|
container
|
||||||
|
mb={1}
|
||||||
|
mt={0}
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
<Grid item xs={2}>
|
<Grid item xs={2}>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -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()} {shown_data.length}/{deviceEntities.length} {LL.ENTITIES(deviceEntities.length)}
|
{LL.SHOWING()} {shown_data.length}/{deviceEntities.length}
|
||||||
|
{LL.ENTITIES(deviceEntities.length)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
<Table
|
||||||
{(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)} (
|
{formatName(de, false)} (
|
||||||
<Link target="_blank" href={APIURL + selectedDeviceName + '/' + de.id}>
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={APIURL + selectedDeviceName + '/' + de.id}
|
||||||
|
>
|
||||||
{de.id}
|
{de.id}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
|
<Cell>
|
||||||
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
|
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}
|
||||||
|
</Cell>
|
||||||
|
<Cell>
|
||||||
|
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}
|
||||||
|
</Cell>
|
||||||
<Cell>{formatValue(de.v)}</Cell>
|
<Cell>{formatValue(de.v)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
() => EMSESP.readCoreData(),
|
||||||
|
{
|
||||||
initialData: {
|
initialData: {
|
||||||
connected: true,
|
connected: true,
|
||||||
devices: []
|
devices: []
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { data: deviceData, send: readDeviceData } = useRequest((id) => EMSESP.readDeviceData(id), {
|
const { data: deviceData, send: readDeviceData } = useRequest(
|
||||||
|
(id: number) => EMSESP.readDeviceData(id),
|
||||||
|
{
|
||||||
initialData: {
|
initialData: {
|
||||||
data: []
|
data: []
|
||||||
},
|
},
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { loading: submitting, send: writeDeviceValue } = useRequest((data) => EMSESP.writeDeviceValue(data), {
|
const { loading: submitting, send: writeDeviceValue } = useRequest(
|
||||||
|
(data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data),
|
||||||
|
{
|
||||||
immediate: false
|
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' }}> ({device.e})</span>
|
<span style={{ color: 'lightblue' }}>
|
||||||
|
({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)}
|
{dv.id.slice(2)}
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && <StarIcon color="primary" sx={{ fontSize: 12 }} />}
|
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && <EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
|
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
|
)}
|
||||||
|
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||||
|
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
|
)}
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
||||||
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
)}
|
)}
|
||||||
@@ -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} | {coreData.devices[deviceIndex].n}
|
{coreData.devices[deviceIndex].tn} |
|
||||||
|
{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,8 +720,13 @@ 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 &&
|
||||||
|
!hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => showDeviceValue(dv)}
|
||||||
|
>
|
||||||
{dv.v === '' && dv.c ? (
|
{dv.v === '' && dv.c ? (
|
||||||
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||||
) : (
|
) : (
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
(data: APIcall) => EMSESP.API(data),
|
||||||
|
{
|
||||||
immediate: false
|
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>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user