mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-05-04 04:55:53 +00:00
Compare commits
232 Commits
26b42b4eea
...
core3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847fa4f46c | ||
|
|
c5897b7ee1 | ||
|
|
99ef4c0c18 | ||
|
|
cc118adec6 | ||
|
|
ca94e37495 | ||
|
|
e2bd721c3e | ||
|
|
033ce24fb7 | ||
|
|
eab7cdd7b5 | ||
|
|
666ba41f67 | ||
|
|
2579450eae | ||
|
|
87a3ca8393 | ||
|
|
9ff4be41f7 | ||
|
|
da3ed6cd3a | ||
|
|
23519a8a90 | ||
|
|
242708358e | ||
|
|
3cc3c74e5a | ||
|
|
cb4cb39396 | ||
|
|
363799c9c6 | ||
|
|
132f83aa79 | ||
|
|
f998714225 | ||
|
|
323fc1bb99 | ||
|
|
3062d3f0e3 | ||
|
|
8f37bb7623 | ||
|
|
a57ed90756 | ||
|
|
eaf8332d16 | ||
|
|
522286ff74 | ||
|
|
747047556e | ||
|
|
df3d75c702 | ||
|
|
e40beeadd4 | ||
|
|
751f10603d | ||
|
|
751c540cb3 | ||
|
|
d7bbc329bb | ||
|
|
41cd49a61c | ||
|
|
fd5a39702b | ||
|
|
4d3408254e | ||
|
|
2cbb5ec5f2 | ||
|
|
3b765b308e | ||
|
|
53ac82520e | ||
|
|
381fcf4080 | ||
|
|
a3f0faf022 | ||
|
|
b3a8737a71 | ||
|
|
6e76bcc9af | ||
|
|
6473c55317 | ||
|
|
1a880f14a0 | ||
|
|
e39af36589 | ||
|
|
c5b262af8a | ||
|
|
43ec5c1925 | ||
|
|
5e260f0239 | ||
|
|
ab67f97b40 | ||
|
|
9ac35e2e14 | ||
|
|
7c6259dddd | ||
|
|
1cff1abc33 | ||
|
|
d834d46586 | ||
|
|
1107e1bdf3 | ||
|
|
3a11327e7e | ||
|
|
74062bab57 | ||
|
|
6802336b6b | ||
|
|
a9db134d3a | ||
|
|
ee7be1d907 | ||
|
|
5ecda88457 | ||
|
|
7056c446fa | ||
|
|
147c09ae64 | ||
|
|
112adf9eb0 | ||
|
|
469d412951 | ||
|
|
6edbac86e2 | ||
|
|
0e08334132 | ||
|
|
3d51acf9e7 | ||
|
|
fd6ea5ed7e | ||
|
|
db2be70d66 | ||
|
|
c36f231990 | ||
|
|
d18e5b1f14 | ||
|
|
20327d817d | ||
|
|
26102121e1 | ||
|
|
8e64c6303e | ||
|
|
051c332426 | ||
|
|
a09258325e | ||
|
|
74c76eb90b | ||
|
|
daffdcf58e | ||
|
|
61dca0cbda | ||
|
|
2bff299193 | ||
|
|
4bc4fa903f | ||
|
|
1329b13db3 | ||
|
|
29380f0303 | ||
|
|
9dd894f0fe | ||
|
|
6b2370b79d | ||
|
|
dbc636c9bf | ||
|
|
30d1ae5642 | ||
|
|
79aceef382 | ||
|
|
a28e52210a | ||
|
|
0c0660c04b | ||
|
|
08eb294213 | ||
|
|
c9fd076394 | ||
|
|
888baed81a | ||
|
|
4de3955db2 | ||
|
|
25f08c7624 | ||
|
|
35550553be | ||
|
|
06ff219385 | ||
|
|
e705a5629f | ||
|
|
1e8013100c | ||
|
|
62c8f55568 | ||
|
|
cb3c9653ce | ||
|
|
0b5a83f6ae | ||
|
|
a079169005 | ||
|
|
845c51d5f9 | ||
|
|
c40d828749 | ||
|
|
d6d3a034ad | ||
|
|
84ad08887a | ||
|
|
ece08d96ee | ||
|
|
ed0a678020 | ||
|
|
854f4d559a | ||
|
|
f186f2a8f2 | ||
|
|
37107d8500 | ||
|
|
6b68cb7c61 | ||
|
|
a1e0288e09 | ||
|
|
e6c173bdf9 | ||
|
|
dde6a8c5db | ||
|
|
e2750b8572 | ||
|
|
acd23925b5 | ||
|
|
b0db054e11 | ||
|
|
d9b6de0652 | ||
|
|
c54da18822 | ||
|
|
51cea8e757 | ||
|
|
bbb086ea41 | ||
|
|
539e6ed080 | ||
|
|
555801dc5c | ||
|
|
1d33a26318 | ||
|
|
86a20fc97a | ||
|
|
d6e00c4534 | ||
|
|
6f81945da6 | ||
|
|
865c309475 | ||
|
|
77b8b21aea | ||
|
|
2f5edffec6 | ||
|
|
71de64502e | ||
|
|
6994d3559a | ||
|
|
a7d484d218 | ||
|
|
a810c41acd | ||
|
|
2fbfdf94ab | ||
|
|
2d7c8f0863 | ||
|
|
c3b734ab47 | ||
|
|
644abf105d | ||
|
|
5a8a451774 | ||
|
|
dae139aa01 | ||
|
|
b13fcd8939 | ||
|
|
a813d38108 | ||
|
|
685a49c212 | ||
|
|
2c8eb534af | ||
|
|
5210fab4cb | ||
|
|
49787d27f1 | ||
|
|
dfe7b46461 | ||
|
|
f8257de0dd | ||
|
|
3b3ecc9f1d | ||
|
|
84105acf5d | ||
|
|
def5173692 | ||
|
|
6b31fef1af | ||
|
|
c9c059ca65 | ||
|
|
4d3b31e5a1 | ||
|
|
51d90095aa | ||
|
|
16c0370443 | ||
|
|
67bb38dcf4 | ||
|
|
049231a36e | ||
|
|
349d6b7375 | ||
|
|
b72b368d3c | ||
|
|
7f9fd44a02 | ||
|
|
a400c5974c | ||
|
|
afca995fe5 | ||
|
|
81504fedc5 | ||
|
|
3da3345683 | ||
|
|
c6c2889306 | ||
|
|
b60f0d260a | ||
|
|
cd750e4777 | ||
|
|
4e5d503b35 | ||
|
|
bd09e17e49 | ||
|
|
835eb743bb | ||
|
|
69a129d80e | ||
|
|
434bf483fd | ||
|
|
2b8e170b40 | ||
|
|
dc9b95f3e7 | ||
|
|
1616b0da0a | ||
|
|
91c457b22b | ||
|
|
70c60647c7 | ||
|
|
c0bea66d27 | ||
|
|
ed7cc078ed | ||
|
|
60b7d6d795 | ||
|
|
947f29cca0 | ||
|
|
d2a13ec0da | ||
|
|
cc39ba409e | ||
|
|
ac9db6256e | ||
|
|
096f628d97 | ||
|
|
22312812bb | ||
|
|
3584975acb | ||
|
|
30b9ca4e6c | ||
|
|
7c6ff01ebe | ||
|
|
a54edcaf5b | ||
|
|
e446954844 | ||
|
|
4a2d0d6787 | ||
|
|
9725314135 | ||
|
|
e610f0d57f | ||
|
|
8244af2940 | ||
|
|
40f371d23b | ||
|
|
817b791e59 | ||
|
|
25a7aac360 | ||
|
|
37115a174d | ||
|
|
1397f81fd0 | ||
|
|
56365cb403 | ||
|
|
dfd245ee7b | ||
|
|
9c81e4b34d | ||
|
|
67676df131 | ||
|
|
a73b129596 | ||
|
|
4600d886b5 | ||
|
|
0fe45a2405 | ||
|
|
db87213242 | ||
|
|
5c3c010d5a | ||
|
|
c804cedd7a | ||
|
|
aa30ca99bf | ||
|
|
c0ca9d1069 | ||
|
|
5e79e1d57f | ||
|
|
8c732f9f1e | ||
|
|
5e94c2f636 | ||
|
|
64e5d29996 | ||
|
|
b320d8ded2 | ||
|
|
0be1b20996 | ||
|
|
6c55460622 | ||
|
|
d627404dc2 | ||
|
|
f317123c26 | ||
|
|
e4df1887b0 | ||
|
|
34142c3e85 | ||
|
|
6e7f8bdf02 | ||
|
|
3dd9fcfb58 | ||
|
|
35e2954b8b | ||
|
|
59aa63db0f | ||
|
|
7a41a190f8 | ||
|
|
6741232450 |
22
.github/workflows/dev_release.yml
vendored
22
.github/workflows/dev_release.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build webUI
|
- name: Build webUI
|
||||||
run: |
|
run: |
|
||||||
platformio run -e build_webUI
|
platformio run -e build-webUI
|
||||||
|
|
||||||
- name: Build modbus
|
- name: Build modbus
|
||||||
run: |
|
run: |
|
||||||
@@ -77,3 +77,23 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
CHANGELOG_LATEST.md
|
CHANGELOG_LATEST.md
|
||||||
./build/firmware/*.*
|
./build/firmware/*.*
|
||||||
|
|
||||||
|
- name: Update version in Cloudflare KV store
|
||||||
|
if: github.repository == 'emsesp/EMS-ESP32'
|
||||||
|
env:
|
||||||
|
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||||
|
CF_NAMESPACE_ID: ${{ secrets.CF_NAMESPACE_ID }}
|
||||||
|
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||||
|
VERSION: ${{ steps.build_info.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
JSON_DATA=$(jq -n \
|
||||||
|
--arg version "$VERSION" \
|
||||||
|
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
'{version: $version, date: $date}')
|
||||||
|
echo "JSON_DATA: $JSON_DATA"
|
||||||
|
curl -sS --fail-with-body \
|
||||||
|
-X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${CF_NAMESPACE_ID}/values/dev" \
|
||||||
|
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_DATA"
|
||||||
|
echo
|
||||||
|
|||||||
31
.github/workflows/stable_release.yml
vendored
31
.github/workflows/stable_release.yml
vendored
@@ -27,10 +27,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable pnpm
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
|
||||||
|
- name: Get the EMS-ESP version
|
||||||
|
id: build_info
|
||||||
|
run: |
|
||||||
|
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'`
|
||||||
|
echo "VERSION=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Install PlatformIO
|
- name: Install PlatformIO
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@@ -39,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build webUI
|
- name: Build webUI
|
||||||
run: |
|
run: |
|
||||||
platformio run -e build_webUI
|
platformio run -e build-webUI
|
||||||
|
|
||||||
- name: Build modbus
|
- name: Build modbus
|
||||||
run: |
|
run: |
|
||||||
@@ -61,3 +68,23 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
./build/firmware/*.*
|
./build/firmware/*.*
|
||||||
|
|
||||||
|
- name: Update version in Cloudflare KV store
|
||||||
|
if: github.repository == 'emsesp/EMS-ESP32'
|
||||||
|
env:
|
||||||
|
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||||
|
CF_NAMESPACE_ID: ${{ secrets.CF_NAMESPACE_ID }}
|
||||||
|
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||||
|
VERSION: ${{ steps.build_info.outputs.VERSION }}
|
||||||
|
run: |
|
||||||
|
JSON_DATA=$(jq -n \
|
||||||
|
--arg version "$VERSION" \
|
||||||
|
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
'{version: $version, date: $date}')
|
||||||
|
echo "JSON_DATA: $JSON_DATA"
|
||||||
|
curl -sS --fail-with-body \
|
||||||
|
-X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${CF_NAMESPACE_ID}/values/stable" \
|
||||||
|
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_DATA"
|
||||||
|
echo
|
||||||
|
|||||||
2
.github/workflows/test_release.yml
vendored
2
.github/workflows/test_release.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build webUI
|
- name: Build webUI
|
||||||
run: |
|
run: |
|
||||||
platformio run -e build_webUI
|
platformio run -e build-webUI
|
||||||
|
|
||||||
- name: Build modbus
|
- name: Build modbus
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
40
.github/workflows/update_versions.yml
vendored
Normal file
40
.github/workflows/update_versions.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: 'Update versions'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-version:
|
||||||
|
name: 'Update versions in Cloudflare KV store'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Get and Send EMS-ESP version to Cloudflare
|
||||||
|
env:
|
||||||
|
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
|
||||||
|
CF_NAMESPACE_ID: ${{ secrets.CF_NAMESPACE_ID }}
|
||||||
|
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
version=$(grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}')
|
||||||
|
if [ "$GITHUB_REF" = "refs/heads/main" ]; then
|
||||||
|
KV_ENV="stable"
|
||||||
|
else
|
||||||
|
KV_ENV="dev"
|
||||||
|
fi
|
||||||
|
JSON_DATA=$(jq -n \
|
||||||
|
--arg version "$version" \
|
||||||
|
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
'{version: $version, date: $date}')
|
||||||
|
echo "KV_ENV: $KV_ENV"
|
||||||
|
echo "JSON_DATA: $JSON_DATA"
|
||||||
|
curl -sS --fail-with-body \
|
||||||
|
-X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${CF_NAMESPACE_ID}/values/${KV_ENV}" \
|
||||||
|
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON_DATA"
|
||||||
|
echo
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
.vscode/c_cpp_properties.json
|
.vscode/c_cpp_properties.json
|
||||||
.vscode/extensions.json
|
.vscode/extensions.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
|
.vscode/settings.json
|
||||||
|
|
||||||
# c++ compiling
|
# c++ compiling
|
||||||
.clang_complete
|
.clang_complete
|
||||||
@@ -63,7 +64,7 @@ words-found-verbose.txt
|
|||||||
# sonarlint
|
# sonarlint
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
|
||||||
# pioarduino + hybrid
|
# other files
|
||||||
managed_components
|
managed_components
|
||||||
dependencies.lock
|
dependencies.lock
|
||||||
CMakeLists.txt
|
CMakeLists.txt
|
||||||
@@ -75,3 +76,4 @@ pnpm-lock.yaml
|
|||||||
.cache/
|
.cache/
|
||||||
interface/.tsbuildinfo
|
interface/.tsbuildinfo
|
||||||
test/test_api/package-lock.json
|
test/test_api/package-lock.json
|
||||||
|
.clangd
|
||||||
|
|||||||
101
.vscode/settings.json
vendored
101
.vscode/settings.json
vendored
@@ -1,101 +0,0 @@
|
|||||||
{
|
|
||||||
"search.exclude": {
|
|
||||||
"**/.yarn": true,
|
|
||||||
"**/.pnp.*": true
|
|
||||||
},
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll": "explicit"
|
|
||||||
},
|
|
||||||
"eslint.validate": [
|
|
||||||
"typescript"
|
|
||||||
],
|
|
||||||
"eslint.codeActionsOnSave.rules": null,
|
|
||||||
"eslint.nodePath": "interface/.yarn/sdks",
|
|
||||||
"eslint.workingDirectories": ["interface"],
|
|
||||||
"prettier.prettierPath": "",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
|
||||||
"files.associations": {
|
|
||||||
"*.tsx": "typescriptreact",
|
|
||||||
"*.tcc": "cpp",
|
|
||||||
"optional": "cpp",
|
|
||||||
"istream": "cpp",
|
|
||||||
"ostream": "cpp",
|
|
||||||
"ratio": "cpp",
|
|
||||||
"system_error": "cpp",
|
|
||||||
"array": "cpp",
|
|
||||||
"functional": "cpp",
|
|
||||||
"regex": "cpp",
|
|
||||||
"tuple": "cpp",
|
|
||||||
"type_traits": "cpp",
|
|
||||||
"utility": "cpp",
|
|
||||||
"string": "cpp",
|
|
||||||
"string_view": "cpp",
|
|
||||||
"atomic": "cpp",
|
|
||||||
"bitset": "cpp",
|
|
||||||
"cctype": "cpp",
|
|
||||||
"chrono": "cpp",
|
|
||||||
"clocale": "cpp",
|
|
||||||
"cmath": "cpp",
|
|
||||||
"condition_variable": "cpp",
|
|
||||||
"cstdarg": "cpp",
|
|
||||||
"cstddef": "cpp",
|
|
||||||
"cstdint": "cpp",
|
|
||||||
"cstdio": "cpp",
|
|
||||||
"cstdlib": "cpp",
|
|
||||||
"cstring": "cpp",
|
|
||||||
"ctime": "cpp",
|
|
||||||
"cwchar": "cpp",
|
|
||||||
"cwctype": "cpp",
|
|
||||||
"deque": "cpp",
|
|
||||||
"list": "cpp",
|
|
||||||
"unordered_map": "cpp",
|
|
||||||
"unordered_set": "cpp",
|
|
||||||
"vector": "cpp",
|
|
||||||
"exception": "cpp",
|
|
||||||
"algorithm": "cpp",
|
|
||||||
"iterator": "cpp",
|
|
||||||
"map": "cpp",
|
|
||||||
"memory": "cpp",
|
|
||||||
"memory_resource": "cpp",
|
|
||||||
"numeric": "cpp",
|
|
||||||
"random": "cpp",
|
|
||||||
"set": "cpp",
|
|
||||||
"fstream": "cpp",
|
|
||||||
"initializer_list": "cpp",
|
|
||||||
"iomanip": "cpp",
|
|
||||||
"iosfwd": "cpp",
|
|
||||||
"iostream": "cpp",
|
|
||||||
"limits": "cpp",
|
|
||||||
"mutex": "cpp",
|
|
||||||
"new": "cpp",
|
|
||||||
"sstream": "cpp",
|
|
||||||
"stdexcept": "cpp",
|
|
||||||
"streambuf": "cpp",
|
|
||||||
"thread": "cpp",
|
|
||||||
"cinttypes": "cpp",
|
|
||||||
"typeinfo": "cpp"
|
|
||||||
},
|
|
||||||
"todo-tree.filtering.excludeGlobs": [
|
|
||||||
"**/vendor/**",
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/dist/**",
|
|
||||||
"**/bower_components/**",
|
|
||||||
"**/build/**",
|
|
||||||
"**/.vscode/**",
|
|
||||||
"**/.github/**",
|
|
||||||
"**/_output/**",
|
|
||||||
"**/*.min.*",
|
|
||||||
"**/*.map",
|
|
||||||
"**/ArduinoJson/**"
|
|
||||||
],
|
|
||||||
"cSpell.enableFiletypes": [
|
|
||||||
"ini",
|
|
||||||
"makefile"
|
|
||||||
],
|
|
||||||
"typescript.preferences.preferTypeOnlyAutoImports": true,
|
|
||||||
"sonarlint.pathToCompileCommands": "${workspaceFolder}/compile_commands.json",
|
|
||||||
"sonarlint.connectedMode.project": {
|
|
||||||
"connectionId": "emsesp",
|
|
||||||
"projectKey": "emsesp_EMS-ESP32"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
For more details go to [emsesp.org](https://emsesp.org/).
|
For more details go to [emsesp.org](https://emsesp.org/).
|
||||||
|
|
||||||
## [3.8.2]
|
## [3.9.0]
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
@@ -12,13 +12,16 @@ For more details go to [emsesp.org](https://emsesp.org/).
|
|||||||
- prometheus metrics for temperature/analog/scheduler/custom [#2962](https://github.com/emsesp/EMS-ESP32/issues/2962)
|
- prometheus metrics for temperature/analog/scheduler/custom [#2962](https://github.com/emsesp/EMS-ESP32/issues/2962)
|
||||||
- boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965)
|
- boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965)
|
||||||
- heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
|
- heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
|
||||||
|
- e-mail notification using ReadyMail Client
|
||||||
- 2.nd freshwater module (dhw4, dhw5) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991)
|
- 2.nd freshwater module (dhw4, dhw5) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991)
|
||||||
- full system backup and restore
|
- full system backup and restore
|
||||||
|
- updated version check [#3047](https://github.com/emsesp/EMS-ESP32/issues/3047)
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) and [#2960](https://github.com/emsesp/EMS-ESP32/issues/2960)
|
- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) and [#2960](https://github.com/emsesp/EMS-ESP32/issues/2960)
|
||||||
- missing translations [#3015](https://github.com/emsesp/EMS-ESP32/issues/3015)
|
- missing translations [#3015](https://github.com/emsesp/EMS-ESP32/issues/3015)
|
||||||
|
- custom entities check fetch length
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
@@ -30,5 +33,9 @@ For more details go to [emsesp.org](https://emsesp.org/).
|
|||||||
- support `minflowtemp` and `baseflowtemp` [#2969](https://github.com/emsesp/EMS-ESP32/discussions/2969)
|
- support `minflowtemp` and `baseflowtemp` [#2969](https://github.com/emsesp/EMS-ESP32/discussions/2969)
|
||||||
- update version if it is 00.00 in first read [#2981](https://github.com/emsesp/EMS-ESP32/issues/2981)
|
- update version if it is 00.00 in first read [#2981](https://github.com/emsesp/EMS-ESP32/issues/2981)
|
||||||
- device class for % values [#2980](https://github.com/emsesp/EMS-ESP32/issues/2980)
|
- device class for % values [#2980](https://github.com/emsesp/EMS-ESP32/issues/2980)
|
||||||
|
- use tasmota core 2026.03.30
|
||||||
|
- secure mqtt uses ESP_SSLClient
|
||||||
- fetch telegrams: set length to fetch [#3017](https://github.com/emsesp/EMS-ESP32/issues/3017)
|
- fetch telegrams: set length to fetch [#3017](https://github.com/emsesp/EMS-ESP32/issues/3017)
|
||||||
- move http client from stack to heap
|
- move http client from stack to heap
|
||||||
|
- heap optimizations [#3021](https://github.com/emsesp/EMS-ESP32/discussions/3021)
|
||||||
|
- refactored network code into a single class [#3052](https://github.com/emsesp/EMS-ESP32/pull/3052)
|
||||||
6
Makefile
6
Makefile
@@ -47,8 +47,8 @@ MAKEFLAGS += -j$(JOBS) -l$(shell echo $$(($(JOBS) * 2)))
|
|||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
TARGET := emsesp
|
TARGET := emsesp
|
||||||
BUILD := build
|
BUILD := build
|
||||||
SOURCES := src/core src/devices src/web src/test lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton
|
SOURCES := src/core src/devices src/web src/test lib_standalone lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton
|
||||||
INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/semver lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src
|
INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src
|
||||||
LIBRARIES :=
|
LIBRARIES :=
|
||||||
|
|
||||||
CPPCHECK = cppcheck
|
CPPCHECK = cppcheck
|
||||||
@@ -113,7 +113,7 @@ CXX := /usr/bin/g++
|
|||||||
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
|
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
|
||||||
CPPFLAGS += -ggdb -g3 -MMD
|
CPPFLAGS += -ggdb -g3 -MMD
|
||||||
CPPFLAGS += -flto=auto
|
CPPFLAGS += -flto=auto
|
||||||
CPPFLAGS += -Wall -Wextra -Werror -Wswitch-enum
|
CPPFLAGS += -Wall -Wextra -Werror -Wno-switch-enum
|
||||||
CPPFLAGS += -Wno-unused-parameter -Wno-missing-braces -Wno-vla-cxx-extension
|
CPPFLAGS += -Wno-unused-parameter -Wno-missing-braces -Wno-vla-cxx-extension
|
||||||
CPPFLAGS += -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics
|
CPPFLAGS += -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics
|
||||||
CPPFLAGS += -Os -DNDEBUG
|
CPPFLAGS += -Os -DNDEBUG
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
"-DTASMOTA_SDK",
|
"-DNO_TLS_SUPPORT",
|
||||||
"-DARDUINO_LOLIN_C3_MINI",
|
"-DARDUINO_LOLIN_C3_MINI",
|
||||||
"-DARDUINO_USB_MODE=1",
|
"-DARDUINO_USB_MODE=1",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
"-DBOARD_HAS_PSRAM",
|
"-DBOARD_HAS_PSRAM",
|
||||||
"-DTASMOTA_SDK",
|
"-DNO_TLS_SUPPORT",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
"-DARDUINO_USB_MODE=0"
|
"-DARDUINO_USB_MODE=0"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Espressif ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
|
"name": "Tasmota ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "32MB",
|
"flash_size": "32MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": "-DTASMOTA_SDK",
|
"extra_flags": "-DNO_TLS_SUPPORT",
|
||||||
"f_cpu": "240000000L",
|
"f_cpu": "240000000L",
|
||||||
"f_flash": "40000000L",
|
"f_flash": "40000000L",
|
||||||
"flash_mode": "dio",
|
"flash_mode": "dio",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Espressif ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
|
"name": "Tasmota ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "16MB",
|
"flash_size": "16MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Espressif ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
|
"name": "Tasmota ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "16MB",
|
"flash_size": "16MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": "-DTASMOTA_SDK",
|
"extra_flags": "-DNO_TLS_SUPPORT",
|
||||||
"f_cpu": "240000000L",
|
"f_cpu": "240000000L",
|
||||||
"f_flash": "40000000L",
|
"f_flash": "40000000L",
|
||||||
"flash_mode": "dio",
|
"flash_mode": "dio",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
|
"-DNO_TLS_SUPPORT",
|
||||||
"-DARDUINO_XIAO_ESP32C6",
|
"-DARDUINO_XIAO_ESP32C6",
|
||||||
"-DARDUINO_USB_MODE=1",
|
"-DARDUINO_USB_MODE=1",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"vite.config.ts",
|
"vite.config.ts",
|
||||||
"lib/esp32-psram/**",
|
"lib/esp32-psram/**",
|
||||||
"test/test_api/test_api.h",
|
"test/test_api/test_api.h",
|
||||||
"lib_standalone/**"
|
"lib_standalone/**",
|
||||||
|
"**/*.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -171,6 +171,7 @@ telegram_type_id,name,is_fetched
|
|||||||
0x0468,HPSet,
|
0x0468,HPSet,
|
||||||
0x0469,HPSet,
|
0x0469,HPSet,
|
||||||
0x046A,HPSet,
|
0x046A,HPSet,
|
||||||
|
0x0470,RC300Summer2,
|
||||||
0x0471,RC300Summer2,
|
0x0471,RC300Summer2,
|
||||||
0x0472,RC300Summer2,
|
0x0472,RC300Summer2,
|
||||||
0x0473,RC300Summer2,
|
0x0473,RC300Summer2,
|
||||||
@@ -178,7 +179,6 @@ telegram_type_id,name,is_fetched
|
|||||||
0x0475,RC300Summer2,
|
0x0475,RC300Summer2,
|
||||||
0x0476,RC300Summer2,
|
0x0476,RC300Summer2,
|
||||||
0x0477,RC300Summer2,
|
0x0477,RC300Summer2,
|
||||||
0x0478,RC300Summer2,
|
|
||||||
0x047B,HP2,
|
0x047B,HP2,
|
||||||
0x0484,HPSilentMode,fetched
|
0x0484,HPSilentMode,fetched
|
||||||
0x0485,HpCooling,fetched
|
0x0485,HpCooling,fetched
|
||||||
|
|||||||
|
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"version": "3.8.2",
|
"version": "3.9.0",
|
||||||
"description": "EMS-ESP WebUI",
|
"description": "EMS-ESP WebUI",
|
||||||
"homepage": "https://emsesp.org",
|
"homepage": "https://emsesp.org",
|
||||||
"author": "proddy, emsesp.org",
|
"author": "emsesp.org",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"",
|
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"",
|
||||||
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite dev\"",
|
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite dev\"",
|
||||||
"typesafe-i18n": "typesafe-i18n --no-watch",
|
"typesafe-i18n": "typesafe-i18n --no-watch",
|
||||||
"build_webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js",
|
"build-webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js",
|
||||||
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
|
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
|
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
|
||||||
@@ -28,44 +28,37 @@
|
|||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^9.0.0",
|
"@mui/icons-material": "^9.0.0",
|
||||||
"@mui/material": "^9.0.0",
|
"@mui/material": "^9.0.0",
|
||||||
"@preact/compat": "^18.3.2",
|
|
||||||
"@table-library/react-table-library": "4.1.15",
|
"@table-library/react-table-library": "4.1.15",
|
||||||
"alova": "3.5.1",
|
"alova": "^3.5.1",
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
"etag": "^1.8.1",
|
"etag": "^1.8.1",
|
||||||
"formidable": "^3.5.4",
|
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"magic-string": "^0.30.21",
|
|
||||||
"mime-types": "^3.0.2",
|
"mime-types": "^3.0.2",
|
||||||
"preact": "^10.29.1",
|
"preact": "^10.29.1",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
"react-router": "^7.14.1",
|
"react-router": "^7.14.2",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.1.0",
|
||||||
"typesafe-i18n": "^5.27.1",
|
"typesafe-i18n": "^5.27.1",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.29.0",
|
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@preact/compat": "^18.3.2",
|
|
||||||
"@preact/preset-vite": "^2.10.5",
|
"@preact/preset-vite": "^2.10.5",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"axe-core": "^4.11.3",
|
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^10.2.0",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"terser": "^5.46.1",
|
"terser": "^5.46.2",
|
||||||
"typescript-eslint": "^8.58.2",
|
"typescript-eslint": "^8.59.1",
|
||||||
"vite": "^8.0.8",
|
"vite": "^8.0.10",
|
||||||
"vite-plugin-imagemin": "^0.6.1",
|
"vite-plugin-imagemin": "^0.6.1"
|
||||||
"vite-tsconfig-paths": "^6.1.1"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8"
|
||||||
}
|
}
|
||||||
|
|||||||
652
interface/pnpm-lock.yaml
generated
652
interface/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -24,20 +24,26 @@ let bundleStats = {
|
|||||||
other: { count: 0, uncompressed: 0, compressed: 0 }
|
other: { count: 0, uncompressed: 0, compressed: 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateWWWClass =
|
// AsyncWebHandler that performs the lookup.
|
||||||
() => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
|
const generateWWWClass = () => `// Bundle Statistics:
|
||||||
// Bundle Statistics:
|
|
||||||
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB
|
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB
|
||||||
// - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB
|
// - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB
|
||||||
// - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}%
|
// - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}%
|
||||||
// - Generated on: ${new Date().toISOString()}
|
// - Generated on: ${new Date().toISOString()}
|
||||||
|
|
||||||
class WWWData {
|
struct WWWAsset {
|
||||||
${INDENT}public:
|
${INDENT}const char * uri;
|
||||||
${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
${INDENT}const char * contentType;
|
||||||
${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, ${f.hash});`).join('\n')}
|
${INDENT}const uint8_t * content;
|
||||||
${INDENT.repeat(2)}}
|
${INDENT}size_t len;
|
||||||
|
${INDENT}const char * etag; // already includes enclosing double quotes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const WWWAsset WWW_ASSETS[] = {
|
||||||
|
${fileInfo.map((f) => `${INDENT}{"${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "\\"${f.rawHash}\\""},`).join('\n')}
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr size_t WWW_ASSETS_COUNT = sizeof(WWW_ASSETS) / sizeof(WWW_ASSETS[0]);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const getFilesSync = (dir, files = []) => {
|
const getFilesSync = (dir, files = []) => {
|
||||||
@@ -72,6 +78,7 @@ const writeFile = (relativeFilePath, buffer) => {
|
|||||||
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
|
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
|
||||||
// const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
|
// const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
|
||||||
const hash = etag(zipBuffer); // use smaller md5 instead of sha256
|
const hash = etag(zipBuffer); // use smaller md5 instead of sha256
|
||||||
|
const rawHash = hash.replace(/^"|"$/g, '');
|
||||||
|
|
||||||
zipBuffer.forEach((b) => {
|
zipBuffer.forEach((b) => {
|
||||||
if (!(size % bytesPerLine)) {
|
if (!(size % bytesPerLine)) {
|
||||||
@@ -94,7 +101,8 @@ const writeFile = (relativeFilePath, buffer) => {
|
|||||||
mimeType,
|
mimeType,
|
||||||
variable,
|
variable,
|
||||||
size,
|
size,
|
||||||
hash
|
hash,
|
||||||
|
rawHash
|
||||||
});
|
});
|
||||||
|
|
||||||
totalSize += size;
|
totalSize += size;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { ToastContainer, Zoom } from 'react-toastify';
|
import { ToastContainer, Zoom } from 'react-toastify';
|
||||||
|
|
||||||
import AppRouting from 'AppRouting';
|
import AppRouting from 'AppRouting';
|
||||||
@@ -46,19 +46,17 @@ const App = memo(() => {
|
|||||||
const [wasLoaded, setWasLoaded] = useState(false);
|
const [wasLoaded, setWasLoaded] = useState(false);
|
||||||
const [locale, setLocale] = useState<Locales>('en');
|
const [locale, setLocale] = useState<Locales>('en');
|
||||||
|
|
||||||
// Memoize locale initialization to prevent unnecessary re-runs
|
|
||||||
const initializeLocale = useCallback(async () => {
|
|
||||||
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
|
|
||||||
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
|
|
||||||
localStorage.setItem('lang', newLocale);
|
|
||||||
setLocale(newLocale);
|
|
||||||
await loadLocaleAsync(newLocale);
|
|
||||||
setWasLoaded(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const initializeLocale = async () => {
|
||||||
|
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
|
||||||
|
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
|
||||||
|
localStorage.setItem('lang', newLocale);
|
||||||
|
setLocale(newLocale);
|
||||||
|
await loadLocaleAsync(newLocale);
|
||||||
|
setWasLoaded(true);
|
||||||
|
};
|
||||||
void initializeLocale();
|
void initializeLocale();
|
||||||
}, [initializeLocale]);
|
}, []);
|
||||||
|
|
||||||
if (!wasLoaded) return null;
|
if (!wasLoaded) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { type FC, Suspense, lazy, memo, useContext, useEffect, useRef } from 'react';
|
import { type FC, memo, useContext, useEffect, useRef } from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router';
|
import { Navigate, Route, Routes } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import {
|
import AuthenticatedRouting from 'AuthenticatedRouting';
|
||||||
LoadingSpinner,
|
import SignIn from 'SignIn';
|
||||||
RequireAuthenticated,
|
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
||||||
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';
|
||||||
|
|
||||||
// Lazy load route components for better code splitting
|
|
||||||
const SignIn = lazy(() => import('SignIn'));
|
|
||||||
const AuthenticatedRouting = lazy(() => import('AuthenticatedRouting'));
|
|
||||||
|
|
||||||
interface SecurityRedirectProps {
|
interface SecurityRedirectProps {
|
||||||
readonly message: string;
|
readonly message: string;
|
||||||
readonly signOut?: boolean;
|
readonly signOut?: boolean;
|
||||||
@@ -45,34 +39,32 @@ const AppRouting: FC = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Authentication>
|
<Authentication>
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Routes>
|
||||||
<Routes>
|
<Route
|
||||||
<Route
|
path="/unauthorized"
|
||||||
path="/unauthorized"
|
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
||||||
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="/fileUpdated"
|
||||||
path="/fileUpdated"
|
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
||||||
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="/"
|
||||||
path="/"
|
element={
|
||||||
element={
|
<RequireUnauthenticated>
|
||||||
<RequireUnauthenticated>
|
<SignIn />
|
||||||
<SignIn />
|
</RequireUnauthenticated>
|
||||||
</RequireUnauthenticated>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="/*"
|
||||||
path="/*"
|
element={
|
||||||
element={
|
<RequireAuthenticated>
|
||||||
<RequireAuthenticated>
|
<AuthenticatedRouting />
|
||||||
<AuthenticatedRouting />
|
</RequireAuthenticated>
|
||||||
</RequireAuthenticated>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Routes>
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
</Authentication>
|
</Authentication>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,86 +1,77 @@
|
|||||||
import { Suspense, lazy, memo, useContext } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router';
|
import { Navigate, Route, Routes } from 'react-router';
|
||||||
|
|
||||||
import { Layout, LoadingSpinner } from 'components';
|
import CustomEntities from 'app/main/CustomEntities';
|
||||||
|
import Customizations from 'app/main/Customizations';
|
||||||
|
import Dashboard from 'app/main/Dashboard';
|
||||||
|
import Devices from 'app/main/Devices';
|
||||||
|
import Help from 'app/main/Help';
|
||||||
|
import Modules from 'app/main/Modules';
|
||||||
|
import Scheduler from 'app/main/Scheduler';
|
||||||
|
import Sensors from 'app/main/Sensors';
|
||||||
|
import UserProfile from 'app/main/UserProfile';
|
||||||
|
import APSettings from 'app/settings/APSettings';
|
||||||
|
import ApplicationSettings from 'app/settings/ApplicationSettings';
|
||||||
|
import DownloadUpload from 'app/settings/DownloadUpload';
|
||||||
|
import MqttSettings from 'app/settings/MqttSettings';
|
||||||
|
import NTPSettings from 'app/settings/NTPSettings';
|
||||||
|
import Settings from 'app/settings/Settings';
|
||||||
|
import Version from 'app/settings/Version';
|
||||||
|
import Network from 'app/settings/network/Network';
|
||||||
|
import Security from 'app/settings/security/Security';
|
||||||
|
import APStatus from 'app/status/APStatus';
|
||||||
|
import Activity from 'app/status/Activity';
|
||||||
|
import HardwareStatus from 'app/status/HardwareStatus';
|
||||||
|
import MqttStatus from 'app/status/MqttStatus';
|
||||||
|
import NTPStatus from 'app/status/NTPStatus';
|
||||||
|
import NetworkStatus from 'app/status/NetworkStatus';
|
||||||
|
import Status from 'app/status/Status';
|
||||||
|
import SystemLog from 'app/status/SystemLog';
|
||||||
|
import { Layout } from 'components';
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
// Lazy load all route components for better code splitting
|
|
||||||
const Dashboard = lazy(() => import('app/main/Dashboard'));
|
|
||||||
const Devices = lazy(() => import('app/main/Devices'));
|
|
||||||
const Sensors = lazy(() => import('app/main/Sensors'));
|
|
||||||
const Help = lazy(() => import('app/main/Help'));
|
|
||||||
const Customizations = lazy(() => import('app/main/Customizations'));
|
|
||||||
const Scheduler = lazy(() => import('app/main/Scheduler'));
|
|
||||||
const CustomEntities = lazy(() => import('app/main/CustomEntities'));
|
|
||||||
const Modules = lazy(() => import('app/main/Modules'));
|
|
||||||
const UserProfile = lazy(() => import('app/main/UserProfile'));
|
|
||||||
|
|
||||||
const Status = lazy(() => import('app/status/Status'));
|
|
||||||
const HardwareStatus = lazy(() => import('app/status/HardwareStatus'));
|
|
||||||
const Activity = lazy(() => import('app/status/Activity'));
|
|
||||||
const SystemLog = lazy(() => import('app/status/SystemLog'));
|
|
||||||
const MqttStatus = lazy(() => import('app/status/MqttStatus'));
|
|
||||||
const NTPStatus = lazy(() => import('app/status/NTPStatus'));
|
|
||||||
const APStatus = lazy(() => import('app/status/APStatus'));
|
|
||||||
const NetworkStatus = lazy(() => import('app/status/NetworkStatus'));
|
|
||||||
const Version = lazy(() => import('app/status/Version'));
|
|
||||||
|
|
||||||
const Settings = lazy(() => import('app/settings/Settings'));
|
|
||||||
const ApplicationSettings = lazy(() => import('app/settings/ApplicationSettings'));
|
|
||||||
const MqttSettings = lazy(() => import('app/settings/MqttSettings'));
|
|
||||||
const NTPSettings = lazy(() => import('app/settings/NTPSettings'));
|
|
||||||
const APSettings = lazy(() => import('app/settings/APSettings'));
|
|
||||||
const DownloadUpload = lazy(() => import('app/settings/DownloadUpload'));
|
|
||||||
const Network = lazy(() => import('app/settings/network/Network'));
|
|
||||||
const Security = lazy(() => import('app/settings/security/Security'));
|
|
||||||
|
|
||||||
const AuthenticatedRouting = memo(() => {
|
const AuthenticatedRouting = memo(() => {
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const { me } = useContext(AuthenticatedContext);
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Routes>
|
||||||
<Routes>
|
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
<Route path="/devices/*" element={<Devices />} />
|
||||||
<Route path="/devices/*" element={<Devices />} />
|
<Route path="/sensors/*" element={<Sensors />} />
|
||||||
<Route path="/sensors/*" element={<Sensors />} />
|
<Route path="/help/*" element={<Help />} />
|
||||||
<Route path="/help/*" element={<Help />} />
|
<Route path="/user/*" element={<UserProfile />} />
|
||||||
<Route path="/user/*" element={<UserProfile />} />
|
|
||||||
|
|
||||||
<Route path="/status/*" element={<Status />} />
|
<Route path="/status/*" element={<Status />} />
|
||||||
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
|
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
|
||||||
<Route path="/status/activity" element={<Activity />} />
|
<Route path="/status/activity" element={<Activity />} />
|
||||||
<Route path="/status/log" element={<SystemLog />} />
|
<Route path="/status/log" element={<SystemLog />} />
|
||||||
<Route path="/status/mqtt" element={<MqttStatus />} />
|
<Route path="/status/mqtt" element={<MqttStatus />} />
|
||||||
<Route path="/status/ntp" element={<NTPStatus />} />
|
<Route path="/status/ntp" element={<NTPStatus />} />
|
||||||
<Route path="/status/ap" element={<APStatus />} />
|
<Route path="/status/ap" element={<APStatus />} />
|
||||||
<Route path="/status/network" element={<NetworkStatus />} />
|
<Route path="/status/network" element={<NetworkStatus />} />
|
||||||
<Route path="/status/version" element={<Version />} />
|
|
||||||
|
|
||||||
{me.admin && (
|
{me.admin && (
|
||||||
<>
|
<>
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route
|
<Route path="/settings/version" element={<Version />} />
|
||||||
path="/settings/application"
|
<Route path="/settings/application" element={<ApplicationSettings />} />
|
||||||
element={<ApplicationSettings />}
|
<Route path="/settings/mqtt" element={<MqttSettings />} />
|
||||||
/>
|
<Route path="/settings/ntp" element={<NTPSettings />} />
|
||||||
<Route path="/settings/mqtt" element={<MqttSettings />} />
|
<Route path="/settings/ap" element={<APSettings />} />
|
||||||
<Route path="/settings/ntp" element={<NTPSettings />} />
|
<Route path="/settings/modules" element={<Modules />} />
|
||||||
<Route path="/settings/ap" element={<APSettings />} />
|
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
|
||||||
<Route path="/settings/modules" element={<Modules />} />
|
|
||||||
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
|
|
||||||
|
|
||||||
<Route path="/settings/network/*" element={<Network />} />
|
<Route path="/settings/network/*" element={<Network />} />
|
||||||
<Route path="/settings/security/*" element={<Security />} />
|
<Route path="/settings/security/*" element={<Security />} />
|
||||||
|
|
||||||
<Route path="/customizations" element={<Customizations />} />
|
<Route path="/customizations" element={<Customizations />} />
|
||||||
<Route path="/scheduler" element={<Scheduler />} />
|
<Route path="/scheduler" element={<Scheduler />} />
|
||||||
<Route path="/customentities" element={<CustomEntities />} />
|
<Route path="/customentities" element={<CustomEntities />} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Route path="/*" element={<Navigate to="/" />} />
|
<Route path="/*" element={<Navigate to="/" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { PROJECT_NAME } from 'env';
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { SignInRequest } from 'types';
|
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, ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
const SignIn = memo(() => {
|
const SignIn = memo(() => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
@@ -43,7 +43,6 @@ const SignIn = memo(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize callback to prevent recreation on every render
|
|
||||||
const updateLoginRequestValue = useMemo(
|
const updateLoginRequestValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateValue((updater) =>
|
updateValue((updater) =>
|
||||||
@@ -65,7 +64,7 @@ const SignIn = memo(() => {
|
|||||||
});
|
});
|
||||||
}, [callSignIn, signInRequest, LL]);
|
}, [callSignIn, signInRequest, LL]);
|
||||||
|
|
||||||
const validateAndSignIn = useCallback(async () => {
|
const validateAndSignIn = async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
SIGN_IN_REQUEST_VALIDATOR.messages({
|
SIGN_IN_REQUEST_VALIDATOR.messages({
|
||||||
required: LL.IS_REQUIRED('%s')
|
required: LL.IS_REQUIRED('%s')
|
||||||
@@ -74,10 +73,10 @@ const SignIn = memo(() => {
|
|||||||
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
|
||||||
await signIn();
|
await signIn();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
}, [signInRequest, signIn, LL]);
|
};
|
||||||
|
|
||||||
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
|
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
|
||||||
|
|
||||||
|
|||||||
@@ -57,12 +57,3 @@ export const alovaInstance = createAlova({
|
|||||||
onSuccess: handleResponse
|
onSuccess: handleResponse
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const alovaInstanceGH = createAlova({
|
|
||||||
baseURL:
|
|
||||||
process.env.NODE_ENV === 'development'
|
|
||||||
? '/gh'
|
|
||||||
: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
|
|
||||||
statesHook: ReactHook,
|
|
||||||
requestAdapter: xhrRequestAdapter()
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LogSettings, SystemStatus } from 'types';
|
import type { LogSettings, SystemStatus } from 'types';
|
||||||
|
|
||||||
import { alovaInstance, alovaInstanceGH } from './endpoints';
|
import { alovaInstance } from './endpoints';
|
||||||
|
|
||||||
// systemStatus - also used to ping in System Monitor for pinging
|
// systemStatus - also used to ping in System Monitor for pinging
|
||||||
export const readSystemStatus = () =>
|
export const readSystemStatus = () =>
|
||||||
@@ -13,29 +13,6 @@ export const updateLogSettings = (data: LogSettings) =>
|
|||||||
alovaInstance.Post('/rest/logSettings', data);
|
alovaInstance.Post('/rest/logSettings', data);
|
||||||
export const fetchLogES = () => alovaInstance.Get('/es/log');
|
export const fetchLogES = () => alovaInstance.Get('/es/log');
|
||||||
|
|
||||||
// Get versions from GitHub
|
|
||||||
// cache for 10 minutes to stop getting the IP blocked by GitHub
|
|
||||||
export const getStableVersion = () =>
|
|
||||||
alovaInstanceGH.Get('latest', {
|
|
||||||
cacheFor: 60 * 10 * 1000,
|
|
||||||
transform(response: { data: { name: string; published_at: string } }) {
|
|
||||||
return {
|
|
||||||
name: response.data.name.substring(1),
|
|
||||||
published_at: response.data.published_at
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export const getDevVersion = () =>
|
|
||||||
alovaInstanceGH.Get('tags/latest', {
|
|
||||||
cacheFor: 60 * 10 * 1000,
|
|
||||||
transform(response: { data: { name: string; published_at: string } }) {
|
|
||||||
return {
|
|
||||||
name: response.data.name.split(/\s+/).splice(-1)[0]?.substring(1) || '',
|
|
||||||
published_at: response.data.published_at
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const UPLOAD_TIMEOUT = 60000; // 1 minute
|
const UPLOAD_TIMEOUT = 60000; // 1 minute
|
||||||
|
|
||||||
export const uploadFile = (file: File) => {
|
export const uploadFile = (file: File) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -57,20 +57,18 @@ const CustomEntities = () => {
|
|||||||
initialData: []
|
initialData: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const intervalCallback = useCallback(() => {
|
useInterval(() => {
|
||||||
if (!dialogOpen && !numChanges) {
|
if (!dialogOpen && !numChanges) {
|
||||||
void fetchEntities();
|
void fetchEntities();
|
||||||
}
|
}
|
||||||
}, [dialogOpen, numChanges, fetchEntities]);
|
});
|
||||||
|
|
||||||
useInterval(intervalCallback);
|
|
||||||
|
|
||||||
const { send: writeEntities } = useRequest(
|
const { send: writeEntities } = useRequest(
|
||||||
(data: Entities) => writeCustomEntities(data),
|
(data: Entities) => writeCustomEntities(data),
|
||||||
{ immediate: false }
|
{ immediate: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasEntityChanged = useCallback((ei: EntityItem) => {
|
const hasEntityChanged = (ei: EntityItem) => {
|
||||||
return (
|
return (
|
||||||
ei.id !== ei.o_id ||
|
ei.id !== ei.o_id ||
|
||||||
ei.ram !== ei.o_ram ||
|
ei.ram !== ei.o_ram ||
|
||||||
@@ -86,21 +84,19 @@ const CustomEntities = () => {
|
|||||||
ei.deleted !== ei.o_deleted ||
|
ei.deleted !== ei.o_deleted ||
|
||||||
(ei.value || '') !== (ei.o_value || '')
|
(ei.value || '') !== (ei.o_value || '')
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const entity_theme = useMemo(
|
const entity_theme = useTheme({
|
||||||
() =>
|
Table: `
|
||||||
useTheme({
|
|
||||||
Table: `
|
|
||||||
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
|
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
.td {
|
.td {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
BaseCell: `
|
BaseCell: `
|
||||||
&:nth-of-type(1) {
|
&:nth-of-type(1) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
@@ -120,7 +116,7 @@ const CustomEntities = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
HeaderRow: `
|
HeaderRow: `
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: #90CAF9;
|
color: #90CAF9;
|
||||||
@@ -129,7 +125,7 @@ const CustomEntities = () => {
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Row: `
|
Row: `
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -140,11 +136,9 @@ const CustomEntities = () => {
|
|||||||
background-color: #177ac9;
|
background-color: #177ac9;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveEntities = useCallback(async () => {
|
const saveEntities = async () => {
|
||||||
await writeEntities({
|
await writeEntities({
|
||||||
entities: entities
|
entities: entities
|
||||||
.filter((ei: EntityItem) => !ei.deleted)
|
.filter((ei: EntityItem) => !ei.deleted)
|
||||||
@@ -173,44 +167,41 @@ const CustomEntities = () => {
|
|||||||
await fetchEntities();
|
await fetchEntities();
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [entities, writeEntities, LL, fetchEntities]);
|
};
|
||||||
|
|
||||||
const editEntityItem = useCallback((ei: EntityItem) => {
|
const editEntityItem = (ei: EntityItem) => {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
setSelectedEntityItem(ei);
|
setSelectedEntityItem(ei);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogClose = useCallback(() => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogCancel = useCallback(async () => {
|
const onDialogCancel = async () => {
|
||||||
await fetchEntities().then(() => {
|
await fetchEntities().then(() => {
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [fetchEntities]);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: EntityItem) => {
|
||||||
(updatedItem: EntityItem) => {
|
setDialogOpen(false);
|
||||||
setDialogOpen(false);
|
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
||||||
void updateState(readCustomEntities(), (data: EntityItem[]) => {
|
const new_data = creating
|
||||||
const new_data = creating
|
? [
|
||||||
? [
|
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
||||||
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
|
updatedItem
|
||||||
updatedItem
|
]
|
||||||
]
|
: data.map((ei) =>
|
||||||
: data.map((ei) =>
|
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : 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;
|
});
|
||||||
});
|
};
|
||||||
},
|
|
||||||
[creating, hasEntityChanged]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDialogDup = useCallback((item: EntityItem) => {
|
const onDialogDup = (item: EntityItem) => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setSelectedEntityItem({
|
setSelectedEntityItem({
|
||||||
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
||||||
@@ -228,9 +219,9 @@ const CustomEntities = () => {
|
|||||||
value: item.value
|
value: item.value
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const addEntityItem = useCallback(() => {
|
const addEntityItem = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setSelectedEntityItem({
|
setSelectedEntityItem({
|
||||||
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
||||||
@@ -248,30 +239,27 @@ const CustomEntities = () => {
|
|||||||
value: ''
|
value: ''
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const formatValue = useCallback((value: unknown, uom: number) => {
|
const formatValue = (value: unknown, uom: number) => {
|
||||||
return value === undefined
|
return value === undefined
|
||||||
? ''
|
? ''
|
||||||
: typeof value === 'number'
|
: typeof value === 'number'
|
||||||
? new Intl.NumberFormat().format(value) +
|
? new Intl.NumberFormat().format(value) +
|
||||||
(uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`)
|
(uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`)
|
||||||
: `${value as string}${uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`}`;
|
: `${value as string}${uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`}`;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const showHex = useCallback((value: number, digit: number) => {
|
const showHex = (value: number, digit: number) => {
|
||||||
return `0x${value.toString(16).toUpperCase().padStart(digit, '0')}`;
|
return `0x${value.toString(16).toUpperCase().padStart(digit, '0')}`;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const filteredAndSortedEntities = useMemo(
|
const filteredAndSortedEntities =
|
||||||
() =>
|
entities
|
||||||
entities
|
?.filter((ei: EntityItem) => !ei.deleted)
|
||||||
?.filter((ei: EntityItem) => !ei.deleted)
|
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [];
|
||||||
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [],
|
|
||||||
[entities]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderEntity = useCallback(() => {
|
const renderEntity = () => {
|
||||||
if (!entities) {
|
if (!entities) {
|
||||||
return (
|
return (
|
||||||
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
|
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
|
||||||
@@ -328,17 +316,7 @@ const CustomEntities = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
entities,
|
|
||||||
error,
|
|
||||||
fetchEntities,
|
|
||||||
entity_theme,
|
|
||||||
editEntityItem,
|
|
||||||
LL,
|
|
||||||
filteredAndSortedEntities,
|
|
||||||
showHex,
|
|
||||||
formatValue
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
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';
|
||||||
@@ -28,7 +28,7 @@ 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 { ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types';
|
||||||
import type { EntityItem } from './types';
|
import type { EntityItem } from './types';
|
||||||
@@ -68,14 +68,10 @@ const CustomEntitiesDialog = ({
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue(
|
||||||
() =>
|
setEditItem as unknown as React.Dispatch<
|
||||||
updateValue(
|
React.SetStateAction<Record<string, unknown>>
|
||||||
setEditItem as unknown as React.Dispatch<
|
>
|
||||||
React.SetStateAction<Record<string, unknown>>
|
|
||||||
>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -105,16 +101,16 @@ const CustomEntitiesDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
_event: React.SyntheticEvent,
|
||||||
if (reason !== 'backdropClick') {
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
onClose();
|
) => {
|
||||||
}
|
if (reason !== 'backdropClick') {
|
||||||
},
|
onClose();
|
||||||
[onClose]
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
@@ -136,29 +132,23 @@ const CustomEntitiesDialog = ({
|
|||||||
}
|
}
|
||||||
onSave(processedItem);
|
onSave(processedItem);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const remove = useCallback(() => {
|
const remove = () => {
|
||||||
const itemWithDeleted = { ...editItem, deleted: true };
|
onSave({ ...editItem, deleted: true });
|
||||||
onSave(itemWithDeleted);
|
};
|
||||||
}, [editItem, onSave]);
|
|
||||||
|
|
||||||
const dup = useCallback(() => {
|
const dup = () => {
|
||||||
onDup(editItem);
|
onDup(editItem);
|
||||||
}, [editItem, onDup]);
|
};
|
||||||
|
|
||||||
// Memoize UOM menu items to avoid recreating on every render
|
const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
|
||||||
const uomMenuItems = useMemo(
|
<MenuItem key={val} value={i}>
|
||||||
() =>
|
{val}
|
||||||
DeviceValueUOM_s.map((val, i) => (
|
</MenuItem>
|
||||||
<MenuItem key={val} value={i}>
|
));
|
||||||
{val}
|
|
||||||
</MenuItem>
|
|
||||||
)),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
@@ -215,7 +205,7 @@ const CustomEntitiesDialog = ({
|
|||||||
name="value"
|
name="value"
|
||||||
label={LL.DEFAULT(0) + ' ' + LL.VALUE(0)}
|
label={LL.DEFAULT(0) + ' ' + LL.VALUE(0)}
|
||||||
type="string"
|
type="string"
|
||||||
value={editItem.value as string}
|
value={editItem.value}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -260,7 +250,7 @@ const CustomEntitiesDialog = ({
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
sx={{ width: '11ch' }}
|
sx={{ width: '11ch' }}
|
||||||
type="string"
|
type="string"
|
||||||
value={editItem.device_id as string}
|
value={editItem.device_id}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
@@ -280,7 +270,7 @@ const CustomEntitiesDialog = ({
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
sx={{ width: '11ch' }}
|
sx={{ width: '11ch' }}
|
||||||
type="string"
|
type="string"
|
||||||
value={editItem.type_id as string}
|
value={editItem.type_id}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
@@ -381,7 +371,7 @@ const CustomEntitiesDialog = ({
|
|||||||
fieldErrors={fieldErrors || {}}
|
fieldErrors={fieldErrors || {}}
|
||||||
name="factor"
|
name="factor"
|
||||||
label={LL.BITMASK()}
|
label={LL.BITMASK()}
|
||||||
value={editItem.factor as string}
|
value={editItem.factor}
|
||||||
sx={{ width: '11ch' }}
|
sx={{ width: '11ch' }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useBlocker, useLocation } from 'react-router';
|
import { useBlocker, useLocation } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -171,19 +171,17 @@ const Customizations = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const entities_theme = useMemo(
|
const entities_theme = useTheme({
|
||||||
() =>
|
Table: `
|
||||||
useTheme({
|
|
||||||
Table: `
|
|
||||||
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
.td {
|
.td {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
BaseCell: `
|
BaseCell: `
|
||||||
&:nth-of-type(3) {
|
&:nth-of-type(3) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -194,7 +192,7 @@ const Customizations = () => {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
HeaderRow: `
|
HeaderRow: `
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: #90CAF9;
|
color: #90CAF9;
|
||||||
@@ -206,7 +204,7 @@ const Customizations = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Row: `
|
Row: `
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -222,7 +220,7 @@ const Customizations = () => {
|
|||||||
background-color: #177ac9;
|
background-color: #177ac9;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Cell: `
|
Cell: `
|
||||||
&:nth-of-type(2) {
|
&:nth-of-type(2) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
@@ -236,9 +234,7 @@ const Customizations = () => {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
function hasEntityChanged(de: DeviceEntity) {
|
function hasEntityChanged(de: DeviceEntity) {
|
||||||
return (
|
return (
|
||||||
@@ -287,26 +283,23 @@ const Customizations = () => {
|
|||||||
return value as string;
|
return value as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCommand = useCallback((de: DeviceEntity) => {
|
const isCommand = (de: DeviceEntity) => {
|
||||||
return de.n && de.n[0] === '!';
|
return de.n && de.n[0] === '!';
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const formatName = useCallback(
|
const formatName = (de: DeviceEntity, withShortname: boolean) => {
|
||||||
(de: DeviceEntity, withShortname: boolean) => {
|
let name: string;
|
||||||
let name: string;
|
if (isCommand(de)) {
|
||||||
if (isCommand(de)) {
|
name = de.t
|
||||||
name = de.t
|
? `${LL.COMMAND(1)}: ${de.t} ${de.n?.slice(1)}`
|
||||||
? `${LL.COMMAND(1)}: ${de.t} ${de.n?.slice(1)}`
|
: `${LL.COMMAND(1)}: ${de.n?.slice(1)}`;
|
||||||
: `${LL.COMMAND(1)}: ${de.n?.slice(1)}`;
|
} else if (de.cn && de.cn !== '') {
|
||||||
} else if (de.cn && de.cn !== '') {
|
name = de.t ? `${de.t} ${de.cn}` : de.cn;
|
||||||
name = de.t ? `${de.t} ${de.cn}` : de.cn;
|
} else {
|
||||||
} else {
|
name = de.t ? `${de.t} ${de.n}` : de.n || '';
|
||||||
name = de.t ? `${de.t} ${de.n}` : de.n || '';
|
}
|
||||||
}
|
return withShortname ? `${name} ${de.id}` : name;
|
||||||
return withShortname ? `${name} ${de.id}` : name;
|
};
|
||||||
},
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMaskNumber = (newMask: string[]) => {
|
const getMaskNumber = (newMask: string[]) => {
|
||||||
let new_mask = 0;
|
let new_mask = 0;
|
||||||
@@ -336,33 +329,27 @@ const Customizations = () => {
|
|||||||
return new_masks;
|
return new_masks;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter_entity = useCallback(
|
const filter_entity = (de: DeviceEntity) =>
|
||||||
(de: DeviceEntity) =>
|
(de.m & selectedFilters || !selectedFilters) &&
|
||||||
(de.m & selectedFilters || !selectedFilters) &&
|
formatName(de, true).toLowerCase().includes(search.toLowerCase());
|
||||||
formatName(de, true).toLowerCase().includes(search.toLowerCase()),
|
|
||||||
[selectedFilters, search, formatName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const maskDisabled = useCallback(
|
const maskDisabled = (set: boolean) => {
|
||||||
(set: boolean) => {
|
setDeviceEntities((prev) =>
|
||||||
setDeviceEntities((prev) =>
|
prev.map((de) => {
|
||||||
prev.map((de) => {
|
if (filter_entity(de)) {
|
||||||
if (filter_entity(de)) {
|
const excludeMask =
|
||||||
const excludeMask =
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
|
return {
|
||||||
return {
|
...de,
|
||||||
...de,
|
m: set ? de.m | excludeMask : de.m & ~excludeMask
|
||||||
m: set ? de.m | excludeMask : de.m & ~excludeMask
|
};
|
||||||
};
|
}
|
||||||
}
|
return de;
|
||||||
return de;
|
})
|
||||||
})
|
);
|
||||||
);
|
};
|
||||||
},
|
|
||||||
[filter_entity]
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetCustomization = useCallback(async () => {
|
const resetCustomization = async () => {
|
||||||
try {
|
try {
|
||||||
await sendResetCustomizations();
|
await sendResetCustomizations();
|
||||||
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
toast.info(LL.CUSTOMIZATIONS_RESTART());
|
||||||
@@ -372,30 +359,27 @@ const Customizations = () => {
|
|||||||
setConfirmReset(false);
|
setConfirmReset(false);
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
}
|
}
|
||||||
}, [sendResetCustomizations, LL]);
|
};
|
||||||
|
|
||||||
const onDialogClose = () => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => {
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
setDeviceEntities(
|
setDeviceEntities(
|
||||||
(prev) =>
|
(prev) =>
|
||||||
prev?.map((de) =>
|
prev?.map((de) =>
|
||||||
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
|
||||||
) ?? []
|
) ?? []
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: DeviceEntity) => {
|
||||||
(updatedItem: DeviceEntity) => {
|
setDialogOpen(false);
|
||||||
setDialogOpen(false);
|
updateDeviceEntity(updatedItem);
|
||||||
updateDeviceEntity(updatedItem);
|
};
|
||||||
},
|
|
||||||
[updateDeviceEntity]
|
|
||||||
);
|
|
||||||
|
|
||||||
const editDeviceEntity = useCallback((de: DeviceEntity) => {
|
const editDeviceEntity = (de: DeviceEntity) => {
|
||||||
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
if (de.n === undefined || (de.n && de.n[0] === '!')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -406,9 +390,9 @@ const Customizations = () => {
|
|||||||
|
|
||||||
setSelectedDeviceEntity(de);
|
setSelectedDeviceEntity(de);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const saveCustomization = useCallback(async () => {
|
const saveCustomization = async () => {
|
||||||
if (!devices || !deviceEntities || selectedDevice === -1) {
|
if (!devices || !deviceEntities || selectedDevice === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -441,9 +425,9 @@ const Customizations = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setOriginalSettings(deviceEntities);
|
setOriginalSettings(deviceEntities);
|
||||||
});
|
});
|
||||||
}, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]);
|
};
|
||||||
|
|
||||||
const renameDevice = useCallback(async () => {
|
const renameDevice = async () => {
|
||||||
await sendDeviceName({
|
await sendDeviceName({
|
||||||
id: selectedDevice,
|
id: selectedDevice,
|
||||||
name: selectedDeviceName,
|
name: selectedDeviceName,
|
||||||
@@ -459,14 +443,7 @@ const Customizations = () => {
|
|||||||
setRename(false);
|
setRename(false);
|
||||||
await fetchCoreData();
|
await fetchCoreData();
|
||||||
});
|
});
|
||||||
}, [
|
};
|
||||||
selectedDevice,
|
|
||||||
selectedDeviceName,
|
|
||||||
selectedDeviceBrand,
|
|
||||||
sendDeviceName,
|
|
||||||
LL,
|
|
||||||
fetchCoreData
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderDeviceList = () => (
|
const renderDeviceList = () => (
|
||||||
<>
|
<>
|
||||||
@@ -562,10 +539,7 @@ const Customizations = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredEntities = useMemo(
|
const filteredEntities = deviceEntities.filter((de) => filter_entity(de));
|
||||||
() => deviceEntities.filter((de) => filter_entity(de)),
|
|
||||||
[deviceEntities, filter_entity]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderDeviceData = () => {
|
const renderDeviceData = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, 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';
|
||||||
@@ -57,23 +57,16 @@ const CustomizationsDialog = ({
|
|||||||
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue(
|
||||||
() =>
|
setEditItem as unknown as React.Dispatch<
|
||||||
updateValue(
|
React.SetStateAction<Record<string, unknown>>
|
||||||
setEditItem as unknown as React.Dispatch<
|
>
|
||||||
React.SetStateAction<Record<string, unknown>>
|
|
||||||
>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isWriteableNumber = useMemo(
|
const isWriteableNumber =
|
||||||
() =>
|
typeof editItem.v === 'number' &&
|
||||||
typeof editItem.v === 'number' &&
|
editItem.w &&
|
||||||
editItem.w &&
|
!(editItem.m & DeviceEntityMask.DV_READONLY);
|
||||||
!(editItem.m & DeviceEntityMask.DV_READONLY),
|
|
||||||
[editItem.v, editItem.w, editItem.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -82,16 +75,16 @@ const CustomizationsDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
_event: React.SyntheticEvent,
|
||||||
if (reason !== 'backdropClick') {
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
onClose();
|
) => {
|
||||||
}
|
if (reason !== 'backdropClick') {
|
||||||
},
|
onClose();
|
||||||
[onClose]
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
const save = useCallback(() => {
|
const save = () => {
|
||||||
if (
|
if (
|
||||||
isWriteableNumber &&
|
isWriteableNumber &&
|
||||||
editItem.mi &&
|
editItem.mi &&
|
||||||
@@ -102,34 +95,31 @@ const CustomizationsDialog = ({
|
|||||||
} else {
|
} else {
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
}
|
}
|
||||||
}, [isWriteableNumber, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => {
|
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
|
||||||
setEditItem((prev) => ({ ...prev, m: updatedItem.m }));
|
setEditItem((prev) => ({ ...prev, m: updatedItem.m }));
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(() => `${LL.EDIT()} ${LL.ENTITY()}`, [LL]);
|
|
||||||
|
|
||||||
const writeableIcon = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.w ? (
|
|
||||||
<DoneIcon color="success" sx={{ fontSize: ICON_SIZE }} />
|
|
||||||
) : (
|
|
||||||
<CloseIcon color="error" sx={{ fontSize: ICON_SIZE }} />
|
|
||||||
),
|
|
||||||
[editItem.w]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{`${LL.EDIT()} ${LL.ENTITY()}`}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<LabelValue label={LL.ID_OF(LL.ENTITY())} value={editItem.id} />
|
<LabelValue label={LL.ID_OF(LL.ENTITY())} value={editItem.id} />
|
||||||
<LabelValue
|
<LabelValue
|
||||||
label={`${LL.DEFAULT(1)} ${LL.ENTITY_NAME(1)}`}
|
label={`${LL.DEFAULT(1)} ${LL.ENTITY_NAME(1)}`}
|
||||||
value={editItem.n}
|
value={editItem.n}
|
||||||
/>
|
/>
|
||||||
<LabelValue label={LL.WRITEABLE()} value={writeableIcon} />
|
<LabelValue
|
||||||
|
label={LL.WRITEABLE()}
|
||||||
|
value={
|
||||||
|
editItem.w ? (
|
||||||
|
<DoneIcon color="success" sx={{ fontSize: ICON_SIZE }} />
|
||||||
|
) : (
|
||||||
|
<CloseIcon color="error" sx={{ fontSize: ICON_SIZE }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box sx={{ mt: 1, mb: 2 }}>
|
<Box sx={{ mt: 1, mb: 2 }}>
|
||||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import { memo, useContext, useEffect, useState } from 'react';
|
||||||
import { IconContext } from 'react-icons/lib';
|
import { IconContext } from 'react-icons/lib';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -77,40 +77,35 @@ const Dashboard = memo(() => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const deviceValueDialogSave = useCallback(
|
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||||
async (devicevalue: DeviceValue) => {
|
if (!selectedDashboardItem) {
|
||||||
if (!selectedDashboardItem) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
|
||||||
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
|
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
|
||||||
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
|
.then(() => {
|
||||||
.then(() => {
|
toast.success(LL.WRITE_CMD_SENT());
|
||||||
toast.success(LL.WRITE_CMD_SENT());
|
})
|
||||||
})
|
.catch((error: Error) => {
|
||||||
.catch((error: Error) => {
|
toast.error(error.message);
|
||||||
toast.error(error.message);
|
})
|
||||||
})
|
.finally(() => {
|
||||||
.finally(() => {
|
setDeviceValueDialogOpen(false);
|
||||||
setDeviceValueDialogOpen(false);
|
setSelectedDashboardItem(undefined);
|
||||||
setSelectedDashboardItem(undefined);
|
});
|
||||||
});
|
};
|
||||||
},
|
|
||||||
[selectedDashboardItem, sendDeviceValue, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dashboard_theme = useMemo(
|
const dashboard_theme = useTheme({
|
||||||
() =>
|
Table: `
|
||||||
useTheme({
|
|
||||||
Table: `
|
|
||||||
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
|
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
.td {
|
.td {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Row: `
|
Row: `
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
&:nth-of-type(odd) .td {
|
&:nth-of-type(odd) .td {
|
||||||
@@ -120,7 +115,7 @@ const Dashboard = memo(() => {
|
|||||||
background-color: #177ac9;
|
background-color: #177ac9;
|
||||||
},
|
},
|
||||||
`,
|
`,
|
||||||
BaseCell: `
|
BaseCell: `
|
||||||
&:nth-of-type(2) {
|
&:nth-of-type(2) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -128,9 +123,7 @@ const Dashboard = memo(() => {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tree = useTree(
|
const tree = useTree(
|
||||||
{ nodes: [...data.nodes] },
|
{ nodes: [...data.nodes] },
|
||||||
@@ -164,79 +157,64 @@ const Dashboard = memo(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodeIds = useMemo(
|
|
||||||
() => data.nodes.map((item: DashboardItem) => item.id),
|
|
||||||
[data.nodes]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const nodeIds = data.nodes.map((item: DashboardItem) => item.id);
|
||||||
showAll
|
showAll
|
||||||
? tree.fns.onAddAll(nodeIds) // expand tree
|
? tree.fns.onAddAll(nodeIds) // expand tree
|
||||||
: tree.fns.onRemoveAll(); // collapse tree
|
: tree.fns.onRemoveAll(); // collapse tree
|
||||||
}, [parentNodes]);
|
}, [parentNodes]);
|
||||||
|
|
||||||
const showType = useCallback(
|
const showType = (n?: string, t?: number) => {
|
||||||
(n?: string, t?: number) => {
|
// if we have a name show it
|
||||||
// if we have a name show it
|
if (n) {
|
||||||
if (n) {
|
return n;
|
||||||
return n;
|
}
|
||||||
|
if (t) {
|
||||||
|
// otherwise pick translation based on type
|
||||||
|
switch (t) {
|
||||||
|
case DeviceType.CUSTOM:
|
||||||
|
return LL.CUSTOM_ENTITIES(0);
|
||||||
|
case DeviceType.ANALOGSENSOR:
|
||||||
|
return LL.ANALOG_SENSORS();
|
||||||
|
case DeviceType.TEMPERATURESENSOR:
|
||||||
|
return LL.TEMP_SENSORS();
|
||||||
|
case DeviceType.SCHEDULER:
|
||||||
|
return LL.SCHEDULER();
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (t) {
|
}
|
||||||
// otherwise pick translation based on type
|
return '';
|
||||||
switch (t) {
|
};
|
||||||
case DeviceType.CUSTOM:
|
|
||||||
return LL.CUSTOM_ENTITIES(0);
|
|
||||||
case DeviceType.ANALOGSENSOR:
|
|
||||||
return LL.ANALOG_SENSORS();
|
|
||||||
case DeviceType.TEMPERATURESENSOR:
|
|
||||||
return LL.TEMP_SENSORS();
|
|
||||||
case DeviceType.SCHEDULER:
|
|
||||||
return LL.SCHEDULER();
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showName = useCallback(
|
const showName = (di: DashboardItem) => {
|
||||||
(di: DashboardItem) => {
|
if (di.id < 100) {
|
||||||
if (di.id < 100) {
|
// if its a device (parent node) and has entities
|
||||||
// if its a device (parent node) and has entities
|
if (di.nodes?.length) {
|
||||||
if (di.nodes?.length) {
|
return (
|
||||||
return (
|
<span style={{ fontSize: '15px' }}>
|
||||||
<span style={{ fontSize: '15px' }}>
|
<DeviceIcon type_id={di.t ?? 0} />
|
||||||
<DeviceIcon type_id={di.t ?? 0} />
|
{showType(di.n, di.t)}
|
||||||
{showType(di.n, di.t)}
|
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||||
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
</span>
|
||||||
</span>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (di.dv) {
|
}
|
||||||
return <span>{di.dv.id.slice(2)}</span>;
|
if (di.dv) {
|
||||||
}
|
return <span>{di.dv.id.slice(2)}</span>;
|
||||||
return null;
|
}
|
||||||
},
|
return null;
|
||||||
[showType]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const hasMask = useCallback(
|
const hasMask = (id: string, mask: number) =>
|
||||||
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const editDashboardValue = useCallback(
|
const editDashboardValue = (di: DashboardItem) => {
|
||||||
(di: DashboardItem) => {
|
if (me.admin && di.dv?.c) {
|
||||||
if (me.admin && di.dv?.c) {
|
setSelectedDashboardItem(di);
|
||||||
setSelectedDashboardItem(di);
|
setDeviceValueDialogOpen(true);
|
||||||
setDeviceValueDialogOpen(true);
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[me.admin]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleShowAll = (
|
const handleShowAll = (
|
||||||
_event: React.MouseEvent<HTMLElement>,
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
@@ -248,10 +226,9 @@ const Dashboard = memo(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFavEntities = useMemo(
|
const hasFavEntities = data.nodes.filter(
|
||||||
() => data.nodes.filter((item: DashboardItem) => item.id <= 90).length,
|
(item: DashboardItem) => item.id <= 90
|
||||||
[data.nodes]
|
).length;
|
||||||
);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { IconContext } from 'react-icons';
|
import { IconContext } from 'react-icons';
|
||||||
@@ -133,21 +132,19 @@ const Devices = memo(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const leftOffset = useCallback(() => {
|
const leftOffset = () => {
|
||||||
const devicesWindow = document.getElementById('devices-window');
|
const devicesWindow = document.getElementById('devices-window');
|
||||||
if (!devicesWindow) return 0;
|
if (!devicesWindow) return 0;
|
||||||
const { left, right } = devicesWindow.getBoundingClientRect();
|
const { left, right } = devicesWindow.getBoundingClientRect();
|
||||||
if (!left || !right) return 0;
|
if (!left || !right) return 0;
|
||||||
return left + (right - left < 400 ? 0 : 200);
|
return left + (right - left < 400 ? 0 : 200);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const common_theme = useMemo(
|
const common_theme = useTheme({
|
||||||
() =>
|
BaseRow: `
|
||||||
useTheme({
|
|
||||||
BaseRow: `
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`,
|
`,
|
||||||
HeaderRow: `
|
HeaderRow: `
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: #90CAF9;
|
color: #90CAF9;
|
||||||
@@ -155,7 +152,7 @@ const Devices = memo(() => {
|
|||||||
border-bottom: 1px solid #565656;
|
border-bottom: 1px solid #565656;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Row: `
|
Row: `
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #1E1E1E;
|
background-color: #1E1E1E;
|
||||||
.td {
|
.td {
|
||||||
@@ -165,88 +162,78 @@ const Devices = memo(() => {
|
|||||||
background-color: #177ac9;
|
background-color: #177ac9;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const device_theme = useMemo(
|
const device_theme = useTheme([
|
||||||
() =>
|
common_theme,
|
||||||
useTheme([
|
{
|
||||||
common_theme,
|
BaseRow: `
|
||||||
{
|
font-size: 15px;
|
||||||
BaseRow: `
|
.td {
|
||||||
font-size: 15px;
|
height: 28px;
|
||||||
.td {
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
Table: `
|
|
||||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
|
|
||||||
`,
|
|
||||||
HeaderRow: `
|
|
||||||
.th {
|
|
||||||
padding: 8px;
|
|
||||||
`,
|
|
||||||
Row: `
|
|
||||||
&:nth-of-type(odd) .td {
|
|
||||||
background-color: #303030;
|
|
||||||
},
|
|
||||||
&:hover .td {
|
|
||||||
background-color: #177ac9;
|
|
||||||
},
|
|
||||||
`
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
[common_theme]
|
|
||||||
);
|
|
||||||
|
|
||||||
const data_theme = useMemo(
|
|
||||||
() =>
|
|
||||||
useTheme([
|
|
||||||
common_theme,
|
|
||||||
{
|
|
||||||
Table: `
|
|
||||||
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
|
|
||||||
height: auto;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
display:none;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
Table: `
|
||||||
.td {
|
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
|
||||||
height: 32px;
|
`,
|
||||||
}
|
HeaderRow: `
|
||||||
`,
|
.th {
|
||||||
BaseCell: `
|
padding: 8px;
|
||||||
&:nth-of-type(1) {
|
`,
|
||||||
border-left: 1px solid #177ac9;
|
Row: `
|
||||||
},
|
&:nth-of-type(odd) .td {
|
||||||
&:nth-of-type(2) {
|
|
||||||
text-align: right;
|
|
||||||
},
|
|
||||||
&:nth-of-type(3) {
|
|
||||||
border-right: 1px solid #177ac9;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
HeaderRow: `
|
|
||||||
.th {
|
|
||||||
border-top: 1px solid #565656;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
Row: `
|
|
||||||
&:nth-of-type(odd) .td {
|
|
||||||
background-color: #303030;
|
background-color: #303030;
|
||||||
},
|
},
|
||||||
&:hover .td {
|
&:hover .td {
|
||||||
background-color: #177ac9;
|
background-color: #177ac9;
|
||||||
|
},
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const data_theme = useTheme([
|
||||||
|
common_theme,
|
||||||
|
{
|
||||||
|
Table: `
|
||||||
|
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display:none;
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
}
|
BaseRow: `
|
||||||
]),
|
.td {
|
||||||
[common_theme]
|
height: 32px;
|
||||||
);
|
}
|
||||||
|
`,
|
||||||
|
BaseCell: `
|
||||||
|
&:nth-of-type(1) {
|
||||||
|
border-left: 1px solid #177ac9;
|
||||||
|
},
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
text-align: right;
|
||||||
|
},
|
||||||
|
&:nth-of-type(3) {
|
||||||
|
border-right: 1px solid #177ac9;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HeaderRow: `
|
||||||
|
.th {
|
||||||
|
border-top: 1px solid #565656;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Row: `
|
||||||
|
&:nth-of-type(odd) .td {
|
||||||
|
background-color: #303030;
|
||||||
|
},
|
||||||
|
&:hover .td {
|
||||||
|
background-color: #177ac9;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
const getSortIcon = (state: State, sortKey: unknown) => {
|
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||||
if (state.sortKey === sortKey && state.reverse) {
|
if (state.sortKey === sortKey && state.reverse) {
|
||||||
@@ -345,10 +332,8 @@ const Devices = memo(() => {
|
|||||||
return sc;
|
return sc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasMask = useCallback(
|
const hasMask = (id: string, mask: number) =>
|
||||||
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDownloadCsv = () => {
|
const handleDownloadCsv = () => {
|
||||||
const deviceIndex = coreData.devices.findIndex(
|
const deviceIndex = coreData.devices.findIndex(
|
||||||
@@ -607,41 +592,35 @@ const Devices = memo(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDeviceValue = useCallback((dv: DeviceValue) => {
|
const showDeviceValue = (dv: DeviceValue) => {
|
||||||
setSelectedDeviceValue(dv);
|
setSelectedDeviceValue(dv);
|
||||||
setDeviceValueDialogOpen(true);
|
setDeviceValueDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const renderNameCell = useCallback(
|
const renderNameCell = (dv: DeviceValue) => (
|
||||||
(dv: DeviceValue) => (
|
<>
|
||||||
<>
|
{dv.id.slice(2)}
|
||||||
{dv.id.slice(2)}
|
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
)}
|
||||||
)}
|
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||||
<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 }} />
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
|
||||||
),
|
|
||||||
[hasMask]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const shown_data = useMemo(() => {
|
const shown_data = onlyFav
|
||||||
if (onlyFav) {
|
? deviceData.nodes.filter(
|
||||||
return deviceData.nodes.filter(
|
|
||||||
(dv: DeviceValue) =>
|
(dv: DeviceValue) =>
|
||||||
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
|
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
|
||||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
: deviceData.nodes.filter((dv: DeviceValue) =>
|
||||||
|
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return deviceData.nodes.filter((dv: DeviceValue) =>
|
|
||||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [deviceData.nodes, onlyFav, search]);
|
|
||||||
|
|
||||||
const deviceIndex = coreData.devices.findIndex(
|
const deviceIndex = coreData.devices.findIndex(
|
||||||
(d: Device) => d.id === device_select.state.id
|
(d: Device) => d.id === device_select.state.id
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
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';
|
||||||
@@ -24,7 +24,7 @@ import type { ValidateFieldsError } from 'async-validator';
|
|||||||
import { ValidatedTextField } from 'components';
|
import { 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 { ValidationError, 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';
|
||||||
@@ -52,7 +52,7 @@ const DevicesDialog = ({
|
|||||||
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = useMemo(() => updateValue(setEditItem), [setEditItem]);
|
const updateFormValue = updateValue(setEditItem);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -61,36 +61,33 @@ const DevicesDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const setUom = useCallback(
|
const setUom = (uom?: DeviceValueUOM) => {
|
||||||
(uom?: DeviceValueUOM) => {
|
if (uom === undefined) {
|
||||||
if (uom === undefined) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
switch (uom) {
|
||||||
switch (uom) {
|
case DeviceValueUOM.HOURS:
|
||||||
case DeviceValueUOM.HOURS:
|
return LL.HOURS();
|
||||||
return LL.HOURS();
|
case DeviceValueUOM.MINUTES:
|
||||||
case DeviceValueUOM.MINUTES:
|
return LL.MINUTES();
|
||||||
return LL.MINUTES();
|
case DeviceValueUOM.SECONDS:
|
||||||
case DeviceValueUOM.SECONDS:
|
return LL.SECONDS();
|
||||||
return LL.SECONDS();
|
default:
|
||||||
default:
|
return DeviceValueUOM_s[uom];
|
||||||
return DeviceValueUOM_s[uom];
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showHelperText = useCallback((dv: DeviceValue) => {
|
const showHelperText = (dv: DeviceValue) => {
|
||||||
if (dv.h) return dv.h;
|
if (dv.h) return dv.h;
|
||||||
if (dv.l) return dv.l.join(' | ');
|
if (dv.l) return dv.l.join(' | ');
|
||||||
if (dv.m !== undefined && dv.x !== undefined) {
|
if (dv.m !== undefined && dv.x !== undefined) {
|
||||||
@@ -101,26 +98,16 @@ const DevicesDialog = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const isCommand = useMemo(
|
const isCommand = selectedItem.v === '' && selectedItem.c;
|
||||||
() => selectedItem.v === '' && selectedItem.c,
|
const dialogTitle = isCommand
|
||||||
[selectedItem.v, selectedItem.c]
|
? LL.RUN_COMMAND()
|
||||||
);
|
: writeable
|
||||||
|
? LL.CHANGE_VALUE()
|
||||||
const dialogTitle = useMemo(() => {
|
: LL.VALUE(0);
|
||||||
if (isCommand) return LL.RUN_COMMAND();
|
const buttonLabel = isCommand ? LL.EXECUTE() : LL.UPDATE();
|
||||||
return writeable ? LL.CHANGE_VALUE() : LL.VALUE(0);
|
const helperText = showHelperText(editItem);
|
||||||
}, [isCommand, writeable, LL]);
|
|
||||||
|
|
||||||
const buttonLabel = useMemo(() => {
|
|
||||||
return isCommand ? LL.EXECUTE() : LL.UPDATE();
|
|
||||||
}, [isCommand, LL]);
|
|
||||||
|
|
||||||
const helperText = useMemo(
|
|
||||||
() => showHelperText(editItem),
|
|
||||||
[editItem, showHelperText]
|
|
||||||
);
|
|
||||||
|
|
||||||
const valueLabel = LL.VALUE(0);
|
const valueLabel = LL.VALUE(0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||||
|
|
||||||
import OptionIcon from './OptionIcon';
|
import OptionIcon from './OptionIcon';
|
||||||
@@ -11,7 +9,6 @@ interface EntityMaskToggleProps {
|
|||||||
de: DeviceEntity;
|
de: DeviceEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available mask values
|
|
||||||
const MASK_VALUES = [
|
const MASK_VALUES = [
|
||||||
DeviceEntityMask.DV_WEB_EXCLUDE, // 1
|
DeviceEntityMask.DV_WEB_EXCLUDE, // 1
|
||||||
DeviceEntityMask.DV_API_MQTT_EXCLUDE, // 2
|
DeviceEntityMask.DV_API_MQTT_EXCLUDE, // 2
|
||||||
@@ -20,123 +17,95 @@ const MASK_VALUES = [
|
|||||||
DeviceEntityMask.DV_DELETED // 128
|
DeviceEntityMask.DV_DELETED // 128
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
const getMaskNumber = (newMask: string[]): number =>
|
||||||
* Converts an array of mask strings to a bitmask number
|
newMask.reduce((mask, entry) => mask | Number(entry), 0);
|
||||||
*/
|
|
||||||
const getMaskNumber = (newMask: string[]): number => {
|
|
||||||
return newMask.reduce((mask, entry) => mask | Number(entry), 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const getMaskString = (mask: number): string[] =>
|
||||||
* Converts a bitmask number to an array of mask strings
|
MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
|
||||||
*/
|
|
||||||
const getMaskString = (mask: number): string[] => {
|
|
||||||
return MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
|
|
||||||
String(value)
|
String(value)
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a specific mask bit is set
|
|
||||||
*/
|
|
||||||
const hasMask = (mask: number, flag: number): boolean => (mask & flag) === flag;
|
const hasMask = (mask: number, flag: number): boolean => (mask & flag) === flag;
|
||||||
|
|
||||||
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
|
||||||
const handleChange = useCallback(
|
const handleChange = (_event: unknown, mask: string[]) => {
|
||||||
(_event: unknown, mask: string[]) => {
|
const newMask = getMaskNumber(mask);
|
||||||
// Convert selected masks to a number
|
const updatedDe = { ...de };
|
||||||
const newMask = getMaskNumber(mask);
|
|
||||||
const updatedDe = { ...de };
|
|
||||||
|
|
||||||
// Apply business logic for mask interactions
|
// If entity has no name and is set to readonly, also exclude from web
|
||||||
// If entity has no name and is set to readonly, also exclude from web
|
if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) {
|
||||||
if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) {
|
updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||||
updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE;
|
} else {
|
||||||
} else {
|
updatedDe.m = newMask;
|
||||||
updatedDe.m = newMask;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If excluded from web, cannot be favorite
|
// If excluded from web, cannot be favorite
|
||||||
if (hasMask(updatedDe.m, DeviceEntityMask.DV_WEB_EXCLUDE)) {
|
if (hasMask(updatedDe.m, DeviceEntityMask.DV_WEB_EXCLUDE)) {
|
||||||
updatedDe.m = updatedDe.m & ~DeviceEntityMask.DV_FAVORITE;
|
updatedDe.m = updatedDe.m & ~DeviceEntityMask.DV_FAVORITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate(updatedDe);
|
onUpdate(updatedDe);
|
||||||
},
|
};
|
||||||
[de, onUpdate]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize mask string value
|
|
||||||
const maskStringValue = useMemo(() => getMaskString(de.m), [de.m]);
|
|
||||||
|
|
||||||
// Memoize disabled states
|
|
||||||
const isFavoriteDisabled = useMemo(
|
|
||||||
() =>
|
|
||||||
hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_DELETED) ||
|
|
||||||
de.n === undefined,
|
|
||||||
[de.m, de.n]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isReadonlyDisabled = useMemo(
|
|
||||||
() =>
|
|
||||||
!de.w ||
|
|
||||||
hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_FAVORITE),
|
|
||||||
[de.w, de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isApiMqttExcludeDisabled = useMemo(
|
|
||||||
() => de.n === '' || hasMask(de.m, DeviceEntityMask.DV_DELETED),
|
|
||||||
[de.n, de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isWebExcludeDisabled = useMemo(
|
|
||||||
() => de.n === undefined || hasMask(de.m, DeviceEntityMask.DV_DELETED),
|
|
||||||
[de.n, de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize mask flag checks
|
|
||||||
const isFavoriteSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_FAVORITE),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isReadonlySet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_READONLY),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isApiMqttExcludeSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_API_MQTT_EXCLUDE),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isWebExcludeSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
const isDeletedSet = useMemo(
|
|
||||||
() => hasMask(de.m, DeviceEntityMask.DV_DELETED),
|
|
||||||
[de.m]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
value={maskStringValue}
|
value={getMaskString(de.m)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<ToggleButton value="8" disabled={isFavoriteDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="favorite" isSet={isFavoriteSet} />
|
value="8"
|
||||||
|
disabled={
|
||||||
|
hasMask(
|
||||||
|
de.m,
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_DELETED
|
||||||
|
) || de.n === undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="favorite"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_FAVORITE)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="4" disabled={isReadonlyDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="readonly" isSet={isReadonlySet} />
|
value="4"
|
||||||
|
disabled={
|
||||||
|
!de.w ||
|
||||||
|
hasMask(
|
||||||
|
de.m,
|
||||||
|
DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_FAVORITE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="readonly"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_READONLY)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="2" disabled={isApiMqttExcludeDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={isApiMqttExcludeSet} />
|
value="2"
|
||||||
|
disabled={de.n === '' || hasMask(de.m, DeviceEntityMask.DV_DELETED)}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="api_mqtt_exclude"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_API_MQTT_EXCLUDE)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="1" disabled={isWebExcludeDisabled}>
|
<ToggleButton
|
||||||
<OptionIcon type="web_exclude" isSet={isWebExcludeSet} />
|
value="1"
|
||||||
|
disabled={de.n === undefined || hasMask(de.m, DeviceEntityMask.DV_DELETED)}
|
||||||
|
>
|
||||||
|
<OptionIcon
|
||||||
|
type="web_exclude"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value="128">
|
<ToggleButton value="128">
|
||||||
<OptionIcon type="deleted" isSet={isDeletedSet} />
|
<OptionIcon
|
||||||
|
type="deleted"
|
||||||
|
isSet={hasMask(de.m, DeviceEntityMask.DV_DELETED)}
|
||||||
|
/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useMemo, useState } from 'react';
|
import { memo, useContext, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -60,6 +60,8 @@ const AVATAR_STYLES: SxProps<Theme> = {
|
|||||||
bgcolor: '#72caf9'
|
bgcolor: '#72caf9'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SYSTEM_INFO_API: APIcall = { device: 'system', cmd: 'info', id: 0 };
|
||||||
|
|
||||||
const HelpComponent = () => {
|
const HelpComponent = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
useLayoutTitle(LL.HELP());
|
useLayoutTitle(LL.HELP());
|
||||||
@@ -72,12 +74,7 @@ const HelpComponent = () => {
|
|||||||
});
|
});
|
||||||
const [imgError, setImgError] = useState<boolean>(false);
|
const [imgError, setImgError] = useState<boolean>(false);
|
||||||
|
|
||||||
const getCustomSupportMethod = useMemo(
|
useRequest(callAction({ action: 'getCustomSupport' })).onSuccess((event) => {
|
||||||
() => callAction({ action: 'getCustomSupport' }),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useRequest(getCustomSupportMethod).onSuccess((event) => {
|
|
||||||
if (event?.data && Object.keys(event.data).length !== 0) {
|
if (event?.data && Object.keys(event.data).length !== 0) {
|
||||||
const { Support } = event.data as {
|
const { Support } = event.data as {
|
||||||
Support: { img_url?: string; html?: string[] };
|
Support: { img_url?: string; html?: string[] };
|
||||||
@@ -100,47 +97,26 @@ const HelpComponent = () => {
|
|||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optimize API call memoization
|
const helpLinks: HelpLink[] = [
|
||||||
const apiCall = useMemo(() => ({ device: 'system', cmd: 'info', id: 0 }), []);
|
{
|
||||||
|
href: 'https://emsesp.org',
|
||||||
|
icon: <MenuBookIcon />,
|
||||||
|
label: () => LL.HELP_INFORMATION_1()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://discord.gg/GP9DPSgeJq',
|
||||||
|
icon: <CommentIcon />,
|
||||||
|
label: () => LL.HELP_INFORMATION_2()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://github.com/emsesp/EMS-ESP32/issues/new/choose',
|
||||||
|
icon: <GitHubIcon />,
|
||||||
|
label: () => LL.HELP_INFORMATION_3()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const handleDownloadSystemInfo = useCallback(() => {
|
const imageSrc =
|
||||||
void sendAPI(apiCall);
|
imgError || !customSupport.img_url ? DEFAULT_IMAGE_URL : customSupport.img_url;
|
||||||
}, [sendAPI, apiCall]);
|
|
||||||
|
|
||||||
const handleImageError = useCallback(() => {
|
|
||||||
setImgError(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Memoize help links to prevent recreation on every render
|
|
||||||
const helpLinks: HelpLink[] = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
href: 'https://emsesp.org',
|
|
||||||
icon: <MenuBookIcon />,
|
|
||||||
label: () => LL.HELP_INFORMATION_1()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'https://discord.gg/GP9DPSgeJq',
|
|
||||||
icon: <CommentIcon />,
|
|
||||||
label: () => LL.HELP_INFORMATION_2()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'https://github.com/emsesp/EMS-ESP32/issues/new/choose',
|
|
||||||
icon: <GitHubIcon />,
|
|
||||||
label: () => LL.HELP_INFORMATION_3()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isAdmin = useMemo(() => me?.admin ?? false, [me?.admin]);
|
|
||||||
|
|
||||||
// Memoize image source computation
|
|
||||||
const imageSrc = useMemo(
|
|
||||||
() =>
|
|
||||||
imgError || !customSupport.img_url ? DEFAULT_IMAGE_URL : customSupport.img_url,
|
|
||||||
[imgError, customSupport.img_url]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
@@ -157,13 +133,13 @@ const HelpComponent = () => {
|
|||||||
component="img"
|
component="img"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
sx={IMAGE_STYLES}
|
sx={IMAGE_STYLES}
|
||||||
onError={handleImageError}
|
onError={() => setImgError(true)}
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAdmin && (
|
{me?.admin && (
|
||||||
<List>
|
<List>
|
||||||
{helpLinks.map(({ href, icon, label }) => (
|
{helpLinks.map(({ href, icon, label }) => (
|
||||||
<ListItem key={href}>
|
<ListItem key={href}>
|
||||||
@@ -191,7 +167,7 @@ const HelpComponent = () => {
|
|||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDownloadSystemInfo}
|
onClick={() => void sendAPI(SYSTEM_INFO_API)}
|
||||||
>
|
>
|
||||||
{LL.SUPPORT_INFORMATION(0)}
|
{LL.SUPPORT_INFORMATION(0)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -214,7 +190,6 @@ const HelpComponent = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memoize the component to prevent unnecessary re-renders
|
|
||||||
const Help = memo(HelpComponent);
|
const Help = memo(HelpComponent);
|
||||||
|
|
||||||
export default Help;
|
export default Help;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -69,58 +69,53 @@ const Modules = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const modules_theme = useTheme(
|
const modules_theme = useTheme({
|
||||||
useMemo(
|
Table: `
|
||||||
() => ({
|
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
|
||||||
Table: `
|
`,
|
||||||
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
|
BaseRow: `
|
||||||
`,
|
font-size: 14px;
|
||||||
BaseRow: `
|
.td {
|
||||||
font-size: 14px;
|
height: 32px;
|
||||||
.td {
|
}
|
||||||
height: 32px;
|
`,
|
||||||
}
|
BaseCell: `
|
||||||
`,
|
&:nth-of-type(1) {
|
||||||
BaseCell: `
|
text-align: center;
|
||||||
&:nth-of-type(1) {
|
}
|
||||||
text-align: center;
|
`,
|
||||||
}
|
HeaderRow: `
|
||||||
`,
|
text-transform: uppercase;
|
||||||
HeaderRow: `
|
background-color: black;
|
||||||
text-transform: uppercase;
|
color: #90CAF9;
|
||||||
background-color: black;
|
.th {
|
||||||
color: #90CAF9;
|
border-bottom: 1px solid #565656;
|
||||||
.th {
|
height: 36px;
|
||||||
border-bottom: 1px solid #565656;
|
}
|
||||||
height: 36px;
|
`,
|
||||||
}
|
Row: `
|
||||||
`,
|
background-color: #1e1e1e;
|
||||||
Row: `
|
position: relative;
|
||||||
background-color: #1e1e1e;
|
cursor: pointer;
|
||||||
position: relative;
|
.td {
|
||||||
cursor: pointer;
|
border-top: 1px solid #565656;
|
||||||
.td {
|
border-bottom: 1px solid #565656;
|
||||||
border-top: 1px solid #565656;
|
}
|
||||||
border-bottom: 1px solid #565656;
|
&:hover .td {
|
||||||
}
|
border-top: 1px solid #177ac9;
|
||||||
&:hover .td {
|
border-bottom: 1px solid #177ac9;
|
||||||
border-top: 1px solid #177ac9;
|
}
|
||||||
border-bottom: 1px solid #177ac9;
|
&:nth-of-type(odd) .td {
|
||||||
}
|
background-color: #303030;
|
||||||
&:nth-of-type(odd) .td {
|
}
|
||||||
background-color: #303030;
|
`
|
||||||
}
|
});
|
||||||
`
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDialogClose = useCallback(() => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const updateModuleItem = useCallback((updatedItem: ModuleItem) => {
|
const updateModuleItem = (updatedItem: ModuleItem) => {
|
||||||
void updateState(readModules(), (data: ModuleItem[]) => {
|
void updateState(readModules(), (data: ModuleItem[]) => {
|
||||||
const new_data = data.map((mi) =>
|
const new_data = data.map((mi) =>
|
||||||
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
|
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
|
||||||
@@ -128,28 +123,25 @@ const Modules = () => {
|
|||||||
setNumChanges(new_data.filter(hasModulesChanged).length);
|
setNumChanges(new_data.filter(hasModulesChanged).length);
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: ModuleItem) => {
|
||||||
(updatedItem: ModuleItem) => {
|
setDialogOpen(false);
|
||||||
setDialogOpen(false);
|
updateModuleItem(updatedItem);
|
||||||
updateModuleItem(updatedItem);
|
};
|
||||||
},
|
|
||||||
[updateModuleItem]
|
|
||||||
);
|
|
||||||
|
|
||||||
const editModuleItem = useCallback((mi: ModuleItem) => {
|
const editModuleItem = (mi: ModuleItem) => {
|
||||||
setSelectedModuleItem(mi);
|
setSelectedModuleItem(mi);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onCancel = useCallback(async () => {
|
const onCancel = async () => {
|
||||||
await fetchModules().then(() => {
|
await fetchModules().then(() => {
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [fetchModules]);
|
};
|
||||||
|
|
||||||
const saveModules = useCallback(async () => {
|
const saveModules = async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
modules.map((condensed_mi: ModuleItem) =>
|
modules.map((condensed_mi: ModuleItem) =>
|
||||||
@@ -167,9 +159,9 @@ const Modules = () => {
|
|||||||
await fetchModules();
|
await fetchModules();
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
}
|
}
|
||||||
}, [modules, updateModules, LL, fetchModules]);
|
};
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const renderContent = () => {
|
||||||
if (!modules) {
|
if (!modules) {
|
||||||
return (
|
return (
|
||||||
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
|
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
|
||||||
@@ -262,22 +254,12 @@ const Modules = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
modules,
|
|
||||||
fetchModules,
|
|
||||||
error,
|
|
||||||
modules_theme,
|
|
||||||
editModuleItem,
|
|
||||||
LL,
|
|
||||||
numChanges,
|
|
||||||
onCancel,
|
|
||||||
saveModules
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content}
|
{renderContent()}
|
||||||
{selectedModuleItem && (
|
{selectedModuleItem && (
|
||||||
<ModulesDialog
|
<ModulesDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
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';
|
||||||
@@ -37,14 +37,10 @@ const ModulesDialog = ({
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
|
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue(
|
||||||
() =>
|
setEditItem as unknown as React.Dispatch<
|
||||||
updateValue(
|
React.SetStateAction<Record<string, unknown>>
|
||||||
setEditItem as unknown as React.Dispatch<
|
>
|
||||||
React.SetStateAction<Record<string, unknown>>
|
|
||||||
>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync form state when dialog opens or selected item changes
|
// Sync form state when dialog opens or selected item changes
|
||||||
@@ -54,18 +50,13 @@ const ModulesDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = () => {
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
}, [editItem, onSave]);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(
|
|
||||||
() => `${LL.EDIT()} ${editItem.key}`,
|
|
||||||
[LL, editItem.key]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
|
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{`${LL.EDIT()} ${editItem.key}`}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ const Scheduler = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasScheduleChanged = useCallback((si: ScheduleItem) => {
|
const hasScheduleChanged = (si: ScheduleItem) => {
|
||||||
return (
|
return (
|
||||||
si.id !== si.o_id ||
|
si.id !== si.o_id ||
|
||||||
(si.name || '') !== (si.o_name || '') ||
|
(si.name || '') !== (si.o_name || '') ||
|
||||||
@@ -143,15 +143,13 @@ const Scheduler = () => {
|
|||||||
si.cmd !== si.o_cmd ||
|
si.cmd !== si.o_cmd ||
|
||||||
si.value !== si.o_value
|
si.value !== si.o_value
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const intervalCallback = useCallback(() => {
|
useInterval(() => {
|
||||||
if (numChanges === 0) {
|
if (numChanges === 0) {
|
||||||
void fetchSchedule();
|
void fetchSchedule();
|
||||||
}
|
}
|
||||||
}, [numChanges, fetchSchedule]);
|
}, INTERVAL_DELAY);
|
||||||
|
|
||||||
useInterval(intervalCallback, INTERVAL_DELAY);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const formatter = new Intl.DateTimeFormat(locale, {
|
const formatter = new Intl.DateTimeFormat(locale, {
|
||||||
@@ -169,7 +167,7 @@ const Scheduler = () => {
|
|||||||
|
|
||||||
const schedule_theme = useTheme(scheduleTheme);
|
const schedule_theme = useTheme(scheduleTheme);
|
||||||
|
|
||||||
const saveSchedule = useCallback(async () => {
|
const saveSchedule = async () => {
|
||||||
try {
|
try {
|
||||||
await updateSchedule({
|
await updateSchedule({
|
||||||
schedule: schedule
|
schedule: schedule
|
||||||
@@ -192,46 +190,43 @@ const Scheduler = () => {
|
|||||||
await fetchSchedule();
|
await fetchSchedule();
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
}
|
}
|
||||||
}, [LL, schedule, updateSchedule, fetchSchedule]);
|
};
|
||||||
|
|
||||||
const editScheduleItem = useCallback((si: ScheduleItem) => {
|
const editScheduleItem = (si: ScheduleItem) => {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
setSelectedScheduleItem(si);
|
setSelectedScheduleItem(si);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
if (si.o_name === undefined) {
|
if (si.o_name === undefined) {
|
||||||
si.o_name = si.name;
|
si.o_name = si.name;
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogClose = useCallback(() => {
|
const onDialogClose = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onDialogCancel = useCallback(async () => {
|
const onDialogCancel = async () => {
|
||||||
await fetchSchedule().then(() => {
|
await fetchSchedule().then(() => {
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
});
|
});
|
||||||
}, [fetchSchedule]);
|
};
|
||||||
|
|
||||||
const onDialogSave = useCallback(
|
const onDialogSave = (updatedItem: ScheduleItem) => {
|
||||||
(updatedItem: ScheduleItem) => {
|
setDialogOpen(false);
|
||||||
setDialogOpen(false);
|
void updateState(readSchedule(), (data: ScheduleItem[]) => {
|
||||||
void updateState(readSchedule(), (data: ScheduleItem[]) => {
|
const new_data = creating
|
||||||
const new_data = creating
|
? [...data, updatedItem]
|
||||||
? [...data, updatedItem]
|
: data.map((si) =>
|
||||||
: data.map((si) =>
|
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
|
||||||
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
|
);
|
||||||
);
|
|
||||||
|
|
||||||
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
|
||||||
|
|
||||||
return new_data;
|
return new_data;
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[creating, hasScheduleChanged]
|
|
||||||
);
|
|
||||||
|
|
||||||
const addScheduleItem = useCallback(() => {
|
const addScheduleItem = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
const newItem: ScheduleItem = {
|
const newItem: ScheduleItem = {
|
||||||
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
|
||||||
@@ -239,36 +234,29 @@ const Scheduler = () => {
|
|||||||
};
|
};
|
||||||
setSelectedScheduleItem(newItem);
|
setSelectedScheduleItem(newItem);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const filteredAndSortedSchedule = useMemo(
|
const filteredAndSortedSchedule = schedule
|
||||||
() =>
|
.filter((si: ScheduleItem) => !si.deleted)
|
||||||
schedule
|
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags);
|
||||||
.filter((si: ScheduleItem) => !si.deleted)
|
|
||||||
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags),
|
|
||||||
[schedule]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dayBox = useCallback(
|
const dayBox = (si: ScheduleItem, flag: number) => {
|
||||||
(si: ScheduleItem, flag: number) => {
|
const dayIndex = Math.log(flag) / LOG_2;
|
||||||
const dayIndex = Math.log(flag) / LOG_2;
|
const isActive = (si.flags & flag) === flag;
|
||||||
const isActive = (si.flags & flag) === flag;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography sx={{ fontSize: 11 }} color={isActive ? 'primary' : 'grey'}>
|
<Typography sx={{ fontSize: 11 }} color={isActive ? 'primary' : 'grey'}>
|
||||||
{dow[dayIndex]}
|
{dow[dayIndex]}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
[dow]
|
|
||||||
);
|
|
||||||
|
|
||||||
const scheduleType = useCallback((si: ScheduleItem) => {
|
const scheduleType = (si: ScheduleItem) => {
|
||||||
const label = scheduleTypeLabels[si.flags];
|
const label = scheduleTypeLabels[si.flags];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -278,9 +266,9 @@ const Scheduler = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const renderSchedule = useCallback(() => {
|
const renderSchedule = () => {
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
return (
|
return (
|
||||||
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
|
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
|
||||||
@@ -343,17 +331,7 @@ const Scheduler = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
schedule,
|
|
||||||
error,
|
|
||||||
fetchSchedule,
|
|
||||||
filteredAndSortedSchedule,
|
|
||||||
schedule_theme,
|
|
||||||
editScheduleItem,
|
|
||||||
LL,
|
|
||||||
dayBox,
|
|
||||||
scheduleType
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
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';
|
||||||
@@ -26,7 +26,7 @@ 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 { updateValue } from 'utils';
|
import { updateValue } from 'utils';
|
||||||
import { validate } from 'validators';
|
import { ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
import { ScheduleFlag } from './types';
|
import { ScheduleFlag } from './types';
|
||||||
import type { ScheduleItem } from './types';
|
import type { ScheduleItem } from './types';
|
||||||
@@ -60,6 +60,12 @@ const FLAG_VALUES = [
|
|||||||
ScheduleFlag.SCHEDULE_SAT
|
ScheduleFlag.SCHEDULE_SAT
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const getFlagDOWnumber = (flags: string[]) =>
|
||||||
|
flags.reduce((acc, flag) => acc | Number(flag), 0) & FLAG_MASK_127;
|
||||||
|
|
||||||
|
const getFlagDOWstring = (f: number) =>
|
||||||
|
FLAG_VALUES.filter((flag) => (f & flag) === flag).map((flag) => String(flag));
|
||||||
|
|
||||||
interface SchedulerDialogProps {
|
interface SchedulerDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
@@ -84,14 +90,10 @@ const SchedulerDialog = ({
|
|||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const [scheduleType, setScheduleType] = useState<ScheduleFlag>();
|
const [scheduleType, setScheduleType] = useState<ScheduleFlag>();
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue(
|
||||||
() =>
|
setEditItem as unknown as React.Dispatch<
|
||||||
updateValue(
|
React.SetStateAction<Record<string, unknown>>
|
||||||
setEditItem as unknown as React.Dispatch<
|
>
|
||||||
React.SetStateAction<Record<string, unknown>>
|
|
||||||
>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -112,129 +114,95 @@ const SchedulerDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
// Helper function to handle save operations
|
const handleSave = async (itemToSave: ScheduleItem) => {
|
||||||
const handleSave = useCallback(
|
try {
|
||||||
async (itemToSave: ScheduleItem) => {
|
setFieldErrors(undefined);
|
||||||
try {
|
await validate(validator, itemToSave);
|
||||||
setFieldErrors(undefined);
|
onSave(itemToSave);
|
||||||
await validate(validator, itemToSave);
|
} catch (error) {
|
||||||
onSave(itemToSave);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
} catch (error) {
|
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[validator, onSave]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
|
||||||
await handleSave(editItem);
|
|
||||||
}, [editItem, handleSave]);
|
|
||||||
|
|
||||||
const saveandactivate = useCallback(async () => {
|
|
||||||
await handleSave({ ...editItem, active: true });
|
|
||||||
}, [editItem, handleSave]);
|
|
||||||
|
|
||||||
const remove = useCallback(() => {
|
|
||||||
onSave({ ...editItem, deleted: true });
|
|
||||||
}, [editItem, onSave]);
|
|
||||||
|
|
||||||
// Optimize DOW flag conversion
|
|
||||||
const getFlagDOWnumber = useCallback((flags: string[]) => {
|
|
||||||
return flags.reduce((acc, flag) => acc | Number(flag), 0) & FLAG_MASK_127;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getFlagDOWstring = useCallback((f: number) => {
|
|
||||||
return FLAG_VALUES.filter((flag) => (f & flag) === flag).map((flag) =>
|
|
||||||
String(flag)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Day of week display component
|
|
||||||
const DayOfWeekButton = useCallback(
|
|
||||||
(flag: number) => {
|
|
||||||
const dayIndex = Math.log2(flag);
|
|
||||||
const isSelected = (editItem.flags & flag) === flag;
|
|
||||||
return (
|
|
||||||
<Typography
|
|
||||||
sx={{ fontSize: TYPOGRAPHY_FONT_SIZE }}
|
|
||||||
color={isSelected ? 'primary' : 'grey'}
|
|
||||||
>
|
|
||||||
{dow[dayIndex]}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[editItem.flags, dow]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClose = useCallback(
|
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
|
||||||
if (reason !== 'backdropClick') {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleScheduleTypeChange = useCallback(
|
|
||||||
(_event: React.SyntheticEvent<HTMLElement>, flag: ScheduleFlag | null) => {
|
|
||||||
if (flag !== null) {
|
|
||||||
setFieldErrors(undefined); // clear any validation errors
|
|
||||||
setScheduleType(flag);
|
|
||||||
// wipe the time field when changing the schedule type
|
|
||||||
// set the flags based on type
|
|
||||||
const newFlags = flag === ScheduleFlag.SCHEDULE_DAY ? FLAG_ALL_DAYS : flag;
|
|
||||||
setEditItem((prev) => ({ ...prev, time: '', flags: newFlags }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDOWChange = useCallback(
|
|
||||||
(_event: React.SyntheticEvent<HTMLElement>, flags: string[]) => {
|
|
||||||
const newFlags =
|
|
||||||
getFlagDOWnumber(flags) === 0 ? FLAG_ALL_DAYS : getFlagDOWnumber(flags);
|
|
||||||
setEditItem((prev) => ({ ...prev, flags: newFlags }));
|
|
||||||
},
|
|
||||||
[getFlagDOWnumber]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize derived values
|
|
||||||
const isDaySchedule = useMemo(
|
|
||||||
() => scheduleType === ScheduleFlag.SCHEDULE_DAY,
|
|
||||||
[scheduleType]
|
|
||||||
);
|
|
||||||
const isTimerSchedule = useMemo(
|
|
||||||
() => scheduleType === ScheduleFlag.SCHEDULE_TIMER,
|
|
||||||
[scheduleType]
|
|
||||||
);
|
|
||||||
const isImmediateSchedule = useMemo(
|
|
||||||
() => scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE,
|
|
||||||
[scheduleType]
|
|
||||||
);
|
|
||||||
const needsTimeField = useMemo(
|
|
||||||
() => isDaySchedule || isTimerSchedule,
|
|
||||||
[isDaySchedule, isTimerSchedule]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dowFlags = useMemo(
|
|
||||||
() => getFlagDOWstring(editItem.flags),
|
|
||||||
[editItem.flags, getFlagDOWstring]
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeFieldValue = useMemo(() => {
|
|
||||||
if (needsTimeField) {
|
|
||||||
return editItem.time === '' ? DEFAULT_TIME : editItem.time;
|
|
||||||
}
|
}
|
||||||
return editItem.time === DEFAULT_TIME ? '' : editItem.time;
|
};
|
||||||
}, [editItem.time, needsTimeField]);
|
|
||||||
|
|
||||||
const timeFieldLabel = useMemo(() => {
|
const save = async () => {
|
||||||
|
await handleSave(editItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveandactivate = async () => {
|
||||||
|
await handleSave({ ...editItem, active: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
onSave({ ...editItem, deleted: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const DayOfWeekButton = (flag: number) => {
|
||||||
|
const dayIndex = Math.log2(flag);
|
||||||
|
const isSelected = (editItem.flags & flag) === flag;
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
sx={{ fontSize: TYPOGRAPHY_FONT_SIZE }}
|
||||||
|
color={isSelected ? 'primary' : 'grey'}
|
||||||
|
>
|
||||||
|
{dow[dayIndex]}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (
|
||||||
|
_event: React.SyntheticEvent,
|
||||||
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
|
) => {
|
||||||
|
if (reason !== 'backdropClick') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScheduleTypeChange = (
|
||||||
|
_event: React.SyntheticEvent<HTMLElement>,
|
||||||
|
flag: ScheduleFlag | null
|
||||||
|
) => {
|
||||||
|
if (flag !== null) {
|
||||||
|
setFieldErrors(undefined); // clear any validation errors
|
||||||
|
setScheduleType(flag);
|
||||||
|
// wipe the time field when changing the schedule type
|
||||||
|
// set the flags based on type
|
||||||
|
const newFlags = flag === ScheduleFlag.SCHEDULE_DAY ? FLAG_ALL_DAYS : flag;
|
||||||
|
setEditItem((prev) => ({ ...prev, time: '', flags: newFlags }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDOWChange = (
|
||||||
|
_event: React.SyntheticEvent<HTMLElement>,
|
||||||
|
flags: string[]
|
||||||
|
) => {
|
||||||
|
const newFlags =
|
||||||
|
getFlagDOWnumber(flags) === 0 ? FLAG_ALL_DAYS : getFlagDOWnumber(flags);
|
||||||
|
setEditItem((prev) => ({ ...prev, flags: newFlags }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDaySchedule = scheduleType === ScheduleFlag.SCHEDULE_DAY;
|
||||||
|
const isTimerSchedule = scheduleType === ScheduleFlag.SCHEDULE_TIMER;
|
||||||
|
const isImmediateSchedule = scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE;
|
||||||
|
const needsTimeField = isDaySchedule || isTimerSchedule;
|
||||||
|
|
||||||
|
const dowFlags = getFlagDOWstring(editItem.flags);
|
||||||
|
|
||||||
|
const timeFieldValue = needsTimeField
|
||||||
|
? editItem.time === ''
|
||||||
|
? DEFAULT_TIME
|
||||||
|
: editItem.time
|
||||||
|
: editItem.time === DEFAULT_TIME
|
||||||
|
? ''
|
||||||
|
: editItem.time;
|
||||||
|
|
||||||
|
const timeFieldLabel = (() => {
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_TIMER) return LL.TIMER(1);
|
if (scheduleType === ScheduleFlag.SCHEDULE_TIMER) return LL.TIMER(1);
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION();
|
if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION();
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE();
|
if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE();
|
||||||
if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE();
|
if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE();
|
||||||
return LL.TIME(1);
|
return LL.TIME(1);
|
||||||
}, [scheduleType, LL]);
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext, useMemo, useRef, useState } from 'react';
|
import { useContext, useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
|
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
|
||||||
@@ -158,18 +158,16 @@ const Sensors = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const intervalCallback = useCallback(() => {
|
useInterval(() => {
|
||||||
if (!temperatureDialogOpen && !analogDialogOpen) {
|
if (!temperatureDialogOpen && !analogDialogOpen) {
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
}
|
}
|
||||||
}, [temperatureDialogOpen, analogDialogOpen, fetchSensorData]);
|
});
|
||||||
|
|
||||||
useInterval(intervalCallback);
|
|
||||||
|
|
||||||
const temperature_theme = useTheme([common_theme, temperature_theme_config]);
|
const temperature_theme = useTheme([common_theme, temperature_theme_config]);
|
||||||
const analog_theme = useTheme([common_theme, analog_theme_config]);
|
const analog_theme = useTheme([common_theme, analog_theme_config]);
|
||||||
|
|
||||||
const getSortIcon = useCallback((state: State, sortKey: unknown) => {
|
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||||
if (state.sortKey === sortKey && state.reverse) {
|
if (state.sortKey === sortKey && state.reverse) {
|
||||||
return <KeyboardArrowDownOutlinedIcon />;
|
return <KeyboardArrowDownOutlinedIcon />;
|
||||||
}
|
}
|
||||||
@@ -177,7 +175,7 @@ const Sensors = () => {
|
|||||||
return <KeyboardArrowUpOutlinedIcon />;
|
return <KeyboardArrowUpOutlinedIcon />;
|
||||||
}
|
}
|
||||||
return <UnfoldMoreOutlinedIcon />;
|
return <UnfoldMoreOutlinedIcon />;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const analog_sort = useSort(
|
const analog_sort = useSort(
|
||||||
{ nodes: sensorData.as },
|
{ nodes: sensorData.as },
|
||||||
@@ -234,119 +232,104 @@ const Sensors = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.SENSORS());
|
useLayoutTitle(LL.SENSORS());
|
||||||
|
|
||||||
const formatDurationMin = useCallback(
|
const formatDurationMin = (duration_min: number) => {
|
||||||
(duration_min: number) => {
|
const totalMs = duration_min * MS_PER_MINUTE;
|
||||||
const totalMs = duration_min * MS_PER_MINUTE;
|
const days = Math.trunc(totalMs / MS_PER_DAY);
|
||||||
const days = Math.trunc(totalMs / MS_PER_DAY);
|
const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24;
|
||||||
const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24;
|
const minutes = Math.trunc(totalMs / MS_PER_MINUTE) % 60;
|
||||||
const minutes = Math.trunc(totalMs / MS_PER_MINUTE) % 60;
|
|
||||||
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
parts.push(LL.NUM_DAYS({ num: days }));
|
parts.push(LL.NUM_DAYS({ num: days }));
|
||||||
}
|
}
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
parts.push(LL.NUM_HOURS({ num: hours }));
|
parts.push(LL.NUM_HOURS({ num: hours }));
|
||||||
}
|
}
|
||||||
if (minutes > 0) {
|
if (minutes > 0) {
|
||||||
parts.push(LL.NUM_MINUTES({ num: minutes }));
|
parts.push(LL.NUM_MINUTES({ num: minutes }));
|
||||||
}
|
}
|
||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
},
|
};
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatValue = useCallback(
|
const formatValue = (value: unknown, uom: DeviceValueUOM) => {
|
||||||
(value: unknown, uom: DeviceValueUOM) => {
|
if (value === undefined) {
|
||||||
if (value === undefined) {
|
return '';
|
||||||
return '';
|
}
|
||||||
}
|
if (typeof value !== 'number') {
|
||||||
if (typeof value !== 'number') {
|
return value as string;
|
||||||
return value as string;
|
}
|
||||||
}
|
switch (uom) {
|
||||||
switch (uom) {
|
case DeviceValueUOM.HOURS:
|
||||||
case DeviceValueUOM.HOURS:
|
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
|
||||||
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
|
case DeviceValueUOM.MINUTES:
|
||||||
case DeviceValueUOM.MINUTES:
|
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
|
||||||
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
|
case DeviceValueUOM.SECONDS:
|
||||||
case DeviceValueUOM.SECONDS:
|
return LL.NUM_SECONDS({ num: value });
|
||||||
return LL.NUM_SECONDS({ num: value });
|
case DeviceValueUOM.NONE:
|
||||||
case DeviceValueUOM.NONE:
|
return new Intl.NumberFormat().format(value);
|
||||||
return new Intl.NumberFormat().format(value);
|
case DeviceValueUOM.DEGREES:
|
||||||
case DeviceValueUOM.DEGREES:
|
case DeviceValueUOM.DEGREES_R:
|
||||||
case DeviceValueUOM.DEGREES_R:
|
case DeviceValueUOM.FAHRENHEIT:
|
||||||
case DeviceValueUOM.FAHRENHEIT:
|
return (
|
||||||
return (
|
new Intl.NumberFormat(undefined, {
|
||||||
new Intl.NumberFormat(undefined, {
|
minimumFractionDigits: 1
|
||||||
minimumFractionDigits: 1
|
}).format(value) +
|
||||||
}).format(value) +
|
' ' +
|
||||||
' ' +
|
DeviceValueUOM_s[uom]
|
||||||
DeviceValueUOM_s[uom]
|
);
|
||||||
);
|
default:
|
||||||
default:
|
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
||||||
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[formatDurationMin, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateTemperatureSensor = useCallback(
|
const updateTemperatureSensor = (ts: TemperatureSensor) => {
|
||||||
(ts: TemperatureSensor) => {
|
if (me.admin) {
|
||||||
if (me.admin) {
|
ts.o_n = ts.n;
|
||||||
ts.o_n = ts.n;
|
setSelectedTemperatureSensor(ts);
|
||||||
setSelectedTemperatureSensor(ts);
|
setTemperatureDialogOpen(true);
|
||||||
setTemperatureDialogOpen(true);
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[me.admin]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTemperatureDialogClose = useCallback(() => {
|
const onTemperatureDialogClose = () => {
|
||||||
setTemperatureDialogOpen(false);
|
setTemperatureDialogOpen(false);
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
}, [fetchSensorData]);
|
};
|
||||||
|
|
||||||
const onTemperatureDialogSave = useCallback(
|
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
|
||||||
async (ts: TemperatureSensor) => {
|
await sendTemperatureSensor({
|
||||||
await sendTemperatureSensor({
|
id: ts.id,
|
||||||
id: ts.id,
|
name: ts.n,
|
||||||
name: ts.n,
|
offset: ts.o,
|
||||||
offset: ts.o,
|
is_system: ts.s
|
||||||
is_system: ts.s
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
|
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.finally(() => {
|
||||||
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
|
setTemperatureDialogOpen(false);
|
||||||
})
|
setSelectedTemperatureSensor(undefined);
|
||||||
.finally(() => {
|
void fetchSensorData();
|
||||||
setTemperatureDialogOpen(false);
|
});
|
||||||
setSelectedTemperatureSensor(undefined);
|
};
|
||||||
void fetchSensorData();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[sendTemperatureSensor, LL, fetchSensorData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateAnalogSensor = useCallback(
|
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||||
(as: AnalogSensor) => {
|
if (me.admin) {
|
||||||
if (me.admin) {
|
setCreating(false);
|
||||||
setCreating(false);
|
as.o_n = as.n;
|
||||||
as.o_n = as.n;
|
setSelectedAnalogSensor(as);
|
||||||
setSelectedAnalogSensor(as);
|
setAnalogDialogOpen(true);
|
||||||
setAnalogDialogOpen(true);
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[me.admin]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onAnalogDialogClose = useCallback(() => {
|
const onAnalogDialogClose = () => {
|
||||||
setAnalogDialogOpen(false);
|
setAnalogDialogOpen(false);
|
||||||
void fetchSensorData();
|
void fetchSensorData();
|
||||||
}, [fetchSensorData]);
|
};
|
||||||
|
|
||||||
const addAnalogSensor = useCallback(() => {
|
const addAnalogSensor = () => {
|
||||||
if (firstAvailableGPIO.current === undefined) {
|
if (firstAvailableGPIO.current === undefined) {
|
||||||
toast.error(LL.NO_GPIO());
|
toast.error(LL.NO_GPIO());
|
||||||
return;
|
return;
|
||||||
@@ -366,194 +349,167 @@ const Sensors = () => {
|
|||||||
o_n: ''
|
o_n: ''
|
||||||
});
|
});
|
||||||
setAnalogDialogOpen(true);
|
setAnalogDialogOpen(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onAnalogDialogSave = useCallback(
|
const onAnalogDialogSave = async (as: AnalogSensor) => {
|
||||||
async (as: AnalogSensor) => {
|
await sendAnalogSensor({
|
||||||
await sendAnalogSensor({
|
id: as.id,
|
||||||
id: as.id,
|
gpio: as.g,
|
||||||
gpio: as.g,
|
name: as.n,
|
||||||
name: as.n,
|
offset: as.o,
|
||||||
offset: as.o,
|
factor: as.f,
|
||||||
factor: as.f,
|
uom: as.u,
|
||||||
uom: as.u,
|
type: as.t,
|
||||||
type: as.t,
|
deleted: as.d,
|
||||||
deleted: as.d,
|
is_system: as.s
|
||||||
is_system: as.s
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
|
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.finally(() => {
|
||||||
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
|
setAnalogDialogOpen(false);
|
||||||
})
|
setSelectedAnalogSensor(undefined);
|
||||||
.finally(() => {
|
void fetchSensorData();
|
||||||
setAnalogDialogOpen(false);
|
});
|
||||||
setSelectedAnalogSensor(undefined);
|
};
|
||||||
void fetchSensorData();
|
|
||||||
});
|
const RenderAnalogSensors = (
|
||||||
},
|
<Table
|
||||||
[sendAnalogSensor, LL, fetchSensorData]
|
data={{ nodes: sensorData.as }}
|
||||||
|
theme={analog_theme}
|
||||||
|
sort={analog_sort}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
>
|
||||||
|
{(tableList: AnalogSensor[]) => (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell stiff>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={HEADER_BUTTON_STYLE}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
|
||||||
|
>
|
||||||
|
GPIO
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell resize>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={HEADER_BUTTON_STYLE}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'NAME')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
||||||
|
>
|
||||||
|
{LL.NAME(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell stiff>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={HEADER_BUTTON_STYLE}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
|
||||||
|
>
|
||||||
|
{LL.TYPE(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
<HeaderCell stiff>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
style={HEADER_BUTTON_STYLE_END}
|
||||||
|
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
|
||||||
|
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
|
||||||
|
>
|
||||||
|
{LL.VALUE(0)}
|
||||||
|
</Button>
|
||||||
|
</HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
{tableList.map((as: AnalogSensor) => (
|
||||||
|
<Row
|
||||||
|
style={{ color: as.s ? 'grey' : 'inherit' }}
|
||||||
|
key={as.id}
|
||||||
|
item={as}
|
||||||
|
onClick={() => updateAnalogSensor(as)}
|
||||||
|
>
|
||||||
|
<Cell stiff>{as.g}</Cell>
|
||||||
|
<Cell>{as.n}</Cell>
|
||||||
|
<Cell stiff>{AnalogTypeNames[as.t - 1]} </Cell>
|
||||||
|
{(as.t === AnalogType.DIGITAL_OUT &&
|
||||||
|
as.g !== GPIO_25 &&
|
||||||
|
as.g !== GPIO_26) ||
|
||||||
|
as.t === AnalogType.DIGITAL_IN ||
|
||||||
|
as.t === AnalogType.PULSE ? (
|
||||||
|
<Cell stiff>{as.v ? LL.ON() : LL.OFF()}</Cell>
|
||||||
|
) : (
|
||||||
|
<Cell stiff>{formatValue(as.v, as.u)}</Cell>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
);
|
);
|
||||||
|
|
||||||
const RenderAnalogSensors = useMemo(
|
const RenderTemperatureSensors = (
|
||||||
() => (
|
<Table
|
||||||
<Table
|
data={{ nodes: sensorData.ts }}
|
||||||
data={{ nodes: sensorData.as }}
|
theme={temperature_theme}
|
||||||
theme={analog_theme}
|
sort={temperature_sort}
|
||||||
sort={analog_sort}
|
layout={{ custom: true }}
|
||||||
layout={{ custom: true }}
|
>
|
||||||
>
|
{(tableList: TemperatureSensor[]) => (
|
||||||
{(tableList: AnalogSensor[]) => (
|
<>
|
||||||
<>
|
<Header>
|
||||||
<Header>
|
<HeaderRow>
|
||||||
<HeaderRow>
|
<HeaderCell resize>
|
||||||
<HeaderCell stiff>
|
<Button
|
||||||
<Button
|
fullWidth
|
||||||
fullWidth
|
style={HEADER_BUTTON_STYLE}
|
||||||
style={HEADER_BUTTON_STYLE}
|
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
|
||||||
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
|
onClick={() =>
|
||||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
|
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
|
||||||
>
|
}
|
||||||
GPIO
|
|
||||||
</Button>
|
|
||||||
</HeaderCell>
|
|
||||||
<HeaderCell resize>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={HEADER_BUTTON_STYLE}
|
|
||||||
endIcon={getSortIcon(analog_sort.state, 'NAME')}
|
|
||||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
|
|
||||||
>
|
|
||||||
{LL.NAME(0)}
|
|
||||||
</Button>
|
|
||||||
</HeaderCell>
|
|
||||||
<HeaderCell stiff>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={HEADER_BUTTON_STYLE}
|
|
||||||
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
|
|
||||||
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
|
|
||||||
>
|
|
||||||
{LL.TYPE(0)}
|
|
||||||
</Button>
|
|
||||||
</HeaderCell>
|
|
||||||
<HeaderCell stiff>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={HEADER_BUTTON_STYLE_END}
|
|
||||||
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
|
|
||||||
onClick={() =>
|
|
||||||
analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{LL.VALUE(0)}
|
|
||||||
</Button>
|
|
||||||
</HeaderCell>
|
|
||||||
</HeaderRow>
|
|
||||||
</Header>
|
|
||||||
<Body>
|
|
||||||
{tableList.map((as: AnalogSensor) => (
|
|
||||||
<Row
|
|
||||||
style={{ color: as.s ? 'grey' : 'inherit' }}
|
|
||||||
key={as.id}
|
|
||||||
item={as}
|
|
||||||
onClick={() => updateAnalogSensor(as)}
|
|
||||||
>
|
>
|
||||||
<Cell stiff>{as.g}</Cell>
|
{LL.NAME(0)}
|
||||||
<Cell>{as.n}</Cell>
|
</Button>
|
||||||
<Cell stiff>{AnalogTypeNames[as.t - 1]} </Cell>
|
</HeaderCell>
|
||||||
{(as.t === AnalogType.DIGITAL_OUT &&
|
<HeaderCell stiff>
|
||||||
as.g !== GPIO_25 &&
|
<Button
|
||||||
as.g !== GPIO_26) ||
|
fullWidth
|
||||||
as.t === AnalogType.DIGITAL_IN ||
|
style={HEADER_BUTTON_STYLE_END}
|
||||||
as.t === AnalogType.PULSE ? (
|
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
|
||||||
<Cell stiff>{as.v ? LL.ON() : LL.OFF()}</Cell>
|
onClick={() =>
|
||||||
) : (
|
temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
|
||||||
<Cell stiff>{formatValue(as.v, as.u)}</Cell>
|
}
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
</Body>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Table>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
analog_sort,
|
|
||||||
analog_theme,
|
|
||||||
getSortIcon,
|
|
||||||
sensorData.as,
|
|
||||||
LL,
|
|
||||||
updateAnalogSensor,
|
|
||||||
formatValue
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const RenderTemperatureSensors = useMemo(
|
|
||||||
() => (
|
|
||||||
<Table
|
|
||||||
data={{ nodes: sensorData.ts }}
|
|
||||||
theme={temperature_theme}
|
|
||||||
sort={temperature_sort}
|
|
||||||
layout={{ custom: true }}
|
|
||||||
>
|
|
||||||
{(tableList: TemperatureSensor[]) => (
|
|
||||||
<>
|
|
||||||
<Header>
|
|
||||||
<HeaderRow>
|
|
||||||
<HeaderCell resize>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={HEADER_BUTTON_STYLE}
|
|
||||||
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
|
|
||||||
onClick={() =>
|
|
||||||
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{LL.NAME(0)}
|
|
||||||
</Button>
|
|
||||||
</HeaderCell>
|
|
||||||
<HeaderCell stiff>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
style={HEADER_BUTTON_STYLE_END}
|
|
||||||
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
|
|
||||||
onClick={() =>
|
|
||||||
temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{LL.VALUE(0)}
|
|
||||||
</Button>
|
|
||||||
</HeaderCell>
|
|
||||||
</HeaderRow>
|
|
||||||
</Header>
|
|
||||||
<Body>
|
|
||||||
{tableList.map((ts: TemperatureSensor) => (
|
|
||||||
<Row
|
|
||||||
style={{ color: ts.s ? 'grey' : 'inherit' }}
|
|
||||||
key={ts.id}
|
|
||||||
item={ts}
|
|
||||||
onClick={() => updateTemperatureSensor(ts)}
|
|
||||||
>
|
>
|
||||||
<Cell>{ts.n}</Cell>
|
{LL.VALUE(0)}
|
||||||
<Cell>{formatValue(ts.t, ts.u)}</Cell>
|
</Button>
|
||||||
</Row>
|
</HeaderCell>
|
||||||
))}
|
</HeaderRow>
|
||||||
</Body>
|
</Header>
|
||||||
</>
|
<Body>
|
||||||
)}
|
{tableList.map((ts: TemperatureSensor) => (
|
||||||
</Table>
|
<Row
|
||||||
),
|
style={{ color: ts.s ? 'grey' : 'inherit' }}
|
||||||
[
|
key={ts.id}
|
||||||
temperature_sort,
|
item={ts}
|
||||||
temperature_theme,
|
onClick={() => updateTemperatureSensor(ts)}
|
||||||
getSortIcon,
|
>
|
||||||
sensorData.ts,
|
<Cell>{ts.n}</Cell>
|
||||||
LL,
|
<Cell>{formatValue(ts.t, ts.u)}</Cell>
|
||||||
updateTemperatureSensor,
|
</Row>
|
||||||
formatValue
|
))}
|
||||||
]
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
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';
|
||||||
@@ -23,7 +23,7 @@ import type { ValidateFieldsError } from 'async-validator';
|
|||||||
import { ValidatedTextField } from 'components';
|
import { 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 { ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types';
|
import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types';
|
||||||
import type { AnalogSensor } from './types';
|
import type { AnalogSensor } from './types';
|
||||||
@@ -53,84 +53,54 @@ const SensorsAnalogDialog = ({
|
|||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
|
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue((updater) =>
|
||||||
() =>
|
setEditItem(
|
||||||
updateValue((updater) =>
|
(prev) =>
|
||||||
setEditItem(
|
updater(
|
||||||
(prev) =>
|
prev as unknown as Record<string, unknown>
|
||||||
updater(
|
) as unknown as AnalogSensor
|
||||||
prev as unknown as Record<string, unknown>
|
)
|
||||||
) as unknown as AnalogSensor
|
|
||||||
)
|
|
||||||
),
|
|
||||||
[setEditItem]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize helper functions to check sensor type conditions
|
const isCounterOrRate =
|
||||||
const isCounterOrRate = useMemo(
|
editItem.t === AnalogType.COUNTER ||
|
||||||
() =>
|
editItem.t === AnalogType.RATE ||
|
||||||
editItem.t === AnalogType.COUNTER ||
|
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
|
||||||
editItem.t === AnalogType.RATE ||
|
const isCounter =
|
||||||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2),
|
editItem.t === AnalogType.COUNTER ||
|
||||||
[editItem.t]
|
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
|
||||||
);
|
const isFreqType =
|
||||||
const isCounter = useMemo(
|
editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2;
|
||||||
() =>
|
const isPWM =
|
||||||
editItem.t === AnalogType.COUNTER ||
|
editItem.t === AnalogType.PWM_0 ||
|
||||||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2),
|
editItem.t === AnalogType.PWM_1 ||
|
||||||
[editItem.t]
|
editItem.t === AnalogType.PWM_2;
|
||||||
);
|
const isDACOutGPIO =
|
||||||
const isFreqType = useMemo(
|
editItem.t === AnalogType.DIGITAL_OUT &&
|
||||||
() => editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2,
|
(editItem.g === 25 || editItem.g === 26);
|
||||||
[editItem.t]
|
const isDigitalOutGPIO =
|
||||||
);
|
editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26;
|
||||||
const isPWM = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.PWM_0 ||
|
|
||||||
editItem.t === AnalogType.PWM_1 ||
|
|
||||||
editItem.t === AnalogType.PWM_2,
|
|
||||||
[editItem.t]
|
|
||||||
);
|
|
||||||
const isDACOutGPIO = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.DIGITAL_OUT &&
|
|
||||||
(editItem.g === 25 || editItem.g === 26),
|
|
||||||
[editItem.t, editItem.g]
|
|
||||||
);
|
|
||||||
const isDigitalOutGPIO = useMemo(
|
|
||||||
() =>
|
|
||||||
editItem.t === AnalogType.DIGITAL_OUT &&
|
|
||||||
editItem.g !== 25 &&
|
|
||||||
editItem.g !== 26,
|
|
||||||
[editItem.t, editItem.g]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize menu items to avoid recreation on each render
|
const analogTypeMenuItems = AnalogTypeNames.map((val, i) => ({
|
||||||
const analogTypeMenuItems = useMemo(
|
name: val,
|
||||||
() =>
|
value: i + 1
|
||||||
AnalogTypeNames.map((val, i) => ({ name: val, value: i + 1 }))
|
}))
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map(({ name, value }) => (
|
.map(({ name, value }) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={name}
|
key={name}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabledTypeList?.includes(value)}
|
disabled={disabledTypeList?.includes(value)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)),
|
));
|
||||||
[disabledTypeList]
|
|
||||||
);
|
|
||||||
|
|
||||||
const uomMenuItems = useMemo(
|
const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
|
||||||
() =>
|
<MenuItem key={val} value={i}>
|
||||||
DeviceValueUOM_s.map((val, i) => (
|
{val}
|
||||||
<MenuItem key={val} value={i}>
|
</MenuItem>
|
||||||
{val}
|
));
|
||||||
</MenuItem>
|
|
||||||
)),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const analogGPIOMenuItems = () =>
|
const analogGPIOMenuItems = () =>
|
||||||
// add selectedItem.g to the list
|
// add selectedItem.g to the list
|
||||||
@@ -157,34 +127,30 @@ const SensorsAnalogDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (
|
||||||
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
|
_event: React.SyntheticEvent,
|
||||||
if (reason !== 'backdropClick') {
|
reason: 'backdropClick' | 'escapeKeyDown'
|
||||||
onClose();
|
) => {
|
||||||
}
|
if (reason !== 'backdropClick') {
|
||||||
},
|
onClose();
|
||||||
[onClose]
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const remove = useCallback(() => {
|
const remove = () => {
|
||||||
onSave({ ...editItem, d: true });
|
onSave({ ...editItem, d: true });
|
||||||
}, [editItem, onSave]);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(
|
const dialogTitle = `${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`;
|
||||||
() =>
|
|
||||||
`${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`,
|
|
||||||
[creating, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
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';
|
||||||
@@ -21,7 +21,7 @@ import type { ValidateFieldsError } from 'async-validator';
|
|||||||
import { ValidatedTextField } from 'components';
|
import { 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 { ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
import type { TemperatureSensor } from './types';
|
import type { TemperatureSensor } from './types';
|
||||||
|
|
||||||
@@ -50,16 +50,12 @@ const SensorsTemperatureDialog = ({
|
|||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValue(
|
||||||
() =>
|
setEditItem as unknown as (
|
||||||
updateValue(
|
updater: (
|
||||||
setEditItem as unknown as (
|
prevState: Readonly<Record<string, unknown>>
|
||||||
updater: (
|
) => Record<string, unknown>
|
||||||
prevState: Readonly<Record<string, unknown>>
|
) => void
|
||||||
) => Record<string, unknown>
|
|
||||||
) => void
|
|
||||||
),
|
|
||||||
[setEditItem]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -69,46 +65,25 @@ const SensorsTemperatureDialog = ({
|
|||||||
}
|
}
|
||||||
}, [open, selectedItem]);
|
}, [open, selectedItem]);
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = (_event: React.SyntheticEvent, reason?: string) => {
|
||||||
(_event: React.SyntheticEvent, reason?: string) => {
|
if (reason !== 'backdropClick') {
|
||||||
if (reason !== 'backdropClick') {
|
onClose();
|
||||||
onClose();
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[onClose]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, editItem);
|
await validate(validator, editItem);
|
||||||
onSave(editItem);
|
onSave(editItem);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [validator, editItem, onSave]);
|
};
|
||||||
|
|
||||||
const dialogTitle = useMemo(() => `${LL.EDIT()} ${LL.TEMP_SENSOR()}`, [LL]);
|
|
||||||
|
|
||||||
const offsetValue = useMemo(() => numberValue(editItem.o), [editItem.o]);
|
|
||||||
|
|
||||||
const slotProps = useMemo(
|
|
||||||
() => ({
|
|
||||||
input: {
|
|
||||||
startAdornment: <InputAdornment position="start">{TEMP_UNIT}</InputAdornment>
|
|
||||||
},
|
|
||||||
htmlInput: {
|
|
||||||
min: OFFSET_MIN,
|
|
||||||
max: OFFSET_MAX,
|
|
||||||
step: OFFSET_STEP
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{`${LL.EDIT()} ${LL.TEMP_SENSOR()}`}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography sx={{ mb: 2 }} color="warning" variant="body2">
|
<Typography sx={{ mb: 2 }} color="warning" variant="body2">
|
||||||
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
||||||
@@ -128,12 +103,23 @@ const SensorsTemperatureDialog = ({
|
|||||||
<TextField
|
<TextField
|
||||||
name="o"
|
name="o"
|
||||||
label={LL.OFFSET()}
|
label={LL.OFFSET()}
|
||||||
value={offsetValue}
|
value={numberValue(editItem.o)}
|
||||||
sx={{ width: '11ch' }}
|
sx={{ width: '11ch' }}
|
||||||
type="number"
|
type="number"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
slotProps={slotProps}
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">{TEMP_UNIT}</InputAdornment>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
htmlInput: {
|
||||||
|
min: OFFSET_MIN,
|
||||||
|
max: OFFSET_MAX,
|
||||||
|
step: OFFSET_STEP
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
|
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import {
|
import {
|
||||||
@@ -23,9 +23,9 @@ const UserProfileComponent = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.USER_PROFILE());
|
useLayoutTitle(LL.USER_PROFILE());
|
||||||
|
|
||||||
const handleSignOut = useCallback(() => {
|
const handleSignOut = () => {
|
||||||
signOut(true);
|
signOut(true);
|
||||||
}, [signOut]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ export interface Settings {
|
|||||||
modbus_port: number;
|
modbus_port: number;
|
||||||
modbus_max_clients: number;
|
modbus_max_clients: number;
|
||||||
modbus_timeout: number;
|
modbus_timeout: number;
|
||||||
|
email_enabled: boolean;
|
||||||
|
email_ssl?: boolean;
|
||||||
|
email_starttls?: boolean;
|
||||||
|
email_server: string;
|
||||||
|
email_port: number;
|
||||||
|
email_login: string;
|
||||||
|
email_pass: string;
|
||||||
|
email_sender: string;
|
||||||
|
email_recp: string;
|
||||||
|
email_subject: string;
|
||||||
developer_mode: boolean;
|
developer_mode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { 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';
|
||||||
@@ -21,10 +21,9 @@ import { useI18nContext } from 'i18n/i18n-react';
|
|||||||
import type { APSettingsType } from 'types';
|
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 { ValidationError, 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_DISCONNECTED;
|
||||||
|
|
||||||
// Efficient range function without recursion
|
// Efficient range function without recursion
|
||||||
@@ -63,22 +62,16 @@ const APSettings = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValueDirty(
|
||||||
() =>
|
origData as unknown as Record<string, unknown>,
|
||||||
updateValueDirty(
|
dirtyFlags,
|
||||||
origData as unknown as Record<string, unknown>,
|
setDirtyFlags,
|
||||||
dirtyFlags,
|
updateDataValue as (value: unknown) => void
|
||||||
setDirtyFlags,
|
|
||||||
updateDataValue as (value: unknown) => void
|
|
||||||
),
|
|
||||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize AP enabled state
|
const apEnabled = data ? isAPEnabled(data) : false;
|
||||||
const apEnabled = useMemo(() => (data ? isAPEnabled(data) : false), [data]);
|
|
||||||
|
|
||||||
// Memoize validation and submit handler
|
const validateAndSubmit = async () => {
|
||||||
const validateAndSubmit = useCallback(async () => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -86,9 +79,9 @@ const APSettings = () => {
|
|||||||
await validate(createAPSettingsValidator(data), data);
|
await validate(createAPSettingsValidator(data), data);
|
||||||
await saveData();
|
await saveData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -108,9 +101,6 @@ const APSettings = () => {
|
|||||||
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_DISCONNECTED}>
|
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
|
||||||
{LL.AP_PROVIDE_TEXT_2()}
|
{LL.AP_PROVIDE_TEXT_2()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -28,12 +28,13 @@ import {
|
|||||||
FormLoader,
|
FormLoader,
|
||||||
MessageBox,
|
MessageBox,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
|
ValidatedPasswordField,
|
||||||
ValidatedTextField,
|
ValidatedTextField,
|
||||||
useLayoutTitle
|
useLayoutTitle
|
||||||
} from 'components';
|
} from 'components';
|
||||||
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 { ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
|
import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
|
||||||
import { BOARD_PROFILES } from '../main/types';
|
import { BOARD_PROFILES } from '../main/types';
|
||||||
@@ -106,82 +107,80 @@ const ApplicationSettings = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoized input props to prevent recreation on every render
|
const SecondsInputProps = {
|
||||||
const SecondsInputProps = useMemo(
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
() => ({
|
};
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
|
||||||
}),
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const MinutesInputProps = useMemo(
|
const MinutesInputProps = {
|
||||||
() => ({
|
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
||||||
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
|
};
|
||||||
}),
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const HoursInputProps = useMemo(
|
const HoursInputProps = {
|
||||||
() => ({
|
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
||||||
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
|
};
|
||||||
}),
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
(error: Error) => {
|
(error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
const updateBoardProfile = useCallback(
|
const updateBoardProfile = async (board_profile: string) => {
|
||||||
async (board_profile: string) => {
|
await readBoardProfile(board_profile).catch((error: Error) => {
|
||||||
await readBoardProfile(board_profile).catch((error: Error) => {
|
toast.error(error.message);
|
||||||
toast.error(error.message);
|
});
|
||||||
});
|
};
|
||||||
},
|
|
||||||
[readBoardProfile]
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutTitle(LL.APPLICATION());
|
useLayoutTitle(LL.APPLICATION());
|
||||||
|
|
||||||
const validateAndSubmit = useCallback(async () => {
|
const validateAndSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(createSettingsValidator(data), data);
|
await validate(createSettingsValidator(data), data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
} finally {
|
} finally {
|
||||||
await saveData();
|
await saveData();
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
const changeBoardProfile = useCallback(
|
const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
const boardProfile = event.target.value;
|
||||||
const boardProfile = event.target.value;
|
updateFormValue(event);
|
||||||
updateFormValue(event);
|
if (boardProfile === 'CUSTOM') {
|
||||||
if (boardProfile === 'CUSTOM') {
|
updateDataValue({
|
||||||
updateDataValue({
|
...data,
|
||||||
...data,
|
board_profile: boardProfile
|
||||||
board_profile: boardProfile
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
void updateBoardProfile(boardProfile);
|
||||||
void updateBoardProfile(boardProfile);
|
}
|
||||||
}
|
};
|
||||||
},
|
|
||||||
[data, updateBoardProfile, updateFormValue, updateDataValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
const restart = useCallback(async () => {
|
const restart = async () => {
|
||||||
await validateAndSubmit();
|
await validateAndSubmit();
|
||||||
await doRestart();
|
await doRestart();
|
||||||
}, [validateAndSubmit, doRestart]);
|
};
|
||||||
|
|
||||||
// Memoize board profile select items to prevent recreation
|
const sendmail = async () => {
|
||||||
const boardProfileItems = useMemo(() => boardProfileSelectItems(), []);
|
await sendAPI({
|
||||||
|
device: 'system',
|
||||||
|
cmd: 'sendmail',
|
||||||
|
data: 'testmail',
|
||||||
|
id: 0
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(LL.SUCCESSFUL());
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const boardProfileItems = boardProfileSelectItems();
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data || !hardwareData) {
|
if (!data || !hardwareData) {
|
||||||
@@ -351,6 +350,169 @@ const ApplicationSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
<Typography color="secondary">eMail</Typography>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.email_enabled}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="email_enabled"
|
||||||
|
disabled={!hardwareData.psram}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography color={!hardwareData.psram ? 'grey' : 'default'}>
|
||||||
|
Enable eMail notification
|
||||||
|
{!hardwareData.psram && (
|
||||||
|
<Typography variant="caption">
|
||||||
|
({LL.IS_REQUIRED('PSRAM')})
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{data.email_enabled && (
|
||||||
|
<>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={2}
|
||||||
|
direction="row"
|
||||||
|
sx={{ justifyContent: 'flex-start', alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors || {}}
|
||||||
|
name="email_server"
|
||||||
|
label="SMTP Server"
|
||||||
|
variant="outlined"
|
||||||
|
value={data.email_server}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors || {}}
|
||||||
|
sx={{ width: '12ch' }}
|
||||||
|
name="email_port"
|
||||||
|
variant="outlined"
|
||||||
|
label="Port"
|
||||||
|
value={numberValue(data.email_port)}
|
||||||
|
type="number"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
size={4}
|
||||||
|
sx={{ mt: !data.email_ssl && !data.email_starttls ? 0 : 3 }}
|
||||||
|
>
|
||||||
|
{!data.email_starttls && (
|
||||||
|
<BlockFormControlLabel
|
||||||
|
sx={{ width: '12ch' }}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.email_ssl}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="email_ssl"
|
||||||
|
disabled={
|
||||||
|
data.email_starttls || data.email_ssl === undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="SSL/TLS"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!data.email_ssl && (
|
||||||
|
<BlockFormControlLabel
|
||||||
|
sx={{ width: '12ch' }}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.email_starttls}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
name="email_starttls"
|
||||||
|
disabled={
|
||||||
|
data.email_ssl || data.email_starttls === undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="STARTTLS"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors || {}}
|
||||||
|
name="email_login"
|
||||||
|
label="Login"
|
||||||
|
variant="outlined"
|
||||||
|
value={data.email_login}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedPasswordField
|
||||||
|
fieldErrors={fieldErrors || {}}
|
||||||
|
name="email_pass"
|
||||||
|
label="Password"
|
||||||
|
variant="outlined"
|
||||||
|
value={data.email_pass}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors || {}}
|
||||||
|
name="email_sender"
|
||||||
|
label="From"
|
||||||
|
variant="outlined"
|
||||||
|
value={data.email_sender}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors || {}}
|
||||||
|
name="email_recp"
|
||||||
|
label="To"
|
||||||
|
variant="outlined"
|
||||||
|
value={data.email_recp}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<ValidatedTextField
|
||||||
|
fieldErrors={fieldErrors || {}}
|
||||||
|
name="email_subject"
|
||||||
|
label="Subject"
|
||||||
|
variant="outlined"
|
||||||
|
value={data.email_subject}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Button
|
||||||
|
sx={{ mt: 3 }}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
disabled={dirtyFlags.length !== 0}
|
||||||
|
onClick={sendmail}
|
||||||
|
>
|
||||||
|
Send Testmail
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
|
||||||
{LL.SENSORS()}
|
{LL.SENSORS()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -57,7 +57,7 @@ const DownloadUpload = () => {
|
|||||||
|
|
||||||
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
try {
|
try {
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 });
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 });
|
||||||
@@ -65,16 +65,33 @@ const DownloadUpload = () => {
|
|||||||
toast.error((error as Error).message);
|
toast.error((error as Error).message);
|
||||||
setRestarting(false);
|
setRestarting(false);
|
||||||
}
|
}
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
|
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
|
||||||
|
|
||||||
const handleCloseBackupDialog = useCallback(() => {
|
const handleCloseBackupDialog = () => {
|
||||||
setConfirmBackup(false);
|
setConfirmBackup(false);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const renderBackupDialog = useMemo(
|
const handleDownload = (type: string) => () => {
|
||||||
() => (
|
void sendExportData(type);
|
||||||
|
setConfirmBackup(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (restarting) {
|
||||||
|
return <SystemMonitor />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
<Dialog
|
<Dialog
|
||||||
sx={dialogStyle}
|
sx={dialogStyle}
|
||||||
open={confirmBackup}
|
open={confirmBackup}
|
||||||
@@ -98,39 +115,13 @@ const DownloadUpload = () => {
|
|||||||
<Button
|
<Button
|
||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => handleDownload('systembackup')()}
|
onClick={handleDownload('systembackup')}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{LL.DOWNLOAD(0)}
|
{LL.DOWNLOAD(0)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
),
|
|
||||||
[confirmBackup, handleCloseBackupDialog, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDownload = useCallback(
|
|
||||||
(type: string) => () => {
|
|
||||||
void sendExportData(type);
|
|
||||||
},
|
|
||||||
[sendExportData]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (restarting) {
|
|
||||||
return <SystemMonitor />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<SectionContent>
|
|
||||||
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
|
||||||
</SectionContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionContent>
|
|
||||||
{renderBackupDialog}
|
|
||||||
|
|
||||||
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
||||||
{LL.DOWNLOAD(0)}
|
{LL.DOWNLOAD(0)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { MqttSettingsType } from 'types';
|
import type { MqttSettingsType } from 'types';
|
||||||
import { numberValue, updateValueDirty, useRest } from 'utils';
|
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||||
import { createMqttSettingsValidator, validate } from 'validators';
|
import { ValidationError, createMqttSettingsValidator, validate } from 'validators';
|
||||||
|
|
||||||
import { callAction } from '../../api/app';
|
import { callAction } from '../../api/app';
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ const MqttSettings = () => {
|
|||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const sendResetMQTT = useCallback(() => {
|
const sendResetMQTT = () => {
|
||||||
void callAction({ action: 'resetMQTT' })
|
void callAction({ action: 'resetMQTT' })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('MQTT ' + LL.REFRESH() + ' successful');
|
toast.success('MQTT ' + LL.REFRESH() + ' successful');
|
||||||
@@ -65,56 +65,44 @@ const MqttSettings = () => {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const updateFormValue = useMemo(
|
const updateFormValue = updateValueDirty(
|
||||||
() =>
|
origData as unknown as Record<string, unknown>,
|
||||||
updateValueDirty(
|
dirtyFlags,
|
||||||
origData as unknown as Record<string, unknown>,
|
setDirtyFlags,
|
||||||
dirtyFlags,
|
updateDataValue as (value: unknown) => void
|
||||||
setDirtyFlags,
|
|
||||||
updateDataValue as (value: unknown) => void
|
|
||||||
),
|
|
||||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecondsInputProps = useMemo(
|
const SecondsInputProps = {
|
||||||
() => ({
|
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
||||||
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
|
};
|
||||||
}),
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const emptyFieldErrors = useMemo(() => ({}), []);
|
const validateAndSubmit = async () => {
|
||||||
|
|
||||||
const validateAndSubmit = useCallback(async () => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(createMqttSettingsValidator(data), data);
|
await validate(createMqttSettingsValidator(data), data);
|
||||||
await saveData();
|
await saveData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
const publishIntervalFields = useMemo(
|
const publishIntervalFields = [
|
||||||
() => [
|
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
|
||||||
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
|
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
|
||||||
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
|
{
|
||||||
{
|
name: 'publish_time_thermostat',
|
||||||
name: 'publish_time_thermostat',
|
label: LL.MQTT_INT_THERMOSTATS(),
|
||||||
label: LL.MQTT_INT_THERMOSTATS(),
|
validated: false
|
||||||
validated: false
|
},
|
||||||
},
|
{ name: 'publish_time_solar', label: LL.MQTT_INT_SOLAR(), validated: false },
|
||||||
{ name: 'publish_time_solar', label: LL.MQTT_INT_SOLAR(), validated: false },
|
{ name: 'publish_time_mixer', label: LL.MQTT_INT_MIXER(), validated: false },
|
||||||
{ name: 'publish_time_mixer', label: LL.MQTT_INT_MIXER(), validated: false },
|
{ name: 'publish_time_water', label: LL.MQTT_INT_WATER(), validated: false },
|
||||||
{ name: 'publish_time_water', label: LL.MQTT_INT_WATER(), validated: false },
|
{ name: 'publish_time_sensor', label: LL.SENSORS(), validated: false },
|
||||||
{ name: 'publish_time_sensor', label: LL.SENSORS(), validated: false },
|
{ name: 'publish_time_other', label: LL.DEFAULT(0), validated: false }
|
||||||
{ name: 'publish_time_other', label: LL.DEFAULT(0), validated: false }
|
];
|
||||||
],
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +142,7 @@ const MqttSettings = () => {
|
|||||||
<Grid container spacing={2} rowSpacing={0}>
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="host"
|
name="host"
|
||||||
label={LL.ADDRESS_OF(LL.BROKER())}
|
label={LL.ADDRESS_OF(LL.BROKER())}
|
||||||
multiline
|
multiline
|
||||||
@@ -166,7 +154,7 @@ const MqttSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="port"
|
name="port"
|
||||||
label="Port"
|
label="Port"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -178,7 +166,7 @@ const MqttSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="base"
|
name="base"
|
||||||
label={LL.BASE_TOPIC()}
|
label={LL.BASE_TOPIC()}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -219,7 +207,7 @@ const MqttSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name="keep_alive"
|
name="keep_alive"
|
||||||
label="Keep Alive"
|
label="Keep Alive"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@@ -438,7 +426,7 @@ const MqttSettings = () => {
|
|||||||
<Grid key={field.name}>
|
<Grid key={field.name}>
|
||||||
{field.validated ? (
|
{field.validated ? (
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors ?? emptyFieldErrors}
|
fieldErrors={fieldErrors ?? {}}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { NTPSettingsType, Time } from 'types';
|
import type { NTPSettingsType, Time } from 'types';
|
||||||
import { formatLocalDateTime, updateValueDirty, useRest } from 'utils';
|
import { formatLocalDateTime, updateValueDirty, useRest } from 'utils';
|
||||||
import { validate } from 'validators';
|
import { ValidationError, validate } from 'validators';
|
||||||
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
||||||
|
|
||||||
import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ';
|
import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ';
|
||||||
@@ -61,14 +61,11 @@ const NTPSettings = () => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
useLayoutTitle('NTP');
|
useLayoutTitle('NTP');
|
||||||
|
|
||||||
// Memoized timezone select items for better performance
|
|
||||||
const timeZoneItems = useTimeZoneSelectItems();
|
const timeZoneItems = useTimeZoneSelectItems();
|
||||||
|
|
||||||
// Memoized selected timezone value
|
const selectedTzValue = data
|
||||||
const selectedTzValue = useMemo(
|
? selectedTimeZone(data.tz_label, data.tz_format)
|
||||||
() => (data ? selectedTimeZone(data.tz_label, data.tz_format) : undefined),
|
: undefined;
|
||||||
[data?.tz_label, data?.tz_format]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [localTime, setLocalTime] = useState<string>('');
|
const [localTime, setLocalTime] = useState<string>('');
|
||||||
const [settingTime, setSettingTime] = useState<boolean>(false);
|
const [settingTime, setSettingTime] = useState<boolean>(false);
|
||||||
@@ -82,32 +79,22 @@ const NTPSettings = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize updateFormValue to prevent recreation on every render
|
const updateFormValue = updateValueDirty(
|
||||||
const updateFormValue = useMemo(
|
origData as unknown as Record<string, unknown>,
|
||||||
() =>
|
dirtyFlags,
|
||||||
updateValueDirty(
|
setDirtyFlags,
|
||||||
origData as unknown as Record<string, unknown>,
|
updateDataValue as (value: unknown) => void
|
||||||
dirtyFlags,
|
|
||||||
setDirtyFlags,
|
|
||||||
updateDataValue as (value: unknown) => void
|
|
||||||
),
|
|
||||||
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoize updateLocalTime handler
|
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
const updateLocalTime = useCallback(
|
setLocalTime(event.target.value);
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize openSetTime handler
|
const openSetTime = () => {
|
||||||
const openSetTime = useCallback(() => {
|
|
||||||
setLocalTime(formatLocalDateTime(new Date()));
|
setLocalTime(formatLocalDateTime(new Date()));
|
||||||
setSettingTime(true);
|
setSettingTime(true);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
// Memoize configureTime handler
|
const configureTime = async () => {
|
||||||
const configureTime = useCallback(async () => {
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -120,38 +107,31 @@ const NTPSettings = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
}, [localTime, updateTime, LL, loadData]);
|
};
|
||||||
|
|
||||||
// Memoize close dialog handler
|
const handleCloseSetTime = () => setSettingTime(false);
|
||||||
const handleCloseSetTime = useCallback(() => setSettingTime(false), []);
|
|
||||||
|
|
||||||
// Memoize validate and submit handler
|
const validateAndSubmit = async () => {
|
||||||
const validateAndSubmit = useCallback(async () => {
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(NTP_SETTINGS_VALIDATOR, data);
|
await validate(NTP_SETTINGS_VALIDATOR, data);
|
||||||
await saveData();
|
await saveData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [data, saveData]);
|
};
|
||||||
|
|
||||||
// Memoize timezone change handler
|
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const changeTimeZone = useCallback(
|
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
...settings,
|
||||||
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
|
tz_label: event.target.value,
|
||||||
...settings,
|
tz_format: TIME_ZONES[event.target.value]
|
||||||
tz_label: event.target.value,
|
}));
|
||||||
tz_format: TIME_ZONES[event.target.value]
|
updateFormValue(event);
|
||||||
}));
|
};
|
||||||
updateFormValue(event);
|
|
||||||
},
|
|
||||||
[updateFormValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize render content to prevent unnecessary re-renders
|
const renderContent = () => {
|
||||||
const renderContent = useMemo(() => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
|
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
|
||||||
}
|
}
|
||||||
@@ -236,26 +216,12 @@ const NTPSettings = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
data,
|
|
||||||
errorMessage,
|
|
||||||
loadData,
|
|
||||||
updateFormValue,
|
|
||||||
fieldErrors,
|
|
||||||
selectedTzValue,
|
|
||||||
changeTimeZone,
|
|
||||||
timeZoneItems,
|
|
||||||
dirtyFlags,
|
|
||||||
openSetTime,
|
|
||||||
saving,
|
|
||||||
validateAndSubmit,
|
|
||||||
LL
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{renderContent}
|
{renderContent()}
|
||||||
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
|
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
|
||||||
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
|
|||||||
@@ -1,190 +1,108 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import BuildIcon from '@mui/icons-material/Build';
|
||||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||||
import ImportExportIcon from '@mui/icons-material/ImportExport';
|
import ImportExportIcon from '@mui/icons-material/ImportExport';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
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 ViewModuleIcon from '@mui/icons-material/ViewModule';
|
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||||
import {
|
import { List } from '@mui/material';
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
Divider,
|
|
||||||
List
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
import { API } from 'api/app';
|
|
||||||
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
|
||||||
import { useRequest } from 'alova/client';
|
|
||||||
import type { APIcall } from 'app/main/types';
|
|
||||||
import { SectionContent, useLayoutTitle } from 'components';
|
import { SectionContent, useLayoutTitle } from 'components';
|
||||||
import ListMenuItem from 'components/layout/ListMenuItem';
|
import ListMenuItem from 'components/layout/ListMenuItem';
|
||||||
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
import SystemMonitor from '../status/SystemMonitor';
|
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
const { versions } = useContext(AuthenticatedContext);
|
||||||
useLayoutTitle(LL.SETTINGS(0));
|
useLayoutTitle(LL.SETTINGS(0));
|
||||||
|
|
||||||
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
|
const upgradeAvailable = versions?.current?.upgradeable ?? false;
|
||||||
const [restarting, setRestarting] = useState<boolean>();
|
const firmwareText = versions?.current?.version
|
||||||
|
? `v${versions.current.version}${upgradeAvailable ? ` (${LL.UPDATE_AVAILABLE()})` : ''}`
|
||||||
|
: '';
|
||||||
|
|
||||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
return (
|
||||||
immediate: false
|
<SectionContent>
|
||||||
});
|
<List>
|
||||||
|
<ListMenuItem
|
||||||
|
icon={BuildIcon}
|
||||||
|
bgcolor="#72caf9"
|
||||||
|
label="EMS-ESP Firmware"
|
||||||
|
text={firmwareText}
|
||||||
|
to="/settings/version"
|
||||||
|
badge={upgradeAvailable}
|
||||||
|
/>
|
||||||
|
|
||||||
const doFormat = useCallback(async () => {
|
<ListMenuItem
|
||||||
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
|
icon={TuneIcon}
|
||||||
setRestarting(true);
|
bgcolor="#134ba2"
|
||||||
setConfirmFactoryReset(false);
|
label={LL.APPLICATION()}
|
||||||
});
|
text={LL.APPLICATION_SETTINGS_1()}
|
||||||
}, [sendAPI]);
|
to="application"
|
||||||
|
/>
|
||||||
|
|
||||||
const handleFactoryResetClose = useCallback(() => {
|
<ListMenuItem
|
||||||
setConfirmFactoryReset(false);
|
icon={SettingsEthernetIcon}
|
||||||
}, []);
|
bgcolor="#40828f"
|
||||||
|
label={LL.NETWORK(0)}
|
||||||
|
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
|
||||||
|
to="network"
|
||||||
|
/>
|
||||||
|
|
||||||
const handleFactoryResetClick = useCallback(() => {
|
<ListMenuItem
|
||||||
setConfirmFactoryReset(true);
|
icon={SettingsInputAntennaIcon}
|
||||||
}, []);
|
bgcolor="#5f9a5f"
|
||||||
|
label={LL.ACCESS_POINT(0)}
|
||||||
|
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
|
||||||
|
to="ap"
|
||||||
|
/>
|
||||||
|
|
||||||
const content = useMemo(() => {
|
<ListMenuItem
|
||||||
return (
|
icon={AccessTimeIcon}
|
||||||
<>
|
bgcolor="#c5572c"
|
||||||
<List>
|
label="NTP"
|
||||||
<ListMenuItem
|
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
|
||||||
icon={TuneIcon}
|
to="ntp"
|
||||||
bgcolor="#134ba2"
|
/>
|
||||||
label={LL.APPLICATION()}
|
|
||||||
text={LL.APPLICATION_SETTINGS_1()}
|
|
||||||
to="application"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
<ListMenuItem
|
||||||
icon={SettingsEthernetIcon}
|
icon={DeviceHubIcon}
|
||||||
bgcolor="#40828f"
|
bgcolor="#68374d"
|
||||||
label={LL.NETWORK(0)}
|
label="MQTT"
|
||||||
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
|
text={LL.CONFIGURE('MQTT')}
|
||||||
to="network"
|
to="mqtt"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListMenuItem
|
<ListMenuItem
|
||||||
icon={SettingsInputAntennaIcon}
|
icon={LockIcon}
|
||||||
bgcolor="#5f9a5f"
|
label={LL.SECURITY(0)}
|
||||||
label={LL.ACCESS_POINT(0)}
|
text={LL.SECURITY_1()}
|
||||||
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
|
to="security"
|
||||||
to="ap"
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
<ListMenuItem
|
||||||
icon={AccessTimeIcon}
|
icon={ViewModuleIcon}
|
||||||
bgcolor="#c5572c"
|
bgcolor="#efc34b"
|
||||||
label="NTP"
|
label={LL.MODULES()}
|
||||||
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
|
text={LL.MODULES_1()}
|
||||||
to="ntp"
|
to="modules"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListMenuItem
|
<ListMenuItem
|
||||||
icon={DeviceHubIcon}
|
icon={ImportExportIcon}
|
||||||
bgcolor="#68374d"
|
bgcolor="#5d89f7"
|
||||||
label="MQTT"
|
label={LL.DOWNLOAD_UPLOAD()}
|
||||||
text={LL.CONFIGURE('MQTT')}
|
text={LL.DOWNLOAD_UPLOAD_1()}
|
||||||
to="mqtt"
|
to="downloadUpload"
|
||||||
/>
|
/>
|
||||||
|
</List>
|
||||||
<ListMenuItem
|
</SectionContent>
|
||||||
icon={LockIcon}
|
);
|
||||||
label={LL.SECURITY(0)}
|
|
||||||
text={LL.SECURITY_1()}
|
|
||||||
to="security"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
icon={ViewModuleIcon}
|
|
||||||
bgcolor="#efc34b"
|
|
||||||
label={LL.MODULES()}
|
|
||||||
text={LL.MODULES_1()}
|
|
||||||
to="modules"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
icon={ImportExportIcon}
|
|
||||||
bgcolor="#5d89f7"
|
|
||||||
label={LL.DOWNLOAD_UPLOAD()}
|
|
||||||
text={LL.DOWNLOAD_UPLOAD_1()}
|
|
||||||
to="downloadUpload"
|
|
||||||
/>
|
|
||||||
</List>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
sx={dialogStyle}
|
|
||||||
open={confirmFactoryReset}
|
|
||||||
onClose={handleFactoryResetClose}
|
|
||||||
>
|
|
||||||
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
|
||||||
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
startIcon={<CancelIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleFactoryResetClose}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<SettingsBackupRestoreIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={doFormat}
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{LL.FACTORY_RESET()}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: 2,
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
flexWrap: 'nowrap',
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
startIcon={<SettingsBackupRestoreIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleFactoryResetClick}
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{LL.FACTORY_RESET()}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
LL,
|
|
||||||
handleFactoryResetClick,
|
|
||||||
handleFactoryResetClose,
|
|
||||||
doFormat,
|
|
||||||
confirmFactoryReset,
|
|
||||||
restarting
|
|
||||||
]);
|
|
||||||
|
|
||||||
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { MenuItem } from '@mui/material';
|
import { MenuItem } from '@mui/material';
|
||||||
|
|
||||||
export const TIME_ZONES: Record<string, string> = {
|
export const TIME_ZONES: Record<string, string> = {
|
||||||
@@ -472,26 +470,16 @@ export function selectedTimeZone(label: string, format: string) {
|
|||||||
return TIME_ZONES[label] === format ? label : undefined;
|
return TIME_ZONES[label] === format ? label : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoized version for use in components
|
|
||||||
export function useTimeZoneSelectItems() {
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
TIME_ZONE_LABELS.map((label) => (
|
|
||||||
<MenuItem key={label} value={label}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
)),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback export for backward compatibility - now memoized
|
|
||||||
const precomputedTimeZoneItems = TIME_ZONE_LABELS.map((label) => (
|
const precomputedTimeZoneItems = TIME_ZONE_LABELS.map((label) => (
|
||||||
<MenuItem key={label} value={label}>
|
<MenuItem key={label} value={label}>
|
||||||
{label}
|
{label}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
export function useTimeZoneSelectItems() {
|
||||||
|
return precomputedTimeZoneItems;
|
||||||
|
}
|
||||||
|
|
||||||
export function timeZoneSelectItems() {
|
export function timeZoneSelectItems() {
|
||||||
return precomputedTimeZoneItems;
|
return precomputedTimeZoneItems;
|
||||||
}
|
}
|
||||||
|
|||||||
958
interface/src/app/settings/Version.tsx
Normal file
958
interface/src/app/settings/Version.tsx
Normal file
@@ -0,0 +1,958 @@
|
|||||||
|
import { memo, useContext, useMemo, useState } from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import CheckIcon from '@mui/icons-material/Done';
|
||||||
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import * as SystemApi from 'api/system';
|
||||||
|
import { API, callAction } from 'api/app';
|
||||||
|
|
||||||
|
import { dialogStyle } from 'CustomTheme';
|
||||||
|
import { useRequest } from 'alova/client';
|
||||||
|
import type { APIcall } from 'app/main/types';
|
||||||
|
import SystemMonitor from 'app/status/SystemMonitor';
|
||||||
|
import {
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
SingleUpload,
|
||||||
|
useLayoutTitle
|
||||||
|
} from 'components';
|
||||||
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import type { TranslationFunctions } from 'i18n/i18n-types';
|
||||||
|
import type { VersionInfo } from 'types';
|
||||||
|
import { prettyDateTime } from 'utils/time';
|
||||||
|
|
||||||
|
// Constants moved outside component to avoid recreation
|
||||||
|
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
||||||
|
const STABLE_RELNOTES_URL =
|
||||||
|
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
||||||
|
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||||
|
const DEV_RELNOTES_URL =
|
||||||
|
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
||||||
|
|
||||||
|
// Types for better type safety
|
||||||
|
interface PartitionData {
|
||||||
|
partition: string;
|
||||||
|
version: string;
|
||||||
|
install_date?: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionData {
|
||||||
|
emsesp_version: string;
|
||||||
|
arduino_version: string;
|
||||||
|
esp_platform: string;
|
||||||
|
flash_chip_size: number;
|
||||||
|
psram: boolean;
|
||||||
|
build_flags?: string;
|
||||||
|
partition: string;
|
||||||
|
partitions: PartitionData[];
|
||||||
|
developer_mode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoized components for better performance
|
||||||
|
const VersionInfoDialog = memo(
|
||||||
|
({
|
||||||
|
showVersionInfo,
|
||||||
|
latestVersion,
|
||||||
|
latestDevVersion,
|
||||||
|
partitionVersion,
|
||||||
|
partition,
|
||||||
|
currentPartition,
|
||||||
|
size,
|
||||||
|
locale,
|
||||||
|
LL,
|
||||||
|
onClose
|
||||||
|
}: {
|
||||||
|
showVersionInfo: number;
|
||||||
|
latestVersion: VersionInfo | undefined;
|
||||||
|
latestDevVersion: VersionInfo | undefined;
|
||||||
|
partitionVersion: VersionInfo | undefined;
|
||||||
|
partition: string;
|
||||||
|
currentPartition: string;
|
||||||
|
size: number;
|
||||||
|
locale: string;
|
||||||
|
LL: TranslationFunctions;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
if (showVersionInfo === 0) return null;
|
||||||
|
|
||||||
|
const isStable = showVersionInfo === 1;
|
||||||
|
const isDev = showVersionInfo === 2;
|
||||||
|
const isPartition = showVersionInfo === 3;
|
||||||
|
|
||||||
|
const version = isStable
|
||||||
|
? latestVersion
|
||||||
|
: isDev
|
||||||
|
? latestDevVersion
|
||||||
|
: partitionVersion;
|
||||||
|
const relNotesUrl = isStable
|
||||||
|
? STABLE_RELNOTES_URL
|
||||||
|
: isDev
|
||||||
|
? DEV_RELNOTES_URL
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
|
||||||
|
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||||
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
sx={{
|
||||||
|
color: 'lightblue',
|
||||||
|
borderBottom: 'none',
|
||||||
|
pr: 1,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: 13
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{LL.VERSION()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||||
|
{isPartition
|
||||||
|
? typeof version === 'string'
|
||||||
|
? version
|
||||||
|
: version?.version
|
||||||
|
: version?.version}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||||
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
sx={{
|
||||||
|
color: 'lightblue',
|
||||||
|
borderBottom: 'none',
|
||||||
|
pr: 1,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: 13,
|
||||||
|
width: 140
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPartition ? LL.TYPE(0) : LL.RELEASE_TYPE()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||||
|
{partition === currentPartition && LL.ACTIVE() + ' '}
|
||||||
|
{isStable
|
||||||
|
? LL.STABLE()
|
||||||
|
: isDev
|
||||||
|
? LL.DEVELOPMENT()
|
||||||
|
: 'Partition ' + LL.VERSION()}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{isPartition && (
|
||||||
|
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||||
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
sx={{
|
||||||
|
color: 'lightblue',
|
||||||
|
borderBottom: 'none',
|
||||||
|
pr: 1,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: 13
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Partition
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||||
|
{partition}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{isPartition && (
|
||||||
|
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||||
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
sx={{
|
||||||
|
color: 'lightblue',
|
||||||
|
borderBottom: 'none',
|
||||||
|
pr: 1,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: 13
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||||
|
{size} KB
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{version && version.date && (
|
||||||
|
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||||
|
<TableCell
|
||||||
|
component="th"
|
||||||
|
scope="row"
|
||||||
|
sx={{
|
||||||
|
color: 'lightblue',
|
||||||
|
borderBottom: 'none',
|
||||||
|
pr: 1,
|
||||||
|
py: 0.5,
|
||||||
|
fontSize: 13
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPartition ? 'Install Date' : 'Build Date'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||||
|
{prettyDateTime(locale, new Date(version.date))}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{!isPartition && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
component="a"
|
||||||
|
href={relNotesUrl}
|
||||||
|
target="_blank"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Changelog
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="outlined" onClick={onClose} color="secondary">
|
||||||
|
{LL.CLOSE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const InstallDialog = memo(
|
||||||
|
({
|
||||||
|
openInstallDialog,
|
||||||
|
fetchDevVersion,
|
||||||
|
latestVersion,
|
||||||
|
latestDevVersion,
|
||||||
|
upgradeImportantMessageType,
|
||||||
|
downloadOnly,
|
||||||
|
platform,
|
||||||
|
LL,
|
||||||
|
onClose,
|
||||||
|
onInstall
|
||||||
|
}: {
|
||||||
|
openInstallDialog: boolean;
|
||||||
|
fetchDevVersion: boolean;
|
||||||
|
latestVersion: VersionInfo | undefined;
|
||||||
|
latestDevVersion: VersionInfo | undefined;
|
||||||
|
upgradeImportantMessageType: number;
|
||||||
|
downloadOnly: boolean;
|
||||||
|
platform: string;
|
||||||
|
LL: TranslationFunctions;
|
||||||
|
onClose: () => void;
|
||||||
|
onInstall: (url: string) => void;
|
||||||
|
}) => {
|
||||||
|
const binURL = (() => {
|
||||||
|
if (!latestVersion || !latestDevVersion) return '';
|
||||||
|
const version = fetchDevVersion ? latestDevVersion : latestVersion;
|
||||||
|
const filename = `EMS-ESP-${version.version.replaceAll('.', '_')}-${platform}.bin`;
|
||||||
|
return fetchDevVersion
|
||||||
|
? `${DEV_URL}${filename}`
|
||||||
|
: `${STABLE_URL}v${version.version}/${filename}`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
|
||||||
|
<DialogTitle>
|
||||||
|
{`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Typography sx={{ mb: 2 }}>
|
||||||
|
{LL.INSTALL_VERSION(
|
||||||
|
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
|
||||||
|
fetchDevVersion ? latestDevVersion?.version : latestVersion?.version
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
{upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()}
|
||||||
|
{upgradeImportantMessageType === 1 && (
|
||||||
|
<>
|
||||||
|
{LL.UPGRADE_IMPORTANT_MESSAGES_1()}
|
||||||
|
<Typography sx={{ mt: 2 }}>
|
||||||
|
<Link to="/settings/downloadUpload" style={{ color: 'lightblue' }}>
|
||||||
|
{LL.DOWNLOAD_SYSTEM_BACKUP()}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Typography sx={{ mt: 2 }}>
|
||||||
|
<Link
|
||||||
|
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
style={{ color: 'lightblue' }}
|
||||||
|
>
|
||||||
|
{LL.ONLINE_HELP()}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={binURL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
style={{ color: 'lightblue', textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
{LL.DOWNLOAD(0)}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{!downloadOnly && (
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onInstall(binURL)}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{LL.INSTALL()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const InstallPartitionDialog = memo(
|
||||||
|
({
|
||||||
|
openInstallPartitionDialog,
|
||||||
|
version,
|
||||||
|
partition,
|
||||||
|
LL,
|
||||||
|
onClose,
|
||||||
|
onInstall
|
||||||
|
}: {
|
||||||
|
openInstallPartitionDialog: boolean;
|
||||||
|
version: string;
|
||||||
|
partition: string;
|
||||||
|
LL: TranslationFunctions;
|
||||||
|
onClose: () => void;
|
||||||
|
onInstall: (partition: string) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
||||||
|
<DialogTitle>
|
||||||
|
{LL.INSTALL()} {LL.STORED_VERSIONS()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Typography sx={{ mb: 2 }}>
|
||||||
|
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onInstall(partition)}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{LL.INSTALL()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper function moved outside component
|
||||||
|
const getPlatform = (data: VersionData): string => {
|
||||||
|
return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Version = () => {
|
||||||
|
const { LL, locale } = useI18nContext();
|
||||||
|
const { me, versions } = useContext(AuthenticatedContext);
|
||||||
|
|
||||||
|
const [restarting, setRestarting] = useState<boolean>(false);
|
||||||
|
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
|
||||||
|
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||||
|
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [partitionVersion, setPartitionVersion] = useState<VersionInfo | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [partition, setPartition] = useState<string>('');
|
||||||
|
const [openInstallPartitionDialog, setOpenInstallPartitionDialog] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
|
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
|
||||||
|
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
|
||||||
|
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
|
||||||
|
const [firmwareSize, setFirmwareSize] = useState<number>(0);
|
||||||
|
|
||||||
|
const latestVersion = useMemo<VersionInfo | undefined>(
|
||||||
|
() =>
|
||||||
|
versions?.stable
|
||||||
|
? { version: versions.stable.version, date: versions.stable.date }
|
||||||
|
: undefined,
|
||||||
|
[versions?.stable]
|
||||||
|
);
|
||||||
|
const latestDevVersion = useMemo<VersionInfo | undefined>(
|
||||||
|
() =>
|
||||||
|
versions?.dev
|
||||||
|
? { version: versions.dev.version, date: versions.dev.date }
|
||||||
|
: undefined,
|
||||||
|
[versions?.dev]
|
||||||
|
);
|
||||||
|
const usingDevVersion = versions?.current?.type === 'dev';
|
||||||
|
const stableUpgradeAvailable = versions?.stable?.upgradeable ?? false;
|
||||||
|
const devUpgradeAvailable = versions?.dev?.upgradeable ?? false;
|
||||||
|
const internetLive = Boolean(versions?.stable || versions?.dev);
|
||||||
|
|
||||||
|
const { send: sendSetPartition } = useRequest(
|
||||||
|
(partition: string) => callAction({ action: 'setPartition', param: partition }),
|
||||||
|
{ immediate: false }
|
||||||
|
).onError((error) => {
|
||||||
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
send: loadData,
|
||||||
|
error
|
||||||
|
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
|
||||||
|
const systemData = event.data as VersionData;
|
||||||
|
if (systemData.arduino_version.startsWith('Tasmota')) {
|
||||||
|
setDownloadOnly(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { send: sendUploadURL } = useRequest(
|
||||||
|
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
||||||
|
{ immediate: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||||
|
immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
|
||||||
|
useState<number>(0);
|
||||||
|
|
||||||
|
const { send: checkUpgradeImportantMessages } = useRequest(
|
||||||
|
(version: string) =>
|
||||||
|
callAction({ action: 'upgradeImportantMessages', param: version }),
|
||||||
|
{
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onSuccess((event) => {
|
||||||
|
const upgradeImportantMessageType_n = (
|
||||||
|
event.data as { upgradeImportantMessageType: number }
|
||||||
|
).upgradeImportantMessageType;
|
||||||
|
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
|
||||||
|
})
|
||||||
|
.onError((error) => {
|
||||||
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const platform = data ? getPlatform(data) : '';
|
||||||
|
|
||||||
|
const otherPartitions =
|
||||||
|
data?.partitions.filter((p) => p.partition !== data.partition) ?? [];
|
||||||
|
|
||||||
|
const setPartitionVersionInfo = (partition: string) => {
|
||||||
|
setShowVersionInfo(3);
|
||||||
|
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
||||||
|
if (partitionData) {
|
||||||
|
setPartitionVersion({
|
||||||
|
version: partitionData.version,
|
||||||
|
date: partitionData.install_date ?? ''
|
||||||
|
});
|
||||||
|
setPartition(partitionData.partition);
|
||||||
|
setFirmwareSize(partitionData.size);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doRestart = async () => {
|
||||||
|
setConfirmRestart(false);
|
||||||
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
|
(error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setRestarting(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const doFormat = async () => {
|
||||||
|
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
|
||||||
|
setRestarting(true);
|
||||||
|
setConfirmFactoryReset(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFactoryResetClose = () => setConfirmFactoryReset(false);
|
||||||
|
const handleFactoryResetClick = () => setConfirmFactoryReset(true);
|
||||||
|
const handleRestartClose = () => setConfirmRestart(false);
|
||||||
|
const handleRestartClick = () => setConfirmRestart(true);
|
||||||
|
|
||||||
|
const installFirmwareURL = async (url: string) => {
|
||||||
|
await sendUploadURL(url).catch((error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
await doRestart();
|
||||||
|
};
|
||||||
|
|
||||||
|
const installPartitionFirmware = async (partition: string) => {
|
||||||
|
await sendSetPartition(partition).catch((error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
setRestarting(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showPartitionDialog = (
|
||||||
|
version: string,
|
||||||
|
partition: string,
|
||||||
|
install_date: string
|
||||||
|
) => {
|
||||||
|
setOpenInstallPartitionDialog(true);
|
||||||
|
setPartitionVersion({ version: version, date: install_date });
|
||||||
|
setPartition(partition);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFirmwareDialog = (useDevVersion: boolean) => {
|
||||||
|
setFetchDevVersion(useDevVersion);
|
||||||
|
const targetVersion = useDevVersion
|
||||||
|
? latestDevVersion?.version
|
||||||
|
: latestVersion?.version;
|
||||||
|
if (targetVersion) {
|
||||||
|
void checkUpgradeImportantMessages(targetVersion);
|
||||||
|
}
|
||||||
|
setOpenInstallDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeInstallDialog = () => setOpenInstallDialog(false);
|
||||||
|
const closeInstallPartitionDialog = () => setOpenInstallPartitionDialog(false);
|
||||||
|
|
||||||
|
const handleVersionInfoClose = () => {
|
||||||
|
setShowVersionInfo(0);
|
||||||
|
setPartitionVersion(undefined);
|
||||||
|
setPartition('');
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutTitle('EMS-ESP Firmware');
|
||||||
|
|
||||||
|
const showButtons = (showingDev: boolean) => {
|
||||||
|
const choice = showingDev
|
||||||
|
? !usingDevVersion
|
||||||
|
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
|
||||||
|
: devUpgradeAvailable
|
||||||
|
? LL.UPDATE_AVAILABLE()
|
||||||
|
: undefined
|
||||||
|
: usingDevVersion
|
||||||
|
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
|
||||||
|
: stableUpgradeAvailable
|
||||||
|
? LL.UPDATE_AVAILABLE()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!choice) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CheckIcon
|
||||||
|
color="success"
|
||||||
|
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
|
||||||
|
/>
|
||||||
|
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
|
||||||
|
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => showFirmwareDialog(showingDev)}
|
||||||
|
>
|
||||||
|
{LL.REINSTALL()}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!me.admin) return null;
|
||||||
|
|
||||||
|
const isUpdateAvailable = choice === LL.UPDATE_AVAILABLE();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
variant="outlined"
|
||||||
|
color={isUpdateAvailable ? 'success' : 'warning'}
|
||||||
|
size="small"
|
||||||
|
onClick={() => showFirmwareDialog(showingDev)}
|
||||||
|
>
|
||||||
|
{choice}
|
||||||
|
{isUpdateAvailable && (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
aria-label="update available"
|
||||||
|
sx={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
ml: 1,
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#ffeb3b',
|
||||||
|
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (restarting) {
|
||||||
|
return <SystemMonitor />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
|
||||||
|
<Typography sx={{ mb: 1 }} variant="h6" color="primary">
|
||||||
|
{LL.THIS_VERSION()}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
alignItems: 'baseline'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid size={{ xs: 4, md: 2 }}>
|
||||||
|
<Typography color="secondary">{LL.VERSION()}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
|
<Typography>
|
||||||
|
{data.emsesp_version}
|
||||||
|
{data.build_flags && (
|
||||||
|
<Typography variant="caption">
|
||||||
|
({data.build_flags})
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setPartitionVersionInfo(data.partition)}
|
||||||
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 4, md: 2 }}>
|
||||||
|
<Typography color="secondary">{LL.PLATFORM()}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
|
<Typography>
|
||||||
|
{platform}
|
||||||
|
<Typography variant="caption">
|
||||||
|
(
|
||||||
|
{data.psram ? (
|
||||||
|
<CheckIcon
|
||||||
|
color="success"
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.5em',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CloseIcon
|
||||||
|
color="error"
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.5em',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
PSRAM)
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{internetLive ? (
|
||||||
|
<>
|
||||||
|
<Typography sx={{ mt: 4, mb: 1 }} variant="h6" color="primary">
|
||||||
|
{LL.AVAILABLE_VERSION()}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
rowSpacing={1}
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
alignItems: 'baseline'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{otherPartitions.length > 0 && data.developer_mode && (
|
||||||
|
<>
|
||||||
|
<Grid size={{ xs: 4, md: 2 }}>
|
||||||
|
<Typography color="secondary">{LL.STORED_VERSIONS()}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
|
{otherPartitions.map((partition) => (
|
||||||
|
<Typography key={partition.partition} sx={{ mb: 1 }}>
|
||||||
|
{partition.version}
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
setPartitionVersionInfo(partition.partition)
|
||||||
|
}
|
||||||
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 0 }}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() =>
|
||||||
|
showPartitionDialog(
|
||||||
|
partition.version,
|
||||||
|
partition.partition,
|
||||||
|
partition.install_date ?? ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{LL.INSTALL()}
|
||||||
|
</Button>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Grid size={{ xs: 4, md: 2 }}>
|
||||||
|
<Typography color="secondary">{LL.STABLE()}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
|
<Typography>
|
||||||
|
{latestVersion?.version}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowVersionInfo(1)}
|
||||||
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
{showButtons(false)}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 4, md: 2 }}>
|
||||||
|
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
|
<Typography>
|
||||||
|
{latestDevVersion?.version}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowVersionInfo(2)}
|
||||||
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
{showButtons(true)}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Typography sx={{ mt: 2 }} color="warning">
|
||||||
|
<WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} />
|
||||||
|
{LL.INTERNET_CONNECTION_REQUIRED()}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{me.admin && (
|
||||||
|
<>
|
||||||
|
<VersionInfoDialog
|
||||||
|
showVersionInfo={showVersionInfo}
|
||||||
|
latestVersion={latestVersion}
|
||||||
|
latestDevVersion={latestDevVersion}
|
||||||
|
partitionVersion={partitionVersion}
|
||||||
|
locale={locale}
|
||||||
|
partition={partition}
|
||||||
|
currentPartition={data?.partition ?? ''}
|
||||||
|
size={firmwareSize}
|
||||||
|
LL={LL}
|
||||||
|
onClose={handleVersionInfoClose}
|
||||||
|
/>
|
||||||
|
<InstallDialog
|
||||||
|
openInstallDialog={openInstallDialog}
|
||||||
|
fetchDevVersion={fetchDevVersion}
|
||||||
|
latestVersion={latestVersion}
|
||||||
|
latestDevVersion={latestDevVersion}
|
||||||
|
upgradeImportantMessageType={upgradeImportantMessageType}
|
||||||
|
downloadOnly={downloadOnly}
|
||||||
|
platform={platform}
|
||||||
|
LL={LL}
|
||||||
|
onClose={closeInstallDialog}
|
||||||
|
onInstall={installFirmwareURL}
|
||||||
|
/>
|
||||||
|
<InstallPartitionDialog
|
||||||
|
openInstallPartitionDialog={openInstallPartitionDialog}
|
||||||
|
version={partitionVersion?.version || ''}
|
||||||
|
partition={partition}
|
||||||
|
LL={LL}
|
||||||
|
onClose={closeInstallPartitionDialog}
|
||||||
|
onInstall={installPartitionFirmware}
|
||||||
|
/>
|
||||||
|
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||||
|
{LL.UPLOAD()}
|
||||||
|
</Typography>
|
||||||
|
<SingleUpload doRestart={doRestart} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{me.admin && (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmFactoryReset}
|
||||||
|
onClose={handleFactoryResetClose}
|
||||||
|
>
|
||||||
|
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
|
||||||
|
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleFactoryResetClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<SettingsBackupRestoreIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={doFormat}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{LL.FACTORY_RESET()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmRestart}
|
||||||
|
onClose={handleRestartClose}
|
||||||
|
>
|
||||||
|
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
||||||
|
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleRestartClose}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={doRestart}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{LL.RESTART()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
gap: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<PowerSettingsNewIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleRestartClick}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{LL.RESTART()}
|
||||||
|
</Button>
|
||||||
|
{data.developer_mode && (
|
||||||
|
<Button
|
||||||
|
startIcon={<SettingsBackupRestoreIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleFactoryResetClick}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{LL.FACTORY_RESET()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Version);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Navigate,
|
Navigate,
|
||||||
Route,
|
Route,
|
||||||
@@ -40,26 +40,20 @@ const Network = () => {
|
|||||||
|
|
||||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
|
||||||
|
|
||||||
const selectNetwork = useCallback(
|
const selectNetwork = (network: WiFiNetwork) => {
|
||||||
(network: WiFiNetwork) => {
|
setSelectedNetwork(network);
|
||||||
setSelectedNetwork(network);
|
void navigate('/settings/network/settings');
|
||||||
void navigate('/settings/network/settings');
|
};
|
||||||
},
|
|
||||||
[navigate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deselectNetwork = useCallback(() => {
|
const deselectNetwork = () => {
|
||||||
setSelectedNetwork(undefined);
|
setSelectedNetwork(undefined);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = {
|
||||||
() => ({
|
...(selectedNetwork && { selectedNetwork }),
|
||||||
...(selectedNetwork && { selectedNetwork }),
|
selectNetwork,
|
||||||
selectNetwork,
|
deselectNetwork
|
||||||
deselectNetwork
|
};
|
||||||
}),
|
|
||||||
[selectedNetwork, selectNetwork, deselectNetwork]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WiFiConnectionContext.Provider value={contextValue}>
|
<WiFiConnectionContext.Provider value={contextValue}>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { NetworkSettingsType } from 'types';
|
import type { NetworkSettingsType } from 'types';
|
||||||
import { updateValueDirty, useRest } from 'utils';
|
import { updateValueDirty, useRest } from 'utils';
|
||||||
import { validate } from 'validators';
|
import { ValidationError, validate } from 'validators';
|
||||||
import { createNetworkSettingsValidator } from 'validators/network';
|
import { createNetworkSettingsValidator } from 'validators/network';
|
||||||
|
|
||||||
import SystemMonitor from '../../status/SystemMonitor';
|
import SystemMonitor from '../../status/SystemMonitor';
|
||||||
@@ -89,7 +89,7 @@ const NetworkSettings = () => {
|
|||||||
static_ip_config: false,
|
static_ip_config: false,
|
||||||
bandwidth20: false,
|
bandwidth20: false,
|
||||||
tx_power: 0,
|
tx_power: 0,
|
||||||
nosleep: false,
|
nosleep: true,
|
||||||
enableMDNS: true,
|
enableMDNS: true,
|
||||||
enableCORS: false,
|
enableCORS: false,
|
||||||
CORSOrigin: '*'
|
CORSOrigin: '*'
|
||||||
@@ -116,24 +116,24 @@ const NetworkSettings = () => {
|
|||||||
await validate(createNetworkSettingsValidator(data), data);
|
await validate(createNetworkSettingsValidator(data), data);
|
||||||
await saveData();
|
await saveData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
deselectNetwork();
|
deselectNetwork();
|
||||||
}, [data, saveData, deselectNetwork]);
|
}, [data, saveData, deselectNetwork]);
|
||||||
|
|
||||||
const setCancel = useCallback(async () => {
|
const setCancel = async () => {
|
||||||
deselectNetwork();
|
deselectNetwork();
|
||||||
await loadData();
|
await loadData();
|
||||||
}, [deselectNetwork, loadData]);
|
};
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
const doRestart = async () => {
|
||||||
setRestarting(true);
|
setRestarting(true);
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
(error: Error) => {
|
(error: Error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [sendAPI]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -173,7 +173,7 @@ const NetworkSettings = () => {
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors || {}}
|
fieldErrors={fieldErrors || {}}
|
||||||
name="ssid"
|
name="ssid"
|
||||||
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
|
label="SSID"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.ssid}
|
value={data.ssid}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useRef, useState } from 'react';
|
import { memo, useRef, useState } 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';
|
||||||
@@ -48,12 +48,12 @@ const WiFiNetworkScanner = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderNetworkScanner = useCallback(() => {
|
const renderNetworkScanner = () => {
|
||||||
if (!networkList) {
|
if (!networkList) {
|
||||||
return <FormLoader errorMessage={errorMessage || ''} />;
|
return <FormLoader errorMessage={errorMessage || ''} />;
|
||||||
}
|
}
|
||||||
return <WiFiNetworkSelector networkList={networkList} />;
|
return <WiFiNetworkSelector networkList={networkList} />;
|
||||||
}, [networkList, errorMessage]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext } from 'react';
|
import { memo, useContext } 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';
|
||||||
@@ -63,34 +63,31 @@ const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList })
|
|||||||
|
|
||||||
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
const wifiConnectionContext = useContext(WiFiConnectionContext);
|
||||||
|
|
||||||
const renderNetwork = useCallback(
|
const renderNetwork = (network: WiFiNetwork) => (
|
||||||
(network: WiFiNetwork) => (
|
<ListItem
|
||||||
<ListItem
|
key={network.bssid}
|
||||||
key={network.bssid}
|
onClick={() => wifiConnectionContext.selectNetwork(network)}
|
||||||
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: ' +
|
||||||
'Security: ' +
|
networkSecurityMode(network) +
|
||||||
networkSecurityMode(network) +
|
', Ch: ' +
|
||||||
', Ch: ' +
|
network.channel +
|
||||||
network.channel +
|
', bssid: ' +
|
||||||
', bssid: ' +
|
network.bssid
|
||||||
network.bssid
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<ListItemIcon>
|
||||||
<ListItemIcon>
|
<Badge badgeContent={network.rssi + 'dBm'}>
|
||||||
<Badge badgeContent={network.rssi + 'dBm'}>
|
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
|
||||||
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
|
</Badge>
|
||||||
</Badge>
|
</ListItemIcon>
|
||||||
</ListItemIcon>
|
</ListItem>
|
||||||
</ListItem>
|
|
||||||
),
|
|
||||||
[wifiConnectionContext, theme]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (networkList.networks.length === 0) {
|
if (networkList.networks.length === 0) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useMemo, useState } from 'react';
|
import { memo, useCallback, useContext, useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -55,16 +55,14 @@ const ManageUsers = () => {
|
|||||||
const blocker = useBlocker(changed !== 0);
|
const blocker = useBlocker(changed !== 0);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const table_theme = useMemo(
|
const table_theme = useTheme({
|
||||||
() =>
|
Table: `
|
||||||
useTheme({
|
|
||||||
Table: `
|
|
||||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
|
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`,
|
`,
|
||||||
HeaderRow: `
|
HeaderRow: `
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: #90CAF9;
|
color: #90CAF9;
|
||||||
@@ -74,7 +72,7 @@ const ManageUsers = () => {
|
|||||||
border-bottom: 1px solid #565656;
|
border-bottom: 1px solid #565656;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Row: `
|
Row: `
|
||||||
.td {
|
.td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-top: 1px solid #565656;
|
border-top: 1px solid #565656;
|
||||||
@@ -87,7 +85,7 @@ const ManageUsers = () => {
|
|||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
BaseCell: `
|
BaseCell: `
|
||||||
&:nth-of-type(2) {
|
&:nth-of-type(2) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -95,44 +93,36 @@ const ManageUsers = () => {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const noAdminConfigured = useCallback(
|
const noAdminConfigured = () => !data?.users.find((u) => u.admin);
|
||||||
() => !data?.users.find((u) => u.admin),
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeUser = useCallback(
|
const removeUser = (toRemove: UserType) => {
|
||||||
(toRemove: UserType) => {
|
if (!data) return;
|
||||||
if (!data) return;
|
const users = data.users.filter((u) => u.username !== toRemove.username);
|
||||||
const users = data.users.filter((u) => u.username !== toRemove.username);
|
updateDataValue({ ...data, users });
|
||||||
updateDataValue({ ...data, users });
|
setChanged(changed + 1);
|
||||||
setChanged(changed + 1);
|
};
|
||||||
},
|
|
||||||
[data, updateDataValue, changed]
|
|
||||||
);
|
|
||||||
|
|
||||||
const createUser = useCallback(() => {
|
const createUser = () => {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setUser({
|
setUser({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
admin: true
|
admin: true
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const editUser = useCallback((toEdit: UserType) => {
|
const editUser = (toEdit: UserType) => {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
setUser({ ...toEdit });
|
setUser({ ...toEdit });
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const cancelEditingUser = useCallback(() => {
|
const cancelEditingUser = () => {
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const doneEditingUser = useCallback(() => {
|
const doneEditingUser = () => {
|
||||||
if (user && data) {
|
if (user && data) {
|
||||||
const users = [
|
const users = [
|
||||||
...data.users.filter(
|
...data.users.filter(
|
||||||
@@ -144,26 +134,26 @@ const ManageUsers = () => {
|
|||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
setChanged(changed + 1);
|
setChanged(changed + 1);
|
||||||
}
|
}
|
||||||
}, [user, data, updateDataValue, changed]);
|
};
|
||||||
|
|
||||||
const closeGenerateToken = useCallback(() => {
|
const closeGenerateToken = useCallback(() => {
|
||||||
setGeneratingToken(undefined);
|
setGeneratingToken(undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const generateTokenForUser = useCallback((username: string) => {
|
const generateTokenForUser = (username: string) => {
|
||||||
setGeneratingToken(username);
|
setGeneratingToken(username);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onSubmit = useCallback(async () => {
|
const onSubmit = async () => {
|
||||||
await saveData();
|
await saveData();
|
||||||
await authenticatedContext.refresh();
|
await authenticatedContext.refresh();
|
||||||
setChanged(0);
|
setChanged(0);
|
||||||
}, [saveData, authenticatedContext]);
|
};
|
||||||
|
|
||||||
const onCancelSubmit = useCallback(async () => {
|
const onCancelSubmit = async () => {
|
||||||
await loadData();
|
await loadData();
|
||||||
setChanged(0);
|
setChanged(0);
|
||||||
}, [loadData]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -177,15 +167,10 @@ const ManageUsers = () => {
|
|||||||
admin: boolean;
|
admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add id to the type, needed for the table
|
const user_table = data.users.map((u) => ({
|
||||||
const user_table = useMemo(
|
...u,
|
||||||
() =>
|
id: u.username
|
||||||
data.users.map((u) => ({
|
})) as UserType2[];
|
||||||
...u,
|
|
||||||
id: u.username
|
|
||||||
})) as UserType2[],
|
|
||||||
[data.users]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
|
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
|
||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
@@ -15,19 +15,15 @@ const Security = () => {
|
|||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const matchedRoutes = useMemo(
|
const matchedRoutes = matchRoutes(
|
||||||
() =>
|
[
|
||||||
matchRoutes(
|
{
|
||||||
[
|
path: '/settings/security/settings',
|
||||||
{
|
element: <ManageUsers />
|
||||||
path: '/settings/security/settings',
|
},
|
||||||
element: <ManageUsers />
|
{ path: '/settings/security/users', element: <SecuritySettings /> }
|
||||||
},
|
],
|
||||||
{ path: '/settings/security/users', element: <SecuritySettings /> }
|
location
|
||||||
],
|
|
||||||
location
|
|
||||||
),
|
|
||||||
[location]
|
|
||||||
);
|
);
|
||||||
const routerTab = matchedRoutes?.[0]?.route.path || false;
|
const routerTab = matchedRoutes?.[0]?.route.path || false;
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { AuthenticatedContext } from 'contexts/authentication';
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { SecuritySettingsType } from 'types';
|
import type { SecuritySettingsType } from 'types';
|
||||||
import { updateValueDirty, useRest } from 'utils';
|
import { updateValueDirty, useRest } from 'utils';
|
||||||
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
|
import { SECURITY_SETTINGS_VALIDATOR, ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
const SecuritySettings = () => {
|
const SecuritySettings = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
@@ -58,7 +58,7 @@ const SecuritySettings = () => {
|
|||||||
await saveData();
|
await saveData();
|
||||||
await authenticatedContext.refresh();
|
await authenticatedContext.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}, [data, saveData, authenticatedContext]);
|
}, [data, saveData, authenticatedContext]);
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ const SecuritySettings = () => {
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
|
<MessageBox level="info" message={LL.SU_TEXT()} sx={{ mt: 1 }} />
|
||||||
{dirtyFlags && dirtyFlags.length !== 0 && (
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { UserType } from 'types';
|
import type { UserType } from 'types';
|
||||||
import { updateValue } from 'utils';
|
import { updateValue } from 'utils';
|
||||||
import { validate } from 'validators';
|
import { ValidationError, validate } from 'validators';
|
||||||
|
|
||||||
interface UserFormProps {
|
interface UserFormProps {
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
@@ -62,17 +62,17 @@ const User: FC<UserFormProps> = ({
|
|||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const validateAndDone = useCallback(async () => {
|
const validateAndDone = async () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
try {
|
try {
|
||||||
setFieldErrors(undefined);
|
setFieldErrors(undefined);
|
||||||
await validate(validator, user);
|
await validate(validator, user);
|
||||||
onDoneEditing();
|
onDoneEditing();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFieldErrors(error as ValidateFieldsError);
|
setFieldErrors((error as ValidationError).fieldErrors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [user, validator, onDoneEditing]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Cell,
|
Cell,
|
||||||
@@ -36,16 +34,14 @@ const SystemActivity = () => {
|
|||||||
|
|
||||||
useLayoutTitle(LL.DATA_TRAFFIC());
|
useLayoutTitle(LL.DATA_TRAFFIC());
|
||||||
|
|
||||||
const stats_theme = tableTheme(
|
const stats_theme = tableTheme({
|
||||||
useMemo(
|
Table: `
|
||||||
() => ({
|
|
||||||
Table: `
|
|
||||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`,
|
`,
|
||||||
HeaderRow: `
|
HeaderRow: `
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: #90CAF9;
|
color: #90CAF9;
|
||||||
@@ -55,7 +51,7 @@ const SystemActivity = () => {
|
|||||||
border-bottom: 1px solid #565656;
|
border-bottom: 1px solid #565656;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
Row: `
|
Row: `
|
||||||
.td {
|
.td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-top: 1px solid #565656;
|
border-top: 1px solid #565656;
|
||||||
@@ -69,26 +65,20 @@ const SystemActivity = () => {
|
|||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
BaseCell: `
|
BaseCell: `
|
||||||
&:not(:first-of-type) {
|
&:not(:first-of-type) {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const showName = useCallback(
|
const showName = (id: number) => {
|
||||||
(id: number) => {
|
const name: keyof Translation['STATUS_NAMES'] =
|
||||||
const name: keyof Translation['STATUS_NAMES'] =
|
id.toString() as keyof Translation['STATUS_NAMES'];
|
||||||
id.toString() as keyof Translation['STATUS_NAMES'];
|
return LL.STATUS_NAMES[name]();
|
||||||
return LL.STATUS_NAMES[name]();
|
};
|
||||||
},
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showQuality = useCallback((stat: Stat) => {
|
const showQuality = (stat: Stat) => {
|
||||||
if (stat.q === 0 || stat.s + stat.f === 0) {
|
if (stat.q === 0 || stat.s + stat.f === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,14 +90,18 @@ const SystemActivity = () => {
|
|||||||
} else {
|
} else {
|
||||||
return <div style={{ color: QUALITY_COLORS.POOR }}>{stat.q}%</div>;
|
return <div style={{ color: QUALITY_COLORS.POOR }}>{stat.q}%</div>;
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) {
|
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: data.stats }}
|
data={{ nodes: data.stats }}
|
||||||
theme={stats_theme}
|
theme={stats_theme}
|
||||||
@@ -136,10 +130,8 @@ const SystemActivity = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
</SectionContent>
|
||||||
}, [data, loadData, error?.message, stats_theme, LL, showName, showQuality]);
|
);
|
||||||
|
|
||||||
return <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SystemActivity;
|
export default SystemActivity;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, memo, useMemo } from 'react';
|
import { type FC, memo } 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';
|
||||||
@@ -127,16 +127,15 @@ const MqttStatus = () => {
|
|||||||
void loadData();
|
void loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize error message separately to avoid re-renders on error object changes
|
|
||||||
const errorMessage = error?.message || '';
|
const errorMessage = error?.message || '';
|
||||||
|
|
||||||
const mqttStatusText = useMemo(() => {
|
const mqttStatusText = !data
|
||||||
if (!data) return '';
|
? ''
|
||||||
if (!data.enabled) return LL.NOT_ENABLED();
|
: !data.enabled
|
||||||
return data.connected
|
? LL.NOT_ENABLED()
|
||||||
? `${LL.CONNECTED(0)} (${data.connect_count})`
|
: data.connected
|
||||||
: `${LL.DISCONNECTED()} (${data.connect_count})`;
|
? `${LL.CONNECTED(0)} (${data.connect_count})`
|
||||||
}, [data, LL]);
|
: `${LL.DISCONNECTED()} (${data.connect_count})`;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||||
import DnsIcon from '@mui/icons-material/Dns';
|
import DnsIcon from '@mui/icons-material/Dns';
|
||||||
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
|
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
|
||||||
@@ -67,12 +65,16 @@ const NTPStatus = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const content = useMemo(() => {
|
if (!data) {
|
||||||
if (!data) {
|
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@@ -121,10 +123,8 @@ const NTPStatus = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
);
|
</SectionContent>
|
||||||
}, [data, error, loadData, LL, theme]);
|
);
|
||||||
|
|
||||||
return <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NTPStatus;
|
export default NTPStatus;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useMemo } 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';
|
||||||
@@ -124,16 +122,20 @@ const NetworkStatus = () => {
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const content = useMemo(() => {
|
if (!data) {
|
||||||
if (!data) {
|
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusText = getNetworkStatusText(data.status, data.reconnect_count, LL);
|
|
||||||
const statusColor = networkStatusHighlight(data, theme);
|
|
||||||
const qualityColor = networkQualityHighlight(data, theme);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = getNetworkStatusText(data.status, data.reconnect_count, LL);
|
||||||
|
const statusColor = networkStatusHighlight(data, theme);
|
||||||
|
const qualityColor = networkQualityHighlight(data, theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContent>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@@ -227,10 +229,8 @@ const NetworkStatus = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
);
|
</SectionContent>
|
||||||
}, [data, error, loadData, LL, theme]);
|
);
|
||||||
|
|
||||||
return <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NetworkStatus;
|
export default NetworkStatus;
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
import { useContext } 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 CancelIcon from '@mui/icons-material/Cancel';
|
|
||||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||||
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
|
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
|
||||||
import LogoDevIcon from '@mui/icons-material/LogoDev';
|
import LogoDevIcon from '@mui/icons-material/LogoDev';
|
||||||
import MemoryIcon from '@mui/icons-material/Memory';
|
import MemoryIcon from '@mui/icons-material/Memory';
|
||||||
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
|
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
|
||||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
|
||||||
import RouterIcon from '@mui/icons-material/Router';
|
import RouterIcon from '@mui/icons-material/Router';
|
||||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||||
import WifiIcon from '@mui/icons-material/Wifi';
|
import WifiIcon from '@mui/icons-material/Wifi';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
@@ -27,12 +18,10 @@ import {
|
|||||||
useTheme
|
useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import { API } from 'api/app';
|
|
||||||
import { readSystemStatus } from 'api/system';
|
import { readSystemStatus } from 'api/system';
|
||||||
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
|
||||||
import { useRequest } from 'alova/client';
|
import { useRequest } from 'alova/client';
|
||||||
import { type APIcall, busConnectionStatus } from 'app/main/types';
|
import { busConnectionStatus } from 'app/main/types';
|
||||||
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';
|
||||||
@@ -41,9 +30,6 @@ import { NTPSyncStatus, NetworkConnectionStatus, SystemStatusCodes } from 'types
|
|||||||
import { useInterval } from 'utils';
|
import { useInterval } from 'utils';
|
||||||
import { formatDateTime } from 'utils/time';
|
import { formatDateTime } from 'utils/time';
|
||||||
|
|
||||||
import SystemMonitor from './SystemMonitor';
|
|
||||||
|
|
||||||
// Pure functions moved outside component to avoid recreation on each render
|
|
||||||
const formatNumber = (num: number) => new Intl.NumberFormat().format(num);
|
const formatNumber = (num: number) => new Intl.NumberFormat().format(num);
|
||||||
|
|
||||||
const formatDurationSec = (
|
const formatDurationSec = (
|
||||||
@@ -72,24 +58,7 @@ const SystemStatus = () => {
|
|||||||
|
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const { me } = useContext(AuthenticatedContext);
|
||||||
|
|
||||||
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
const { data, send: loadData, error } = useRequest(readSystemStatus);
|
||||||
const [restarting, setRestarting] = useState<boolean>();
|
|
||||||
|
|
||||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
|
||||||
immediate: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
send: loadData,
|
|
||||||
error
|
|
||||||
} = useRequest(readSystemStatus, {
|
|
||||||
async middleware(_, next) {
|
|
||||||
if (!restarting) {
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
void loadData();
|
void loadData();
|
||||||
@@ -97,10 +66,8 @@ const SystemStatus = () => {
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
// Memoize derived status values to avoid recalculation on every render
|
const busStatus = (() => {
|
||||||
const busStatus = useMemo(() => {
|
|
||||||
if (!data) return 'EMS state unknown';
|
if (!data) return 'EMS state unknown';
|
||||||
|
|
||||||
switch (data.bus_status) {
|
switch (data.bus_status) {
|
||||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||||
return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`;
|
return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`;
|
||||||
@@ -111,12 +78,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return 'EMS state unknown';
|
return 'EMS state unknown';
|
||||||
}
|
}
|
||||||
}, [data?.bus_status, data?.bus_uptime, LL]);
|
})();
|
||||||
|
|
||||||
// Memoize derived status values to avoid recalculation on every render
|
const systemStatus = (() => {
|
||||||
const systemStatus = useMemo(() => {
|
|
||||||
if (!data) return '??';
|
if (!data) return '??';
|
||||||
|
|
||||||
switch (data.status) {
|
switch (data.status) {
|
||||||
case SystemStatusCodes.SYSTEM_STATUS_PENDING_UPLOAD:
|
case SystemStatusCodes.SYSTEM_STATUS_PENDING_UPLOAD:
|
||||||
case SystemStatusCodes.SYSTEM_STATUS_UPLOADING:
|
case SystemStatusCodes.SYSTEM_STATUS_UPLOADING:
|
||||||
@@ -129,14 +94,12 @@ const SystemStatus = () => {
|
|||||||
case SystemStatusCodes.SYSTEM_STATUS_INVALID_GPIO:
|
case SystemStatusCodes.SYSTEM_STATUS_INVALID_GPIO:
|
||||||
return LL.GPIO_OF(LL.FAILED(0));
|
return LL.GPIO_OF(LL.FAILED(0));
|
||||||
default:
|
default:
|
||||||
// SystemStatusCodes.SYSTEM_STATUS_NORMAL
|
|
||||||
return 'OK';
|
return 'OK';
|
||||||
}
|
}
|
||||||
}, [data?.status, LL]);
|
})();
|
||||||
|
|
||||||
const busStatusHighlight = useMemo(() => {
|
const busStatusHighlight = (() => {
|
||||||
if (!data) return theme.palette.warning.main;
|
if (!data) return theme.palette.warning.main;
|
||||||
|
|
||||||
switch (data.bus_status) {
|
switch (data.bus_status) {
|
||||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
@@ -147,11 +110,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}, [data?.bus_status, theme.palette]);
|
})();
|
||||||
|
|
||||||
const ntpStatus = useMemo(() => {
|
const ntpStatus = (() => {
|
||||||
if (!data) return LL.UNKNOWN();
|
if (!data) return LL.UNKNOWN();
|
||||||
|
|
||||||
switch (data.ntp_status) {
|
switch (data.ntp_status) {
|
||||||
case NTPSyncStatus.NTP_DISABLED:
|
case NTPSyncStatus.NTP_DISABLED:
|
||||||
return LL.NOT_ENABLED();
|
return LL.NOT_ENABLED();
|
||||||
@@ -164,11 +126,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return LL.UNKNOWN();
|
return LL.UNKNOWN();
|
||||||
}
|
}
|
||||||
}, [data?.ntp_status, data?.ntp_time, LL]);
|
})();
|
||||||
|
|
||||||
const ntpStatusHighlight = useMemo(() => {
|
const ntpStatusHighlight = (() => {
|
||||||
if (!data) return theme.palette.error.main;
|
if (!data) return theme.palette.error.main;
|
||||||
|
|
||||||
switch (data.ntp_status) {
|
switch (data.ntp_status) {
|
||||||
case NTPSyncStatus.NTP_DISABLED:
|
case NTPSyncStatus.NTP_DISABLED:
|
||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
@@ -179,11 +140,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
}
|
||||||
}, [data?.ntp_status, theme.palette]);
|
})();
|
||||||
|
|
||||||
const networkStatusHighlight = useMemo(() => {
|
const networkStatusHighlight = (() => {
|
||||||
if (!data) return theme.palette.warning.main;
|
if (!data) return theme.palette.warning.main;
|
||||||
|
|
||||||
switch (data.network_status) {
|
switch (data.network_status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
@@ -198,11 +158,10 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}, [data?.network_status, theme.palette]);
|
})();
|
||||||
|
|
||||||
const networkStatus = useMemo(() => {
|
const networkStatus = (() => {
|
||||||
if (!data) return LL.UNKNOWN();
|
if (!data) return LL.UNKNOWN();
|
||||||
|
|
||||||
switch (data.network_status) {
|
switch (data.network_status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||||
return LL.INACTIVE(1);
|
return LL.INACTIVE(1);
|
||||||
@@ -223,227 +182,103 @@ const SystemStatus = () => {
|
|||||||
default:
|
default:
|
||||||
return LL.UNKNOWN();
|
return LL.UNKNOWN();
|
||||||
}
|
}
|
||||||
}, [data?.network_status, data?.wifi_rssi, LL]);
|
})();
|
||||||
|
|
||||||
const activeHighlight = useCallback(
|
const activeHighlight = (value: boolean) =>
|
||||||
(value: boolean) =>
|
value ? theme.palette.success.main : theme.palette.info.main;
|
||||||
value ? theme.palette.success.main : theme.palette.info.main,
|
|
||||||
[theme.palette]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
|
||||||
setConfirmRestart(false);
|
|
||||||
setRestarting(true);
|
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
|
||||||
(error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [sendAPI]);
|
|
||||||
|
|
||||||
const handleCloseRestartDialog = useCallback(() => {
|
|
||||||
setConfirmRestart(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderRestartDialog = useMemo(
|
|
||||||
() => (
|
|
||||||
<Dialog
|
|
||||||
sx={dialogStyle}
|
|
||||||
open={confirmRestart}
|
|
||||||
onClose={handleCloseRestartDialog}
|
|
||||||
>
|
|
||||||
<DialogTitle>{LL.RESTART()}</DialogTitle>
|
|
||||||
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
startIcon={<CancelIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleCloseRestartDialog}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<PowerSettingsNewIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={doRestart}
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{LL.RESTART()}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
),
|
|
||||||
[confirmRestart, handleCloseRestartDialog, doRestart, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize formatted values
|
|
||||||
const firmwareVersion = useMemo(
|
|
||||||
() => `v${data?.emsesp_version || ''}`,
|
|
||||||
[data?.emsesp_version]
|
|
||||||
);
|
|
||||||
|
|
||||||
const uptimeText = useMemo(
|
|
||||||
() => (data ? formatDurationSec(data.uptime, LL) : ''),
|
|
||||||
[data?.uptime, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const freeMemoryText = useMemo(
|
|
||||||
() => (data ? `${formatNumber(data.free_heap)} KB ${LL.FREE_MEMORY()}` : ''),
|
|
||||||
[data?.free_heap, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const networkIcon = useMemo(
|
|
||||||
() =>
|
|
||||||
data?.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
|
||||||
? WifiIcon
|
|
||||||
: RouterIcon,
|
|
||||||
[data?.network_status]
|
|
||||||
);
|
|
||||||
|
|
||||||
const mqttStatusText = useMemo(
|
|
||||||
() => (data?.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)),
|
|
||||||
[data?.mqtt_status, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const apStatusText = useMemo(
|
|
||||||
() => (data?.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)),
|
|
||||||
[data?.ap_status, LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRestartClick = useCallback(() => {
|
|
||||||
setConfirmRestart(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data || !LL) {
|
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!data || !LL) {
|
||||||
return (
|
return (
|
||||||
<>
|
<SectionContent>
|
||||||
<List>
|
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
|
||||||
<ListMenuItem
|
</SectionContent>
|
||||||
icon={BuildIcon}
|
|
||||||
bgcolor="#72caf9"
|
|
||||||
label="EMS-ESP Firmware"
|
|
||||||
text={firmwareVersion}
|
|
||||||
to="version"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
|
|
||||||
<MonitorHeartIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={LL.STATUS_OF(LL.SYSTEM(0))}
|
|
||||||
secondary={`${systemStatus} (${LL.UPTIME()}: ${uptimeText})`}
|
|
||||||
/>
|
|
||||||
{me.admin && (
|
|
||||||
<Button
|
|
||||||
startIcon={<PowerSettingsNewIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={handleRestartClick}
|
|
||||||
>
|
|
||||||
{LL.RESTART()}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={MemoryIcon}
|
|
||||||
bgcolor="#68374d"
|
|
||||||
label={LL.HARDWARE()}
|
|
||||||
text={freeMemoryText}
|
|
||||||
to="/status/hardwarestatus"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={DirectionsBusIcon}
|
|
||||||
bgcolor={busStatusHighlight}
|
|
||||||
label={LL.DATA_TRAFFIC()}
|
|
||||||
text={busStatus}
|
|
||||||
to="/status/activity"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={networkIcon}
|
|
||||||
bgcolor={networkStatusHighlight}
|
|
||||||
label={LL.NETWORK(1)}
|
|
||||||
text={networkStatus}
|
|
||||||
to="/status/network"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={DeviceHubIcon}
|
|
||||||
bgcolor={activeHighlight(data.mqtt_status)}
|
|
||||||
label="MQTT"
|
|
||||||
text={mqttStatusText}
|
|
||||||
to="/status/mqtt"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={AccessTimeIcon}
|
|
||||||
bgcolor={ntpStatusHighlight}
|
|
||||||
label="NTP"
|
|
||||||
text={ntpStatus}
|
|
||||||
to="/status/ntp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={SettingsInputAntennaIcon}
|
|
||||||
bgcolor={activeHighlight(data.ap_status)}
|
|
||||||
label={LL.ACCESS_POINT(0)}
|
|
||||||
text={apStatusText}
|
|
||||||
to="/status/ap"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListMenuItem
|
|
||||||
disabled={!me.admin}
|
|
||||||
icon={LogoDevIcon}
|
|
||||||
bgcolor="#40828f"
|
|
||||||
label={LL.LOG_OF(LL.SYSTEM(0))}
|
|
||||||
text={LL.VIEW_LOG()}
|
|
||||||
to="/status/log"
|
|
||||||
/>
|
|
||||||
</List>
|
|
||||||
|
|
||||||
{renderRestartDialog}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}, [
|
}
|
||||||
data,
|
|
||||||
LL,
|
|
||||||
firmwareVersion,
|
|
||||||
uptimeText,
|
|
||||||
freeMemoryText,
|
|
||||||
networkIcon,
|
|
||||||
mqttStatusText,
|
|
||||||
apStatusText,
|
|
||||||
busStatus,
|
|
||||||
busStatusHighlight,
|
|
||||||
networkStatusHighlight,
|
|
||||||
networkStatus,
|
|
||||||
ntpStatusHighlight,
|
|
||||||
ntpStatus,
|
|
||||||
activeHighlight,
|
|
||||||
me.admin,
|
|
||||||
handleRestartClick,
|
|
||||||
error,
|
|
||||||
loadData,
|
|
||||||
renderRestartDialog
|
|
||||||
]);
|
|
||||||
|
|
||||||
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
|
return (
|
||||||
|
<SectionContent>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
|
||||||
|
<MonitorHeartIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={LL.STATUS_OF(LL.SYSTEM(0))}
|
||||||
|
secondary={`${systemStatus} (${LL.UPTIME()}: ${formatDurationSec(data.uptime, LL)})`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={MemoryIcon}
|
||||||
|
bgcolor="#68374d"
|
||||||
|
label={LL.HARDWARE()}
|
||||||
|
text={`${formatNumber(data.free_heap)} KB ${LL.FREE_MEMORY()}`}
|
||||||
|
to="/status/hardwarestatus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={DirectionsBusIcon}
|
||||||
|
bgcolor={busStatusHighlight}
|
||||||
|
label={LL.DATA_TRAFFIC()}
|
||||||
|
text={busStatus}
|
||||||
|
to="/status/activity"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={
|
||||||
|
data.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
||||||
|
? WifiIcon
|
||||||
|
: RouterIcon
|
||||||
|
}
|
||||||
|
bgcolor={networkStatusHighlight}
|
||||||
|
label={LL.NETWORK(1)}
|
||||||
|
text={networkStatus}
|
||||||
|
to="/status/network"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={DeviceHubIcon}
|
||||||
|
bgcolor={activeHighlight(data.mqtt_status)}
|
||||||
|
label="MQTT"
|
||||||
|
text={data.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)}
|
||||||
|
to="/status/mqtt"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={AccessTimeIcon}
|
||||||
|
bgcolor={ntpStatusHighlight}
|
||||||
|
label="NTP"
|
||||||
|
text={ntpStatus}
|
||||||
|
to="/status/ntp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={SettingsInputAntennaIcon}
|
||||||
|
bgcolor={activeHighlight(data.ap_status)}
|
||||||
|
label={LL.ACCESS_POINT(0)}
|
||||||
|
text={data.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)}
|
||||||
|
to="/status/ap"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListMenuItem
|
||||||
|
disabled={!me.admin}
|
||||||
|
icon={LogoDevIcon}
|
||||||
|
bgcolor="#40828f"
|
||||||
|
label={LL.LOG_OF(LL.SYSTEM(0))}
|
||||||
|
text={LL.VIEW_LOG()}
|
||||||
|
to="/status/log"
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SystemStatus;
|
export default SystemStatus;
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
@@ -185,8 +178,7 @@ const SystemLog = () => {
|
|||||||
};
|
};
|
||||||
}, [data]); // Recalculate when data changes (in case layout shifts)
|
}, [data]); // Recalculate when data changes (in case layout shifts)
|
||||||
|
|
||||||
// Memoize message handler to avoid recreating on every render
|
const handleLogMessage = (message: { data: string }) => {
|
||||||
const handleLogMessage = useCallback((message: { data: string }) => {
|
|
||||||
const rawData = message.data;
|
const rawData = message.data;
|
||||||
const logentry = JSON.parse(rawData) as LogEntry;
|
const logentry = JSON.parse(rawData) as LogEntry;
|
||||||
setLogEntries((log) => {
|
setLogEntries((log) => {
|
||||||
@@ -200,7 +192,7 @@ const SystemLog = () => {
|
|||||||
const newLog = [...log, logentry];
|
const newLog = [...log, logentry];
|
||||||
return newLog;
|
return newLog;
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
useSSE(fetchLogES, {
|
useSSE(fetchLogES, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@@ -211,7 +203,7 @@ const SystemLog = () => {
|
|||||||
toast.error('No connection to Log service');
|
toast.error('No connection to Log service');
|
||||||
});
|
});
|
||||||
|
|
||||||
const onDownload = useCallback(() => {
|
const onDownload = () => {
|
||||||
const result = logEntries
|
const result = logEntries
|
||||||
.map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`)
|
.map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
@@ -225,11 +217,11 @@ const SystemLog = () => {
|
|||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
}, [logEntries]);
|
};
|
||||||
|
|
||||||
const saveSettings = useCallback(async () => {
|
const saveSettings = async () => {
|
||||||
await saveData();
|
await saveData();
|
||||||
}, [saveData]);
|
};
|
||||||
|
|
||||||
// handle scrolling - optimized to only scroll when needed
|
// handle scrolling - optimized to only scroll when needed
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -246,7 +238,7 @@ const SystemLog = () => {
|
|||||||
}
|
}
|
||||||
}, [logEntries.length, autoscroll]);
|
}, [logEntries.length, autoscroll]);
|
||||||
|
|
||||||
const sendReadCommand = useCallback(() => {
|
const sendReadCommand = () => {
|
||||||
if (readValue === '') {
|
if (readValue === '') {
|
||||||
setReadOpen(!readOpen);
|
setReadOpen(!readOpen);
|
||||||
return;
|
return;
|
||||||
@@ -257,7 +249,7 @@ const SystemLog = () => {
|
|||||||
setReadOpen(false);
|
setReadOpen(false);
|
||||||
setReadValue('');
|
setReadValue('');
|
||||||
}
|
}
|
||||||
}, [readValue, readOpen, send]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import { Box, Button, Typography } from '@mui/material';
|
import { Box, Button, Typography } from '@mui/material';
|
||||||
@@ -57,39 +57,31 @@ const SystemMonitor = () => {
|
|||||||
void send();
|
void send();
|
||||||
}, 1000); // check every 1 second
|
}, 1000); // check every 1 second
|
||||||
|
|
||||||
const { statusMessage, isUploading, progressValue } = useMemo(() => {
|
const status = data?.status;
|
||||||
const status = data?.status;
|
|
||||||
|
|
||||||
const message =
|
const statusMessage =
|
||||||
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
|
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
|
||||||
? LL.WAIT_FIRMWARE()
|
? LL.WAIT_FIRMWARE()
|
||||||
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
|
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
|
||||||
? LL.APPLICATION_RESTARTING()
|
? LL.APPLICATION_RESTARTING()
|
||||||
: status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
|
: status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
|
||||||
? LL.RESTARTING_PRE()
|
? LL.RESTARTING_PRE()
|
||||||
: status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
|
: status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
|
||||||
? 'Upload Failed'
|
? 'Upload Failed'
|
||||||
: LL.RESTARTING_POST();
|
: LL.RESTARTING_POST();
|
||||||
|
|
||||||
const uploading =
|
const isUploading =
|
||||||
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
|
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
|
||||||
const progress =
|
const progressValue =
|
||||||
uploading && status
|
isUploading && status
|
||||||
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
|
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return {
|
const onCancel = async () => {
|
||||||
statusMessage: message,
|
|
||||||
isUploading: uploading,
|
|
||||||
progressValue: progress
|
|
||||||
};
|
|
||||||
}, [data?.status, LL]);
|
|
||||||
|
|
||||||
const onCancel = useCallback(async () => {
|
|
||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
|
||||||
document.location.href = '/';
|
document.location.href = '/';
|
||||||
}, [setSystemStatus]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -1,931 +0,0 @@
|
|||||||
import {
|
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
|
||||||
import CheckIcon from '@mui/icons-material/Done';
|
|
||||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
|
||||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
Grid,
|
|
||||||
IconButton,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
Typography
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
import * as SystemApi from 'api/system';
|
|
||||||
import { API, callAction } from 'api/app';
|
|
||||||
import { getDevVersion, getStableVersion } from 'api/system';
|
|
||||||
|
|
||||||
import { dialogStyle } from 'CustomTheme';
|
|
||||||
import { useRequest } from 'alova/client';
|
|
||||||
import type { APIcall } from 'app/main/types';
|
|
||||||
import SystemMonitor from 'app/status/SystemMonitor';
|
|
||||||
import {
|
|
||||||
FormLoader,
|
|
||||||
SectionContent,
|
|
||||||
SingleUpload,
|
|
||||||
useLayoutTitle
|
|
||||||
} from 'components';
|
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
|
||||||
import type { TranslationFunctions } from 'i18n/i18n-types';
|
|
||||||
import { prettyDateTime } from 'utils/time';
|
|
||||||
|
|
||||||
// Constants moved outside component to avoid recreation
|
|
||||||
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
|
||||||
const STABLE_RELNOTES_URL =
|
|
||||||
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
|
||||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
|
||||||
const DEV_RELNOTES_URL =
|
|
||||||
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
|
||||||
|
|
||||||
// Types for better type safety
|
|
||||||
interface PartitionData {
|
|
||||||
partition: string;
|
|
||||||
version: string;
|
|
||||||
install_date?: string;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VersionData {
|
|
||||||
emsesp_version: string;
|
|
||||||
arduino_version: string;
|
|
||||||
esp_platform: string;
|
|
||||||
flash_chip_size: number;
|
|
||||||
psram: boolean;
|
|
||||||
build_flags?: string;
|
|
||||||
partition: string;
|
|
||||||
partitions: PartitionData[];
|
|
||||||
developer_mode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpgradeCheckData {
|
|
||||||
emsesp_version: string;
|
|
||||||
dev_upgradeable: boolean;
|
|
||||||
stable_upgradeable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VersionInfo {
|
|
||||||
name: string;
|
|
||||||
published_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memoized components for better performance
|
|
||||||
const VersionInfoDialog = memo(
|
|
||||||
({
|
|
||||||
showVersionInfo,
|
|
||||||
latestVersion,
|
|
||||||
latestDevVersion,
|
|
||||||
partitionVersion,
|
|
||||||
partition,
|
|
||||||
currentPartition,
|
|
||||||
size,
|
|
||||||
locale,
|
|
||||||
LL,
|
|
||||||
onClose
|
|
||||||
}: {
|
|
||||||
showVersionInfo: number;
|
|
||||||
latestVersion?: VersionInfo;
|
|
||||||
latestDevVersion?: VersionInfo;
|
|
||||||
partitionVersion?: VersionInfo | undefined;
|
|
||||||
partition: string;
|
|
||||||
currentPartition: string;
|
|
||||||
size: number;
|
|
||||||
locale: string;
|
|
||||||
LL: TranslationFunctions;
|
|
||||||
onClose: () => void;
|
|
||||||
}) => {
|
|
||||||
if (showVersionInfo === 0) return null;
|
|
||||||
|
|
||||||
const isStable = showVersionInfo === 1;
|
|
||||||
const isDev = showVersionInfo === 2;
|
|
||||||
const isPartition = showVersionInfo === 3;
|
|
||||||
|
|
||||||
const version = isStable
|
|
||||||
? latestVersion
|
|
||||||
: isDev
|
|
||||||
? latestDevVersion
|
|
||||||
: partitionVersion;
|
|
||||||
const relNotesUrl = isStable
|
|
||||||
? STABLE_RELNOTES_URL
|
|
||||||
: isDev
|
|
||||||
? DEV_RELNOTES_URL
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
|
|
||||||
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
|
||||||
<TableCell
|
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
sx={{
|
|
||||||
color: 'lightblue',
|
|
||||||
borderBottom: 'none',
|
|
||||||
pr: 1,
|
|
||||||
py: 0.5,
|
|
||||||
fontSize: 13
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{LL.VERSION()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
|
||||||
{isPartition
|
|
||||||
? typeof version === 'string'
|
|
||||||
? version
|
|
||||||
: version?.name
|
|
||||||
: version?.name}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
|
||||||
<TableCell
|
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
sx={{
|
|
||||||
color: 'lightblue',
|
|
||||||
borderBottom: 'none',
|
|
||||||
pr: 1,
|
|
||||||
py: 0.5,
|
|
||||||
fontSize: 13,
|
|
||||||
width: 140
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPartition ? LL.TYPE(0) : LL.RELEASE_TYPE()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
|
||||||
{partition === currentPartition && LL.ACTIVE() + ' '}
|
|
||||||
{isStable
|
|
||||||
? LL.STABLE()
|
|
||||||
: isDev
|
|
||||||
? LL.DEVELOPMENT()
|
|
||||||
: 'Partition ' + LL.VERSION()}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{isPartition && (
|
|
||||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
|
||||||
<TableCell
|
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
sx={{
|
|
||||||
color: 'lightblue',
|
|
||||||
borderBottom: 'none',
|
|
||||||
pr: 1,
|
|
||||||
py: 0.5,
|
|
||||||
fontSize: 13
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Partition
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
|
||||||
{partition}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
{isPartition && (
|
|
||||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
|
||||||
<TableCell
|
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
sx={{
|
|
||||||
color: 'lightblue',
|
|
||||||
borderBottom: 'none',
|
|
||||||
pr: 1,
|
|
||||||
py: 0.5,
|
|
||||||
fontSize: 13
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Size
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
|
||||||
{size} KB
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
{version?.published_at && (
|
|
||||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
|
||||||
<TableCell
|
|
||||||
component="th"
|
|
||||||
scope="row"
|
|
||||||
sx={{
|
|
||||||
color: 'lightblue',
|
|
||||||
borderBottom: 'none',
|
|
||||||
pr: 1,
|
|
||||||
py: 0.5,
|
|
||||||
fontSize: 13
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPartition ? 'Install Date' : 'Build Date'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
|
||||||
{prettyDateTime(locale, new Date(version.published_at))}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
{!isPartition && (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
component="a"
|
|
||||||
href={relNotesUrl}
|
|
||||||
target="_blank"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
Changelog
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button variant="outlined" onClick={onClose} color="secondary">
|
|
||||||
{LL.CLOSE()}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const InstallDialog = memo(
|
|
||||||
({
|
|
||||||
openInstallDialog,
|
|
||||||
fetchDevVersion,
|
|
||||||
latestVersion,
|
|
||||||
latestDevVersion,
|
|
||||||
upgradeImportantMessageType,
|
|
||||||
downloadOnly,
|
|
||||||
platform,
|
|
||||||
LL,
|
|
||||||
onClose,
|
|
||||||
onInstall
|
|
||||||
}: {
|
|
||||||
openInstallDialog: boolean;
|
|
||||||
fetchDevVersion: boolean;
|
|
||||||
latestVersion?: VersionInfo;
|
|
||||||
latestDevVersion?: VersionInfo;
|
|
||||||
upgradeImportantMessageType: number;
|
|
||||||
downloadOnly: boolean;
|
|
||||||
platform: string;
|
|
||||||
LL: TranslationFunctions;
|
|
||||||
onClose: () => void;
|
|
||||||
onInstall: (url: string) => void;
|
|
||||||
}) => {
|
|
||||||
const binURL = useMemo(() => {
|
|
||||||
if (!latestVersion || !latestDevVersion) return '';
|
|
||||||
|
|
||||||
const version = fetchDevVersion ? latestDevVersion : latestVersion;
|
|
||||||
const filename = `EMS-ESP-${version.name.replaceAll('.', '_')}-${platform}.bin`;
|
|
||||||
|
|
||||||
return fetchDevVersion
|
|
||||||
? `${DEV_URL}${filename}`
|
|
||||||
: `${STABLE_URL}v${version.name}/${filename}`;
|
|
||||||
}, [fetchDevVersion, latestVersion, latestDevVersion, platform]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
|
|
||||||
<DialogTitle>
|
|
||||||
{`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Typography sx={{ mb: 2 }}>
|
|
||||||
{LL.INSTALL_VERSION(
|
|
||||||
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
|
|
||||||
fetchDevVersion ? latestDevVersion?.name : latestVersion?.name
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
{upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()}
|
|
||||||
{upgradeImportantMessageType === 1 && (
|
|
||||||
<>
|
|
||||||
{LL.UPGRADE_IMPORTANT_MESSAGES_1()}
|
|
||||||
<Typography sx={{ mt: 2 }}>
|
|
||||||
<Link to="/settings/downloadUpload" style={{ color: 'lightblue' }}>
|
|
||||||
{LL.DOWNLOAD_SYSTEM_BACKUP()}
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Typography sx={{ mt: 2 }}>
|
|
||||||
<Link
|
|
||||||
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
style={{ color: 'lightblue' }}
|
|
||||||
>
|
|
||||||
{LL.ONLINE_HELP()}
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
startIcon={<CancelIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={onClose}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<DownloadIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={onClose}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={binURL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
style={{ color: 'lightblue', textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
{LL.DOWNLOAD(0)}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
{!downloadOnly && (
|
|
||||||
<Button
|
|
||||||
startIcon={<WarningIcon color="warning" />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => onInstall(binURL)}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
{LL.INSTALL()}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const InstallPartitionDialog = memo(
|
|
||||||
({
|
|
||||||
openInstallPartitionDialog,
|
|
||||||
version,
|
|
||||||
partition,
|
|
||||||
LL,
|
|
||||||
onClose,
|
|
||||||
onInstall
|
|
||||||
}: {
|
|
||||||
openInstallPartitionDialog: boolean;
|
|
||||||
version: string;
|
|
||||||
partition: string;
|
|
||||||
LL: TranslationFunctions;
|
|
||||||
onClose: () => void;
|
|
||||||
onInstall: (partition: string) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
|
||||||
<DialogTitle>
|
|
||||||
{LL.INSTALL()} {LL.STORED_VERSIONS()}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Typography sx={{ mb: 2 }}>
|
|
||||||
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
|
|
||||||
</Typography>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
startIcon={<CancelIcon />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={onClose}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
{LL.CANCEL()}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
startIcon={<WarningIcon color="warning" />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => onInstall(partition)}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
{LL.INSTALL()}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper function moved outside component
|
|
||||||
const getPlatform = (data: VersionData): string => {
|
|
||||||
return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Version = () => {
|
|
||||||
const { LL, locale } = useI18nContext();
|
|
||||||
const { me } = useContext(AuthenticatedContext);
|
|
||||||
|
|
||||||
// State management
|
|
||||||
const [restarting, setRestarting] = useState<boolean>(false);
|
|
||||||
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [partitionVersion, setPartitionVersion] = useState<VersionInfo | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [partition, setPartition] = useState<string>('');
|
|
||||||
const [openInstallPartitionDialog, setOpenInstallPartitionDialog] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false);
|
|
||||||
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
|
|
||||||
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false);
|
|
||||||
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
const [internetLive, setInternetLive] = useState<boolean>(false);
|
|
||||||
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
|
|
||||||
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
|
|
||||||
const [firmwareSize, setFirmwareSize] = useState<number>(0);
|
|
||||||
|
|
||||||
const { send: sendCheckUpgrade } = useRequest(
|
|
||||||
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }),
|
|
||||||
{ immediate: false }
|
|
||||||
).onSuccess((event) => {
|
|
||||||
const data = event.data as UpgradeCheckData;
|
|
||||||
setDevUpgradeAvailable(data.dev_upgradeable);
|
|
||||||
setStableUpgradeAvailable(data.stable_upgradeable);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { send: sendSetPartition } = useRequest(
|
|
||||||
(partition: string) => callAction({ action: 'setPartition', param: partition }),
|
|
||||||
{ immediate: false }
|
|
||||||
).onError((error) => {
|
|
||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
send: loadData,
|
|
||||||
error
|
|
||||||
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
|
|
||||||
const systemData = event.data as VersionData;
|
|
||||||
if (systemData.arduino_version.startsWith('Tasmota')) {
|
|
||||||
setDownloadOnly(true);
|
|
||||||
}
|
|
||||||
setUsingDevVersion(systemData.emsesp_version.includes('dev'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const { send: sendUploadURL } = useRequest(
|
|
||||||
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
|
||||||
{ immediate: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: latestVersion } = useRequest(getStableVersion);
|
|
||||||
const { data: latestDevVersion } = useRequest(getDevVersion);
|
|
||||||
|
|
||||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
|
||||||
immediate: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
|
|
||||||
useState<number>(0);
|
|
||||||
|
|
||||||
const { send: checkUpgradeImportantMessages } = useRequest(
|
|
||||||
(version: string) =>
|
|
||||||
callAction({ action: 'upgradeImportantMessages', param: version }),
|
|
||||||
{
|
|
||||||
immediate: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.onSuccess((event) => {
|
|
||||||
const upgradeImportantMessageType_n = (
|
|
||||||
event.data as { upgradeImportantMessageType: number }
|
|
||||||
).upgradeImportantMessageType;
|
|
||||||
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
|
|
||||||
})
|
|
||||||
.onError((error) => {
|
|
||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Memoized values
|
|
||||||
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
|
|
||||||
|
|
||||||
// Memoize filtered partitions to avoid recomputing on every render
|
|
||||||
const otherPartitions = useMemo(
|
|
||||||
() => data?.partitions.filter((p) => p.partition !== data.partition) ?? [],
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setPartitionVersionInfo = useCallback(
|
|
||||||
(partition: string) => {
|
|
||||||
setShowVersionInfo(3);
|
|
||||||
|
|
||||||
// search for the partition in the data.partitions array
|
|
||||||
const partitionData = data?.partitions.find((p) => p.partition === partition);
|
|
||||||
if (partitionData) {
|
|
||||||
setPartitionVersion({
|
|
||||||
name: partitionData.version,
|
|
||||||
published_at: partitionData.install_date ?? ''
|
|
||||||
});
|
|
||||||
setPartition(partitionData.partition);
|
|
||||||
setFirmwareSize(partitionData.size);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
|
||||||
(error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setRestarting(true);
|
|
||||||
}, [sendAPI]);
|
|
||||||
|
|
||||||
const installFirmwareURL = useCallback(
|
|
||||||
async (url: string) => {
|
|
||||||
await sendUploadURL(url).catch((error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
await doRestart();
|
|
||||||
},
|
|
||||||
[sendUploadURL, doRestart]
|
|
||||||
);
|
|
||||||
|
|
||||||
const installPartitionFirmware = useCallback(
|
|
||||||
async (partition: string) => {
|
|
||||||
await sendSetPartition(partition).catch((error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
setRestarting(true);
|
|
||||||
},
|
|
||||||
[sendSetPartition]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showPartitionDialog = useCallback(
|
|
||||||
(version: string, partition: string, install_date: string) => {
|
|
||||||
setOpenInstallPartitionDialog(true);
|
|
||||||
setPartitionVersion({ name: version, published_at: install_date });
|
|
||||||
setPartition(partition);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showFirmwareDialog = useCallback(
|
|
||||||
(useDevVersion: boolean) => {
|
|
||||||
setFetchDevVersion(useDevVersion);
|
|
||||||
void checkUpgradeImportantMessages(
|
|
||||||
useDevVersion ? latestDevVersion?.name : latestVersion?.name
|
|
||||||
);
|
|
||||||
setOpenInstallDialog(true);
|
|
||||||
},
|
|
||||||
[latestDevVersion, latestVersion, fetchDevVersion]
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeInstallDialog = useCallback(() => {
|
|
||||||
setOpenInstallDialog(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeInstallPartitionDialog = useCallback(() => {
|
|
||||||
setOpenInstallPartitionDialog(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleVersionInfoClose = useCallback(() => {
|
|
||||||
setShowVersionInfo(0);
|
|
||||||
setPartitionVersion(undefined);
|
|
||||||
setPartition('');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// check upgrades - only once when both versions are available
|
|
||||||
const upgradeCheckedRef = useRef(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (latestVersion && latestDevVersion && !upgradeCheckedRef.current) {
|
|
||||||
upgradeCheckedRef.current = true;
|
|
||||||
const versions = `${latestDevVersion.name},${latestVersion.name}`;
|
|
||||||
sendCheckUpgrade(versions)
|
|
||||||
.catch((error: Error) => {
|
|
||||||
toast.error(`Failed to check for upgrades: ${error.message}`);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setInternetLive(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [latestVersion, latestDevVersion, sendCheckUpgrade]);
|
|
||||||
|
|
||||||
useLayoutTitle('EMS-ESP Firmware');
|
|
||||||
|
|
||||||
// Memoized button rendering logic
|
|
||||||
const showButtons = useCallback(
|
|
||||||
(showingDev: boolean) => {
|
|
||||||
const choice = showingDev
|
|
||||||
? !usingDevVersion
|
|
||||||
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
|
|
||||||
: devUpgradeAvailable
|
|
||||||
? LL.UPDATE_AVAILABLE()
|
|
||||||
: undefined
|
|
||||||
: usingDevVersion
|
|
||||||
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
|
|
||||||
: stableUpgradeAvailable
|
|
||||||
? LL.UPDATE_AVAILABLE()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!choice) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CheckIcon
|
|
||||||
color="success"
|
|
||||||
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
|
|
||||||
/>
|
|
||||||
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
|
|
||||||
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
sx={{ ml: 1 }}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onClick={() => showFirmwareDialog(showingDev)}
|
|
||||||
>
|
|
||||||
{LL.REINSTALL()}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!me.admin) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
sx={{ ml: 1 }}
|
|
||||||
variant="outlined"
|
|
||||||
color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
|
|
||||||
size="small"
|
|
||||||
onClick={() => showFirmwareDialog(showingDev)}
|
|
||||||
>
|
|
||||||
{choice}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
usingDevVersion,
|
|
||||||
devUpgradeAvailable,
|
|
||||||
stableUpgradeAvailable,
|
|
||||||
me.admin,
|
|
||||||
LL,
|
|
||||||
showFirmwareDialog
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) {
|
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
|
|
||||||
<Typography sx={{ mb: 1 }} variant="h6" color="primary">
|
|
||||||
{LL.THIS_VERSION()}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
direction="row"
|
|
||||||
sx={{
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
alignItems: 'baseline'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
|
||||||
<Typography color="secondary">{LL.VERSION()}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
|
||||||
<Typography>
|
|
||||||
{data.emsesp_version}
|
|
||||||
{data.build_flags && (
|
|
||||||
<Typography variant="caption">
|
|
||||||
({data.build_flags})
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setPartitionVersionInfo(data.partition)}
|
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
|
||||||
>
|
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
|
||||||
</IconButton>
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
|
||||||
<Typography color="secondary">{LL.PLATFORM()}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
|
||||||
<Typography>
|
|
||||||
{platform}
|
|
||||||
<Typography variant="caption">
|
|
||||||
(
|
|
||||||
{data.psram ? (
|
|
||||||
<CheckIcon
|
|
||||||
color="success"
|
|
||||||
sx={{
|
|
||||||
fontSize: '1.5em',
|
|
||||||
verticalAlign: 'middle'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CloseIcon
|
|
||||||
color="error"
|
|
||||||
sx={{
|
|
||||||
fontSize: '1.5em',
|
|
||||||
verticalAlign: 'middle'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
PSRAM)
|
|
||||||
</Typography>
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{internetLive ? (
|
|
||||||
<>
|
|
||||||
<Typography sx={{ mt: 4, mb: 1 }} variant="h6" color="primary">
|
|
||||||
{LL.AVAILABLE_VERSION()}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
direction="row"
|
|
||||||
rowSpacing={1}
|
|
||||||
sx={{
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
alignItems: 'baseline'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{otherPartitions.length > 0 && data.developer_mode && (
|
|
||||||
<>
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
|
||||||
<Typography color="secondary">
|
|
||||||
{LL.STORED_VERSIONS()}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
|
||||||
{otherPartitions.map((partition) => (
|
|
||||||
<Typography key={partition.partition} sx={{ mb: 1 }}>
|
|
||||||
{partition.version}
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
setPartitionVersionInfo(partition.partition)
|
|
||||||
}
|
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
|
||||||
>
|
|
||||||
<InfoOutlinedIcon
|
|
||||||
color="primary"
|
|
||||||
sx={{ fontSize: 18 }}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
<Button
|
|
||||||
sx={{ ml: 0 }}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
showPartitionDialog(
|
|
||||||
partition.version,
|
|
||||||
partition.partition,
|
|
||||||
partition.install_date ?? ''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{LL.INSTALL()}
|
|
||||||
</Button>
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
|
||||||
<Typography color="secondary">{LL.STABLE()}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
|
||||||
<Typography>
|
|
||||||
{latestVersion?.name}
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setShowVersionInfo(1)}
|
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
|
||||||
>
|
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
|
||||||
</IconButton>
|
|
||||||
{showButtons(false)}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
|
||||||
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
|
||||||
<Typography>
|
|
||||||
{latestDevVersion?.name}
|
|
||||||
<IconButton
|
|
||||||
onClick={() => setShowVersionInfo(2)}
|
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
|
||||||
>
|
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
|
||||||
</IconButton>
|
|
||||||
{showButtons(true)}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Typography sx={{ mt: 2 }} color="warning">
|
|
||||||
<WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} />
|
|
||||||
{LL.INTERNET_CONNECTION_REQUIRED()}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{me.admin && (
|
|
||||||
<>
|
|
||||||
<VersionInfoDialog
|
|
||||||
showVersionInfo={showVersionInfo}
|
|
||||||
latestVersion={latestVersion}
|
|
||||||
latestDevVersion={latestDevVersion}
|
|
||||||
partitionVersion={partitionVersion}
|
|
||||||
locale={locale}
|
|
||||||
partition={partition}
|
|
||||||
currentPartition={data?.partition ?? ''}
|
|
||||||
size={firmwareSize}
|
|
||||||
LL={LL}
|
|
||||||
onClose={handleVersionInfoClose}
|
|
||||||
/>
|
|
||||||
<InstallDialog
|
|
||||||
openInstallDialog={openInstallDialog}
|
|
||||||
fetchDevVersion={fetchDevVersion}
|
|
||||||
latestVersion={latestVersion}
|
|
||||||
latestDevVersion={latestDevVersion}
|
|
||||||
upgradeImportantMessageType={upgradeImportantMessageType}
|
|
||||||
downloadOnly={downloadOnly}
|
|
||||||
platform={platform}
|
|
||||||
LL={LL}
|
|
||||||
onClose={closeInstallDialog}
|
|
||||||
onInstall={installFirmwareURL}
|
|
||||||
/>
|
|
||||||
<InstallPartitionDialog
|
|
||||||
openInstallPartitionDialog={openInstallPartitionDialog}
|
|
||||||
version={partitionVersion?.name || ''}
|
|
||||||
partition={partition}
|
|
||||||
LL={LL}
|
|
||||||
onClose={closeInstallPartitionDialog}
|
|
||||||
onInstall={installPartitionFirmware}
|
|
||||||
/>
|
|
||||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
|
||||||
{LL.UPLOAD()}
|
|
||||||
</Typography>
|
|
||||||
<SingleUpload doRestart={doRestart} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
loadData,
|
|
||||||
LL,
|
|
||||||
platform,
|
|
||||||
internetLive,
|
|
||||||
latestVersion,
|
|
||||||
latestDevVersion,
|
|
||||||
showVersionInfo,
|
|
||||||
locale,
|
|
||||||
openInstallDialog,
|
|
||||||
fetchDevVersion,
|
|
||||||
downloadOnly,
|
|
||||||
me.admin,
|
|
||||||
showButtons,
|
|
||||||
handleVersionInfoClose,
|
|
||||||
closeInstallDialog,
|
|
||||||
installFirmwareURL,
|
|
||||||
doRestart,
|
|
||||||
otherPartitions,
|
|
||||||
setPartitionVersionInfo,
|
|
||||||
showPartitionDialog,
|
|
||||||
partitionVersion,
|
|
||||||
partition,
|
|
||||||
firmwareSize,
|
|
||||||
closeInstallPartitionDialog,
|
|
||||||
installPartitionFirmware
|
|
||||||
]);
|
|
||||||
|
|
||||||
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Version);
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, type PropsWithChildren, memo, useMemo } from 'react';
|
import { type FC, type PropsWithChildren, memo } 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';
|
||||||
@@ -38,18 +38,17 @@ const MessageBox: FC<PropsWithChildren<MessageBoxProps>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { Icon, backgroundColor } = useMemo(() => {
|
const Icon = LEVEL_ICONS[level];
|
||||||
const Icon = LEVEL_ICONS[level];
|
const palettePath = LEVEL_PALETTE_PATHS[level];
|
||||||
const palettePath = LEVEL_PALETTE_PATHS[level];
|
const [paletteKeyName, shade] = palettePath.split('.') as [
|
||||||
const [key, shade] = palettePath.split('.') as [
|
keyof typeof theme.palette,
|
||||||
keyof typeof theme.palette,
|
string
|
||||||
string
|
];
|
||||||
];
|
const paletteKey = theme.palette[paletteKeyName] as unknown as Record<
|
||||||
const paletteKey = theme.palette[key] as unknown as Record<string, string>;
|
string,
|
||||||
const backgroundColor = paletteKey[shade];
|
string
|
||||||
|
>;
|
||||||
return { Icon, backgroundColor };
|
const backgroundColor = paletteKey[shade];
|
||||||
}, [level, theme]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
@@ -44,27 +44,14 @@ const LANGUAGE_OPTIONS: LanguageOption[] = [
|
|||||||
const LanguageSelector = () => {
|
const LanguageSelector = () => {
|
||||||
const { setLocale, locale, LL } = useContext(I18nContext);
|
const { setLocale, locale, LL } = useContext(I18nContext);
|
||||||
|
|
||||||
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = useCallback(
|
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
|
||||||
async ({ target }) => {
|
target
|
||||||
const loc = target.value as Locales;
|
}) => {
|
||||||
localStorage.setItem('lang', loc);
|
const loc = target.value as Locales;
|
||||||
await loadLocaleAsync(loc);
|
localStorage.setItem('lang', loc);
|
||||||
setLocale(loc);
|
await loadLocaleAsync(loc);
|
||||||
},
|
setLocale(loc);
|
||||||
[setLocale]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize menu items to prevent recreation on every render
|
|
||||||
const menuItems = useMemo(
|
|
||||||
() =>
|
|
||||||
LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
|
|
||||||
<MenuItem key={key} value={key}>
|
|
||||||
<img src={flag} style={flagStyle} alt={label} />
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
)),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
@@ -76,7 +63,12 @@ const LanguageSelector = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
select
|
select
|
||||||
>
|
>
|
||||||
{menuItems}
|
{LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
|
||||||
|
<MenuItem key={key} value={key}>
|
||||||
|
<img src={flag} style={flagStyle} alt={label} />
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
@@ -13,9 +13,9 @@ type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
|
|||||||
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) => {
|
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) => {
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
|
||||||
const togglePasswordVisibility = useCallback(() => {
|
const togglePasswordVisibility = () => {
|
||||||
setShowPassword((prev) => !prev);
|
setShowPassword((prev) => !prev);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
const [title, setTitle] = useState(PROJECT_NAME);
|
const [title, setTitle] = useState(PROJECT_NAME);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
// Memoize drawer toggle handler to prevent unnecessary re-renders
|
|
||||||
const handleDrawerToggle = useCallback(() => {
|
const handleDrawerToggle = useCallback(() => {
|
||||||
setMobileOpen((prev) => !prev);
|
setMobileOpen((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -28,7 +27,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
setMobileOpen(false);
|
setMobileOpen(false);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
// Memoize context value to prevent unnecessary re-renders
|
|
||||||
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
|
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router';
|
import { Link, useLocation, useNavigate } from 'react-router';
|
||||||
|
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
@@ -39,14 +39,11 @@ const LayoutAppBarComponent = ({ title, onToggleDrawer }: LayoutAppBarProps) =>
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const pathnames = useMemo(
|
const pathnames = location.pathname.split('/').filter((x) => x);
|
||||||
() => location.pathname.split('/').filter((x) => x),
|
|
||||||
[location.pathname]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleBackClick = useCallback(() => {
|
const handleBackClick = () => {
|
||||||
void navigate('/' + pathnames[0]);
|
void navigate('/' + pathnames[0]);
|
||||||
}, [navigate, pathnames]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="fixed" sx={appBarStyles}>
|
<AppBar position="fixed" sx={appBarStyles}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
||||||
|
|
||||||
@@ -24,22 +24,18 @@ interface LayoutDrawerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
|
||||||
// Memoize drawer content to prevent unnecessary re-renders
|
const drawer = (
|
||||||
const drawer = useMemo(
|
<>
|
||||||
() => (
|
<Toolbar disableGutters>
|
||||||
<>
|
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
|
||||||
<Toolbar disableGutters>
|
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
|
<Typography variant="h6">{PROJECT_NAME}</Typography>
|
||||||
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
|
</Box>
|
||||||
<Typography variant="h6">{PROJECT_NAME}</Typography>
|
<Divider absolute />
|
||||||
</Box>
|
</Toolbar>
|
||||||
<Divider absolute />
|
<Divider />
|
||||||
</Toolbar>
|
<LayoutMenu />
|
||||||
<Divider />
|
</>
|
||||||
<LayoutMenu />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useContext, useState } from 'react';
|
import { memo, useContext, useState } 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';
|
||||||
@@ -18,13 +18,15 @@ import { AuthenticatedContext } from 'contexts/authentication';
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const LayoutMenuComponent = () => {
|
const LayoutMenuComponent = () => {
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const { me, versions } = useContext(AuthenticatedContext);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const [menuOpen, setMenuOpen] = useState(true);
|
const [menuOpen, setMenuOpen] = useState(true);
|
||||||
|
|
||||||
const handleMenuToggle = useCallback(() => {
|
const upgradeAvailable = versions?.current?.upgradeable ?? false;
|
||||||
|
|
||||||
|
const handleMenuToggle = () => {
|
||||||
setMenuOpen((prev) => !prev);
|
setMenuOpen((prev) => !prev);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -105,6 +107,7 @@ const LayoutMenuComponent = () => {
|
|||||||
label={LL.SETTINGS(0)}
|
label={LL.SETTINGS(0)}
|
||||||
disabled={!me.admin}
|
disabled={!me.admin}
|
||||||
to="/settings"
|
to="/settings"
|
||||||
|
badge={upgradeAvailable}
|
||||||
/>
|
/>
|
||||||
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP()} to={`/help`} />
|
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP()} to={`/help`} />
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link, useLocation } from 'react-router';
|
import { Link, useLocation } from 'react-router';
|
||||||
|
|
||||||
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
import { Box, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
||||||
import type { SvgIconProps, SxProps, Theme } from '@mui/material';
|
import type { SvgIconProps, SxProps, Theme } from '@mui/material';
|
||||||
|
|
||||||
import { routeMatches } from 'utils';
|
import { routeMatches } from 'utils';
|
||||||
@@ -11,60 +11,52 @@ interface LayoutMenuItemProps {
|
|||||||
label: string;
|
label: string;
|
||||||
to: string;
|
to: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
badge?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LayoutMenuItemComponent = ({
|
const LayoutMenuItemComponent = ({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
label,
|
label,
|
||||||
to,
|
to,
|
||||||
disabled
|
disabled,
|
||||||
|
badge
|
||||||
}: LayoutMenuItemProps) => {
|
}: LayoutMenuItemProps) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const selected = useMemo(() => routeMatches(to, pathname), [to, pathname]);
|
const selected = routeMatches(to, pathname);
|
||||||
|
|
||||||
// Memoize dynamic styles based on selected state
|
const buttonStyles: SxProps<Theme> = {
|
||||||
const buttonStyles: SxProps<Theme> = useMemo(
|
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||||
() => ({
|
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
|
||||||
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
borderRadius: '8px',
|
||||||
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
|
margin: '2px 8px',
|
||||||
borderRadius: '8px',
|
'&:hover': {
|
||||||
margin: '2px 8px',
|
backgroundColor: 'rgba(68, 82, 211, 0.39)'
|
||||||
'&:hover': {
|
},
|
||||||
backgroundColor: 'rgba(68, 82, 211, 0.39)'
|
'&::before': {
|
||||||
},
|
content: '""',
|
||||||
'&::before': {
|
position: 'absolute',
|
||||||
content: '""',
|
left: 0,
|
||||||
position: 'absolute',
|
top: 0,
|
||||||
left: 0,
|
bottom: 0,
|
||||||
top: 0,
|
width: selected ? '3px' : '0px',
|
||||||
bottom: 0,
|
backgroundColor: '#90caf9',
|
||||||
width: selected ? '3px' : '0px',
|
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
||||||
backgroundColor: '#90caf9',
|
}
|
||||||
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
};
|
||||||
}
|
|
||||||
}),
|
|
||||||
[selected]
|
|
||||||
);
|
|
||||||
|
|
||||||
const iconStyles: SxProps<Theme> = useMemo(
|
const iconStyles: SxProps<Theme> = {
|
||||||
() => ({
|
color: selected ? '#90caf9' : '#9e9e9e',
|
||||||
color: selected ? '#90caf9' : '#9e9e9e',
|
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
||||||
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
transitionProperty: 'color, transform'
|
||||||
transitionProperty: 'color, transform'
|
};
|
||||||
}),
|
|
||||||
[selected]
|
|
||||||
);
|
|
||||||
|
|
||||||
const textStyles: SxProps<Theme> = useMemo(
|
const textStyles: SxProps<Theme> = {
|
||||||
() => ({
|
color: selected ? '#90caf9' : '#f5f5f5',
|
||||||
color: selected ? '#90caf9' : '#f5f5f5',
|
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
transitionProperty: 'color, font-weight'
|
||||||
transitionProperty: 'color, font-weight'
|
};
|
||||||
}),
|
|
||||||
[selected]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
@@ -78,6 +70,20 @@ const LayoutMenuItemComponent = ({
|
|||||||
<Icon />
|
<Icon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText sx={textStyles}>{label}</ListItemText>
|
<ListItemText sx={textStyles}>{label}</ListItemText>
|
||||||
|
{badge && (
|
||||||
|
<Box
|
||||||
|
aria-label="update available"
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
ml: 1,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#ffeb3b',
|
||||||
|
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)',
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Link } from 'react-router';
|
|||||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Box,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
@@ -20,6 +21,7 @@ interface ListMenuItemProps {
|
|||||||
text: string;
|
text: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
badge?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconStyles: CSSProperties = {
|
const iconStyles: CSSProperties = {
|
||||||
@@ -28,15 +30,40 @@ const iconStyles: CSSProperties = {
|
|||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Badge = () => (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
aria-label="update available"
|
||||||
|
sx={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
ml: 1,
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#ffeb3b',
|
||||||
|
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const RenderIcon = memo(
|
const RenderIcon = memo(
|
||||||
({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) => (
|
({ icon: Icon, bgcolor, label, text, badge }: ListMenuItemProps) => (
|
||||||
<>
|
<>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar sx={{ bgcolor, color: 'white' }}>
|
<Avatar sx={{ bgcolor, color: 'white' }}>
|
||||||
<Icon />
|
<Icon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={label} secondary={text} />
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<>
|
||||||
|
{label}
|
||||||
|
{badge && <Badge />}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
secondary={text}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -47,7 +74,8 @@ const LayoutMenuItem = ({
|
|||||||
label,
|
label,
|
||||||
text,
|
text,
|
||||||
to,
|
to,
|
||||||
disabled
|
disabled,
|
||||||
|
badge
|
||||||
}: ListMenuItemProps) => (
|
}: ListMenuItemProps) => (
|
||||||
<>
|
<>
|
||||||
{to && !disabled ? (
|
{to && !disabled ? (
|
||||||
@@ -65,6 +93,7 @@ const LayoutMenuItem = ({
|
|||||||
{...(bgcolor && { bgcolor })}
|
{...(bgcolor && { bgcolor })}
|
||||||
label={label}
|
label={label}
|
||||||
text={text}
|
text={text}
|
||||||
|
{...(badge && { badge })}
|
||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -75,6 +104,7 @@ const LayoutMenuItem = ({
|
|||||||
{...(bgcolor && { bgcolor })}
|
{...(bgcolor && { bgcolor })}
|
||||||
label={label}
|
label={label}
|
||||||
text={text}
|
text={text}
|
||||||
|
{...(badge && { badge })}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
import type { Blocker } from 'react-router';
|
import type { Blocker } from 'react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -15,13 +15,13 @@ import { useI18nContext } from 'i18n/i18n-react';
|
|||||||
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
|
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = () => {
|
||||||
blocker.reset?.();
|
blocker.reset?.();
|
||||||
}, [blocker]);
|
};
|
||||||
|
|
||||||
const handleProceed = useCallback(() => {
|
const handleProceed = () => {
|
||||||
blocker.proceed?.();
|
blocker.proceed?.();
|
||||||
}, [blocker]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}>
|
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
@@ -16,12 +16,9 @@ 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 = useCallback(
|
const handleTabChange = (_event: unknown, path: string) => {
|
||||||
(_event: unknown, path: string) => {
|
void navigate(path);
|
||||||
void navigate(path);
|
};
|
||||||
},
|
|
||||||
[navigate]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
|
|||||||
).upgradeImportantMessageType;
|
).upgradeImportantMessageType;
|
||||||
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
|
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
|
||||||
if (upgradeImportantMessageType_n === 0) {
|
if (upgradeImportantMessageType_n === 0) {
|
||||||
onFileSelected(file);
|
if (file) {
|
||||||
|
onFileSelected(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onError((error) => {
|
.onError((error) => {
|
||||||
@@ -213,19 +215,8 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
|
|||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
{upgradeImportantMessageType === 2 &&
|
{upgradeImportantMessageType === 2 &&
|
||||||
LL.UPGRADE_IMPORTANT_MESSAGES_2()}
|
LL.UPGRADE_IMPORTANT_MESSAGES_2()}
|
||||||
{upgradeImportantMessageType === 1 && (
|
{upgradeImportantMessageType === 1 &&
|
||||||
<>
|
LL.UPGRADE_IMPORTANT_MESSAGES_1()}
|
||||||
{LL.UPGRADE_IMPORTANT_MESSAGES_1()}
|
|
||||||
<Typography sx={{ mt: 2 }}>
|
|
||||||
<Link
|
|
||||||
to="/settings/downloadUpload"
|
|
||||||
style={{ color: 'lightblue' }}
|
|
||||||
>
|
|
||||||
{LL.DOWNLOAD_SYSTEM_BACKUP()}
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}{' '}
|
|
||||||
<Typography sx={{ mt: 2 }}>
|
<Typography sx={{ mt: 2 }}>
|
||||||
<Link
|
<Link
|
||||||
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
|
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { FC } from 'react';
|
|||||||
import { redirect } from 'react-router';
|
import { redirect } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { callAction } from 'api/app';
|
||||||
import { ACCESS_TOKEN } from 'api/endpoints';
|
import { ACCESS_TOKEN } from 'api/endpoints';
|
||||||
|
|
||||||
import * as AuthenticationApi from 'components/routing/authentication';
|
import * as AuthenticationApi from 'components/routing/authentication';
|
||||||
@@ -10,7 +11,7 @@ import { useRequest } from 'alova/client';
|
|||||||
import { LoadingSpinner } from 'components';
|
import { LoadingSpinner } from 'components';
|
||||||
import { verifyAuthorization } from 'components/routing/authentication';
|
import { verifyAuthorization } from 'components/routing/authentication';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import type { Me } from 'types';
|
import type { Me, VersionsResponse } from 'types';
|
||||||
import type { RequiredChildrenProps } from 'utils';
|
import type { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
import { AuthenticationContext } from './context';
|
import { AuthenticationContext } from './context';
|
||||||
@@ -20,17 +21,34 @@ 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 [versions, setVersions] = useState<VersionsResponse>();
|
||||||
|
|
||||||
const { send: sendVerifyAuthorization } = useRequest(verifyAuthorization(), {
|
const { send: sendVerifyAuthorization } = useRequest(verifyAuthorization(), {
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { send: sendGetVersions } = useRequest(
|
||||||
|
() => callAction({ action: 'getVersions' }),
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
.onSuccess((event) => {
|
||||||
|
setVersions(event.data as VersionsResponse);
|
||||||
|
})
|
||||||
|
.onError(() => {
|
||||||
|
setVersions(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshVersions = useCallback(async () => {
|
||||||
|
await sendGetVersions().catch(() => undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const signIn = (accessToken: string) => {
|
const signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
|
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||||
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
|
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
|
||||||
setMe(decodedMe);
|
setMe(decodedMe);
|
||||||
toast.success(LL.LOGGED_IN({ name: decodedMe.username }));
|
toast.success(LL.LOGGED_IN({ name: decodedMe.username }));
|
||||||
|
void refreshVersions();
|
||||||
} catch {
|
} catch {
|
||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
throw new Error('Failed to parse JWT');
|
throw new Error('Failed to parse JWT');
|
||||||
@@ -40,6 +58,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
const signOut = (doRedirect: boolean) => {
|
const signOut = (doRedirect: boolean) => {
|
||||||
AuthenticationApi.clearAccessToken();
|
AuthenticationApi.clearAccessToken();
|
||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
|
setVersions(undefined);
|
||||||
if (doRedirect) {
|
if (doRedirect) {
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
@@ -49,8 +68,9 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
await sendVerifyAuthorization()
|
await sendVerifyAuthorization()
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
setMe(AuthenticationApi.decodeMeJWT(accessToken));
|
setMe(AuthenticationApi.decodeMeJWT(accessToken));
|
||||||
|
await refreshVersions();
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -67,15 +87,16 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
void refresh();
|
void refresh();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
// cache object to prevent re-renders
|
|
||||||
const obj = useMemo(
|
const obj = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
refresh,
|
refresh,
|
||||||
...(me && { me })
|
refreshVersions,
|
||||||
|
...(me && { me }),
|
||||||
|
...(versions && { versions })
|
||||||
}),
|
}),
|
||||||
[signIn, signOut, me, refresh]
|
[signIn, signOut, me, refresh, refreshVersions, versions]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
import type { Me } from 'types';
|
import type { Me, VersionsResponse } from 'types';
|
||||||
|
|
||||||
export interface AuthenticationContextValue {
|
export interface AuthenticationContextValue {
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
signIn: (accessToken: string) => void;
|
signIn: (accessToken: string) => void;
|
||||||
signOut: (redirect: boolean) => void;
|
signOut: (redirect: boolean) => void;
|
||||||
me?: Me;
|
me?: Me;
|
||||||
|
versions?: VersionsResponse;
|
||||||
|
refreshVersions: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const cz: Translation = {
|
|||||||
TIME_ZONE: 'Časová zóna',
|
TIME_ZONE: 'Časová zóna',
|
||||||
ACCESS_POINT: 'Přístupový bod',
|
ACCESS_POINT: 'Přístupový bod',
|
||||||
AP_PROVIDE: 'Povolit přístupový bod',
|
AP_PROVIDE: 'Povolit přístupový bod',
|
||||||
AP_PROVIDE_TEXT_1: 'Vždy',
|
AP_PROVIDE_TEXT_2: 'Když je síťové připojení stratené',
|
||||||
AP_PROVIDE_TEXT_2: 'Když je WiFi odpojena',
|
|
||||||
AP_PROVIDE_TEXT_3: 'Nikdy',
|
AP_PROVIDE_TEXT_3: 'Nikdy',
|
||||||
AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
|
AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
|
||||||
AP_HIDE_SSID: 'Skrýt SSID',
|
AP_HIDE_SSID: 'Skrýt SSID',
|
||||||
@@ -262,7 +261,6 @@ const cz: Translation = {
|
|||||||
SCAN_AGAIN: 'Skenovat znovu',
|
SCAN_AGAIN: 'Skenovat znovu',
|
||||||
NETWORK_SCANNER: 'Síťový skener',
|
NETWORK_SCANNER: 'Síťový skener',
|
||||||
NETWORK_NO_WIFI: 'Nenalezeny žádné WiFi sítě',
|
NETWORK_NO_WIFI: 'Nenalezeny žádné WiFi sítě',
|
||||||
NETWORK_BLANK_SSID: 'ponechte prázdné pro deaktivaci WiFi a povolení ETH',
|
|
||||||
NETWORK_BLANK_BSSID: 'ponechte prázdné pokud použijete jen SSID',
|
NETWORK_BLANK_BSSID: 'ponechte prázdné pokud použijete jen SSID',
|
||||||
TX_POWER: 'Vysílací výkon',
|
TX_POWER: 'Vysílací výkon',
|
||||||
HOSTNAME: 'Název hostitele',
|
HOSTNAME: 'Název hostitele',
|
||||||
@@ -350,7 +348,7 @@ const cz: Translation = {
|
|||||||
NO_DATA_1: 'Nebyly nalezeny žádné oblíbené entity. Použijte modul',
|
NO_DATA_1: 'Nebyly nalezeny žádné oblíbené entity. Použijte modul',
|
||||||
NO_DATA_2: 'pro jejich výběr.',
|
NO_DATA_2: 'pro jejich výběr.',
|
||||||
NO_DATA_3: 'Pro zobrazení všech dostupných entit navštivte stránku',
|
NO_DATA_3: 'Pro zobrazení všech dostupných entit navštivte stránku',
|
||||||
NO_GPIO:'Nebylo nalezeno žádné volné GPIO',
|
NO_GPIO: 'Nebylo nalezeno žádné volné GPIO',
|
||||||
THIS_VERSION: 'Tato verze',
|
THIS_VERSION: 'Tato verze',
|
||||||
PLATFORM: 'Platforma',
|
PLATFORM: 'Platforma',
|
||||||
RELEASE_TYPE: 'Typ sestavení',
|
RELEASE_TYPE: 'Typ sestavení',
|
||||||
@@ -362,7 +360,7 @@ const cz: Translation = {
|
|||||||
STORED_VERSIONS: 'Uložené verze',
|
STORED_VERSIONS: 'Uložené verze',
|
||||||
ONLINE_HELP: 'online nápověda',
|
ONLINE_HELP: 'online nápověda',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
|
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že jste vytvořili zálohu své konfigurace a nastavení před pokračováním a nahrajte ji po instalaci nové verze.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor po instalaci nové verze.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?'
|
WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?'
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ const de: Translation = {
|
|||||||
COMPACT: 'Kompakte Darstellung',
|
COMPACT: 'Kompakte Darstellung',
|
||||||
DOWNLOAD_SETTINGS_TEXT: 'Erstellen Sie eine Sicherung Ihrer Konfigurationen und Einstellungen',
|
DOWNLOAD_SETTINGS_TEXT: 'Erstellen Sie eine Sicherung Ihrer Konfigurationen und Einstellungen',
|
||||||
DOWNLOAD_SETTINGS_TEXT2: 'Exportiere alle Werte',
|
DOWNLOAD_SETTINGS_TEXT2: 'Exportiere alle Werte',
|
||||||
DOWNLOAD_SYSTEM_BACKUP: 'System Sicherung',
|
DOWNLOAD_SYSTEM_BACKUP: 'Systemsicherung',
|
||||||
UPLOAD_TEXT: 'Laden Sie eine neue Firmware-Datei (.bin) oder eine Sicherungsdatei (.json) hoch',
|
UPLOAD_TEXT: 'Laden Sie eine neue Firmware-Datei (.bin) oder eine Sicherungsdatei (.json) hoch',
|
||||||
UPLOAD_DROP_TEXT: 'Legen Sie eine Firmware-Datei (.bin) ab oder klicken Sie hier',
|
UPLOAD_DROP_TEXT: 'Legen Sie eine Firmware-Datei (.bin) ab oder klicken Sie hier',
|
||||||
ERROR: 'Unerwarteter Fehler, bitte versuchen Sie es erneut.',
|
ERROR: 'Unerwarteter Fehler, bitte versuchen Sie es erneut.',
|
||||||
@@ -247,8 +247,7 @@ const de: Translation = {
|
|||||||
TIME_ZONE: 'Zeitzone',
|
TIME_ZONE: 'Zeitzone',
|
||||||
ACCESS_POINT: 'Zugangspunkt',
|
ACCESS_POINT: 'Zugangspunkt',
|
||||||
AP_PROVIDE: 'Aktiviere Zugangspunkt',
|
AP_PROVIDE: 'Aktiviere Zugangspunkt',
|
||||||
AP_PROVIDE_TEXT_1: 'Immer',
|
AP_PROVIDE_TEXT_2: 'Wenn Netzwerkverbindung verloren geht',
|
||||||
AP_PROVIDE_TEXT_2: 'Wenn WiFi nicht verbunden',
|
|
||||||
AP_PROVIDE_TEXT_3: 'Niemals',
|
AP_PROVIDE_TEXT_3: 'Niemals',
|
||||||
AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal',
|
AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal',
|
||||||
AP_HIDE_SSID: 'Verstecke SSID',
|
AP_HIDE_SSID: 'Verstecke SSID',
|
||||||
@@ -262,7 +261,6 @@ const de: Translation = {
|
|||||||
SCAN_AGAIN: 'Erneute Suche',
|
SCAN_AGAIN: 'Erneute Suche',
|
||||||
NETWORK_SCANNER: 'Netzwerksuche',
|
NETWORK_SCANNER: 'Netzwerksuche',
|
||||||
NETWORK_NO_WIFI: 'Keine WiFi-Netzwerke gefunden',
|
NETWORK_NO_WIFI: 'Keine WiFi-Netzwerke gefunden',
|
||||||
NETWORK_BLANK_SSID: 'Freilassen, um WiFi zu deaktivieren und ETH zu aktivieren.',
|
|
||||||
NETWORK_BLANK_BSSID: 'Freilassen, um nur SSID für die Verbindung zu nutzen.',
|
NETWORK_BLANK_BSSID: 'Freilassen, um nur SSID für die Verbindung zu nutzen.',
|
||||||
TX_POWER: 'Tx Leistung',
|
TX_POWER: 'Tx Leistung',
|
||||||
HOSTNAME: 'Hostname',
|
HOSTNAME: 'Hostname',
|
||||||
@@ -350,7 +348,7 @@ const de: Translation = {
|
|||||||
NO_DATA_1: 'Keine favorisierten EMS-Entitäten gefunden! Verwenden Sie das Modul',
|
NO_DATA_1: 'Keine favorisierten EMS-Entitäten gefunden! Verwenden Sie das Modul',
|
||||||
NO_DATA_2: ', um sie zu markieren.',
|
NO_DATA_2: ', um sie zu markieren.',
|
||||||
NO_DATA_3: 'Um alle verfügbaren Entitäten anzuzeigen, gehen Sie zu',
|
NO_DATA_3: 'Um alle verfügbaren Entitäten anzuzeigen, gehen Sie zu',
|
||||||
NO_GPIO:'Keine freien GPIO gefunden',
|
NO_GPIO: 'Keine freien GPIO gefunden',
|
||||||
THIS_VERSION: 'Diese Version',
|
THIS_VERSION: 'Diese Version',
|
||||||
PLATFORM: 'Plattform',
|
PLATFORM: 'Plattform',
|
||||||
RELEASE_TYPE: 'Release Typ',
|
RELEASE_TYPE: 'Release Typ',
|
||||||
@@ -362,9 +360,9 @@ const de: Translation = {
|
|||||||
STORED_VERSIONS: 'Gespeicherte Versionen',
|
STORED_VERSIONS: 'Gespeicherte Versionen',
|
||||||
ONLINE_HELP: 'Online-Hilfe',
|
ONLINE_HELP: 'Online-Hilfe',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
|
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Diese Aktualisierung erfordert eine Werkseinstellung. Stellen Sie sicher, dass Sie eine Sicherung Ihrer Konfiguration und Einstellungen vor dem Fortfahren erstellt haben und diese nach der Installation der neuen Version hochladen.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Für diese Aktualisierung ist ein Werksreset erforderlich. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und -einstellungen erstellen. Alle Passwörter werden im Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?'
|
WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und Einstellungen erstellen. Alle Passwörter werden in dieser Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?'
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const en: Translation = {
|
|||||||
TIME_ZONE: 'Time Zone',
|
TIME_ZONE: 'Time Zone',
|
||||||
ACCESS_POINT: 'Access Point',
|
ACCESS_POINT: 'Access Point',
|
||||||
AP_PROVIDE: 'Enable Access Point',
|
AP_PROVIDE: 'Enable Access Point',
|
||||||
AP_PROVIDE_TEXT_1: 'Always',
|
AP_PROVIDE_TEXT_2: 'When network connection is lost',
|
||||||
AP_PROVIDE_TEXT_2: 'When WiFi is disconnected',
|
|
||||||
AP_PROVIDE_TEXT_3: 'Never',
|
AP_PROVIDE_TEXT_3: 'Never',
|
||||||
AP_PREFERRED_CHANNEL: 'Preferred Channel',
|
AP_PREFERRED_CHANNEL: 'Preferred Channel',
|
||||||
AP_HIDE_SSID: 'Hide SSID',
|
AP_HIDE_SSID: 'Hide SSID',
|
||||||
@@ -262,7 +261,6 @@ const en: Translation = {
|
|||||||
SCAN_AGAIN: 'Scan again',
|
SCAN_AGAIN: 'Scan again',
|
||||||
NETWORK_SCANNER: 'Network Scanner',
|
NETWORK_SCANNER: 'Network Scanner',
|
||||||
NETWORK_NO_WIFI: 'No WiFi networks found',
|
NETWORK_NO_WIFI: 'No WiFi networks found',
|
||||||
NETWORK_BLANK_SSID: 'leave blank to disable WiFi and enable ETH',
|
|
||||||
NETWORK_BLANK_BSSID: 'leave blank to use only SSID',
|
NETWORK_BLANK_BSSID: 'leave blank to use only SSID',
|
||||||
TX_POWER: 'Tx Power',
|
TX_POWER: 'Tx Power',
|
||||||
HOSTNAME: 'Hostname',
|
HOSTNAME: 'Hostname',
|
||||||
@@ -350,7 +348,7 @@ const en: Translation = {
|
|||||||
NO_DATA_1: 'No favorite EMS entities found yet. Use the',
|
NO_DATA_1: 'No favorite EMS entities found yet. Use the',
|
||||||
NO_DATA_2: 'module to mark them.',
|
NO_DATA_2: 'module to mark them.',
|
||||||
NO_DATA_3: 'To see all available entities go to',
|
NO_DATA_3: 'To see all available entities go to',
|
||||||
NO_GPIO:'No available GPIO found',
|
NO_GPIO: 'No available GPIO found',
|
||||||
THIS_VERSION: 'This Version',
|
THIS_VERSION: 'This Version',
|
||||||
PLATFORM: 'Platform',
|
PLATFORM: 'Platform',
|
||||||
RELEASE_TYPE: 'Release Type',
|
RELEASE_TYPE: 'Release Type',
|
||||||
@@ -362,7 +360,7 @@ const en: Translation = {
|
|||||||
STORED_VERSIONS: 'Stored Versions',
|
STORED_VERSIONS: 'Stored Versions',
|
||||||
ONLINE_HELP: 'online help',
|
ONLINE_HELP: 'online help',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
|
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you have made a backup of your configuration and settings before continuing, and upload this after the new version is installed.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file after the new version is installed.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.',
|
||||||
WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?'
|
WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const fr: Translation = {
|
|||||||
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_2: 'quand la connexion réseau est perdue',
|
||||||
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
|
|
||||||
AP_PROVIDE_TEXT_3: 'jamais',
|
AP_PROVIDE_TEXT_3: 'jamais',
|
||||||
AP_PREFERRED_CHANNEL: 'Canal préféré',
|
AP_PREFERRED_CHANNEL: 'Canal préféré',
|
||||||
AP_HIDE_SSID: 'Cacher le SSID',
|
AP_HIDE_SSID: 'Cacher le SSID',
|
||||||
@@ -262,7 +261,6 @@ const fr: Translation = {
|
|||||||
SCAN_AGAIN: 'Rescanner',
|
SCAN_AGAIN: 'Rescanner',
|
||||||
NETWORK_SCANNER: 'Scan réseau',
|
NETWORK_SCANNER: 'Scan réseau',
|
||||||
NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé',
|
NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé',
|
||||||
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi',
|
|
||||||
NETWORK_BLANK_BSSID: 'laisser vide pour utiliser uniquement le SSID',
|
NETWORK_BLANK_BSSID: 'laisser vide pour utiliser uniquement le SSID',
|
||||||
TX_POWER: 'Puissance Tx',
|
TX_POWER: 'Puissance Tx',
|
||||||
HOSTNAME: "Nom d'hôte",
|
HOSTNAME: "Nom d'hôte",
|
||||||
@@ -350,7 +348,7 @@ const fr: Translation = {
|
|||||||
NO_DATA_1: 'Aucune entité EMS favorite trouvée. Utilisez le',
|
NO_DATA_1: 'Aucune entité EMS favorite trouvée. Utilisez le',
|
||||||
NO_DATA_2: 'module pour les marquer.',
|
NO_DATA_2: 'module pour les marquer.',
|
||||||
NO_DATA_3: 'Pour voir toutes les entités disponibles, aller à',
|
NO_DATA_3: 'Pour voir toutes les entités disponibles, aller à',
|
||||||
NO_GPIO:"Aucun GPIO disponible n'a été détecté",
|
NO_GPIO: "Aucun GPIO disponible n'a été détecté",
|
||||||
THIS_VERSION: 'Cette version',
|
THIS_VERSION: 'Cette version',
|
||||||
PLATFORM: 'Plateforme',
|
PLATFORM: 'Plateforme',
|
||||||
RELEASE_TYPE: 'Type de version',
|
RELEASE_TYPE: 'Type de version',
|
||||||
@@ -362,7 +360,7 @@ const fr: Translation = {
|
|||||||
STORED_VERSIONS: 'Versions stockées',
|
STORED_VERSIONS: 'Versions stockées',
|
||||||
ONLINE_HELP: 'aide en ligne',
|
ONLINE_HELP: 'aide en ligne',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
|
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous d\'avoir créé une sauvegarde de vos configurations et paramètres avant de continuer et de la charger après l\'installation de la nouvelle version.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système avant de continuer, et de la charger après l\'installation de la nouvelle version.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?'
|
WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const it: Translation = {
|
|||||||
TIME_ZONE: 'Fuso orario',
|
TIME_ZONE: 'Fuso orario',
|
||||||
ACCESS_POINT: 'Access Point',
|
ACCESS_POINT: 'Access Point',
|
||||||
AP_PROVIDE: 'Abilita Access Point',
|
AP_PROVIDE: 'Abilita Access Point',
|
||||||
AP_PROVIDE_TEXT_1: 'sempre',
|
AP_PROVIDE_TEXT_2: 'quando la connessione di rete è persa',
|
||||||
AP_PROVIDE_TEXT_2: 'quando WiFi é disconnessa',
|
|
||||||
AP_PROVIDE_TEXT_3: 'mai',
|
AP_PROVIDE_TEXT_3: 'mai',
|
||||||
AP_PREFERRED_CHANNEL: 'Canale preferito',
|
AP_PREFERRED_CHANNEL: 'Canale preferito',
|
||||||
AP_HIDE_SSID: 'Nascondi SSID',
|
AP_HIDE_SSID: 'Nascondi SSID',
|
||||||
@@ -262,7 +261,6 @@ const it: Translation = {
|
|||||||
SCAN_AGAIN: 'Scansiona ancora',
|
SCAN_AGAIN: 'Scansiona ancora',
|
||||||
NETWORK_SCANNER: 'Scansione Rete',
|
NETWORK_SCANNER: 'Scansione Rete',
|
||||||
NETWORK_NO_WIFI: 'Nessuana rete WiFi trovata',
|
NETWORK_NO_WIFI: 'Nessuana rete WiFi trovata',
|
||||||
NETWORK_BLANK_SSID: 'lasciare vuoto per disattivare WiFi',
|
|
||||||
NETWORK_BLANK_BSSID: 'lasciare vuoto per usare solo SSID',
|
NETWORK_BLANK_BSSID: 'lasciare vuoto per usare solo SSID',
|
||||||
TX_POWER: 'Potenza Tx',
|
TX_POWER: 'Potenza Tx',
|
||||||
HOSTNAME: 'Nome ospite',
|
HOSTNAME: 'Nome ospite',
|
||||||
@@ -350,7 +348,7 @@ const it: Translation = {
|
|||||||
NO_DATA_1: 'Nessuna entità EMS preferita trovata. Usa il',
|
NO_DATA_1: 'Nessuna entità EMS preferita trovata. Usa il',
|
||||||
NO_DATA_2: 'modulo per marcarle.',
|
NO_DATA_2: 'modulo per marcarle.',
|
||||||
NO_DATA_3: 'Per vedere tutte le entità disponibili vai a',
|
NO_DATA_3: 'Per vedere tutte le entità disponibili vai a',
|
||||||
NO_GPIO:'Non è stato trovato alcun GPIO disponibile',
|
NO_GPIO: 'Non è stato trovato alcun GPIO disponibile',
|
||||||
THIS_VERSION: 'Questa versione',
|
THIS_VERSION: 'Questa versione',
|
||||||
PLATFORM: 'Piattaforma',
|
PLATFORM: 'Piattaforma',
|
||||||
RELEASE_TYPE: 'Tipo di rilascio',
|
RELEASE_TYPE: 'Tipo di rilascio',
|
||||||
@@ -362,7 +360,7 @@ const it: Translation = {
|
|||||||
STORED_VERSIONS: 'Versioni memorizzate',
|
STORED_VERSIONS: 'Versioni memorizzate',
|
||||||
ONLINE_HELP: 'aiuto online',
|
ONLINE_HELP: 'aiuto online',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
|
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di aver creato un backup delle tue configurazioni e impostazioni prima di continuare e di caricarlo dopo l\'installazione della nuova versione.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file dopo l\'installazione della nuova versione.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?'
|
WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const nl: Translation = {
|
|||||||
TIME_ZONE: 'Tijdzone',
|
TIME_ZONE: 'Tijdzone',
|
||||||
ACCESS_POINT: 'Access Point',
|
ACCESS_POINT: 'Access Point',
|
||||||
AP_PROVIDE: 'Activeer Access Point',
|
AP_PROVIDE: 'Activeer Access Point',
|
||||||
AP_PROVIDE_TEXT_1: 'altijd',
|
AP_PROVIDE_TEXT_2: 'als netwerk verbinding verloren gaat',
|
||||||
AP_PROVIDE_TEXT_2: 'als WiFi niet is verbonden',
|
|
||||||
AP_PROVIDE_TEXT_3: 'nooit',
|
AP_PROVIDE_TEXT_3: 'nooit',
|
||||||
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
|
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
|
||||||
AP_HIDE_SSID: 'SSID verbergen',
|
AP_HIDE_SSID: 'SSID verbergen',
|
||||||
@@ -262,7 +261,6 @@ const nl: Translation = {
|
|||||||
SCAN_AGAIN: 'Opnieuw scannen',
|
SCAN_AGAIN: 'Opnieuw scannen',
|
||||||
NETWORK_SCANNER: 'Netwerk Scannen',
|
NETWORK_SCANNER: 'Netwerk Scannen',
|
||||||
NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden',
|
NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden',
|
||||||
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
|
|
||||||
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken',
|
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken',
|
||||||
TX_POWER: 'Tx Vermogen',
|
TX_POWER: 'Tx Vermogen',
|
||||||
HOSTNAME: 'Hostnaam',
|
HOSTNAME: 'Hostnaam',
|
||||||
@@ -350,7 +348,7 @@ const nl: Translation = {
|
|||||||
NO_DATA_1: 'Er zijn nog geen favoriete EMS-entiteiten gevonden. Gebruik de',
|
NO_DATA_1: 'Er zijn nog geen favoriete EMS-entiteiten gevonden. Gebruik de',
|
||||||
NO_DATA_2: 'module om ze te markeren.',
|
NO_DATA_2: 'module om ze te markeren.',
|
||||||
NO_DATA_3: 'Om alle beschikbare entiteiten te zien, ga naar',
|
NO_DATA_3: 'Om alle beschikbare entiteiten te zien, ga naar',
|
||||||
NO_GPIO:'Er is geen beschikbare GPIO gevonden',
|
NO_GPIO: 'Er is geen beschikbare GPIO gevonden',
|
||||||
THIS_VERSION: 'Deze Versie',
|
THIS_VERSION: 'Deze Versie',
|
||||||
PLATFORM: 'Platform',
|
PLATFORM: 'Platform',
|
||||||
RELEASE_TYPE: 'Release Typ',
|
RELEASE_TYPE: 'Release Typ',
|
||||||
@@ -362,7 +360,7 @@ const nl: Translation = {
|
|||||||
STORED_VERSIONS: 'Opgeslagen versies',
|
STORED_VERSIONS: 'Opgeslagen versies',
|
||||||
ONLINE_HELP: 'online help',
|
ONLINE_HELP: 'online help',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
|
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u een back-up van uw configuratie en instellingen hebt gemaakt voordat u doorgaat en upload deze na de installatie van de nieuwe versie.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file na de installatie van de nieuwe versie.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?'
|
WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const no: Translation = {
|
|||||||
TIME_ZONE: 'Tidssone',
|
TIME_ZONE: 'Tidssone',
|
||||||
ACCESS_POINT: 'Aksesspunkt',
|
ACCESS_POINT: 'Aksesspunkt',
|
||||||
AP_PROVIDE: 'Aktiver Aksesspunkt',
|
AP_PROVIDE: 'Aktiver Aksesspunkt',
|
||||||
AP_PROVIDE_TEXT_1: 'alltid',
|
AP_PROVIDE_TEXT_2: 'når nettverksforbindelsen er utilgjengelig',
|
||||||
AP_PROVIDE_TEXT_2: 'når WiFi er utilgjengelig',
|
|
||||||
AP_PROVIDE_TEXT_3: 'aldri',
|
AP_PROVIDE_TEXT_3: 'aldri',
|
||||||
AP_PREFERRED_CHANNEL: 'Foretrukket kanal',
|
AP_PREFERRED_CHANNEL: 'Foretrukket kanal',
|
||||||
AP_HIDE_SSID: 'Skjul SSID',
|
AP_HIDE_SSID: 'Skjul SSID',
|
||||||
@@ -262,7 +261,6 @@ const no: Translation = {
|
|||||||
SCAN_AGAIN: 'Søk igjen',
|
SCAN_AGAIN: 'Søk igjen',
|
||||||
NETWORK_SCANNER: 'Nettverk Scanner',
|
NETWORK_SCANNER: 'Nettverk Scanner',
|
||||||
NETWORK_NO_WIFI: 'Ingen trådløse nett funnet',
|
NETWORK_NO_WIFI: 'Ingen trådløse nett funnet',
|
||||||
NETWORK_BLANK_SSID: 'la feltet være blankt for å deaktivisere trådløst nettverk',
|
|
||||||
NETWORK_BLANK_BSSID: 'la feltet være blankt for å bruke kun SSID',
|
NETWORK_BLANK_BSSID: 'la feltet være blankt for å bruke kun SSID',
|
||||||
TX_POWER: 'Tx Effekt',
|
TX_POWER: 'Tx Effekt',
|
||||||
HOSTNAME: 'Hostname',
|
HOSTNAME: 'Hostname',
|
||||||
@@ -350,7 +348,7 @@ const no: Translation = {
|
|||||||
NO_DATA_1: 'Ingen favoritte EMS enheter funnet enda. Bruk',
|
NO_DATA_1: 'Ingen favoritte EMS enheter funnet enda. Bruk',
|
||||||
NO_DATA_2: 'modul for å markere dem.',
|
NO_DATA_2: 'modul for å markere dem.',
|
||||||
NO_DATA_3: 'For å se alle tilgjengelige enheter, gå til',
|
NO_DATA_3: 'For å se alle tilgjengelige enheter, gå til',
|
||||||
NO_GPIO:'Det ble ikke funnet noen tilgjengelige GPIO-porter',
|
NO_GPIO: 'Det ble ikke funnet noen tilgjengelige GPIO-porter',
|
||||||
THIS_VERSION: 'Denne versjonen',
|
THIS_VERSION: 'Denne versjonen',
|
||||||
PLATFORM: 'Plattform',
|
PLATFORM: 'Plattform',
|
||||||
RELEASE_TYPE: 'Utgivelses type',
|
RELEASE_TYPE: 'Utgivelses type',
|
||||||
@@ -362,7 +360,7 @@ const no: Translation = {
|
|||||||
STORED_VERSIONS: 'Lagret versjoner',
|
STORED_VERSIONS: 'Lagret versjoner',
|
||||||
ONLINE_HELP: 'online hjelp',
|
ONLINE_HELP: 'online hjelp',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
|
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du har laget en sikkerhetskopi av din konfigurasjon og innstillinger før du fortsetter, og last denne opp etter at den nye versjonen er installert.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen etter at den nye versjonen er installert.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?'
|
WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,6 @@ const pl: BaseTranslation = {
|
|||||||
TIME_ZONE: 'Strefa czasowa',
|
TIME_ZONE: 'Strefa czasowa',
|
||||||
ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}',
|
ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}',
|
||||||
AP_PROVIDE: 'Punkt dostępowy',
|
AP_PROVIDE: 'Punkt dostępowy',
|
||||||
AP_PROVIDE_TEXT_1: 'zawsze aktywny',
|
|
||||||
AP_PROVIDE_TEXT_2: 'aktywny jeśli brak połączenia z siecią',
|
AP_PROVIDE_TEXT_2: 'aktywny jeśli brak połączenia z siecią',
|
||||||
AP_PROVIDE_TEXT_3: 'nieaktywny',
|
AP_PROVIDE_TEXT_3: 'nieaktywny',
|
||||||
AP_PREFERRED_CHANNEL: 'Preferowany kanał',
|
AP_PREFERRED_CHANNEL: 'Preferowany kanał',
|
||||||
@@ -262,7 +261,6 @@ const pl: BaseTranslation = {
|
|||||||
SCAN_AGAIN: 'Skanuj ponownie',
|
SCAN_AGAIN: 'Skanuj ponownie',
|
||||||
NETWORK_SCANNER: 'Skaner sieci WiFi',
|
NETWORK_SCANNER: 'Skaner sieci WiFi',
|
||||||
NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu',
|
NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu',
|
||||||
NETWORK_BLANK_SSID: 'pozostaw puste aby wyłączyć WiFi i włączyć ETH',
|
|
||||||
NETWORK_BLANK_BSSID: 'pozostaw puste aby używać tylko SSID',
|
NETWORK_BLANK_BSSID: 'pozostaw puste aby używać tylko SSID',
|
||||||
TX_POWER: 'Moc nadawania',
|
TX_POWER: 'Moc nadawania',
|
||||||
HOSTNAME: 'Nazwa w sieci',
|
HOSTNAME: 'Nazwa w sieci',
|
||||||
@@ -350,7 +348,7 @@ const pl: BaseTranslation = {
|
|||||||
NO_DATA_1: 'Brak ulubionych encji EMS. Użyj',
|
NO_DATA_1: 'Brak ulubionych encji EMS. Użyj',
|
||||||
NO_DATA_2: 'moduł do ich oznaczenia.',
|
NO_DATA_2: 'moduł do ich oznaczenia.',
|
||||||
NO_DATA_3: 'Aby zobaczyć wszystkie dostępne encje przejdź do',
|
NO_DATA_3: 'Aby zobaczyć wszystkie dostępne encje przejdź do',
|
||||||
NO_GPIO:'Nie znaleziono dostępnych pinów GPIO',
|
NO_GPIO: 'Nie znaleziono dostępnych pinów GPIO',
|
||||||
THIS_VERSION: 'Ta wersja',
|
THIS_VERSION: 'Ta wersja',
|
||||||
PLATFORM: 'Platforma',
|
PLATFORM: 'Platforma',
|
||||||
RELEASE_TYPE: 'Typ wydania',
|
RELEASE_TYPE: 'Typ wydania',
|
||||||
@@ -362,7 +360,7 @@ const pl: BaseTranslation = {
|
|||||||
STORED_VERSIONS: 'Zapisane wersje',
|
STORED_VERSIONS: 'Zapisane wersje',
|
||||||
ONLINE_HELP: 'pomoc online',
|
ONLINE_HELP: 'pomoc online',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
|
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że masz utworzoną kopię swoich ustawień i konfiguracji przed kontynuowaniem i przesuń ją po zainstalowaniu nowej wersji.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik po zainstalowaniu nowej wersji.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.',
|
||||||
WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?'
|
WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const sk: Translation = {
|
|||||||
TIME_ZONE: 'Časová zóna',
|
TIME_ZONE: 'Časová zóna',
|
||||||
ACCESS_POINT: 'Prístupový bod',
|
ACCESS_POINT: 'Prístupový bod',
|
||||||
AP_PROVIDE: 'Povoliť prístupový bod',
|
AP_PROVIDE: 'Povoliť prístupový bod',
|
||||||
AP_PROVIDE_TEXT_1: 'vždy',
|
AP_PROVIDE_TEXT_2: 'keď je sieťové pripojenie stratené',
|
||||||
AP_PROVIDE_TEXT_2: 'keď je WiFi odpojená',
|
|
||||||
AP_PROVIDE_TEXT_3: 'nikdy',
|
AP_PROVIDE_TEXT_3: 'nikdy',
|
||||||
AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
|
AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
|
||||||
AP_HIDE_SSID: 'Skryť SSID',
|
AP_HIDE_SSID: 'Skryť SSID',
|
||||||
@@ -262,7 +261,6 @@ const sk: Translation = {
|
|||||||
SCAN_AGAIN: 'Skenovať znova',
|
SCAN_AGAIN: 'Skenovať znova',
|
||||||
NETWORK_SCANNER: 'Sieťový skener',
|
NETWORK_SCANNER: 'Sieťový skener',
|
||||||
NETWORK_NO_WIFI: 'WiFi siete nenájdené',
|
NETWORK_NO_WIFI: 'WiFi siete nenájdené',
|
||||||
NETWORK_BLANK_SSID: 'nechajte prázdne, ak chcete zakázať WiFi a povoliť ETH',
|
|
||||||
NETWORK_BLANK_BSSID: 'ponechajte prázdne, ak chcete používať iba SSID',
|
NETWORK_BLANK_BSSID: 'ponechajte prázdne, ak chcete používať iba SSID',
|
||||||
TX_POWER: 'Tx výkon',
|
TX_POWER: 'Tx výkon',
|
||||||
HOSTNAME: 'Hostname',
|
HOSTNAME: 'Hostname',
|
||||||
@@ -350,7 +348,7 @@ const sk: Translation = {
|
|||||||
NO_DATA_1: 'Nenašli sa žiadne obľúbené entity EMS. Použite',
|
NO_DATA_1: 'Nenašli sa žiadne obľúbené entity EMS. Použite',
|
||||||
NO_DATA_2: 'modul na ich označenie.',
|
NO_DATA_2: 'modul na ich označenie.',
|
||||||
NO_DATA_3: 'Ak chcete zobraziť všetky dostupné entity, prejdite na',
|
NO_DATA_3: 'Ak chcete zobraziť všetky dostupné entity, prejdite na',
|
||||||
NO_GPIO:'Nebol nájdený žiadny dostupný GPIO',
|
NO_GPIO: 'Nebol nájdený žiadny dostupný GPIO',
|
||||||
THIS_VERSION: 'Táto verzia',
|
THIS_VERSION: 'Táto verzia',
|
||||||
PLATFORM: 'Platforma',
|
PLATFORM: 'Platforma',
|
||||||
RELEASE_TYPE: 'Typ vydania',
|
RELEASE_TYPE: 'Typ vydania',
|
||||||
@@ -362,7 +360,7 @@ const sk: Translation = {
|
|||||||
STORED_VERSIONS: 'Uložené verzie',
|
STORED_VERSIONS: 'Uložené verzie',
|
||||||
ONLINE_HELP: 'online pomoc',
|
ONLINE_HELP: 'online pomoc',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
|
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že ste vytvorili zálohu svojich konfigurácií a nastavení pred pokračovaním a nahrajte ju po instalácii novej verzie.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor po instalácii novej verzie.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?'
|
WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const sv: Translation = {
|
|||||||
TIME_ZONE: 'Tidszon',
|
TIME_ZONE: 'Tidszon',
|
||||||
ACCESS_POINT: 'Accesspunkt',
|
ACCESS_POINT: 'Accesspunkt',
|
||||||
AP_PROVIDE: 'Aktivera accesspunkt',
|
AP_PROVIDE: 'Aktivera accesspunkt',
|
||||||
AP_PROVIDE_TEXT_1: 'alltid',
|
AP_PROVIDE_TEXT_2: 'när nätverksanslutningen är bortkopplad',
|
||||||
AP_PROVIDE_TEXT_2: 'när WiFi är nedkopplat',
|
|
||||||
AP_PROVIDE_TEXT_3: 'aldrig',
|
AP_PROVIDE_TEXT_3: 'aldrig',
|
||||||
AP_PREFERRED_CHANNEL: 'Kanal',
|
AP_PREFERRED_CHANNEL: 'Kanal',
|
||||||
AP_HIDE_SSID: 'Göm SSID',
|
AP_HIDE_SSID: 'Göm SSID',
|
||||||
@@ -262,7 +261,6 @@ const sv: Translation = {
|
|||||||
SCAN_AGAIN: 'Sök igen',
|
SCAN_AGAIN: 'Sök igen',
|
||||||
NETWORK_SCANNER: 'Hittade nätverk',
|
NETWORK_SCANNER: 'Hittade nätverk',
|
||||||
NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades',
|
NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades',
|
||||||
NETWORK_BLANK_SSID: 'lämna blankt för att inaktivera WiFi',
|
|
||||||
NETWORK_BLANK_BSSID: 'lämna blankt för att bara använda SSID',
|
NETWORK_BLANK_BSSID: 'lämna blankt för att bara använda SSID',
|
||||||
TX_POWER: 'Tx effekt',
|
TX_POWER: 'Tx effekt',
|
||||||
HOSTNAME: 'Värdnamn',
|
HOSTNAME: 'Värdnamn',
|
||||||
@@ -350,7 +348,7 @@ const sv: Translation = {
|
|||||||
NO_DATA_1: 'Inga favorit EMS enheter hittade än. Använd',
|
NO_DATA_1: 'Inga favorit EMS enheter hittade än. Använd',
|
||||||
NO_DATA_2: 'modul för att markera dem.',
|
NO_DATA_2: 'modul för att markera dem.',
|
||||||
NO_DATA_3: 'För att se alla tillgängliga enheter, gå till',
|
NO_DATA_3: 'För att se alla tillgängliga enheter, gå till',
|
||||||
NO_GPIO:'Inga tillgängliga GPIO-portar hittades',
|
NO_GPIO: 'Inga tillgängliga GPIO-portar hittades',
|
||||||
THIS_VERSION: 'Denna version',
|
THIS_VERSION: 'Denna version',
|
||||||
PLATFORM: 'Plattform',
|
PLATFORM: 'Plattform',
|
||||||
RELEASE_TYPE: 'Utgivelsestyp',
|
RELEASE_TYPE: 'Utgivelsestyp',
|
||||||
@@ -362,7 +360,7 @@ const sv: Translation = {
|
|||||||
STORED_VERSIONS: 'Lagrad versioner',
|
STORED_VERSIONS: 'Lagrad versioner',
|
||||||
ONLINE_HELP: 'online hjälp',
|
ONLINE_HELP: 'online hjälp',
|
||||||
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
|
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du har gjort en säkerhetskopia av din konfiguration och inställningar innan du fortsätter och ladda upp denna efter att den nya versionen är installerad.',
|
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil efter att den nya versionen är installerad.',
|
||||||
UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.',
|
UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.',
|
||||||
WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?'
|
WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?'
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,7 @@ const tr: Translation = {
|
|||||||
TIME_ZONE: 'Saat dilimi',
|
TIME_ZONE: 'Saat dilimi',
|
||||||
ACCESS_POINT: 'Erişim Noktası',
|
ACCESS_POINT: 'Erişim Noktası',
|
||||||
AP_PROVIDE: 'Erişim noktasını çalıştır',
|
AP_PROVIDE: 'Erişim noktasını çalıştır',
|
||||||
AP_PROVIDE_TEXT_1: 'her zaman',
|
AP_PROVIDE_TEXT_2: 'Ağ bağlantısı kesildiğinde',
|
||||||
AP_PROVIDE_TEXT_2: 'Kablosuz bağlantı kesildiğinde',
|
|
||||||
AP_PROVIDE_TEXT_3: 'asla',
|
AP_PROVIDE_TEXT_3: 'asla',
|
||||||
AP_PREFERRED_CHANNEL: 'Tercih edilen kanal',
|
AP_PREFERRED_CHANNEL: 'Tercih edilen kanal',
|
||||||
AP_HIDE_SSID: 'SSID yi gizle',
|
AP_HIDE_SSID: 'SSID yi gizle',
|
||||||
@@ -262,7 +261,6 @@ const tr: Translation = {
|
|||||||
SCAN_AGAIN: 'Tekrar tara',
|
SCAN_AGAIN: 'Tekrar tara',
|
||||||
NETWORK_SCANNER: 'Ağ Tarayıcısı',
|
NETWORK_SCANNER: 'Ağ Tarayıcısı',
|
||||||
NETWORK_NO_WIFI: 'Hiçbir Kablosuz Ağ bulunamadı',
|
NETWORK_NO_WIFI: 'Hiçbir Kablosuz Ağ bulunamadı',
|
||||||
NETWORK_BLANK_SSID: 'Kablosuz ağı devre dışı bırakmak için boş bırakın',
|
|
||||||
NETWORK_BLANK_BSSID: 'sadece SSID kullanmak için boş bırakın',
|
NETWORK_BLANK_BSSID: 'sadece SSID kullanmak için boş bırakın',
|
||||||
TX_POWER: 'Aktarım gücü',
|
TX_POWER: 'Aktarım gücü',
|
||||||
HOSTNAME: 'Ana Makine Adı',
|
HOSTNAME: 'Ana Makine Adı',
|
||||||
@@ -350,7 +348,7 @@ const tr: Translation = {
|
|||||||
NO_DATA_1: 'Henüz bir favori EMS varlığı bulunamadı. Kullanın',
|
NO_DATA_1: 'Henüz bir favori EMS varlığı bulunamadı. Kullanın',
|
||||||
NO_DATA_2: 'modülünü kullanın.',
|
NO_DATA_2: 'modülünü kullanın.',
|
||||||
NO_DATA_3: 'Tüm kullanılabilir varlıkları görmek için git',
|
NO_DATA_3: 'Tüm kullanılabilir varlıkları görmek için git',
|
||||||
NO_GPIO:'Kullanılabilir GPIO bulunamadı',
|
NO_GPIO: 'Kullanılabilir GPIO bulunamadı',
|
||||||
THIS_VERSION: 'Bu Sürüm',
|
THIS_VERSION: 'Bu Sürüm',
|
||||||
PLATFORM: 'Platforma',
|
PLATFORM: 'Platforma',
|
||||||
RELEASE_TYPE: 'Sürüm Tipi',
|
RELEASE_TYPE: 'Sürüm Tipi',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export enum APProvisionMode {
|
export enum APProvisionMode {
|
||||||
AP_MODE_ALWAYS = 0,
|
|
||||||
AP_MODE_DISCONNECTED = 1,
|
AP_MODE_DISCONNECTED = 1,
|
||||||
AP_NEVER = 2
|
AP_NEVER = 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export * from './ntp';
|
|||||||
export * from './security';
|
export * from './security';
|
||||||
export * from './signin';
|
export * from './signin';
|
||||||
export * from './system';
|
export * from './system';
|
||||||
|
export * from './versions';
|
||||||
|
|||||||
23
interface/src/types/versions.ts
Normal file
23
interface/src/types/versions.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Types for the `getVersions` action response coming from the device.
|
||||||
|
// The device proxies the request to emsesp.org/versions.json. If the device
|
||||||
|
// is offline the `stable` and `dev` fields are omitted.
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
version: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteVersionInfo extends VersionInfo {
|
||||||
|
upgradeable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentVersionInfo extends VersionInfo {
|
||||||
|
type: 'stable' | 'dev';
|
||||||
|
upgradeable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionsResponse {
|
||||||
|
current: CurrentVersionInfo;
|
||||||
|
stable?: RemoteVersionInfo;
|
||||||
|
dev?: RemoteVersionInfo;
|
||||||
|
}
|
||||||
@@ -1,34 +1,27 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const usePersistState = <T>(
|
export const usePersistState = <T>(
|
||||||
initial_value: T,
|
initial_value: T,
|
||||||
id: string
|
id: string
|
||||||
): [T, (new_state: T) => void] => {
|
): [T, (new_state: T) => void] => {
|
||||||
// Set initial value - only computed once on mount
|
const [state, setState] = useState<T>(() => {
|
||||||
const _initial_value = useMemo(() => {
|
|
||||||
try {
|
try {
|
||||||
const local_storage_value_str = localStorage.getItem(`state:${id}`);
|
const stored = localStorage.getItem(`state:${id}`);
|
||||||
// If there is a value stored in localStorage, use that
|
if (stored) {
|
||||||
if (local_storage_value_str) {
|
return JSON.parse(stored) as T;
|
||||||
return JSON.parse(local_storage_value_str) as T;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If parsing fails, fall back to initial_value
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed to parse localStorage value for key "state:${id}"`,
|
`Failed to parse localStorage value for key "state:${id}"`,
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Otherwise use initial_value that was passed to the function
|
|
||||||
return initial_value;
|
return initial_value;
|
||||||
}, [id]); // initial_value intentionally omitted - only read on first mount
|
});
|
||||||
|
|
||||||
const [state, setState] = useState(_initial_value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const state_str = JSON.stringify(state);
|
localStorage.setItem(`state:${id}`, JSON.stringify(state));
|
||||||
localStorage.setItem(`state:${id}`, state_str);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed to save state to localStorage for key "state:${id}"`,
|
`Failed to save state to localStorage for key "state:${id}"`,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useBlocker } from 'react-router';
|
import { useBlocker } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
@@ -54,61 +54,44 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
|||||||
}
|
}
|
||||||
}, [readData]);
|
}, [readData]);
|
||||||
|
|
||||||
const saveData = useCallback(async () => {
|
const saveData = async () => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
// Reset states before saving
|
|
||||||
setRestartNeeded(false);
|
setRestartNeeded(false);
|
||||||
setErrorMessage(undefined);
|
setErrorMessage(undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await writeData(data as D);
|
await writeData(data as D);
|
||||||
// Only update origData on successful save (dirtyFlags cleared by onSuccess handler)
|
|
||||||
setOrigData(data as D);
|
setOrigData(data as D);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
if (message === REBOOT_ERROR_MESSAGE) {
|
if (message === REBOOT_ERROR_MESSAGE) {
|
||||||
setRestartNeeded(true);
|
setRestartNeeded(true);
|
||||||
return; // Early return - save succeeded but needs reboot
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore original data on validation error
|
|
||||||
if (origData) {
|
if (origData) {
|
||||||
updateData({ data: origData });
|
updateData({ data: origData });
|
||||||
}
|
}
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
setErrorMessage(message);
|
setErrorMessage(message);
|
||||||
setDirtyFlags([]); // Clear flags so user can retry
|
setDirtyFlags([]);
|
||||||
}
|
}
|
||||||
}, [data, writeData, origData, updateData]);
|
};
|
||||||
|
|
||||||
return useMemo(
|
return {
|
||||||
() => ({
|
loadData,
|
||||||
loadData,
|
saveData,
|
||||||
saveData,
|
saving: !!saving,
|
||||||
saving: !!saving,
|
updateDataValue,
|
||||||
updateDataValue,
|
data: data as D,
|
||||||
data: data as D,
|
origData: origData as D,
|
||||||
origData: origData as D,
|
dirtyFlags,
|
||||||
dirtyFlags,
|
setDirtyFlags,
|
||||||
setDirtyFlags,
|
setOrigData,
|
||||||
setOrigData,
|
blocker,
|
||||||
blocker,
|
errorMessage,
|
||||||
errorMessage,
|
restartNeeded
|
||||||
restartNeeded
|
};
|
||||||
}),
|
|
||||||
[
|
|
||||||
loadData,
|
|
||||||
saveData,
|
|
||||||
saving,
|
|
||||||
updateDataValue,
|
|
||||||
data,
|
|
||||||
origData,
|
|
||||||
dirtyFlags,
|
|
||||||
blocker,
|
|
||||||
errorMessage,
|
|
||||||
restartNeeded
|
|
||||||
]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
import type { InternalRuleItem, ValidateOption } from 'async-validator';
|
import type {
|
||||||
|
InternalRuleItem,
|
||||||
|
ValidateFieldsError,
|
||||||
|
ValidateOption
|
||||||
|
} from 'async-validator';
|
||||||
import type Schema from 'async-validator';
|
import type Schema from 'async-validator';
|
||||||
|
|
||||||
|
export class ValidationError extends Error {
|
||||||
|
readonly fieldErrors: ValidateFieldsError;
|
||||||
|
|
||||||
|
constructor(fieldErrors: ValidateFieldsError) {
|
||||||
|
super('Validation failed');
|
||||||
|
this.name = 'ValidationError';
|
||||||
|
this.fieldErrors = fieldErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const validate = <T extends object>(
|
export const validate = <T extends object>(
|
||||||
validator: Schema,
|
validator: Schema,
|
||||||
source: Partial<T>,
|
source: Partial<T>,
|
||||||
@@ -8,7 +22,7 @@ export const validate = <T extends object>(
|
|||||||
): Promise<T> =>
|
): Promise<T> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
void validator.validate(source, options ?? {}, (errors, fieldErrors) => {
|
void validator.validate(source, options ?? {}, (errors, fieldErrors) => {
|
||||||
errors ? reject(fieldErrors as Error) : resolve(source as T);
|
errors ? reject(new ValidationError(fieldErrors)) : resolve(source as T);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ import { Plugin, PluginOption, defineConfig } from 'vite';
|
|||||||
import viteImagemin from 'vite-plugin-imagemin';
|
import viteImagemin from 'vite-plugin-imagemin';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
|
|
||||||
// @ts-expect-error - mock server doesn't have type declarations
|
|
||||||
import mockServer from '../mock-api/mockServer.js';
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const KB_DIVISOR = 1024;
|
const KB_DIVISOR = 1024;
|
||||||
const REPEAT_CHAR = '=';
|
const REPEAT_CHAR = '=';
|
||||||
const REPEAT_COUNT = 50;
|
const REPEAT_COUNT = 50;
|
||||||
const DEFAULT_OUT_DIR = 'dist';
|
const DEFAULT_OUT_DIR = 'dist';
|
||||||
const ES_TARGET = 'es2020';
|
const ES_TARGET = 'es2020';
|
||||||
const CHUNK_SIZE_WARNING_LIMIT = 512;
|
const CHUNK_SIZE_WARNING_LIMIT = 1024;
|
||||||
const ASSETS_INLINE_LIMIT = 4096;
|
const ASSETS_INLINE_LIMIT = 4096;
|
||||||
|
|
||||||
// Common resolve aliases
|
// Common resolve aliases
|
||||||
@@ -100,6 +97,10 @@ const createPreactPlugin = (devToolsEnabled: boolean) =>
|
|||||||
|
|
||||||
// Patch preact/compat to export stub React 19 APIs (use, useOptimistic) so that
|
// Patch preact/compat to export stub React 19 APIs (use, useOptimistic) so that
|
||||||
// react-router v7 doesn't trigger IMPORT_IS_UNDEFINED warnings from Rolldown.
|
// react-router v7 doesn't trigger IMPORT_IS_UNDEFINED warnings from Rolldown.
|
||||||
|
// Rolldown tracks the constant strings used in `React[REACT_USE]` /
|
||||||
|
// `React[USE_OPTIMISTIC]` lookups inside react-router and resolves them
|
||||||
|
// statically, so simply relying on a runtime guard is not enough — we need
|
||||||
|
// matching (stub) exports on the aliased preact/compat module.
|
||||||
const preactCompatPatchPlugin = (): Plugin => ({
|
const preactCompatPatchPlugin = (): Plugin => ({
|
||||||
name: 'preact-compat-react19-patch',
|
name: 'preact-compat-react19-patch',
|
||||||
transform(code, id) {
|
transform(code, id) {
|
||||||
@@ -130,40 +131,9 @@ const createBasePlugins = (
|
|||||||
return plugins;
|
return plugins;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manual chunk splitting strategy
|
const manualChunks = (id: string): string | undefined => {
|
||||||
const createManualChunks = (detailed = false) => {
|
if (id.includes('node_modules')) return 'vendor';
|
||||||
return (id: string): string | undefined => {
|
return undefined;
|
||||||
if (id.includes('node_modules')) {
|
|
||||||
if (id.includes('preact')) return '@preact';
|
|
||||||
if (detailed) {
|
|
||||||
if (id.includes('react-router')) return '@react-router';
|
|
||||||
if (id.includes('@mui/material')) return '@mui-material';
|
|
||||||
if (id.includes('@mui/icons-material')) return '@mui-icons';
|
|
||||||
if (id.includes('alova')) return '@alova';
|
|
||||||
if (id.includes('typesafe-i18n')) return '@i18n';
|
|
||||||
if (id.includes('react-toastify')) return '@toastify';
|
|
||||||
if (id.includes('@table-library')) return '@table-library';
|
|
||||||
if (id.includes('uuid')) return '@uuid';
|
|
||||||
if (id.includes('axios') || id.includes('fetch')) return '@http';
|
|
||||||
if (id.includes('lodash') || id.includes('ramda')) return '@utils';
|
|
||||||
}
|
|
||||||
return 'vendor';
|
|
||||||
}
|
|
||||||
if (detailed) {
|
|
||||||
// Group circularly dependent modules together to avoid circular chunk warnings
|
|
||||||
// components, app, and utils are tightly coupled, so combine them
|
|
||||||
if (
|
|
||||||
id.includes('components/') ||
|
|
||||||
id.includes('app/') ||
|
|
||||||
id.includes('utils/')
|
|
||||||
) {
|
|
||||||
return 'app';
|
|
||||||
}
|
|
||||||
// Keep api separate as it's typically more independent
|
|
||||||
if (id.includes('api/')) return 'api';
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Common build base configuration
|
// Common build base configuration
|
||||||
@@ -241,9 +211,11 @@ const imageOptimizationPlugin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(
|
||||||
({ command, mode }: { command: string; mode: string }) => {
|
async ({ command, mode }: { command: string; mode: string }) => {
|
||||||
if (command === 'serve') {
|
if (command === 'serve') {
|
||||||
console.log(`Preparing for standalone build with server, mode=${mode}`);
|
console.log(`Preparing for standalone build with server, mode=${mode}`);
|
||||||
|
// @ts-expect-error - mock server doesn't have type declarations
|
||||||
|
const { default: mockServer } = await import('../mock-api/mockServer.js');
|
||||||
return {
|
return {
|
||||||
plugins: [...createBasePlugins(true, true), mockServer()],
|
plugins: [...createBasePlugins(true, true), mockServer()],
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -260,8 +232,7 @@ export default defineConfig(
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false
|
||||||
},
|
},
|
||||||
'/rest': 'http://localhost:3080',
|
'/rest': 'http://localhost:3080'
|
||||||
'/gh': 'http://localhost:3080'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -290,7 +261,7 @@ export default defineConfig(
|
|||||||
moduleSideEffects: false
|
moduleSideEffects: false
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
manualChunks: createManualChunks(false)
|
manualChunks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,7 +301,7 @@ export default defineConfig(
|
|||||||
chunkFileNames: 'assets/[name]-[hash].js',
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
entryFileNames: 'assets/[name]-[hash].js',
|
entryFileNames: 'assets/[name]-[hash].js',
|
||||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||||
manualChunks: createManualChunks(true),
|
manualChunks,
|
||||||
sourcemap: false
|
sourcemap: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user