mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Compare commits
460 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ca0a0c634 | ||
|
|
30ce3f1dc3 | ||
|
|
15541c52ce | ||
|
|
20c3e8c7bd | ||
|
|
59797fb89c | ||
|
|
849cc85398 | ||
|
|
dd318a1c8e | ||
|
|
2edf2a4231 | ||
|
|
d58ee1e693 | ||
|
|
088cb7fe40 | ||
|
|
38498a5587 | ||
|
|
6e3b30b03c | ||
|
|
4e9cf72816 | ||
|
|
7eb1f061b7 | ||
|
|
5e4f5916f2 | ||
|
|
1450737d94 | ||
|
|
bfd20e559e | ||
|
|
98a7932dee | ||
|
|
19e26d0d64 | ||
|
|
1715218864 | ||
|
|
e503c6cd79 | ||
|
|
9515e3d00b | ||
|
|
53e25ae213 | ||
|
|
4863ecc329 | ||
|
|
b5892f5b5e | ||
|
|
d16502c872 | ||
|
|
e29fb9ba8a | ||
|
|
2ee0411582 | ||
|
|
f210466cb1 | ||
|
|
6af28b1c29 | ||
|
|
049be2484e | ||
|
|
8f438e8045 | ||
|
|
a8382dd6ce | ||
|
|
c55385d6d8 | ||
|
|
87e6691433 | ||
|
|
aa9ba65f70 | ||
|
|
39fef48915 | ||
|
|
362fead7e8 | ||
|
|
e809ed3743 | ||
|
|
dc8c322b42 | ||
|
|
c5688ab632 | ||
|
|
d5d75eee63 | ||
|
|
6ec16733c3 | ||
|
|
ce2b2658ad | ||
|
|
3a866b1aea | ||
|
|
1cf938e16a | ||
|
|
1df427366f | ||
|
|
f97cdcb4d6 | ||
|
|
15c682cd1e | ||
|
|
008983be26 | ||
|
|
2f01000665 | ||
|
|
616955daef | ||
|
|
e22b191a48 | ||
|
|
8930c52ada | ||
|
|
6d94335079 | ||
|
|
f2e0b193af | ||
|
|
5b55902cd9 | ||
|
|
694f647a2c | ||
|
|
fc1cb00523 | ||
|
|
2fda59d7db | ||
|
|
a95837404a | ||
|
|
c6db2a1adf | ||
|
|
f6d22732a0 | ||
|
|
220a69938f | ||
|
|
d438866864 | ||
|
|
2e0ed9ce9f | ||
|
|
5de3b69e2c | ||
|
|
dfd6798377 | ||
|
|
074ae2a5a1 | ||
|
|
77f6a18075 | ||
|
|
0762d9e124 | ||
|
|
37dae04715 | ||
|
|
f299a7ad14 | ||
|
|
ba295385ab | ||
|
|
33adf518ae | ||
|
|
93885d0dd5 | ||
|
|
747cda79db | ||
|
|
f3cfc38adc | ||
|
|
dd3a0a706d | ||
|
|
37d001e7b5 | ||
|
|
add09e5a1c | ||
|
|
561d1c0e55 | ||
|
|
239ba335b1 | ||
|
|
ec83123090 | ||
|
|
4f6d5164a4 | ||
|
|
ae1e2eccd2 | ||
|
|
d7bc821bbe | ||
|
|
f8579f7c96 | ||
|
|
046a9ef6f2 | ||
|
|
64e15542a2 | ||
|
|
7dec674452 | ||
|
|
0f48d3e72c | ||
|
|
1f793c49ae | ||
|
|
e581539cf9 | ||
|
|
736eee79df | ||
|
|
7a0fe3819b | ||
|
|
65c9bf7e52 | ||
|
|
1e61b5670e | ||
|
|
6c2bae6296 | ||
|
|
358d6010b0 | ||
|
|
82978a25c5 | ||
|
|
3519696bae | ||
|
|
bd33df2cc7 | ||
|
|
5f44eb14ad | ||
|
|
fb94cf953a | ||
|
|
d7486218bc | ||
|
|
59913cdc4b | ||
|
|
2d7449aeba | ||
|
|
3ea53a8012 | ||
|
|
48cd12ec3d | ||
|
|
7c71ed2dc6 | ||
|
|
24d8ccc52f | ||
|
|
05cd96f2be | ||
|
|
75795ab1e9 | ||
|
|
bb1602f179 | ||
|
|
ac268f0f73 | ||
|
|
d924567e5f | ||
|
|
109d8df782 | ||
|
|
0aaa35098d | ||
|
|
6f57beab28 | ||
|
|
5b26e27834 | ||
|
|
8429f650aa | ||
|
|
3eb2202117 | ||
|
|
c217a40710 | ||
|
|
4a7308c5bb | ||
|
|
5fba51103e | ||
|
|
95a9808f35 | ||
|
|
c09e180c48 | ||
|
|
b09b650c1d | ||
|
|
44a41b963d | ||
|
|
0510189f54 | ||
|
|
16b3cf764d | ||
|
|
c634c39874 | ||
|
|
ae0846e877 | ||
|
|
40e7e1b418 | ||
|
|
e2a5853dde | ||
|
|
e419e67cb0 | ||
|
|
3356a4ce14 | ||
|
|
50459a23fe | ||
|
|
26a4347155 | ||
|
|
5bf53c3389 | ||
|
|
4b7aa95be3 | ||
|
|
b5921d15ac | ||
|
|
82a4f1499a | ||
|
|
e99c9208ad | ||
|
|
65dae7af42 | ||
|
|
050c75944a | ||
|
|
2d96aa1736 | ||
|
|
85161ec09a | ||
|
|
967eee67c4 | ||
|
|
469f78a329 | ||
|
|
1e4eb52c90 | ||
|
|
fdbbfe8ddb | ||
|
|
e79d4603fc | ||
|
|
9ef2e62955 | ||
|
|
c234503a9c | ||
|
|
2056d3ff19 | ||
|
|
270298eb8a | ||
|
|
7e7bd29c9a | ||
|
|
19b37d9e0e | ||
|
|
fc2bcd50ca | ||
|
|
37da9d3755 | ||
|
|
fffed3b411 | ||
|
|
9738c0848d | ||
|
|
fc11db03f0 | ||
|
|
17a28d246d | ||
|
|
3143ed1060 | ||
|
|
50540f1f82 | ||
|
|
fad1b09e19 | ||
|
|
a1912405c7 | ||
|
|
cc30e09e4b | ||
|
|
07a943eedf | ||
|
|
adf4584717 | ||
|
|
b3d647850d | ||
|
|
ee6a09c9df | ||
|
|
a84ae9e7cc | ||
|
|
40206a27ac | ||
|
|
5c282b7a7e | ||
|
|
8dd18aa24d | ||
|
|
db43f2d711 | ||
|
|
e9741ea4f8 | ||
|
|
cf416ee080 | ||
|
|
af41f352ba | ||
|
|
feed65bea6 | ||
|
|
66f14fff82 | ||
|
|
70943f5758 | ||
|
|
3bc280b817 | ||
|
|
9e432efcd1 | ||
|
|
8417c715c1 | ||
|
|
ddb3633fdb | ||
|
|
62b15a5319 | ||
|
|
8dd18802d6 | ||
|
|
8530520a62 | ||
|
|
b077d867ba | ||
|
|
8c48639572 | ||
|
|
4a6ca636e5 | ||
|
|
4bce819bd3 | ||
|
|
de63d10e3d | ||
|
|
c62183f886 | ||
|
|
6a73ee4a0b | ||
|
|
964db8e7d7 | ||
|
|
5b66528c0b | ||
|
|
01fd90f3ed | ||
|
|
5e7bed1063 | ||
|
|
4d69846932 | ||
|
|
fec5ff3132 | ||
|
|
15df0c0552 | ||
|
|
42a362196e | ||
|
|
ab28013ec6 | ||
|
|
75f3a6f82a | ||
|
|
e467e73755 | ||
|
|
505e846dd8 | ||
|
|
7808959d67 | ||
|
|
1ecee740d3 | ||
|
|
47eaeba373 | ||
|
|
e7dbccabec | ||
|
|
6ff3d243bd | ||
|
|
4027003729 | ||
|
|
70fd0ad658 | ||
|
|
94127ad3eb | ||
|
|
44734713f1 | ||
|
|
2f0f45f3ec | ||
|
|
8641e9d9cb | ||
|
|
bc78dd3f50 | ||
|
|
a9fca73f2d | ||
|
|
5cccfacbc4 | ||
|
|
7425d0e095 | ||
|
|
28068bdb98 | ||
|
|
bc69ca0a9b | ||
|
|
fd9ac28254 | ||
|
|
312fd85469 | ||
|
|
57a516a83a | ||
|
|
37bee39cea | ||
|
|
ed843ba58d | ||
|
|
f8c7da6e0c | ||
|
|
e0b1ff1353 | ||
|
|
0f78df517f | ||
|
|
e7dae28922 | ||
|
|
dbd3c04d1b | ||
|
|
e19566ecb8 | ||
|
|
370af11200 | ||
|
|
0e67e8311e | ||
|
|
81f4724d71 | ||
|
|
4a06d328d6 | ||
|
|
4dab735dad | ||
|
|
ce2fa15554 | ||
|
|
c0cb121660 | ||
|
|
efac66835a | ||
|
|
4a269fd508 | ||
|
|
bcef360252 | ||
|
|
bb262ed0df | ||
|
|
039d60abfb | ||
|
|
c6a40d2125 | ||
|
|
d15aa79d18 | ||
|
|
c0d5bd1f05 | ||
|
|
54c2a73d68 | ||
|
|
461aa1fd58 | ||
|
|
9211d29e17 | ||
|
|
3c1b30a5e4 | ||
|
|
7f52ef8bd8 | ||
|
|
32f477726b | ||
|
|
e9068e702e | ||
|
|
e97f6c09e5 | ||
|
|
a57fdaa4b3 | ||
|
|
1ae738016e | ||
|
|
f0e7ede499 | ||
|
|
a595bde1b8 | ||
|
|
e113ebd298 | ||
|
|
5339e0876e | ||
|
|
101978f713 | ||
|
|
23218bca7d | ||
|
|
7d0ed2246a | ||
|
|
c43fe4f9ae | ||
|
|
ee5b1b8c34 | ||
|
|
4f98b4bb21 | ||
|
|
2b95a0d125 | ||
|
|
87b2a05d39 | ||
|
|
44d0b52424 | ||
|
|
de9ff6a3a1 | ||
|
|
4a4e5f1890 | ||
|
|
fcc4831c9f | ||
|
|
6f435cbcfd | ||
|
|
b01264f701 | ||
|
|
e6e507a470 | ||
|
|
2b60eaf462 | ||
|
|
bf892aa5dc | ||
|
|
1bd834924a | ||
|
|
e854161da9 | ||
|
|
018b4af8d3 | ||
|
|
903696726c | ||
|
|
0a82c28fbf | ||
|
|
70d8b6824c | ||
|
|
c4e7747fd1 | ||
|
|
661b8791b3 | ||
|
|
c9a30a23ec | ||
|
|
28fde37f93 | ||
|
|
3797342a93 | ||
|
|
7faa0d6e65 | ||
|
|
23455750fa | ||
|
|
7cabae7ef5 | ||
|
|
7baf5c1d9a | ||
|
|
36780509a9 | ||
|
|
48c3aa7656 | ||
|
|
a951ebc3ed | ||
|
|
8ea48f7c81 | ||
|
|
a633225ad2 | ||
|
|
6b327e3ab3 | ||
|
|
cd43a9feb8 | ||
|
|
cf641476bf | ||
|
|
462a91b122 | ||
|
|
67a8b4eb80 | ||
|
|
e59f349a66 | ||
|
|
031f1abd5d | ||
|
|
73e478c50c | ||
|
|
14199ee4ea | ||
|
|
a9ec926ffb | ||
|
|
9f089bad75 | ||
|
|
8071fe04bc | ||
|
|
47a401b66e | ||
|
|
ddd2684d60 | ||
|
|
784ba7fc23 | ||
|
|
4bcc23641a | ||
|
|
dabb48fb61 | ||
|
|
9aea9aab50 | ||
|
|
b4aed240a7 | ||
|
|
015ab649af | ||
|
|
4cac16093f | ||
|
|
b77d9d4125 | ||
|
|
ac26d58b97 | ||
|
|
ed7b2ef4ef | ||
|
|
5fe5750130 | ||
|
|
314fff587c | ||
|
|
8318981f4e | ||
|
|
365e2fdb6b | ||
|
|
7e196785d8 | ||
|
|
5ef1c7e3bd | ||
|
|
11bdff9132 | ||
|
|
060802c8f1 | ||
|
|
312aeea39d | ||
|
|
6c41c49866 | ||
|
|
9dbc6d4d8f | ||
|
|
33c3ef64e9 | ||
|
|
8c1a138621 | ||
|
|
4f239d035e | ||
|
|
7fa93a8de0 | ||
|
|
84e76e2bd7 | ||
|
|
2021a2e52b | ||
|
|
e1f777e33a | ||
|
|
166f8f6c3a | ||
|
|
3ace3e2b63 | ||
|
|
8c52145c7b | ||
|
|
6e3b496f86 | ||
|
|
88c8cb424b | ||
|
|
74179ab6e9 | ||
|
|
f6fefc9a69 | ||
|
|
601f91e5a7 | ||
|
|
d553542206 | ||
|
|
3bacfc3361 | ||
|
|
45a6cd3606 | ||
|
|
577017bd0c | ||
|
|
9787d1686f | ||
|
|
108f236874 | ||
|
|
d47fcda0fe | ||
|
|
5d21ba2648 | ||
|
|
1b730062b7 | ||
|
|
4841e42286 | ||
|
|
df1c227f2c | ||
|
|
6fb8a4bbe9 | ||
|
|
5c605e15dd | ||
|
|
9983269662 | ||
|
|
d891c7a325 | ||
|
|
06008fcf6c | ||
|
|
15c4a3e9a5 | ||
|
|
89f1fc8282 | ||
|
|
ca083166a1 | ||
|
|
ed177396b2 | ||
|
|
6dd901880e | ||
|
|
9771ea8f2d | ||
|
|
5cf41bdce0 | ||
|
|
f28fafed8d | ||
|
|
81e2c31dd3 | ||
|
|
d9b577d944 | ||
|
|
324a6da0d5 | ||
|
|
391fecadd0 | ||
|
|
4d0032441f | ||
|
|
8e59460845 | ||
|
|
4b6c676992 | ||
|
|
0237cc1ca4 | ||
|
|
fbef1ca69a | ||
|
|
3b4bfaa319 | ||
|
|
2b6a986c4a | ||
|
|
494827299c | ||
|
|
a920e89ea2 | ||
|
|
6a4b7a1ac7 | ||
|
|
621c73ab03 | ||
|
|
ac7003124e | ||
|
|
4208c3551a | ||
|
|
1938c93faf | ||
|
|
2a070ef55f | ||
|
|
0c17e8deb3 | ||
|
|
22b4b66cff | ||
|
|
942d062506 | ||
|
|
7c3b8954fe | ||
|
|
bf90056c61 | ||
|
|
bcd79bc250 | ||
|
|
07c7ef22cf | ||
|
|
6d420662e1 | ||
|
|
fca458687e | ||
|
|
e34620e1e8 | ||
|
|
bcdb49ffff | ||
|
|
ebb71c7724 | ||
|
|
b8dca3db32 | ||
|
|
94d704730f | ||
|
|
0c76ed2c4c | ||
|
|
8bac9f687e | ||
|
|
56b597d45f | ||
|
|
96b83e3eb3 | ||
|
|
e21ad6a6ba | ||
|
|
7fe4b99cef | ||
|
|
0c8dd1d8cf | ||
|
|
cafc6103ea | ||
|
|
6d3feaf81c | ||
|
|
c8d8b50d47 | ||
|
|
0c89d90d56 | ||
|
|
d0fc09fc01 | ||
|
|
c8b6d1e69c | ||
|
|
49d719770c | ||
|
|
c75a1c9e1e | ||
|
|
da7b0e9597 | ||
|
|
8f1243850f | ||
|
|
b931e282f2 | ||
|
|
66df8031ed | ||
|
|
966f82e38c | ||
|
|
118cbd9224 | ||
|
|
def585fa04 | ||
|
|
56a3dfd41a | ||
|
|
cc0f4c43ae | ||
|
|
c341148009 | ||
|
|
9089e5d334 | ||
|
|
720a82b3da | ||
|
|
a83d3a12fb | ||
|
|
1dae9f8beb | ||
|
|
e25d6e4d0b | ||
|
|
c01c098f7e | ||
|
|
fecfe9d791 | ||
|
|
b996c4dcf6 | ||
|
|
273efbcb65 | ||
|
|
7d177ca049 | ||
|
|
83f46ffd6c | ||
|
|
03e43ba839 | ||
|
|
71dfc0e1eb | ||
|
|
355b71cacf | ||
|
|
c660440996 | ||
|
|
70033017fd | ||
|
|
b9c08a58ad | ||
|
|
4db69760c6 | ||
|
|
8ec0731ca2 | ||
|
|
25b1957dbf | ||
|
|
f2dbc26491 | ||
|
|
fd11a09882 |
@@ -2,7 +2,7 @@ Language: Cpp
|
|||||||
BasedOnStyle: LLVM
|
BasedOnStyle: LLVM
|
||||||
UseTab: Never
|
UseTab: Never
|
||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
ColumnLimit: 220
|
ColumnLimit: 160
|
||||||
TabWidth: 4
|
TabWidth: 4
|
||||||
#BreakBeforeBraces: Custom
|
#BreakBeforeBraces: Custom
|
||||||
BraceWrapping:
|
BraceWrapping:
|
||||||
|
|||||||
37
.github/workflows/check_code.yml
vendored
37
.github/workflows/check_code.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Code Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ dev ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ dev ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# Override automatic language detection by changing the below list
|
|
||||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
|
||||||
language: ['cpp']
|
|
||||||
# Learn more...
|
|
||||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
|
|
||||||
- run: |
|
|
||||||
make
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
28
.github/workflows/pre_release.yml
vendored
28
.github/workflows/pre_release.yml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: "pre-release"
|
name: "pre-release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "dev"
|
- "dev"
|
||||||
@@ -13,46 +14,41 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout source code
|
- uses: actions/checkout@v2
|
||||||
uses: actions/checkout@v2
|
- uses: actions/setup-python@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
|
||||||
- name: Get build variables
|
- name: Get EMS-ESP source code and version
|
||||||
id: build_info
|
id: build_info
|
||||||
run: |
|
run: |
|
||||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||||
echo "::set-output name=version::$version"
|
echo "::set-output name=version::$version"
|
||||||
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
|
|
||||||
echo "::set-output name=platform::$platform"
|
|
||||||
|
|
||||||
- name: Compile locally
|
- name: Install PlatformIO
|
||||||
run: make
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
|
|
||||||
- name: Install pio
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
platformio upgrade
|
platformio upgrade
|
||||||
platformio update
|
platformio update
|
||||||
|
|
||||||
- name: Build web
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm install
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run -e ci
|
||||||
|
|
||||||
- name: Release
|
- name: Create a GH Release
|
||||||
id: "automatic_releases"
|
id: "automatic_releases"
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
title: ${{steps.build_info.outputs.platform}} Development Build v${{steps.build_info.outputs.version}}
|
title: ESP32 Development Build v${{steps.build_info.outputs.version}}
|
||||||
automatic_release_tag: "latest"
|
automatic_release_tag: "latest"
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
26
.github/workflows/tagged_release.yml
vendored
26
.github/workflows/tagged_release.yml
vendored
@@ -12,34 +12,24 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Get build variables
|
- uses: actions/checkout@v2
|
||||||
id: build_info
|
- uses: actions/setup-python@v2
|
||||||
run: |
|
- uses: actions/setup-node@v2
|
||||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
with:
|
||||||
echo "::set-output name=version::$version"
|
node-version: '16'
|
||||||
platform=`grep -E '^#define EMSESP_PLATFORM' ./src/version.h | awk -F'"' '{print $2}'`
|
|
||||||
echo "::set-output name=platform::$platform"
|
|
||||||
|
|
||||||
- name: Compile locally
|
- name: Install PlatformIO
|
||||||
run: make
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
|
|
||||||
- name: Install pio
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
platformio upgrade
|
platformio upgrade
|
||||||
platformio update
|
platformio update
|
||||||
|
|
||||||
- name: Build web
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm install
|
npm ci
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,6 +25,6 @@ emsesp
|
|||||||
/data/www
|
/data/www
|
||||||
/lib/framework/WWWData.h
|
/lib/framework/WWWData.h
|
||||||
/interface/build
|
/interface/build
|
||||||
/interface/node_modules
|
node_modules
|
||||||
/interface/.eslintcache
|
/interface/.eslintcache
|
||||||
|
test.sh
|
||||||
170
CHANGELOG.md
170
CHANGELOG.md
@@ -5,12 +5,169 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [3.0.0] March 18 2021
|
# [3.2.1] August 8 2021
|
||||||
|
|
||||||
## **ESP32 version based off ESP-ESP v2.1**
|
## Added
|
||||||
|
|
||||||
|
- json body in API can now take device, name, cmd, hc and id
|
||||||
|
- added example of how to use API directly to control values from Home Assistant
|
||||||
|
- API calls are shown in debug log (For troubleshooting)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- fixed issue with Home Assistant entity naming where boiler's ww was duplicated in entity name
|
||||||
|
- fixed issue where wwSetTemp was written too instead of wwSelTemp
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- fixed case on mqtt names, like 'wwtankmiddletemp'
|
||||||
|
- renamed Product ID to 'EMS Product ID' in Home Assistant
|
||||||
|
- removed brackets around tags, e.g. (hc1) selected room temperature" is now just "hc1 selected room temperature"
|
||||||
|
|
||||||
|
# [3.2.0] August 6 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- support for IPv6 (web/api/mqtt, not syslog yet) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||||
|
- System Log in Web UI will show current time if the NTP Service is enabled [#82](https://github.com/emsesp/EMS-ESP32/issues/82)
|
||||||
|
- Network settings for Tx-power, WiFi-bandwidth, WiFi-sleepmode [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||||
|
- optional low CPU clockrate (160 MHz) [#83](https://github.com/emsesp/EMS-ESP32/issues/83)
|
||||||
|
- select format for enumerated values in web
|
||||||
|
- settings for water hysteresis on/off
|
||||||
|
- dallas sensor name editable. `sensorname` console-command, replace sensorid with a unique name [#84](https://github.com/emsesp/EMS-ESP32/issues/84)
|
||||||
|
- 'restart' system command. Can be invoked via API with authentication. [#87](https://github.com/emsesp/EMS-ESP32/issues/87)
|
||||||
|
- add Download button in Web UI for log
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- set mode allow numbers
|
||||||
|
- Junkers thermostat shows mode as selected by set_mode
|
||||||
|
- HA thermostat mode if bool-format: numbers is selected
|
||||||
|
- Web UI System Log sometimes skipped a few log messages when watching real-time
|
||||||
|
- fix wwactivated [#89](https://github.com/emsesp/EMS-ESP32/issues/89)
|
||||||
|
- don't show commands (like reset) as Device values in the Web or Console
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- removed Rx echo failures counting as incomplete telegrams. Bad telegrams show as Warning and not Errors. [#80](https://github.com/emsesp/EMS-ESP32/issues/80)
|
||||||
|
- add upload_sec to `api/system/info` and removed # from some names to keep consistent with MQTT heartbeat
|
||||||
|
- added debug target to PlatformIO build to help hunt down system crashes
|
||||||
|
- enumerated values always start at zero
|
||||||
|
- maintenance settings for time/date as extra setting
|
||||||
|
- move api/mqtt formats to `settings`, add `enum format`
|
||||||
|
- UI improvements for editing Dallas Sensor details
|
||||||
|
- RESTful GET commands can also require authentication (via bearer access token) for better security
|
||||||
|
- Updated AsyncMqttClient to 0.9.0 and ArduinoJson to 6.18.3
|
||||||
|
- Download buttons for settings and info under the Help tab
|
||||||
|
|
||||||
|
# [3.1.1] June 26 2021
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- new command called `commands` which lists all available commands. `ems-esp/api/{device}/commands`
|
||||||
|
- More Home Assistant icons to match the UOMs
|
||||||
|
- new API. Using secure access tokens and OpenAPI standard. See `doc/EMS-ESP32 API.md` and [#50](https://github.com/emsesp/EMS-ESP32/issues/50)
|
||||||
|
- show log messages in Web UI [#71](https://github.com/emsesp/EMS-ESP32/issues/71)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- HA thermostat mode was not in sync with actual mode [#66](https://github.com/emsesp/EMS-ESP32/issues/66)
|
||||||
|
- Don't publish rssi if Wifi is disabled and ethernet is being used
|
||||||
|
- Booleans are shown as true/false in API GETs
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- `info` command always shows full names in API. For short names query the device or name directly, e.g. `http://ems-esp/api/boiler`
|
||||||
|
- free memory is shown in kilobytes
|
||||||
|
- boiler's warm water entities have ww added to the Home Assistant entity name [#67](https://github.com/emsesp/EMS-ESP32/issues/67)
|
||||||
|
- improved layout and rendering of device values in the WebUI, also the edit value screen
|
||||||
|
|
||||||
|
# [3.1.0] May 4 2021
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Mock API to simulate an ESP, for testing web
|
||||||
|
- Able to write values from the Web UI
|
||||||
|
- check values with `"cmd":<valuename>` and data empty or `?`
|
||||||
|
- set hc for values and commands by id or prefix `hc<x>`+separator, separator can be any char
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Don't create Home Assistant MQTT discovery entries for device values that don't exists (#756 on EMS-ESP repo)
|
||||||
|
- Update shower MQTT when a shower start is detected
|
||||||
|
- S32 board profile
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Icon for Network
|
||||||
|
- MQTT Formatting payload (nested vs single) is a pull-down option
|
||||||
|
- moved mqtt-topics and texts to local_EN, all topics lower case
|
||||||
|
- Re-enabled Shower Alert (still experimental)
|
||||||
|
- lowercased Flow temp in commands
|
||||||
|
- system console commands to main
|
||||||
|
|
||||||
|
# [3.0.1] March 30 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- power settings, disabling BLE and turning off Wifi sleep
|
||||||
|
- Rx and Tx counts to Heartbeat MQTT payload
|
||||||
|
- ethernet support
|
||||||
|
- id to info command to show only a heatingcircuit
|
||||||
|
- add sending devices that are not listed to 0x07
|
||||||
|
- extra MQTT boolean option for "ON" and "OFF"
|
||||||
|
- support for chunked MQTT payloads to allow large data sets > 2kb
|
||||||
|
- external Button support (#708) for resetting to factory defaults and other actions
|
||||||
|
- new console set command in `system`, `set board_profile <profile>` for quickly enabling cabled ethernet connections without using the captive wifi portal
|
||||||
|
- added in MQTT nested mode, for thermostat and mixer, like we had back in v2
|
||||||
|
- cascade MC400 (product-id 210) (3.0.0b6), power values for heating sources (3.0.1b1)
|
||||||
|
- values for wwMaxPower, wwFlowtempOffset
|
||||||
|
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
||||||
|
- syslog port selectable (#744)
|
||||||
|
- individual mqtt commands (#31)
|
||||||
|
- board Profiles (#11)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- telegrams matched to masterthermostat 0x18
|
||||||
|
- multiple roomcontrollers
|
||||||
|
- readback after write with delay (give ems-devices time to set the value)
|
||||||
|
- thermostat ES72/RC20 device 66 to command-set RC20_2
|
||||||
|
- MQTT payloads not adding to queue when MQTT is re-connecting (fixes #369)
|
||||||
|
- fix for HA topics with invalid command formats (#728)
|
||||||
|
- wrong position of values #723, #732
|
||||||
|
- OTA Upload via Web on OSX
|
||||||
|
- Rx and Tx quality % would sometimes show > 100
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
||||||
|
- split `show values` in smaller packages (edited)
|
||||||
|
- extended length of IP/hostname from 32 to 48 chars (#676)
|
||||||
|
- check flowsensor for `tap_water_active`
|
||||||
|
- mqtt prefixed with `Base`
|
||||||
|
- count Dallas sensor fails
|
||||||
|
- switch from SPIFFS to LITTLEFS
|
||||||
|
- added ID to MQTT payloads which is the Device's product ID and used in HA to identify a unique HA device
|
||||||
|
- increased MQTT buffer and reduced wait time between publishes
|
||||||
|
- updated to the latest ArduinoJson library
|
||||||
|
- some names of mqtt-tags like in v2.2.1
|
||||||
|
- new ESP32 partition side to allow for smoother OTA and fallback
|
||||||
|
- network Gateway IP is optional (#682)emsesp/EMS-ESP
|
||||||
|
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
|
||||||
|
- invert LED changed to Hide LED. Default is off.
|
||||||
|
- renamed Scan Network to Scan WiFi Network
|
||||||
|
- added version to cmd=settings
|
||||||
|
- Allow both WiFi and Ethernet together, fall back to AP when Ethernet disconnects
|
||||||
|
|
||||||
|
## Removed
|
||||||
|
|
||||||
|
- Shower Alert (disabled for now)
|
||||||
|
|
||||||
|
# [3.0.0] March 18 2021
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
### Added
|
|
||||||
- Power settings, disabling BLE and turning off Wifi sleep
|
- Power settings, disabling BLE and turning off Wifi sleep
|
||||||
- Rx and Tx counts to Heartbeat MQTT payload
|
- Rx and Tx counts to Heartbeat MQTT payload
|
||||||
- Ethernet support
|
- Ethernet support
|
||||||
@@ -26,7 +183,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
- RC300 `thermostat temp -1` to clear temporary setpoint in auto mode
|
||||||
- Syslog port selectable (#744)
|
- Syslog port selectable (#744)
|
||||||
|
|
||||||
### Fixed
|
## Fixed
|
||||||
|
|
||||||
- telegrams matched to masterthermostat 0x18
|
- telegrams matched to masterthermostat 0x18
|
||||||
- multiple roomcontrollers
|
- multiple roomcontrollers
|
||||||
- readback after write with delay (give ems-devices time to set the value)
|
- readback after write with delay (give ems-devices time to set the value)
|
||||||
@@ -36,7 +194,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- wrong position of values #723, #732
|
- wrong position of values #723, #732
|
||||||
- OTA Upload via Web on OSX
|
- OTA Upload via Web on OSX
|
||||||
|
|
||||||
### Changed
|
## Changed
|
||||||
|
|
||||||
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
||||||
- split `show values` in smaller packages (edited)
|
- split `show values` in smaller packages (edited)
|
||||||
- extended length of IP/hostname from 32 to 48 chars (#676)
|
- extended length of IP/hostname from 32 to 48 chars (#676)
|
||||||
@@ -51,4 +210,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- new ESP32 partition side to allow for smoother OTA and fallback
|
- new ESP32 partition side to allow for smoother OTA and fallback
|
||||||
- Network Gateway IP is optional (#682)emsesp/EMS-ESP
|
- Network Gateway IP is optional (#682)emsesp/EMS-ESP
|
||||||
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
|
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
### Added
|
## Added
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
### Fixed
|
## Changed
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,17 +24,17 @@ This document describes rules that are in effect for this repository, meant for
|
|||||||
|
|
||||||
## Triaging of Issues/PR's
|
## Triaging of Issues/PR's
|
||||||
|
|
||||||
1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so.
|
1. Any contributor to the project can participate in the triaging process, if he/she chooses to do so.
|
||||||
2. An issue that needs to be closed, either due to not complying with this policy, or for other reasons, should be closed by a contributor.
|
2. An issue that needs to be closed, either due to not complying with this policy, or for other reasons, should be closed by a contributor.
|
||||||
3. Issues that are accepted should be marked with appropriate labels.
|
3. Issues that are accepted should be marked with appropriate labels.
|
||||||
4. Issues that could impact functionality for many users should be considered severe.
|
4. Issues that could impact functionality for many users should be considered severe.
|
||||||
5. Issues caused by the SDK or chip should not be marked severe, as there usually isn’t much to be done. Common sense should be applied when deciding. Such issues should be documented in the documentation, for reference by users.
|
5. Issues caused by the SDK or chip should not be marked severe, as there usually isn’t much to be done. Common sense should be applied when deciding. Such issues should be documented in the documentation, for reference by users.
|
||||||
6. Issues with feature requests should be discussed for viability/desirability.
|
6. Issues with feature requests should be discussed for viability/desirability.
|
||||||
7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified.
|
7. Feature requests or changes that are meant to address a very specific/limited use case, especially if at the expense of increased code complexity, may be denied, or may be required to be redesigned, generalized, or simplified.
|
||||||
8. Feature requests that are not accompanied by a PR:
|
8. Feature requests that are not accompanied by a PR:
|
||||||
* could be closed immediately (denied).
|
- could be closed immediately (denied).
|
||||||
* could be closed after some predetermined period of time (left as candidate for somebody to pick up).
|
- could be closed after some predetermined period of time (left as candidate for somebody to pick up).
|
||||||
9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot.
|
9. In some cases, feedback may be requested from the issue reporter, either as additional info for clarification, additional testing, or other. If no feedback is provided, the issue may be closed by a contributor or after 40 days by the STALE bot.
|
||||||
|
|
||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
@@ -42,24 +42,24 @@ A Pull Request (PR) is the process where code modifications are managed in GitHu
|
|||||||
|
|
||||||
The process is straight-forward.
|
The process is straight-forward.
|
||||||
|
|
||||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||||
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
|
- Fork the EMS-ESP Repository [git repository](https://github.com/emsesp/EMS-ESP32).
|
||||||
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
|
- Write/Change the code in your Fork for a new feature, bug fix, new sensor, optimization, etc.
|
||||||
- Ensure tests work.
|
- Ensure tests work.
|
||||||
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
|
- Create a Pull Request against the [**dev**](https://github.com/emsesp/EMS-ESP32/tree/dev) branch of EMS-ESP.
|
||||||
|
|
||||||
1. All pull requests must be done against the dev branch.
|
1. All pull requests must be done against the dev branch.
|
||||||
2. Make sure code is formatting per the `.clang-format`
|
2. Make sure code is formatting per the `.clang-format`.
|
||||||
3. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
|
3. Make sure any new code is clearly commented explaining what the function/logic does.
|
||||||
4. Only one feature/fix should be added per PR.
|
4. Only relevant files should be touched (Also beware if your editor has auto-formatting feature enabled).
|
||||||
5. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
|
5. Only one feature/fix should be added per PR.
|
||||||
6. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
|
6. PRs that don't compile (fail in CI Tests) or cause coding errors will not be merged. Please fix the issue. Same goes for PRs that are raised against older commit in dev - you might need to rebase and resolve conflicts.
|
||||||
7. All pull requests should consider updates to the documentation.
|
7. All pull requests should undergo peer review by at least one contributor other than the creator, excepts for the owner.
|
||||||
8. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
|
8. All pull requests should consider updates to the documentation.
|
||||||
9. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
|
9. Pull requests that address an outstanding issue, particularly an issue deemed to be severe, should be given priority.
|
||||||
10. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
|
10. If a PR is accepted, then it should undergo review and updated based on the feedback provided, then merged.
|
||||||
11. Pull requests that don't meet the above will be denied and closed.
|
11. By submitting a PR, it is needed to use the provided PR template and check all boxes, performing the required tasks and accepting the CLA.
|
||||||
|
12. Pull requests that don't meet the above will be denied and closed.
|
||||||
|
|
||||||
## Semantic Commit Messages
|
## Semantic Commit Messages
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ More Examples:
|
|||||||
|
|
||||||
References:
|
References:
|
||||||
|
|
||||||
- https://www.conventionalcommits.org/
|
- <https://www.conventionalcommits.org/>
|
||||||
|
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ MAKEFLAGS+="j "
|
|||||||
#TARGET := $(notdir $(CURDIR))
|
#TARGET := $(notdir $(CURDIR))
|
||||||
TARGET := emsesp
|
TARGET := emsesp
|
||||||
BUILD := build
|
BUILD := build
|
||||||
SOURCES := src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton src/test
|
SOURCES := src src/* lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src src/devices lib/ArduinoJson/src lib/PButton
|
||||||
INCLUDES := lib/ArduinoJson/src lib_standalone lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/PButton src/devices lib src
|
INCLUDES := src lib_standalone lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src lib/* src/devices
|
||||||
LIBRARIES :=
|
LIBRARIES :=
|
||||||
|
|
||||||
CPPCHECK = cppcheck
|
CPPCHECK = cppcheck
|
||||||
@@ -33,7 +33,7 @@ CXX_STANDARD := -std=c++11
|
|||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Defined Symbols
|
# Defined Symbols
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST
|
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_DEFAULT_BOARD_PROFILE=\"LOLIN\"
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Sources & Files
|
# Sources & Files
|
||||||
@@ -72,9 +72,9 @@ CPPFLAGS += -g3
|
|||||||
CPPFLAGS += -Os
|
CPPFLAGS += -Os
|
||||||
|
|
||||||
CFLAGS += $(CPPFLAGS)
|
CFLAGS += $(CPPFLAGS)
|
||||||
# CFLAGS += -Wall
|
CFLAGS += -Wall
|
||||||
# CFLAGS += -Wno-unused -Wno-restrict
|
CFLAGS += -Wno-unused -Wno-restrict
|
||||||
# CFLAGS += -Wextra
|
CFLAGS += -Wextra
|
||||||
|
|
||||||
CXXFLAGS += $(CFLAGS) -MMD
|
CXXFLAGS += $(CFLAGS) -MMD
|
||||||
|
|
||||||
113
README.md
113
README.md
@@ -2,16 +2,21 @@
|
|||||||
|
|
||||||
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
||||||
|
|
||||||
This is the firmware for the ESP32.
|
This project is the specifically for the ESP32. Compared with the previous ESP8266 (version 2) release it has the following enhancements:
|
||||||
|
|
||||||
|
- Ethernet Support
|
||||||
|
- Pre-configured circuit board layouts
|
||||||
|
- Supports writing EMS values directly from within Web UI
|
||||||
|
- Mock API server for faster offline development and testing
|
||||||
|
- Improved API and MQTT commands
|
||||||
|
- Improvements to Dallas temperature sensors
|
||||||
|
- Embedded log tracing in the Web UI
|
||||||
|
|
||||||
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
||||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://www.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=github.com&utm_medium=referral&utm_content=emsesp/EMS-ESP32&utm_campaign=Badge_Grade)
|
[](https://www.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=github.com&utm_medium=referral&utm_content=emsesp/EMS-ESP32&utm_campaign=Badge_Grade)
|
||||||
[](https://github.com/emsesp/EMS-ESP32/releases)
|
[](https://github.com/emsesp/EMS-ESP32/releases)
|
||||||
[](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Average time to resolve an issue")
|
|
||||||
[](http://isitmaintained.com/project/emsesp/EMS-ESP32 "Percentage of issues still open")
|
|
||||||
<br/>
|
|
||||||
[](https://discord.gg/3J3GgnzpyT)
|
[](https://discord.gg/3J3GgnzpyT)
|
||||||
|
|
||||||
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
||||||
@@ -20,82 +25,104 @@ If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
|||||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||||
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
||||||
|
|
||||||
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at https://bbqkees-electronics.nl.
|
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at <https://bbqkees-electronics.nl> or contact the contributors that can provide the schematic and designs.
|
||||||
|
|
||||||
<img src="media/gateway-integration.jpg" width=40%>
|
<img src="media/gateway-integration.jpg" width=40%>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## **Features**
|
# **Features**
|
||||||
|
|
||||||
- Compatible with both ESP8266 and ESP32
|
|
||||||
- A multi-user secure web interface to change settings and monitor the data
|
- A multi-user secure web interface to change settings and monitor the data
|
||||||
- A console, accessible via Serial and Telnet for more monitoring
|
- A console, accessible via Serial and Telnet for more monitoring
|
||||||
- Native support for Home Assistant via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
- Native support for Home Assistant via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
||||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||||
- Easy first-time configuration via a web Captive Portal
|
- Easy first-time configuration via a web Captive Portal
|
||||||
- Support for more than [70 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
- Support for more than [80 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
||||||
|
|
||||||
## **Screenshots**
|
## **Demo**
|
||||||
|
|
||||||
### Web Interface:
|
See a live demo [here](https://ems-esp.derbyshire.nl) using fake data. Log in with any username/password.
|
||||||
|
|
||||||
| | |
|
# **Screenshots**
|
||||||
| --- | --- |
|
|
||||||
| <img src="media/web_settings.PNG"> | <img src="media/web_status.PNG"> |
|
|
||||||
| <img src="media/web_devices.PNG"> | <img src="media/web_mqtt.PNG"> |
|
|
||||||
|
|
||||||
### Telnet Console:
|
## Web Interface
|
||||||
<img src="media/console.PNG" width=80% height=80%>
|
|
||||||
|
|
||||||
### In Home Assistant:
|
| | |
|
||||||
<img src="media/ha_lovelace.PNG" width=80% height=80%>
|
| ---------------------------------- | -------------------------------- |
|
||||||
|
| <img src="media/web_settings.png"> | <img src="media/web_status.png"> |
|
||||||
|
| <img src="media/web_devices.png"> | <img src="media/web_mqtt.png"> |
|
||||||
|
| <img src="media/web_edit.png"> | <img src="media/web_log.png"> |
|
||||||
|
|
||||||
## **Installing**
|
## Telnet Console
|
||||||
|
|
||||||
|
<img src="media/console.png" width=80% height=80%>
|
||||||
|
|
||||||
|
## In Home Assistant
|
||||||
|
|
||||||
|
<img src="media/ha_lovelace.png" width=80% height=80%>
|
||||||
|
|
||||||
|
# **Installing**
|
||||||
|
|
||||||
Refer to the [official documentation](https://emsesp.github.io/docs) to how to install the firmware and configure it. The documentation is being constantly updated as new features and settings are added.
|
Refer to the [official documentation](https://emsesp.github.io/docs) to how to install the firmware and configure it. The documentation is being constantly updated as new features and settings are added.
|
||||||
|
|
||||||
You can choose to use an pre-built firmware image or compile the code yourself:
|
You can choose to use an pre-built firmware image or compile the code yourself:
|
||||||
|
|
||||||
* [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
|
- [Uploading a pre-built firmware build](https://emsesp.github.io/docs/#/Uploading-firmware)
|
||||||
* [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
|
- [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-firmware)
|
||||||
|
|
||||||
## **Support Information**
|
# **Support Information**
|
||||||
|
|
||||||
If you're looking for support on **EMS-ESP** there are some options available:
|
If you're looking for support on **EMS-ESP** there are some options available:
|
||||||
|
|
||||||
### Documentation
|
## Documentation
|
||||||
|
|
||||||
* [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
|
- [Official EMS-ESP Documentation](https://emsesp.github.io/docs): For information on how to build and upload the firmware
|
||||||
* [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
|
- [FAQ and Troubleshooting](https://emsesp.github.io/docs/#/Troubleshooting): For information on common problems and solutions. See also [BBQKees's wiki](https://bbqkees-electronics.nl/wiki/gateway/troubleshooting.html)
|
||||||
|
|
||||||
### Support Community
|
## Support Community
|
||||||
|
|
||||||
* [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
|
- [Discord Server](https://discord.gg/3J3GgnzpyT): For support, troubleshooting and general questions. You have better chances to get fast answers from members of the community
|
||||||
* [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
- [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
||||||
|
|
||||||
### Developer's Community
|
## Developer's Community
|
||||||
|
|
||||||
* [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
|
- [Bug Report](https://github.com/emsesp/EMS-ESP32/issues/new?template=bug_report.md): For reporting Bugs
|
||||||
* [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
|
- [Feature Request](https://github.com/emsesp/EMS-ESP32/issues/new?template=feature_request.md): For requesting features/functions
|
||||||
* [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new *Troubleshooting & Question* issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
|
- [Troubleshooting](https://github.com/emsesp/EMS-ESP32/issues/new?template=questions---troubleshooting.md): As a last resort, you can open new _Troubleshooting & Question_ issue on GitHub if the solution could not be found using the other channels. Just remember: the more info you provide the more chances you'll have to get an accurate answer
|
||||||
|
|
||||||
## **Contributing**
|
# **Contributors ✨**
|
||||||
|
|
||||||
|
EMS-ESP is a project originally created and owned by [proddy](https://github.com/proddy). Key contributors are:
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/MichaelDvP"><img src="https://avatars.githubusercontent.com/u/59284019?v=3?s=100" width="100px;" alt=""/><br /><sub><b>MichaelDvP</b></sub></a><br /></a> <a href="https://github.com/emsesp/EMS-ESP/commits?author=MichaelDvP" title="v2 Commits">v2</a>
|
||||||
|
<a href="https://github.com/emsesp/EMS-ESP32/commits?author=MichaelDvP" title="v3 Commits">v3</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
You can also contribute to EMS-ESP by
|
||||||
|
|
||||||
You can contribute to EMS-ESP by
|
|
||||||
- providing Pull Requests (Features, Fixes, suggestions)
|
- providing Pull Requests (Features, Fixes, suggestions)
|
||||||
- testing new released features and report issues on your EMS equipment
|
- testing new released features and report issues on your EMS equipment
|
||||||
- contributing to missing [Documentation](https://emsesp.github.io/docs)
|
- contributing to missing [Documentation](https://emsesp.github.io/docs)
|
||||||
|
|
||||||
## **Credits**
|
# **Libraries used**
|
||||||
|
|
||||||
A shout out to the people helping EMS-ESP get to where it is today...
|
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||||
- **@MichaelDvP** for all his amazing contributions and patience. Specifically for the improved uart library, thermostat and mixer logic.
|
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
||||||
- **@BBQKees** for his endless testing and building the awesome circuit boards
|
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for JSON
|
||||||
- **@rjwats** for his [esp8266-react](https://github.com/rjwats/esp8266-react) framework that provides the new Web UI
|
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
||||||
- **@nomis** for his core [console](https://github.com/nomis/mcu-uuid-console), telnet and syslog core libraries
|
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||||
- plus everyone else providing suggestions, PRs and the odd donation that keeps this project open source. Thanks!
|
|
||||||
|
|
||||||
## **License**
|
# **License**
|
||||||
|
|
||||||
This program is licensed under GPL-3.0
|
This program is licensed under GPL-3.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
otadata, data, ota, 0xe000, 0x2000,
|
otadata, data, ota, 0xE000, 0x2000,
|
||||||
app0, app, ota_0, 0x10000, 0x1F0000,
|
app0, app, ota_0, 0x10000, 0x1F0000,
|
||||||
app1, app, ota_1, 0x200000, 0x1F0000,
|
app1, app, ota_1, 0x200000, 0x1F0000,
|
||||||
spiffs, data, spiffs, 0x3F0000,0x10000,
|
spiffs, data, spiffs, 0x3F0000, 0x10000,
|
||||||
|
|||||||
|
5
esp32_partition_debug.csv
Normal file
5
esp32_partition_debug.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xE000, 0x2000,
|
||||||
|
app0, app, ota_0, 0x10000, 0x210000,
|
||||||
|
spiffs, data, spiffs, 0x220000, 0x10000,
|
||||||
|
@@ -1,10 +1,5 @@
|
|||||||
# Change the IP address to that of your ESP device to enable local development of the UI.
|
# Change the IP address to that of your ESP device to enable local development of the UI
|
||||||
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
|
|
||||||
|
|
||||||
# ESP32 dev
|
# REACT_APP_HTTP_ROOT=http://localhost:3000
|
||||||
REACT_APP_HTTP_ROOT=http://10.10.10.101
|
# REACT_APP_WEB_SOCKET_ROOT=ws://localhost:3000
|
||||||
REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.101
|
|
||||||
|
|
||||||
# ESP8266 dev
|
|
||||||
#REACT_APP_HTTP_ROOT=http://10.10.10.140
|
|
||||||
#REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.140
|
|
||||||
|
|||||||
3
interface/.env.hosted
Normal file
3
interface/.env.hosted
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
|
REACT_APP_HOSTED=true
|
||||||
2
interface/.eslintignore
Normal file
2
interface/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# don't ever lint node_modules
|
||||||
|
node_modules
|
||||||
27
interface/.eslintrc
Normal file
27
interface/.eslintrc
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
// 0 = ignore, 1 = warning, 2 = error
|
||||||
|
"no-console": 0,
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
"explicit-function-return-type": 0,
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||||
|
"@typescript-eslint/no-var-requires": 0,
|
||||||
|
"@typescript-eslint/ban-types": 0,
|
||||||
|
"@typescript-eslint/no-non-null-asserted-optional-chain": 0,
|
||||||
|
"@typescript-eslint/no-non-null-assertion": 0,
|
||||||
|
"@typescript-eslint/no-explicit-any": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
6
interface/.prettierrc
Normal file
6
interface/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
||||||
@@ -4,34 +4,49 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
const ProgmemGenerator = require('./progmem-generator.js');
|
const ProgmemGenerator = require('./progmem-generator.js');
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
module.exports = function override(config, env) {
|
module.exports = function override(config, env) {
|
||||||
if (env === "production") {
|
const hosted = process.env.REACT_APP_HOSTED;
|
||||||
// rename the ouput file, we need it's path to be short, for SPIFFS
|
|
||||||
|
if (env === 'production' && !hosted) {
|
||||||
|
console.log('Custom webpack...');
|
||||||
|
|
||||||
|
// rename the output file, we need it's path to be short for LittleFS
|
||||||
config.output.filename = 'js/[id].[chunkhash:4].js';
|
config.output.filename = 'js/[id].[chunkhash:4].js';
|
||||||
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
||||||
|
|
||||||
// take out the manifest and service worker plugins
|
// take out the manifest and service worker plugins
|
||||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin));
|
config.plugins = config.plugins.filter(
|
||||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW));
|
(plugin) => !(plugin instanceof ManifestPlugin)
|
||||||
|
);
|
||||||
|
config.plugins = config.plugins.filter(
|
||||||
|
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW)
|
||||||
|
);
|
||||||
|
|
||||||
// shorten css filenames
|
// shorten css filenames
|
||||||
const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin);
|
const miniCssExtractPlugin = config.plugins.find(
|
||||||
miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css";
|
(plugin) => plugin instanceof MiniCssExtractPlugin
|
||||||
miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css";
|
);
|
||||||
|
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
|
||||||
|
miniCssExtractPlugin.options.chunkFilename =
|
||||||
|
'css/[id].[contenthash:4].c.css';
|
||||||
|
|
||||||
// build progmem data files
|
// build progmem data files
|
||||||
config.plugins.push(new ProgmemGenerator({ outputPath: "../lib/framework/WWWData.h", bytesPerLine: 20 }));
|
config.plugins.push(
|
||||||
|
new ProgmemGenerator({
|
||||||
|
outputPath: '../lib/framework/WWWData.h',
|
||||||
|
bytesPerLine: 20
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// add compression plugin, compress javascript
|
// add compression plugin, compress javascript
|
||||||
config.plugins.push(new CompressionPlugin({
|
config.plugins.push(
|
||||||
filename: "[path].gz[query]",
|
new CompressionPlugin({
|
||||||
algorithm: "gzip",
|
filename: '[path].gz[query]',
|
||||||
test: /\.(js)$/,
|
algorithm: 'gzip',
|
||||||
deleteOriginalAssets: true
|
test: /\.(js)$/,
|
||||||
}));
|
deleteOriginalAssets: true
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
};
|
||||||
|
|||||||
26681
interface/package-lock.json
generated
26681
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "esp8266-react",
|
"name": "emsesp-react",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.3",
|
"@material-ui/core": "^4.11.4",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
|
"@msgpack/msgpack": "^2.7.0",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@types/node": "^12.20.4",
|
"@types/node": "^15.0.1",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.4",
|
||||||
"@types/react-dom": "^17.0.1",
|
"@types/react-dom": "^17.0.3",
|
||||||
"@types/react-material-ui-form-validator": "^2.1.0",
|
"@types/react-material-ui-form-validator": "^2.1.0",
|
||||||
"@types/react-router": "^5.1.12",
|
"@types/react-router": "^5.1.13",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"compression-webpack-plugin": "^4.0.0",
|
"compression-webpack-plugin": "^5.0.2",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime-types": "^2.1.29",
|
"mime-types": "^2.1.30",
|
||||||
"notistack": "^1.0.5",
|
"notistack": "^1.0.6",
|
||||||
"parse-ms": "^2.1.0",
|
"parse-ms": "^3.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-dropzone": "^11.3.1",
|
"react-dropzone": "^11.3.2",
|
||||||
"react-form-validator-core": "^1.1.1",
|
"react-form-validator-core": "^1.1.1",
|
||||||
"react-material-ui-form-validator": "^2.1.4",
|
"react-material-ui-form-validator": "^2.1.4",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "4.0.3",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"typescript": "4.0.5",
|
"typescript": "4.2.4",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
"build": "react-app-rewired build",
|
"build": "react-app-rewired build",
|
||||||
"eject": "react-scripts eject"
|
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
||||||
|
"build-hosted": "env-cmd -f .env.hosted npm run build",
|
||||||
|
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
|
||||||
|
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
||||||
|
"standalone": "npm-run-all -p start mock-api",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
@@ -51,6 +59,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"concurrently": "^6.0.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
|
"http-proxy-middleware": "^1.1.1",
|
||||||
|
"nodemon": "^2.0.7",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
"react-app-rewired": "^2.1.8"
|
"react-app-rewired": "^2.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
const { resolve, relative, sep } = require('path');
|
const { resolve, relative, sep } = require('path');
|
||||||
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
|
const {
|
||||||
|
readdirSync,
|
||||||
|
existsSync,
|
||||||
|
unlinkSync,
|
||||||
|
readFileSync,
|
||||||
|
createWriteStream
|
||||||
|
} = require('fs');
|
||||||
var zlib = require('zlib');
|
var zlib = require('zlib');
|
||||||
var mime = require('mime-types');
|
var mime = require('mime-types');
|
||||||
|
|
||||||
const ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
|
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||||
|
|
||||||
function getFilesSync(dir, files = []) {
|
function getFilesSync(dir, files = []) {
|
||||||
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
|
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||||
const entryPath = resolve(dir, entry.name);
|
const entryPath = resolve(dir, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
getFilesSync(entryPath, files);
|
getFilesSync(entryPath, files);
|
||||||
} else {
|
} else {
|
||||||
files.push(entryPath);
|
files.push(entryPath);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +31,17 @@ function cleanAndOpen(path) {
|
|||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
unlinkSync(path);
|
unlinkSync(path);
|
||||||
}
|
}
|
||||||
return createWriteStream(path, { flags: "w+" });
|
return createWriteStream(path, { flags: 'w+' });
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProgmemGenerator {
|
class ProgmemGenerator {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
const { outputPath, bytesPerLine = 20, indent = " ", includes = ARDUINO_INCLUDES } = options;
|
const {
|
||||||
|
outputPath,
|
||||||
|
bytesPerLine = 20,
|
||||||
|
indent = ' ',
|
||||||
|
includes = ARDUINO_INCLUDES
|
||||||
|
} = options;
|
||||||
this.options = { outputPath, bytesPerLine, indent, includes };
|
this.options = { outputPath, bytesPerLine, indent, includes };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,30 +51,34 @@ class ProgmemGenerator {
|
|||||||
(compilation, callback) => {
|
(compilation, callback) => {
|
||||||
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
||||||
const fileInfo = [];
|
const fileInfo = [];
|
||||||
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
const writeStream = cleanAndOpen(
|
||||||
|
resolve(compilation.options.context, outputPath)
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const writeIncludes = () => {
|
const writeIncludes = () => {
|
||||||
writeStream.write(includes);
|
writeStream.write(includes);
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeFile = (relativeFilePath, buffer) => {
|
const writeFile = (relativeFilePath, buffer) => {
|
||||||
const variable = "ESP_REACT_DATA_" + fileInfo.length;
|
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||||
const mimeType = mime.lookup(relativeFilePath);
|
const mimeType = mime.lookup(relativeFilePath);
|
||||||
var size = 0;
|
var size = 0;
|
||||||
writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
|
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
||||||
const zipBuffer = zlib.gzipSync(buffer);
|
const zipBuffer = zlib.gzipSync(buffer);
|
||||||
zipBuffer.forEach((b) => {
|
zipBuffer.forEach((b) => {
|
||||||
if (!(size % bytesPerLine)) {
|
if (!(size % bytesPerLine)) {
|
||||||
writeStream.write("\n");
|
writeStream.write('\n');
|
||||||
writeStream.write(indent);
|
writeStream.write(indent);
|
||||||
}
|
}
|
||||||
writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
|
writeStream.write(
|
||||||
|
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ','
|
||||||
|
);
|
||||||
size++;
|
size++;
|
||||||
});
|
});
|
||||||
if (size % bytesPerLine) {
|
if (size % bytesPerLine) {
|
||||||
writeStream.write("\n");
|
writeStream.write('\n');
|
||||||
}
|
}
|
||||||
writeStream.write("};\n\n");
|
writeStream.write('};\n\n');
|
||||||
fileInfo.push({
|
fileInfo.push({
|
||||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||||
mimeType,
|
mimeType,
|
||||||
@@ -84,25 +98,37 @@ class ProgmemGenerator {
|
|||||||
// process assets
|
// process assets
|
||||||
const { assets } = compilation;
|
const { assets } = compilation;
|
||||||
Object.keys(assets).forEach((relativeFilePath) => {
|
Object.keys(assets).forEach((relativeFilePath) => {
|
||||||
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
writeFile(
|
||||||
|
relativeFilePath,
|
||||||
|
coherseToBuffer(assets[relativeFilePath].source())
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const generateWWWClass = () => {
|
const generateWWWClass = () => {
|
||||||
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
||||||
|
|
||||||
class WWWData {
|
class WWWData {
|
||||||
${indent}public:
|
${indent}public:
|
||||||
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
${indent.repeat(
|
||||||
${fileInfo.map(file => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`).join('\n')}
|
2
|
||||||
|
)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||||
|
${fileInfo
|
||||||
|
.map(
|
||||||
|
(file) =>
|
||||||
|
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${
|
||||||
|
file.variable
|
||||||
|
}, ${file.size});`
|
||||||
|
)
|
||||||
|
.join('\n')}
|
||||||
${indent.repeat(2)}}
|
${indent.repeat(2)}}
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeWWWClass = () => {
|
const writeWWWClass = () => {
|
||||||
writeStream.write(generateWWWClass());
|
writeStream.write(generateWWWClass());
|
||||||
}
|
};
|
||||||
|
|
||||||
writeIncludes();
|
writeIncludes();
|
||||||
writeFiles();
|
writeFiles();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name":"EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"icons":[
|
"icons": [
|
||||||
{
|
{
|
||||||
"src":"/app/icon.png",
|
"src": "/app/icon.png",
|
||||||
"sizes":"48x48 72x72 96x96 128x128 256x256"
|
"sizes": "48x48 72x72 96x96 128x128 256x256"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url":"/",
|
"start_url": "/",
|
||||||
"display":"fullscreen",
|
"display": "fullscreen",
|
||||||
"orientation":"any"
|
"orientation": "any"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,26 @@
|
|||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/li.woff2) format('woff2');
|
src: local('Roboto Light'), local('Roboto-Light'),
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
url(../fonts/li.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2');
|
src: local('Roboto'), local('Roboto-Regular'),
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
url(../fonts/re.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/me.woff2) format('woff2');
|
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
url(../fonts/me.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@ import FeaturesWrapper from './features/FeaturesWrapper';
|
|||||||
const unauthorizedRedirect = () => <Redirect to="/" />;
|
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
|
||||||
notistackRef: RefObject<any> = React.createRef();
|
notistackRef: RefObject<any> = React.createRef();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -23,21 +22,29 @@ class App extends Component {
|
|||||||
|
|
||||||
onClickDismiss = (key: string | number | undefined) => () => {
|
onClickDismiss = (key: string | number | undefined) => () => {
|
||||||
this.notistackRef.current.closeSnackbar(key);
|
this.notistackRef.current.closeSnackbar(key);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<CustomMuiTheme>
|
<CustomMuiTheme>
|
||||||
<SnackbarProvider maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
<SnackbarProvider
|
||||||
|
autoHideDuration={3000}
|
||||||
|
maxSnack={3}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
ref={this.notistackRef}
|
ref={this.notistackRef}
|
||||||
action={(key) => (
|
action={(key) => (
|
||||||
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<FeaturesWrapper>
|
<FeaturesWrapper>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
<Route
|
||||||
|
exact
|
||||||
|
path="/unauthorized"
|
||||||
|
component={unauthorizedRedirect}
|
||||||
|
/>
|
||||||
<Route component={AppRouting} />
|
<Route component={AppRouting} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</FeaturesWrapper>
|
</FeaturesWrapper>
|
||||||
@@ -47,4 +54,4 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import Mqtt from './mqtt/Mqtt';
|
|||||||
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
||||||
import { Features } from './features/types';
|
import { Features } from './features/types';
|
||||||
|
|
||||||
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/network/";
|
export const getDefaultRoute = (features: Features) =>
|
||||||
|
features.project ? `/${PROJECT_PATH}/` : '/network/';
|
||||||
class AppRouting extends Component<WithFeaturesProps> {
|
class AppRouting extends Component<WithFeaturesProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
Authentication.clearLoginRedirect();
|
Authentication.clearLoginRedirect();
|
||||||
}
|
}
|
||||||
@@ -35,9 +35,17 @@ class AppRouting extends Component<WithFeaturesProps> {
|
|||||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||||
)}
|
)}
|
||||||
{features.project && (
|
{features.project && (
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/*`}
|
||||||
|
component={ProjectRouting}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<AuthenticatedRoute exact path="/network/*" component={NetworkConnection} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/*"
|
||||||
|
component={NetworkConnection}
|
||||||
|
/>
|
||||||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||||
{features.ntp && (
|
{features.ntp && (
|
||||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||||
@@ -52,7 +60,7 @@ class AppRouting extends Component<WithFeaturesProps> {
|
|||||||
<Redirect to={getDefaultRoute(features)} />
|
<Redirect to={getDefaultRoute(features)} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</AuthenticationWrapper>
|
</AuthenticationWrapper>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { CssBaseline } from '@material-ui/core';
|
import { CssBaseline } from '@material-ui/core';
|
||||||
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
|
import {
|
||||||
|
MuiThemeProvider,
|
||||||
|
createMuiTheme,
|
||||||
|
StylesProvider
|
||||||
|
} from '@material-ui/core/styles';
|
||||||
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
|
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const theme = createMuiTheme({
|
||||||
palette: {
|
palette: {
|
||||||
type: "dark",
|
type: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#33bfff',
|
main: '#33bfff'
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#3d5afe',
|
main: '#3d5afe'
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: blueGrey[500]
|
main: blueGrey[500]
|
||||||
@@ -29,7 +33,6 @@ const theme = createMuiTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default class CustomMuiTheme extends Component {
|
export default class CustomMuiTheme extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StylesProvider>
|
<StylesProvider>
|
||||||
@@ -40,5 +43,4 @@ export default class CustomMuiTheme extends Component {
|
|||||||
</StylesProvider>
|
</StylesProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,53 +2,63 @@ import React, { Component } from 'react';
|
|||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
|
import {
|
||||||
|
withStyles,
|
||||||
|
createStyles,
|
||||||
|
Theme,
|
||||||
|
WithStyles
|
||||||
|
} from '@material-ui/core/styles';
|
||||||
import { Paper, Typography, Fab } from '@material-ui/core';
|
import { Paper, Typography, Fab } from '@material-ui/core';
|
||||||
import ForwardIcon from '@material-ui/icons/Forward';
|
import ForwardIcon from '@material-ui/icons/Forward';
|
||||||
|
|
||||||
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
import {
|
||||||
import {PasswordValidator} from './components';
|
withAuthenticationContext,
|
||||||
|
AuthenticationContextProps
|
||||||
|
} from './authentication/AuthenticationContext';
|
||||||
|
import { PasswordValidator } from './components';
|
||||||
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
signInPage: {
|
createStyles({
|
||||||
display: "flex",
|
signInPage: {
|
||||||
height: "100vh",
|
display: 'flex',
|
||||||
margin: "auto",
|
height: '100vh',
|
||||||
padding: theme.spacing(2),
|
margin: 'auto',
|
||||||
justifyContent: "center",
|
padding: theme.spacing(2),
|
||||||
flexDirection: "column",
|
justifyContent: 'center',
|
||||||
maxWidth: theme.breakpoints.values.sm
|
flexDirection: 'column',
|
||||||
},
|
maxWidth: theme.breakpoints.values.sm
|
||||||
signInPanel: {
|
},
|
||||||
textAlign: "center",
|
signInPanel: {
|
||||||
padding: theme.spacing(2),
|
textAlign: 'center',
|
||||||
paddingTop: "200px",
|
padding: theme.spacing(2),
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
paddingTop: '200px',
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundSize: "auto 150px",
|
backgroundPosition: '50% ' + theme.spacing(2) + 'px',
|
||||||
width: "100%"
|
backgroundSize: 'auto 150px',
|
||||||
},
|
width: '100%'
|
||||||
extendedIcon: {
|
},
|
||||||
marginRight: theme.spacing(0.5),
|
extendedIcon: {
|
||||||
},
|
marginRight: theme.spacing(0.5)
|
||||||
button: {
|
},
|
||||||
marginRight: theme.spacing(2),
|
button: {
|
||||||
marginTop: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
}
|
marginTop: theme.spacing(2)
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
type SignInProps = WithSnackbarProps &
|
||||||
|
WithStyles<typeof styles> &
|
||||||
|
AuthenticationContextProps;
|
||||||
|
|
||||||
interface SignInState {
|
interface SignInState {
|
||||||
username: string,
|
username: string;
|
||||||
password: string,
|
password: string;
|
||||||
processing: boolean
|
processing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SignIn extends Component<SignInProps, SignInState> {
|
class SignIn extends Component<SignInProps, SignInState> {
|
||||||
|
|
||||||
constructor(props: SignInProps) {
|
constructor(props: SignInProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -60,10 +70,10 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
|
|
||||||
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
const { name, value } = event.currentTarget;
|
const { name, value } = event.currentTarget;
|
||||||
this.setState(prevState => ({
|
this.setState((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
[name]: value,
|
[name]: value
|
||||||
}))
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
@@ -77,20 +87,21 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else if (response.status === 401) {
|
} else if (response.status === 401) {
|
||||||
throw Error("Invalid credentials.");
|
throw Error('Invalid credentials.');
|
||||||
} else {
|
} else {
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error('Invalid status code: ' + response.status);
|
||||||
}
|
}
|
||||||
}).then(json => {
|
})
|
||||||
|
.then((json) => {
|
||||||
authenticationContext.signIn(json.access_token);
|
authenticationContext.signIn(json.access_token);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message, {
|
this.props.enqueueSnackbar(error.message, {
|
||||||
variant: 'warning',
|
variant: 'warning'
|
||||||
});
|
});
|
||||||
this.setState({ processing: false });
|
this.setState({ processing: false });
|
||||||
});
|
});
|
||||||
@@ -116,8 +127,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
onChange={this.updateInputElement}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
autoCapitalize: "none",
|
autoCapitalize: 'none',
|
||||||
autoCorrect: "off",
|
autoCorrect: 'off'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
@@ -132,7 +143,13 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
onChange={this.updateInputElement}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
<Fab
|
||||||
|
variant="extended"
|
||||||
|
color="primary"
|
||||||
|
className={classes.button}
|
||||||
|
type="submit"
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
<ForwardIcon className={classes.extendedIcon} />
|
<ForwardIcon className={classes.extendedIcon} />
|
||||||
Sign In
|
Sign In
|
||||||
</Fab>
|
</Fab>
|
||||||
@@ -141,7 +158,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
|
export default withAuthenticationContext(
|
||||||
|
withSnackbar(withStyles(styles)(SignIn))
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { APSettings, APProvisionMode } from "./types";
|
import { APSettings, APProvisionMode } from './types';
|
||||||
|
|
||||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
return (
|
||||||
}
|
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||||
|
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { AP_SETTINGS_ENDPOINT } from '../api';
|
import { AP_SETTINGS_ENDPOINT } from '../api';
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import APSettingsForm from './APSettingsForm';
|
import APSettingsForm from './APSettingsForm';
|
||||||
import { APSettings } from './types';
|
import { APSettings } from './types';
|
||||||
@@ -9,7 +14,6 @@ import { APSettings } from './types';
|
|||||||
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
||||||
|
|
||||||
class APSettingsController extends Component<APSettingsControllerProps> {
|
class APSettingsController extends Component<APSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class APSettingsController extends Component<APSettingsControllerProps> {
|
|||||||
<SectionContent title="Access Point Settings" titleGutter>
|
<SectionContent title="Access Point Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <APSettingsForm {...formProps} />}
|
render={(formProps) => <APSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorForm,
|
||||||
|
SelectValidator
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
import { PasswordValidator, RestFormProps, FormActions, FormButton } from '../components';
|
import {
|
||||||
|
PasswordValidator,
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import { isAPEnabled } from './APModes';
|
import { isAPEnabled } from './APModes';
|
||||||
import { APSettings, APProvisionMode } from './types';
|
import { APSettings, APProvisionMode } from './types';
|
||||||
@@ -13,7 +22,6 @@ import { isIP } from '../validators';
|
|||||||
type APSettingsFormProps = RestFormProps<APSettings>;
|
type APSettingsFormProps = RestFormProps<APSettings>;
|
||||||
|
|
||||||
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIP', isIP);
|
ValidatorForm.addValidationRule('isIP', isIP);
|
||||||
}
|
}
|
||||||
@@ -22,23 +30,29 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
||||||
<SelectValidator name="provision_mode"
|
<SelectValidator
|
||||||
|
name="provision_mode"
|
||||||
label="Provide Access Point…"
|
label="Provide Access Point…"
|
||||||
value={data.provision_mode}
|
value={data.provision_mode}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('provision_mode')}
|
onChange={handleValueChange('provision_mode')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
|
>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
|
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
|
||||||
|
When Network Disconnected
|
||||||
|
</MenuItem>
|
||||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
{
|
{isAPEnabled(data) && (
|
||||||
isAPEnabled(data) &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'matchRegexp:^.{1,32}$']}
|
validators={['required', 'matchRegexp:^.{1,32}$']}
|
||||||
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
|
errorMessages={[
|
||||||
|
'Access Point SSID is required',
|
||||||
|
'Access Point SSID must be 32 characters or less'
|
||||||
|
]}
|
||||||
name="ssid"
|
name="ssid"
|
||||||
label="Access Point SSID"
|
label="Access Point SSID"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -49,7 +63,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['required', 'matchRegexp:^.{8,64}$']}
|
validators={['required', 'matchRegexp:^.{8,64}$']}
|
||||||
errorMessages={['Access Point Password is required', 'Access Point Password must be 8-64 characters']}
|
errorMessages={[
|
||||||
|
'Access Point Password is required',
|
||||||
|
'Access Point Password must be 8-64 characters'
|
||||||
|
]}
|
||||||
name="password"
|
name="password"
|
||||||
label="Access Point Password"
|
label="Access Point Password"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -71,7 +88,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
errorMessages={['Gateway IP is required', 'Must be an IP address']}
|
errorMessages={[
|
||||||
|
'Gateway IP is required',
|
||||||
|
'Must be an IP address'
|
||||||
|
]}
|
||||||
name="gateway_ip"
|
name="gateway_ip"
|
||||||
label="Gateway"
|
label="Gateway"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -82,7 +102,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
errorMessages={[
|
||||||
|
'Subnet mask is required',
|
||||||
|
'Must be an IP address'
|
||||||
|
]}
|
||||||
name="subnet_mask"
|
name="subnet_mask"
|
||||||
label="Subnet"
|
label="Subnet"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -92,9 +115,14 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Theme } from "@material-ui/core";
|
import { Theme } from '@material-ui/core';
|
||||||
import { APStatus, APNetworkStatus } from "./types";
|
import { APStatus, APNetworkStatus } from './types';
|
||||||
|
|
||||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -12,17 +12,17 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const apStatus = ({ status }: APStatus) => {
|
export const apStatus = ({ status }: APStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case APNetworkStatus.ACTIVE:
|
case APNetworkStatus.ACTIVE:
|
||||||
return "Active";
|
return 'Active';
|
||||||
case APNetworkStatus.INACTIVE:
|
case APNetworkStatus.INACTIVE:
|
||||||
return "Inactive";
|
return 'Inactive';
|
||||||
case APNetworkStatus.LINGERING:
|
case APNetworkStatus.LINGERING:
|
||||||
return "Lingering until idle";
|
return 'Lingering until idle';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { AP_STATUS_ENDPOINT } from '../api';
|
import { AP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import APStatusForm from './APStatusForm';
|
import APStatusForm from './APStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { APStatus } from './types';
|
|||||||
type APStatusControllerProps = RestControllerProps<APStatus>;
|
type APStatusControllerProps = RestControllerProps<APStatus>;
|
||||||
|
|
||||||
class APStatusController extends Component<APStatusControllerProps> {
|
class APStatusController extends Component<APStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,10 +23,10 @@ class APStatusController extends Component<APStatusControllerProps> {
|
|||||||
<SectionContent title="Access Point Status">
|
<SectionContent title="Access Point Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <APStatusForm {...formProps} />}
|
render={(formProps) => <APStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import ComputerIcon from '@material-ui/icons/Computer';
|
import ComputerIcon from '@material-ui/icons/Computer';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
HighlightAvatar
|
||||||
|
} from '../components';
|
||||||
import { apStatusHighlight, apStatus } from './APStatus';
|
import { apStatusHighlight, apStatus } from './APStatus';
|
||||||
import { APStatus } from './types';
|
import { APStatus } from './types';
|
||||||
|
|
||||||
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
||||||
|
|
||||||
class APStatusForm extends Component<APStatusFormProps> {
|
class APStatusForm extends Component<APStatusFormProps> {
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -61,18 +72,20 @@ class APStatusForm extends Component<APStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(APStatusForm);
|
export default withTheme(APStatusForm);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import APSettingsController from './APSettingsController';
|
import APSettingsController from './APSettingsController';
|
||||||
@@ -12,8 +16,7 @@ import APStatusController from './APStatusController';
|
|||||||
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class AccessPoint extends Component<AccessPointProps> {
|
class AccessPoint extends Component<AccessPointProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,17 +24,33 @@ class AccessPoint extends Component<AccessPointProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Access Point">
|
<MenuAppBar sectionTitle="Access Point">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/ap/status" label="Access Point Status" />
|
<Tab value="/ap/status" label="Access Point Status" />
|
||||||
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
|
value="/ap/settings"
|
||||||
|
label="Access Point Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
|
exact
|
||||||
|
path="/ap/status"
|
||||||
|
component={APStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/ap/settings"
|
||||||
|
component={APSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/ap/status" />
|
<Redirect to="/ap/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { ENDPOINT_ROOT } from './Env';
|
import { ENDPOINT_ROOT } from './Env';
|
||||||
|
|
||||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features';
|
||||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus';
|
||||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings';
|
||||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
|
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time';
|
||||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings';
|
||||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
|
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus';
|
||||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
|
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks';
|
||||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
|
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks';
|
||||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "networkSettings";
|
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings';
|
||||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + "networkStatus";
|
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus';
|
||||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
|
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings';
|
||||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware';
|
||||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
|
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings';
|
||||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
|
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus';
|
||||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
|
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus';
|
||||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn';
|
||||||
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
export const VERIFY_AUTHORIZATION_ENDPOINT =
|
||||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
|
ENDPOINT_ROOT + 'verifyAuthorization';
|
||||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings';
|
||||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken';
|
||||||
|
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart';
|
||||||
|
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset';
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
||||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||||
|
|
||||||
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
|
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
|
||||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");
|
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
|
||||||
|
export const EVENT_SOURCE_ROOT = calculateEndpointRoot('/es/');
|
||||||
|
export const API_ENDPOINT_ROOT = calculateEndpointRoot('/api/');
|
||||||
|
|
||||||
function calculateEndpointRoot(endpointPath: string) {
|
function calculateEndpointRoot(endpointPath: string) {
|
||||||
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
||||||
if (httpRoot) {
|
if (httpRoot) {
|
||||||
return httpRoot + endpointPath;
|
return httpRoot + endpointPath;
|
||||||
}
|
}
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
return location.protocol + "//" + location.host + endpointPath;
|
return location.protocol + '//' + location.host + endpointPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateWebSocketRoot(webSocketPath: string) {
|
function calculateWebSocketRoot(webSocketPath: string) {
|
||||||
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
|
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
|
||||||
if (webSocketRoot) {
|
if (webSocketRoot) {
|
||||||
return webSocketRoot + webSocketPath;
|
return webSocketRoot + webSocketPath;
|
||||||
}
|
}
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
|
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
return webProtocol + "//" + location.host + webSocketPath;
|
return webProtocol + '//' + location.host + webSocketPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './Env'
|
export * from './Env';
|
||||||
export * from './Endpoints'
|
export * from './Endpoints';
|
||||||
|
|||||||
@@ -1,42 +1,56 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
import {
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
RouteProps,
|
||||||
|
RouteComponentProps
|
||||||
|
} from 'react-router-dom';
|
||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
import * as Authentication from './Authentication';
|
import * as Authentication from './Authentication';
|
||||||
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
|
import {
|
||||||
|
withAuthenticationContext,
|
||||||
|
AuthenticationContextProps,
|
||||||
|
AuthenticatedContext,
|
||||||
|
AuthenticatedContextValue
|
||||||
|
} from './AuthenticationContext';
|
||||||
|
|
||||||
type ChildComponent = React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
interface AuthenticatedRouteProps
|
||||||
|
extends RouteProps,
|
||||||
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
|
WithSnackbarProps,
|
||||||
component: ChildComponent;
|
AuthenticationContextProps {
|
||||||
|
component:
|
||||||
|
| React.ComponentType<RouteComponentProps<any>>
|
||||||
|
| React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
const {
|
||||||
|
enqueueSnackbar,
|
||||||
|
authenticationContext,
|
||||||
|
component: Component,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
const renderComponent: RenderComponent = (props) => {
|
const renderComponent: RenderComponent = (props) => {
|
||||||
if (authenticationContext.me) {
|
if (authenticationContext.me) {
|
||||||
return (
|
return (
|
||||||
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
|
<AuthenticatedContext.Provider
|
||||||
|
value={authenticationContext as AuthenticatedContextValue}
|
||||||
|
>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</AuthenticatedContext.Provider>
|
</AuthenticatedContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Authentication.storeLoginRedirect(location);
|
Authentication.storeLoginRedirect(location);
|
||||||
enqueueSnackbar("Please sign in to continue.", { variant: 'info' });
|
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
|
||||||
return (
|
return <Redirect to="/" />;
|
||||||
<Redirect to='/' />
|
};
|
||||||
);
|
return <Route {...rest} render={renderComponent} />;
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Route {...rest} render={renderComponent} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export function clearLoginRedirect() {
|
|||||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
|
export function fetchLoginRedirect(
|
||||||
|
features: Features
|
||||||
|
): H.LocationDescriptorObject {
|
||||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
@@ -38,16 +40,19 @@ export function fetchLoginRedirect(features: Features): H.LocationDescriptorObje
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene with one with provides the access token if present.
|
* Wraps the normal fetch routine with one with provides the access token if present.
|
||||||
*/
|
*/
|
||||||
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
export function authorizedFetch(
|
||||||
|
url: RequestInfo,
|
||||||
|
params?: RequestInit
|
||||||
|
): Promise<Response> {
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
params.credentials = 'include';
|
params.credentials = 'include';
|
||||||
params.headers = {
|
params.headers = {
|
||||||
...params.headers,
|
...params.headers,
|
||||||
"Authorization": 'Bearer ' + accessToken
|
Authorization: 'Bearer ' + accessToken
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return fetch(url, params);
|
return fetch(url, params);
|
||||||
@@ -55,26 +60,31 @@ export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
|
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
|
||||||
* for a single file upload and takes care of adding the Authroization header and redirecting on
|
* for a single file upload and takes care of adding the Authorization header and redirecting on
|
||||||
* authroization errors as we do for normal fetch operations.
|
* authorization errors as we do for normal fetch operations.
|
||||||
*/
|
*/
|
||||||
export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, file: File, onProgress: (event: ProgressEvent<EventTarget>) => void): Promise<void> {
|
export function redirectingAuthorizedUpload(
|
||||||
|
xhr: XMLHttpRequest,
|
||||||
|
url: string,
|
||||||
|
file: File,
|
||||||
|
onProgress: (event: ProgressEvent<EventTarget>) => void
|
||||||
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
xhr.open("POST", url, true);
|
xhr.open('POST', url, true);
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
|
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
|
||||||
}
|
}
|
||||||
xhr.upload.onprogress = onProgress;
|
xhr.upload.onprogress = onProgress;
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
if (xhr.status === 401 || xhr.status === 403) {
|
if (xhr.status === 401 || xhr.status === 403) {
|
||||||
history.push("/unauthorized");
|
history.push('/unauthorized');
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
|
xhr.onerror = function () {
|
||||||
reject(new DOMException('Error', 'UploadError'));
|
reject(new DOMException('Error', 'UploadError'));
|
||||||
};
|
};
|
||||||
xhr.onabort = function () {
|
xhr.onabort = function () {
|
||||||
@@ -87,19 +97,24 @@ export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene which redirects on 401 response.
|
* Wraps the normal fetch routine which redirects on 401 response.
|
||||||
*/
|
*/
|
||||||
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
export function redirectingAuthorizedFetch(
|
||||||
|
url: RequestInfo,
|
||||||
|
params?: RequestInit
|
||||||
|
): Promise<Response> {
|
||||||
return new Promise<Response>((resolve, reject) => {
|
return new Promise<Response>((resolve, reject) => {
|
||||||
authorizedFetch(url, params).then(response => {
|
authorizedFetch(url, params)
|
||||||
if (response.status === 401 || response.status === 403) {
|
.then((response) => {
|
||||||
history.push("/unauthorized");
|
if (response.status === 401 || response.status === 403) {
|
||||||
} else {
|
history.push('/unauthorized');
|
||||||
resolve(response);
|
} else {
|
||||||
}
|
resolve(response);
|
||||||
}).catch(error => {
|
}
|
||||||
reject(error);
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
|
|
||||||
export interface Me {
|
export interface Me {
|
||||||
username: string;
|
username: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
version: string; // proddy added
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticationContextValue {
|
export interface AuthenticationContextValue {
|
||||||
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
|
|||||||
me?: Me;
|
me?: Me;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
|
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
||||||
export const AuthenticationContext = React.createContext(
|
export const AuthenticationContext = React.createContext(
|
||||||
AuthenticationContextDefaultValue
|
AuthenticationContextDefaultValue
|
||||||
);
|
);
|
||||||
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
|
|||||||
authenticationContext: AuthenticationContextValue;
|
authenticationContext: AuthenticationContextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
|
export function withAuthenticationContext<T extends AuthenticationContextProps>(
|
||||||
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
|
return class extends React.Component<
|
||||||
|
Omit<T, keyof AuthenticationContextProps>
|
||||||
|
> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AuthenticationContext.Consumer>
|
<AuthenticationContext.Consumer>
|
||||||
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
|
{(authenticationContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
authenticationContext={authenticationContext}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AuthenticationContext.Consumer>
|
</AuthenticationContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
|||||||
me: Me;
|
me: Me;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
|
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
|
||||||
export const AuthenticatedContext = React.createContext(
|
export const AuthenticatedContext = React.createContext(
|
||||||
AuthenticatedContextDefaultValue
|
AuthenticatedContextDefaultValue
|
||||||
);
|
);
|
||||||
@@ -47,12 +55,21 @@ export interface AuthenticatedContextProps {
|
|||||||
authenticatedContext: AuthenticatedContextValue;
|
authenticatedContext: AuthenticatedContextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
|
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
|
||||||
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
|
return class extends React.Component<
|
||||||
|
Omit<T, keyof AuthenticatedContextProps>
|
||||||
|
> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AuthenticatedContext.Consumer>
|
<AuthenticatedContext.Consumer>
|
||||||
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
|
{(authenticatedContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
authenticatedContext={authenticatedContext}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AuthenticatedContext.Consumer>
|
</AuthenticatedContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import * as React from 'react';
|
|||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import jwtDecode from 'jwt-decode';
|
import jwtDecode from 'jwt-decode';
|
||||||
|
|
||||||
import history from '../history'
|
import history from '../history';
|
||||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||||
import { AuthenticationContext, AuthenticationContextValue, Me } from './AuthenticationContext';
|
import {
|
||||||
|
AuthenticationContext,
|
||||||
|
AuthenticationContextValue,
|
||||||
|
Me
|
||||||
|
} from './AuthenticationContext';
|
||||||
import FullScreenLoading from '../components/FullScreenLoading';
|
import FullScreenLoading from '../components/FullScreenLoading';
|
||||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||||
|
|
||||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
|
export const decodeMeJWT = (accessToken: string): Me =>
|
||||||
|
jwtDecode(accessToken) as Me;
|
||||||
|
|
||||||
interface AuthenticationWrapperState {
|
interface AuthenticationWrapperState {
|
||||||
context: AuthenticationContextValue;
|
context: AuthenticationContextValue;
|
||||||
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
|
|||||||
|
|
||||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||||
|
|
||||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
class AuthenticationWrapper extends React.Component<
|
||||||
|
AuthenticationWrapperProps,
|
||||||
|
AuthenticationWrapperState
|
||||||
|
> {
|
||||||
constructor(props: AuthenticationWrapperProps) {
|
constructor(props: AuthenticationWrapperProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
context: {
|
context: {
|
||||||
refresh: this.refresh,
|
refresh: this.refresh,
|
||||||
signIn: this.signIn,
|
signIn: this.signIn,
|
||||||
signOut: this.signOut,
|
signOut: this.signOut
|
||||||
},
|
},
|
||||||
initialized: false
|
initialized: false
|
||||||
};
|
};
|
||||||
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
|
{this.state.initialized
|
||||||
|
? this.renderContent()
|
||||||
|
: this.renderContentLoading()}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderContentLoading() {
|
renderContentLoading() {
|
||||||
return (
|
return <FullScreenLoading />;
|
||||||
<FullScreenLoading />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh = () => {
|
refresh = () => {
|
||||||
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
const me =
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||||
}).catch(error => {
|
this.setState({
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
initialized: true,
|
||||||
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
context: { ...this.state.context, me }
|
||||||
variant: 'error',
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.setState({
|
||||||
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
'Error verifying authorization: ' + error.message,
|
||||||
|
{
|
||||||
|
variant: 'error'
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
this.setState({
|
||||||
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
signIn = (accessToken: string) => {
|
signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||||
const me: Me = decodeMeJWT(accessToken);
|
const me: Me = decodeMeJWT(accessToken);
|
||||||
this.setState({ context: { ...this.state.context, me } });
|
this.setState({ context: { ...this.state.context, me } });
|
||||||
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
this.props.enqueueSnackbar(`Logged in as ${me.username}`, {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
this.setState({
|
||||||
throw new Error("Failed to parse JWT " + err.message);
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
|
throw new Error('Failed to parse JWT ' + err.message);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
signOut = () => {
|
signOut = () => {
|
||||||
getStorage().removeItem(ACCESS_TOKEN);
|
getStorage().removeItem(ACCESS_TOKEN);
|
||||||
@@ -101,10 +127,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
me: undefined
|
me: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
|
this.props.enqueueSnackbar('You have signed out', { variant: 'success' });
|
||||||
history.push('/');
|
history.push('/');
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
export default withFeatures(withSnackbar(AuthenticationWrapper));
|
||||||
|
|||||||
@@ -1,29 +1,46 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
import {
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
RouteProps,
|
||||||
|
RouteComponentProps
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
|
import {
|
||||||
|
withAuthenticationContext,
|
||||||
|
AuthenticationContextProps
|
||||||
|
} from './AuthenticationContext';
|
||||||
import * as Authentication from './Authentication';
|
import * as Authentication from './Authentication';
|
||||||
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||||
|
|
||||||
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
|
interface UnauthenticatedRouteProps
|
||||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
extends RouteProps,
|
||||||
|
AuthenticationContextProps,
|
||||||
|
WithFeaturesProps {
|
||||||
|
component:
|
||||||
|
| React.ComponentType<RouteComponentProps<any>>
|
||||||
|
| React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
const {
|
||||||
|
authenticationContext,
|
||||||
|
component: Component,
|
||||||
|
features,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
const renderComponent: RenderComponent = (props) => {
|
const renderComponent: RenderComponent = (props) => {
|
||||||
if (authenticationContext.me) {
|
if (authenticationContext.me) {
|
||||||
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
|
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
|
||||||
}
|
}
|
||||||
return (<Component {...props} />);
|
if (Component) {
|
||||||
}
|
return <Component {...props} />;
|
||||||
return (
|
}
|
||||||
<Route {...rest} render={renderComponent} />
|
};
|
||||||
);
|
return <Route {...rest} render={renderComponent} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { makeStyles } from '@material-ui/styles';
|
import { makeStyles } from '@material-ui/styles';
|
||||||
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
|
import { Paper, Typography, Box, CssBaseline } from '@material-ui/core';
|
||||||
import WarningIcon from "@material-ui/icons/Warning"
|
import WarningIcon from '@material-ui/icons/Warning';
|
||||||
|
|
||||||
const styles = makeStyles(
|
const styles = makeStyles({
|
||||||
{
|
siteErrorPage: {
|
||||||
siteErrorPage: {
|
display: 'flex',
|
||||||
display: "flex",
|
height: '100vh',
|
||||||
height: "100vh",
|
justifyContent: 'center',
|
||||||
justifyContent: "center",
|
flexDirection: 'column'
|
||||||
flexDirection: "column"
|
},
|
||||||
},
|
siteErrorPagePanel: {
|
||||||
siteErrorPagePanel: {
|
textAlign: 'center',
|
||||||
textAlign: "center",
|
padding: '280px 0 40px 0',
|
||||||
padding: "280px 0 40px 0",
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundPosition: '50% 40px',
|
||||||
backgroundPosition: "50% 40px",
|
backgroundSize: '200px auto',
|
||||||
backgroundSize: "200px auto",
|
width: '100%'
|
||||||
width: "100%",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
interface ApplicationErrorProps {
|
interface ApplicationErrorProps {
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -33,27 +31,29 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
|
|||||||
<div className={classes.siteErrorPage}>
|
<div className={classes.siteErrorPage}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
||||||
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
<WarningIcon fontSize="large" color="error" />
|
<WarningIcon fontSize="large" color="error" />
|
||||||
<Box ml={2}>
|
<Box ml={2}>
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">Application error</Typography>
|
||||||
Application error
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
<Typography variant="subtitle1" gutterBottom>
|
||||||
Failed to configure the application, please refresh to try again.
|
Failed to configure the application, please refresh to try again.
|
||||||
</Typography>
|
</Typography>
|
||||||
{error &&
|
{error && (
|
||||||
(
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
Error: {error}
|
||||||
Error: {error}
|
</Typography>
|
||||||
</Typography>
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ApplicationError;
|
export default ApplicationError;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { FC } from "react";
|
import { FC } from 'react';
|
||||||
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
|
import { FormControlLabel, FormControlLabelProps } from '@material-ui/core';
|
||||||
|
|
||||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||||
<div>
|
<div>
|
||||||
<FormControlLabel {...props} />
|
<FormControlLabel {...props} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default BlockFormControlLabel;
|
export default BlockFormControlLabel;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Button, styled } from "@material-ui/core";
|
import { Button, styled } from '@material-ui/core';
|
||||||
|
|
||||||
const ErrorButton = styled(Button)(({ theme }) => ({
|
const ErrorButton = styled(Button)(({ theme }) => ({
|
||||||
color: theme.palette.getContrastText(theme.palette.error.main),
|
color: theme.palette.getContrastText(theme.palette.error.main),
|
||||||
backgroundColor: theme.palette.error.main,
|
backgroundColor: theme.palette.error.main,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.error.dark,
|
backgroundColor: theme.palette.error.dark
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { styled, Box } from "@material-ui/core";
|
import { styled, Box } from '@material-ui/core';
|
||||||
|
|
||||||
const FormActions = styled(Box)(({ theme }) => ({
|
const FormActions = styled(Box)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(1)
|
marginTop: theme.spacing(1)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Button, styled } from "@material-ui/core";
|
import { Button, styled } from '@material-ui/core';
|
||||||
|
|
||||||
const FormButton = styled(Button)(({ theme }) => ({
|
const FormButton = styled(Button)(({ theme }) => ({
|
||||||
margin: theme.spacing(0, 1),
|
margin: theme.spacing(0, 1),
|
||||||
'&:last-child': {
|
'&:last-child': {
|
||||||
marginRight: 0,
|
marginRight: 0
|
||||||
},
|
},
|
||||||
'&:first-child': {
|
'&:first-child': {
|
||||||
marginLeft: 0,
|
marginLeft: 0
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,30 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
|||||||
import { Typography, Theme } from '@material-ui/core';
|
import { Typography, Theme } from '@material-ui/core';
|
||||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
fullScreenLoading: {
|
createStyles({
|
||||||
padding: theme.spacing(2),
|
fullScreenLoading: {
|
||||||
display: "flex",
|
padding: theme.spacing(2),
|
||||||
alignItems: "center",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
alignItems: 'center',
|
||||||
height: "100vh",
|
justifyContent: 'center',
|
||||||
flexDirection: "column"
|
height: '100vh',
|
||||||
},
|
flexDirection: 'column'
|
||||||
progress: {
|
},
|
||||||
margin: theme.spacing(4),
|
progress: {
|
||||||
}
|
margin: theme.spacing(4)
|
||||||
}));
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const FullScreenLoading = () => {
|
const FullScreenLoading = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<div className={classes.fullScreenLoading}>
|
<div className={classes.fullScreenLoading}>
|
||||||
<CircularProgress className={classes.progress} size={100} />
|
<CircularProgress className={classes.progress} size={100} />
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">Loading…</Typography>
|
||||||
Loading…
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FullScreenLoading;
|
export default FullScreenLoading;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Avatar, makeStyles } from "@material-ui/core";
|
import { Avatar, makeStyles } from '@material-ui/core';
|
||||||
import React, { FC } from "react";
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface HighlightAvatarProps {
|
interface HighlightAvatarProps {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -13,11 +13,7 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
return (
|
return <Avatar className={classes.root}>{props.children}</Avatar>;
|
||||||
<Avatar className={classes.root}>
|
};
|
||||||
{props.children}
|
|
||||||
</Avatar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HighlightAvatar;
|
export default HighlightAvatar;
|
||||||
|
|||||||
@@ -1,14 +1,41 @@
|
|||||||
import React, { RefObject, Fragment } from 'react';
|
import React, { RefObject, Fragment } from 'react';
|
||||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
|
import {
|
||||||
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
|
Drawer,
|
||||||
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
IconButton
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import {
|
||||||
|
ClickAwayListener,
|
||||||
|
Popper,
|
||||||
|
Hidden,
|
||||||
|
Typography
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar
|
||||||
|
} from '@material-ui/core';
|
||||||
import { Card, CardContent, CardActions } from '@material-ui/core';
|
import { Card, CardContent, CardActions } from '@material-ui/core';
|
||||||
|
|
||||||
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
|
import {
|
||||||
|
withStyles,
|
||||||
|
createStyles,
|
||||||
|
Theme,
|
||||||
|
WithTheme,
|
||||||
|
WithStyles,
|
||||||
|
withTheme
|
||||||
|
} from '@material-ui/core/styles';
|
||||||
|
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
|
||||||
import SettingsIcon from '@material-ui/icons/Settings';
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
||||||
@@ -19,76 +46,84 @@ import MenuIcon from '@material-ui/icons/Menu';
|
|||||||
|
|
||||||
import ProjectMenu from '../project/ProjectMenu';
|
import ProjectMenu from '../project/ProjectMenu';
|
||||||
import { PROJECT_NAME } from '../api';
|
import { PROJECT_NAME } from '../api';
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||||
|
|
||||||
const drawerWidth = 290;
|
const drawerWidth = 290;
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
root: {
|
createStyles({
|
||||||
display: 'flex',
|
root: {
|
||||||
},
|
display: 'flex'
|
||||||
drawer: {
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
},
|
||||||
},
|
drawer: {
|
||||||
title: {
|
[theme.breakpoints.up('md')]: {
|
||||||
flexGrow: 1
|
width: drawerWidth,
|
||||||
},
|
flexShrink: 0
|
||||||
appBar: {
|
}
|
||||||
marginLeft: drawerWidth,
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
width: `calc(100% - ${drawerWidth}px)`,
|
|
||||||
},
|
},
|
||||||
},
|
title: {
|
||||||
toolbarImage: {
|
flexGrow: 1
|
||||||
[theme.breakpoints.up('xs')]: {
|
|
||||||
height: 24,
|
|
||||||
marginRight: theme.spacing(2)
|
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('sm')]: {
|
appBar: {
|
||||||
height: 36,
|
marginLeft: drawerWidth,
|
||||||
marginRight: theme.spacing(3)
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: `calc(100% - ${drawerWidth}px)`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
toolbarImage: {
|
||||||
menuButton: {
|
[theme.breakpoints.up('xs')]: {
|
||||||
marginRight: theme.spacing(2),
|
height: 24,
|
||||||
[theme.breakpoints.up('md')]: {
|
marginRight: theme.spacing(2)
|
||||||
display: 'none',
|
},
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
height: 36,
|
||||||
|
marginRight: theme.spacing(3)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
menuButton: {
|
||||||
toolbar: theme.mixins.toolbar,
|
marginRight: theme.spacing(2),
|
||||||
drawerPaper: {
|
[theme.breakpoints.up('md')]: {
|
||||||
width: drawerWidth,
|
display: 'none'
|
||||||
},
|
}
|
||||||
content: {
|
},
|
||||||
flexGrow: 1
|
toolbar: theme.mixins.toolbar,
|
||||||
},
|
drawerPaper: {
|
||||||
authMenu: {
|
width: drawerWidth
|
||||||
zIndex: theme.zIndex.tooltip,
|
},
|
||||||
maxWidth: 400,
|
content: {
|
||||||
},
|
flexGrow: 1
|
||||||
authMenuActions: {
|
},
|
||||||
padding: theme.spacing(2),
|
authMenu: {
|
||||||
"& > * + *": {
|
zIndex: theme.zIndex.tooltip,
|
||||||
marginLeft: theme.spacing(2),
|
maxWidth: 400
|
||||||
|
},
|
||||||
|
authMenuActions: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
'& > * + *': {
|
||||||
|
marginLeft: theme.spacing(2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
interface MenuAppBarState {
|
interface MenuAppBarState {
|
||||||
mobileOpen: boolean;
|
mobileOpen: boolean;
|
||||||
authMenuOpen: boolean;
|
authMenuOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
|
interface MenuAppBarProps
|
||||||
|
extends WithFeaturesProps,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
WithTheme,
|
||||||
|
WithStyles<typeof styles>,
|
||||||
|
RouteComponentProps {
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||||
|
|
||||||
constructor(props: MenuAppBarProps) {
|
constructor(props: MenuAppBarProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -101,38 +136,48 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
|
|
||||||
handleToggle = () => {
|
handleToggle = () => {
|
||||||
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleClose = (event: React.MouseEvent<Document>) => {
|
handleClose = (event: React.MouseEvent<Document>) => {
|
||||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
|
if (
|
||||||
|
this.anchorRef.current &&
|
||||||
|
this.anchorRef.current.contains(event.currentTarget)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ authMenuOpen: false });
|
this.setState({ authMenuOpen: false });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleDrawerToggle = () => {
|
handleDrawerToggle = () => {
|
||||||
this.setState({ mobileOpen: !this.state.mobileOpen });
|
this.setState({ mobileOpen: !this.state.mobileOpen });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
|
const {
|
||||||
|
classes,
|
||||||
|
theme,
|
||||||
|
children,
|
||||||
|
sectionTitle,
|
||||||
|
authenticatedContext,
|
||||||
|
features
|
||||||
|
} = this.props;
|
||||||
const { mobileOpen, authMenuOpen } = this.state;
|
const { mobileOpen, authMenuOpen } = this.state;
|
||||||
const path = this.props.match.url;
|
const path = this.props.match.url;
|
||||||
const drawer = (
|
const drawer = (
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
<img src="/app/icon.png" className={classes.toolbarImage} alt={PROJECT_NAME} />
|
<img
|
||||||
|
src="/app/icon.png"
|
||||||
|
className={classes.toolbarImage}
|
||||||
|
alt={PROJECT_NAME}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h6" color="textPrimary">
|
<Typography variant="h6" color="textPrimary">
|
||||||
{PROJECT_NAME}
|
{PROJECT_NAME}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography align="right" variant="caption" color="textPrimary">
|
|
||||||
v{authenticatedContext.me.version}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Divider absolute />
|
<Divider absolute />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
@@ -144,20 +189,35 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/network/"
|
||||||
|
selected={path.startsWith('/network/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<WifiIcon />
|
<SettingsEthernetIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Network Connection" />
|
<ListItemText primary="Network Connection" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/ap/"
|
||||||
|
selected={path.startsWith('/ap/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsInputAntennaIcon />
|
<SettingsInputAntennaIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Access Point" />
|
<ListItemText primary="Access Point" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{features.ntp && (
|
{features.ntp && (
|
||||||
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/ntp/"
|
||||||
|
selected={path.startsWith('/ntp/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<AccessTimeIcon />
|
<AccessTimeIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
@@ -165,7 +225,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{features.mqtt && (
|
{features.mqtt && (
|
||||||
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
|
<ListItem
|
||||||
|
to="/mqtt/"
|
||||||
|
selected={path.startsWith('/mqtt/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DeviceHubIcon />
|
<DeviceHubIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
@@ -173,14 +238,25 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
{features.security && (
|
{features.security && (
|
||||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
<ListItem
|
||||||
|
to="/security/"
|
||||||
|
selected={path.startsWith('/security/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Security" />
|
<ListItemText primary="Security" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
|
<ListItem
|
||||||
|
to="/system/"
|
||||||
|
selected={path.startsWith('/system/')}
|
||||||
|
button
|
||||||
|
component={Link}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
@@ -201,7 +277,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
>
|
>
|
||||||
<AccountCircleIcon />
|
<AccountCircleIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
|
<Popper
|
||||||
|
open={authMenuOpen}
|
||||||
|
anchorEl={this.anchorRef.current}
|
||||||
|
transition
|
||||||
|
className={classes.authMenu}
|
||||||
|
>
|
||||||
<ClickAwayListener onClickAway={this.handleClose}>
|
<ClickAwayListener onClickAway={this.handleClose}>
|
||||||
<Card id="menu-list-grow">
|
<Card id="menu-list-grow">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -212,13 +293,27 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
<AccountCircleIcon />
|
<AccountCircleIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
'Signed in as: ' + authenticatedContext.me.username
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
authenticatedContext.me.admin ? 'Admin User' : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CardActions className={classes.authMenuActions}>
|
<CardActions className={classes.authMenuActions}>
|
||||||
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
color="primary"
|
||||||
|
onClick={authenticatedContext.signOut}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
@@ -239,7 +334,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
color="inherit"
|
||||||
|
noWrap
|
||||||
|
className={classes.title}
|
||||||
|
>
|
||||||
{sectionTitle}
|
{sectionTitle}
|
||||||
</Typography>
|
</Typography>
|
||||||
{features.security && userMenu}
|
{features.security && userMenu}
|
||||||
@@ -253,10 +353,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
open={mobileOpen}
|
open={mobileOpen}
|
||||||
onClose={this.handleDrawerToggle}
|
onClose={this.handleDrawerToggle}
|
||||||
classes={{
|
classes={{
|
||||||
paper: classes.drawerPaper,
|
paper: classes.drawerPaper
|
||||||
}}
|
}}
|
||||||
ModalProps={{
|
ModalProps={{
|
||||||
keepMounted: true,
|
keepMounted: true
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{drawer}
|
{drawer}
|
||||||
@@ -265,7 +365,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
<Hidden smDown implementation="css">
|
<Hidden smDown implementation="css">
|
||||||
<Drawer
|
<Drawer
|
||||||
classes={{
|
classes={{
|
||||||
paper: classes.drawerPaper,
|
paper: classes.drawerPaper
|
||||||
}}
|
}}
|
||||||
variant="permanent"
|
variant="permanent"
|
||||||
open
|
open
|
||||||
@@ -285,10 +385,6 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
|||||||
|
|
||||||
export default withRouter(
|
export default withRouter(
|
||||||
withTheme(
|
withTheme(
|
||||||
withFeatures(
|
withFeatures(withAuthenticatedContext(withStyles(styles)(MenuAppBar)))
|
||||||
withAuthenticatedContext(
|
|
||||||
withStyles(styles)(MenuAppBar)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorComponentProps
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||||
import { InputAdornment, IconButton } from '@material-ui/core';
|
import { InputAdornment, IconButton } from '@material-ui/core';
|
||||||
import {Visibility,VisibilityOff } from '@material-ui/icons';
|
import { Visibility, VisibilityOff } from '@material-ui/icons';
|
||||||
|
|
||||||
const styles = createStyles({
|
const styles = createStyles({
|
||||||
input: {
|
input: {
|
||||||
"&::-ms-reveal": {
|
'&::-ms-reveal': {
|
||||||
display: "none"
|
display: 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
type PasswordValidatorProps = WithStyles<typeof styles> &
|
||||||
|
Exclude<ValidatorComponentProps, 'type' | 'InputProps'>;
|
||||||
|
|
||||||
interface PasswordValidatorState {
|
interface PasswordValidatorState {
|
||||||
showPassword: boolean;
|
showPassword: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
class PasswordValidator extends React.Component<
|
||||||
|
PasswordValidatorProps,
|
||||||
|
PasswordValidatorState
|
||||||
|
> {
|
||||||
state = {
|
state = {
|
||||||
showPassword: false
|
showPassword: false
|
||||||
};
|
};
|
||||||
@@ -29,7 +35,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
this.setState({
|
this.setState({
|
||||||
showPassword: !this.state.showPassword
|
showPassword: !this.state.showPassword
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, ...rest } = this.props;
|
const { classes, ...rest } = this.props;
|
||||||
@@ -39,7 +45,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
type={this.state.showPassword ? 'text' : 'password'}
|
type={this.state.showPassword ? 'text' : 'password'}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
classes,
|
classes,
|
||||||
endAdornment:
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Toggle password visibility"
|
aria-label="Toggle password visibility"
|
||||||
@@ -48,11 +54,11 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(PasswordValidator);
|
export default withStyles(styles)(PasswordValidator);
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
|
|||||||
import { redirectingAuthorizedFetch } from '../authentication';
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
|
|
||||||
export interface RestControllerProps<D> extends WithSnackbarProps {
|
export interface RestControllerProps<D> extends WithSnackbarProps {
|
||||||
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
handleValueChange: (
|
||||||
|
name: keyof D
|
||||||
|
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
|
||||||
setData: (data: D, callback?: () => void) => void;
|
setData: (data: D, callback?: () => void) => void;
|
||||||
saveData: () => void;
|
saveData: () => void;
|
||||||
@@ -15,16 +17,18 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
|
export const extractEventValue = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
switch (event.target.type) {
|
switch (event.target.type) {
|
||||||
case "number":
|
case 'number':
|
||||||
return event.target.valueAsNumber;
|
return event.target.valueAsNumber;
|
||||||
case "checkbox":
|
case 'checkbox':
|
||||||
return event.target.checked;
|
return event.target.checked;
|
||||||
default:
|
default:
|
||||||
return event.target.value
|
return event.target.value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
interface RestControllerState<D> {
|
interface RestControllerState<D> {
|
||||||
data?: D;
|
data?: D;
|
||||||
@@ -32,10 +36,15 @@ interface RestControllerState<D> {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
|
export function restController<D, P extends RestControllerProps<D>>(
|
||||||
|
endpointUrl: string,
|
||||||
|
RestController: React.ComponentType<P & RestControllerProps<D>>
|
||||||
|
) {
|
||||||
return withSnackbar(
|
return withSnackbar(
|
||||||
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
|
class extends React.Component<
|
||||||
|
Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps,
|
||||||
|
RestControllerState<D>
|
||||||
|
> {
|
||||||
state: RestControllerState<D> = {
|
state: RestControllerState<D> = {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -43,12 +52,15 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
};
|
};
|
||||||
|
|
||||||
setData = (data: D, callback?: () => void) => {
|
setData = (data: D, callback?: () => void) => {
|
||||||
this.setState({
|
this.setState(
|
||||||
data,
|
{
|
||||||
loading: false,
|
data,
|
||||||
errorMessage: undefined
|
loading: false,
|
||||||
}, callback);
|
errorMessage: undefined
|
||||||
}
|
},
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
loadData = () => {
|
loadData = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -56,19 +68,24 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
loading: true,
|
loading: true,
|
||||||
errorMessage: undefined
|
errorMessage: undefined
|
||||||
});
|
});
|
||||||
redirectingAuthorizedFetch(endpointUrl).then(response => {
|
redirectingAuthorizedFetch(endpointUrl)
|
||||||
if (response.status === 200) {
|
.then((response) => {
|
||||||
return response.json();
|
if (response.status === 200) {
|
||||||
}
|
return response.json();
|
||||||
throw Error("Invalid status code: " + response.status);
|
}
|
||||||
}).then(json => {
|
throw Error('Invalid status code: ' + response.status);
|
||||||
this.setState({ data: json, loading: false })
|
})
|
||||||
}).catch(error => {
|
.then((json) => {
|
||||||
const errorMessage = error.message || "Unknown error";
|
this.setState({ data: json, loading: false });
|
||||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
|
})
|
||||||
this.setState({ data: undefined, loading: false, errorMessage });
|
.catch((error) => {
|
||||||
});
|
const errorMessage = error.message || 'Unknown error';
|
||||||
}
|
this.props.enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({ data: undefined, loading: false, errorMessage });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
saveData = () => {
|
saveData = () => {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
@@ -78,36 +95,47 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}).then(response => {
|
})
|
||||||
if (response.status === 200) {
|
.then((response) => {
|
||||||
return response.json();
|
if (response.status === 200) {
|
||||||
}
|
return response.json();
|
||||||
throw Error("Invalid status code: " + response.status);
|
}
|
||||||
}).then(json => {
|
throw Error('Invalid status code: ' + response.status);
|
||||||
this.props.enqueueSnackbar("Update successful.", { variant: 'success' });
|
})
|
||||||
this.setState({ data: json, loading: false });
|
.then((json) => {
|
||||||
}).catch(error => {
|
this.props.enqueueSnackbar('Update successful.', {
|
||||||
const errorMessage = error.message || "Unknown error";
|
variant: 'success'
|
||||||
this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
|
});
|
||||||
this.setState({ data: undefined, loading: false, errorMessage });
|
this.setState({ data: json, loading: false });
|
||||||
});
|
})
|
||||||
}
|
.catch((error) => {
|
||||||
|
const errorMessage = error.message || 'Unknown error';
|
||||||
|
this.props.enqueueSnackbar('Problem updating: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({ data: undefined, loading: false, errorMessage });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
handleValueChange = (name: keyof D) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <RestController
|
return (
|
||||||
{...this.state}
|
<RestController
|
||||||
{...this.props as P}
|
{...this.state}
|
||||||
handleValueChange={this.handleValueChange}
|
{...(this.props as P)}
|
||||||
setData={this.setData}
|
handleValueChange={this.handleValueChange}
|
||||||
saveData={this.saveData}
|
setData={this.setData}
|
||||||
loadData={this.loadData}
|
saveData={this.saveData}
|
||||||
/>;
|
loadData={this.loadData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,23 @@ import { RestControllerProps } from '.';
|
|||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
loadingSettings: {
|
loadingSettings: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
loadingSettingsDetails: {
|
loadingSettingsDetails: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
textAlign: "center"
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
|
export type RestFormProps<D> = Omit<
|
||||||
|
RestControllerProps<D>,
|
||||||
|
'loading' | 'errorMessage'
|
||||||
|
> & { data: D };
|
||||||
|
|
||||||
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
|
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
|
||||||
render: (props: RestFormProps<D>) => JSX.Element;
|
render: (props: RestFormProps<D>) => JSX.Element;
|
||||||
@@ -46,7 +49,12 @@ export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
|
|||||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className={classes.button}
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
createStyles({
|
createStyles({
|
||||||
content: {
|
content: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
margin: theme.spacing(3),
|
margin: theme.spacing(3)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
interface SectionContentProps {
|
interface SectionContentProps {
|
||||||
title: string;
|
title: string;
|
||||||
titleGutter?: boolean;
|
titleGutter?: boolean;
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionContent: React.FC<SectionContentProps> = (props) => {
|
const SectionContent: React.FC<SectionContentProps> = (props) => {
|
||||||
const { children, title, titleGutter } = props;
|
const { children, title, titleGutter, id } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<Paper className={classes.content}>
|
<Paper id={id} className={classes.content}>
|
||||||
<Typography variant="h6" gutterBottom={titleGutter}>
|
<Typography variant="h6" gutterBottom={titleGutter}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -4,13 +4,20 @@ import { useDropzone, DropzoneState } from 'react-dropzone';
|
|||||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||||
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
||||||
import CancelIcon from '@material-ui/icons/Cancel';
|
import CancelIcon from '@material-ui/icons/Cancel';
|
||||||
import { Theme, Box, Typography, LinearProgress, Button } from '@material-ui/core';
|
import {
|
||||||
|
Theme,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
LinearProgress,
|
||||||
|
Button
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
interface SingleUploadStyleProps extends DropzoneState {
|
interface SingleUploadStyleProps extends DropzoneState {
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
|
const progressPercentage = (progress: ProgressEvent) =>
|
||||||
|
Math.round((progress.loaded * 100) / progress.total);
|
||||||
|
|
||||||
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
||||||
if (props.isDragAccept) {
|
if (props.isDragAccept) {
|
||||||
@@ -23,21 +30,25 @@ const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
|||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
}
|
}
|
||||||
return theme.palette.grey[700];
|
return theme.palette.grey[700];
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
dropzone: {
|
createStyles({
|
||||||
padding: theme.spacing(8, 2),
|
dropzone: {
|
||||||
borderWidth: 2,
|
padding: theme.spacing(8, 2),
|
||||||
borderRadius: 2,
|
borderWidth: 2,
|
||||||
borderStyle: 'dashed',
|
borderRadius: 2,
|
||||||
color: theme.palette.grey[700],
|
borderStyle: 'dashed',
|
||||||
transition: 'border .24s ease-in-out',
|
color: theme.palette.grey[700],
|
||||||
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
|
transition: 'border .24s ease-in-out',
|
||||||
width: '100%',
|
cursor: (props: SingleUploadStyleProps) =>
|
||||||
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
|
props.uploading ? 'default' : 'pointer',
|
||||||
}
|
width: '100%',
|
||||||
}));
|
borderColor: (props: SingleUploadStyleProps) =>
|
||||||
|
getBorderColor(theme, props)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export interface SingleUploadProps {
|
export interface SingleUploadProps {
|
||||||
onDrop: (acceptedFiles: File[]) => void;
|
onDrop: (acceptedFiles: File[]) => void;
|
||||||
@@ -47,26 +58,44 @@ export interface SingleUploadProps {
|
|||||||
progress?: ProgressEvent;
|
progress?: ProgressEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
|
const SingleUpload: FC<SingleUploadProps> = ({
|
||||||
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
|
onDrop,
|
||||||
|
onCancel,
|
||||||
|
accept,
|
||||||
|
uploading,
|
||||||
|
progress
|
||||||
|
}) => {
|
||||||
|
const dropzoneState = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept,
|
||||||
|
disabled: uploading,
|
||||||
|
multiple: false
|
||||||
|
});
|
||||||
const { getRootProps, getInputProps } = dropzoneState;
|
const { getRootProps, getInputProps } = dropzoneState;
|
||||||
const classes = useStyles({ ...dropzoneState, uploading });
|
const classes = useStyles({ ...dropzoneState, uploading });
|
||||||
|
|
||||||
|
|
||||||
const renderProgressText = () => {
|
const renderProgressText = () => {
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
if (progress?.lengthComputable) {
|
if (progress?.lengthComputable) {
|
||||||
return `Uploading: ${progressPercentage(progress)}%`;
|
return `Uploading: ${progressPercentage(progress)}%`;
|
||||||
}
|
}
|
||||||
return "Uploading\u2026";
|
return 'Uploading\u2026';
|
||||||
}
|
}
|
||||||
return "Drop file or click here";
|
return 'Drop file or click here';
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderProgress = (progress?: ProgressEvent) => (
|
const renderProgress = (progress?: ProgressEvent) => (
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
|
variant={
|
||||||
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
|
!progress || progress.lengthComputable ? 'determinate' : 'indeterminate'
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
!progress
|
||||||
|
? 0
|
||||||
|
: progress.lengthComputable
|
||||||
|
? progressPercentage(progress)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -74,16 +103,19 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
|||||||
<div {...getRootProps({ className: classes.dropzone })}>
|
<div {...getRootProps({ className: classes.dropzone })}>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<Box flexDirection="column" display="flex" alignItems="center">
|
<Box flexDirection="column" display="flex" alignItems="center">
|
||||||
<CloudUploadIcon fontSize='large' />
|
<CloudUploadIcon fontSize="large" />
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">{renderProgressText()}</Typography>
|
||||||
{renderProgressText()}
|
|
||||||
</Typography>
|
|
||||||
{uploading && (
|
{uploading && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Box width="100%" p={2}>
|
<Box width="100%" p={2}>
|
||||||
{renderProgress(progress)}
|
{renderProgress(progress)}
|
||||||
</Box>
|
</Box>
|
||||||
<Button startIcon={<CancelIcon />} variant="contained" color="secondary" onClick={onCancel}>
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -91,6 +123,6 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
|||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SingleUpload;
|
export default SingleUpload;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { addAccessTokenParameter } from '../authentication';
|
|||||||
import { extractEventValue } from '.';
|
import { extractEventValue } from '.';
|
||||||
|
|
||||||
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
|
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
|
||||||
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
handleValueChange: (
|
||||||
|
name: keyof D
|
||||||
|
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
|
||||||
setData: (data: D, callback?: () => void) => void;
|
setData: (data: D, callback?: () => void) => void;
|
||||||
saveData: () => void;
|
saveData: () => void;
|
||||||
@@ -25,8 +27,8 @@ interface WebSocketControllerState<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum WebSocketMessageType {
|
enum WebSocketMessageType {
|
||||||
ID = "id",
|
ID = 'id',
|
||||||
PAYLOAD = "payload"
|
PAYLOAD = 'payload'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSocketIdMessage {
|
interface WebSocketIdMessage {
|
||||||
@@ -40,21 +42,32 @@ interface WebSocketPayloadMessage<D> {
|
|||||||
payload: D;
|
payload: D;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>;
|
export type WebSocketMessage<D> =
|
||||||
|
| WebSocketIdMessage
|
||||||
|
| WebSocketPayloadMessage<D>;
|
||||||
|
|
||||||
export function webSocketController<D, P extends WebSocketControllerProps<D>>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>) {
|
export function webSocketController<D, P extends WebSocketControllerProps<D>>(
|
||||||
|
wsUrl: string,
|
||||||
|
wsThrottle: number,
|
||||||
|
WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>
|
||||||
|
) {
|
||||||
return withSnackbar(
|
return withSnackbar(
|
||||||
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
|
class extends React.Component<
|
||||||
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
|
Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps,
|
||||||
|
WebSocketControllerState<D>
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps
|
||||||
|
) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
||||||
onmessage: this.onMessage,
|
onmessage: this.onMessage,
|
||||||
onopen: this.onOpen,
|
onopen: this.onOpen,
|
||||||
onclose: this.onClose,
|
onclose: this.onClose
|
||||||
}),
|
}),
|
||||||
connected: false
|
connected: false
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -64,37 +77,42 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
|||||||
onMessage = (event: MessageEvent) => {
|
onMessage = (event: MessageEvent) => {
|
||||||
const rawData = event.data;
|
const rawData = event.data;
|
||||||
if (typeof rawData === 'string' || rawData instanceof String) {
|
if (typeof rawData === 'string' || rawData instanceof String) {
|
||||||
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
|
this.handleMessage(
|
||||||
|
JSON.parse(rawData as string) as WebSocketMessage<D>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleMessage = (message: WebSocketMessage<D>) => {
|
handleMessage = (message: WebSocketMessage<D>) => {
|
||||||
|
const { clientId, data } = this.state;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.ID:
|
case WebSocketMessageType.ID:
|
||||||
this.setState({ clientId: message.id });
|
this.setState({ clientId: message.id });
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.PAYLOAD:
|
case WebSocketMessageType.PAYLOAD:
|
||||||
const { clientId, data } = this.state;
|
|
||||||
if (clientId && (!data || clientId !== message.origin_id)) {
|
if (clientId && (!data || clientId !== message.origin_id)) {
|
||||||
this.setState(
|
this.setState({ data: message.payload });
|
||||||
{ data: message.payload }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onOpen = () => {
|
onOpen = () => {
|
||||||
this.setState({ connected: true });
|
this.setState({ connected: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.setState({ connected: false, clientId: undefined, data: undefined });
|
this.setState({
|
||||||
}
|
connected: false,
|
||||||
|
clientId: undefined,
|
||||||
|
data: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
setData = (data: D, callback?: () => void) => {
|
setData = (data: D, callback?: () => void) => {
|
||||||
this.setState({ data }, callback);
|
this.setState({ data }, callback);
|
||||||
}
|
};
|
||||||
|
|
||||||
saveData = throttle(() => {
|
saveData = throttle(() => {
|
||||||
const { ws, connected, data } = this.state;
|
const { ws, connected, data } = this.state;
|
||||||
@@ -106,28 +124,35 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
|||||||
saveDataAndClear = throttle(() => {
|
saveDataAndClear = throttle(() => {
|
||||||
const { ws, connected, data } = this.state;
|
const { ws, connected, data } = this.state;
|
||||||
if (connected) {
|
if (connected) {
|
||||||
this.setState({
|
this.setState(
|
||||||
data: undefined
|
{
|
||||||
}, () => ws.json(data));
|
data: undefined
|
||||||
|
},
|
||||||
|
() => ws.json(data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, wsThrottle);
|
}, wsThrottle);
|
||||||
|
|
||||||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
handleValueChange = (name: keyof D) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <WebSocketController
|
return (
|
||||||
{...this.props as P}
|
<WebSocketController
|
||||||
handleValueChange={this.handleValueChange}
|
{...(this.props as P)}
|
||||||
setData={this.setData}
|
handleValueChange={this.handleValueChange}
|
||||||
saveData={this.saveData}
|
setData={this.setData}
|
||||||
saveDataAndClear={this.saveDataAndClear}
|
saveData={this.saveData}
|
||||||
connected={this.state.connected}
|
saveDataAndClear={this.saveDataAndClear}
|
||||||
data={this.state.data}
|
connected={this.state.connected}
|
||||||
/>;
|
data={this.state.data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
import { LinearProgress, Typography } from '@material-ui/core';
|
import { LinearProgress, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -8,22 +6,27 @@ import { WebSocketControllerProps } from '.';
|
|||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
loadingSettings: {
|
loadingSettings: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
loadingSettingsDetails: {
|
loadingSettingsDetails: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
textAlign: "center"
|
textAlign: 'center'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export type WebSocketFormProps<D> = Omit<WebSocketControllerProps<D>, "connected"> & { data: D };
|
export type WebSocketFormProps<D> = Omit<
|
||||||
|
WebSocketControllerProps<D>,
|
||||||
|
'connected'
|
||||||
|
> & { data: D };
|
||||||
|
|
||||||
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
|
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
|
||||||
render: (props: WebSocketFormProps<D>) => JSX.Element;
|
render: (props: WebSocketFormProps<D>) => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebSocketFormLoader<D>(props: WebSocketFormLoaderProps<D>) {
|
export default function WebSocketFormLoader<D>(
|
||||||
|
props: WebSocketFormLoaderProps<D>
|
||||||
|
) {
|
||||||
const { connected, render, data, ...rest } = props;
|
const { connected, render, data, ...rest } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
if (!connected || !data) {
|
if (!connected || !data) {
|
||||||
|
|||||||
14
interface/src/components/WindowSize.tsx
Normal file
14
interface/src/components/WindowSize.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useWindowSize() {
|
||||||
|
const [size, setSize] = useState([0, 0]);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
function updateSize() {
|
||||||
|
setSize([window.innerWidth, window.innerHeight]);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
updateSize();
|
||||||
|
return () => window.removeEventListener('resize', updateSize);
|
||||||
|
}, []);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
@@ -15,3 +15,5 @@ export * from './RestController';
|
|||||||
|
|
||||||
export * from './WebSocketFormLoader';
|
export * from './WebSocketFormLoader';
|
||||||
export * from './WebSocketController';
|
export * from './WebSocketController';
|
||||||
|
|
||||||
|
export * from './WindowSize';
|
||||||
|
|||||||
@@ -5,21 +5,26 @@ export interface FeaturesContextValue {
|
|||||||
features: Features;
|
features: Features;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeaturesContextDefaultValue = {} as FeaturesContextValue
|
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
|
||||||
export const FeaturesContext = React.createContext(
|
export const FeaturesContext = React.createContext(FeaturesContextDefaultValue);
|
||||||
FeaturesContextDefaultValue
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface WithFeaturesProps {
|
export interface WithFeaturesProps {
|
||||||
features: Features;
|
features: Features;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withFeatures<T extends WithFeaturesProps>(Component: React.ComponentType<T>) {
|
export function withFeatures<T extends WithFeaturesProps>(
|
||||||
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
|
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<FeaturesContext.Consumer>
|
<FeaturesContext.Consumer>
|
||||||
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
|
{(featuresContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
features={featuresContext.features}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</FeaturesContext.Consumer>
|
</FeaturesContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { Features } from './types';
|
import { Features } from './types';
|
||||||
import { FeaturesContext } from './FeaturesContext';
|
import { FeaturesContext } from './FeaturesContext';
|
||||||
@@ -9,10 +9,9 @@ import { FEATURES_ENDPOINT } from '../api';
|
|||||||
interface FeaturesWrapperState {
|
interface FeaturesWrapperState {
|
||||||
features?: Features;
|
features?: Features;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||||
|
|
||||||
state: FeaturesWrapperState = {};
|
state: FeaturesWrapperState = {};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -21,41 +20,39 @@ class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
|||||||
|
|
||||||
fetchFeaturesDetails = () => {
|
fetchFeaturesDetails = () => {
|
||||||
fetch(FEATURES_ENDPOINT)
|
fetch(FEATURES_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else {
|
} else {
|
||||||
throw Error("Unexpected status code: " + response.status);
|
throw Error('Unexpected status code: ' + response.status);
|
||||||
}
|
}
|
||||||
}).then(features => {
|
})
|
||||||
|
.then((features) => {
|
||||||
this.setState({ features });
|
this.setState({ features });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.setState({ error: error.message });
|
this.setState({ error: error.message });
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { features, error } = this.state;
|
const { features, error } = this.state;
|
||||||
if (features) {
|
if (features) {
|
||||||
return (
|
return (
|
||||||
<FeaturesContext.Provider value={{
|
<FeaturesContext.Provider
|
||||||
features
|
value={{
|
||||||
}}>
|
features
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</FeaturesContext.Provider>
|
</FeaturesContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return <ApplicationError error={error} />;
|
||||||
<ApplicationError error={error} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return <FullScreenLoading />;
|
||||||
<FullScreenLoading />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FeaturesWrapper;
|
export default FeaturesWrapper;
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import { createBrowserHistory } from 'history';
|
|||||||
|
|
||||||
export default createBrowserHistory({
|
export default createBrowserHistory({
|
||||||
/* pass a configuration object here if needed */
|
/* pass a configuration object here if needed */
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { Router } from 'react-router';
|
|||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
render((
|
render(
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<App/>
|
<App />
|
||||||
</Router>
|
</Router>,
|
||||||
), document.getElementById("root"))
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
import MqttStatusController from './MqttStatusController';
|
import MqttStatusController from './MqttStatusController';
|
||||||
import MqttSettingsController from './MqttSettingsController';
|
import MqttSettingsController from './MqttSettingsController';
|
||||||
@@ -11,8 +15,7 @@ import MqttSettingsController from './MqttSettingsController';
|
|||||||
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class Mqtt extends Component<MqttProps> {
|
class Mqtt extends Component<MqttProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,17 +23,33 @@ class Mqtt extends Component<MqttProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="MQTT">
|
<MenuAppBar sectionTitle="MQTT">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/mqtt/status" label="MQTT Status" />
|
<Tab value="/mqtt/status" label="MQTT Status" />
|
||||||
<Tab value="/mqtt/settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
|
value="/mqtt/settings"
|
||||||
|
label="MQTT Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
|
exact
|
||||||
|
path="/mqtt/status"
|
||||||
|
component={MqttStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/mqtt/settings"
|
||||||
|
component={MqttSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/mqtt/status" />
|
<Redirect to="/mqtt/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { MQTT_SETTINGS_ENDPOINT } from '../api';
|
import { MQTT_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import MqttSettingsForm from './MqttSettingsForm';
|
import MqttSettingsForm from './MqttSettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { MqttSettings } from './types';
|
|||||||
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
||||||
|
|
||||||
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
|||||||
<SectionContent title="MQTT Settings" titleGutter>
|
<SectionContent title="MQTT Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <MqttSettingsForm {...formProps} />}
|
render={(formProps) => <MqttSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorForm,
|
||||||
|
SelectValidator
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Checkbox, TextField, Typography } from '@material-ui/core';
|
import { Checkbox, TextField, Typography } from '@material-ui/core';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel, PasswordValidator } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
BlockFormControlLabel,
|
||||||
|
PasswordValidator
|
||||||
|
} from '../components';
|
||||||
import { isIP, isHostname, or, isPath } from '../validators';
|
import { isIP, isHostname, or, isPath } from '../validators';
|
||||||
|
|
||||||
import { MqttSettings } from './types';
|
import { MqttSettings } from './types';
|
||||||
@@ -13,7 +23,6 @@ import { MqttSettings } from './types';
|
|||||||
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
||||||
|
|
||||||
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
ValidatorForm.addValidationRule('isPath', isPath);
|
ValidatorForm.addValidationRule('isPath', isPath);
|
||||||
@@ -35,7 +44,10 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIPOrHostname']}
|
validators={['required', 'isIPOrHostname']}
|
||||||
errorMessages={['Host is required', "Not a valid IP address or hostname"]}
|
errorMessages={[
|
||||||
|
'Host is required',
|
||||||
|
'Not a valid IP address or hostname'
|
||||||
|
]}
|
||||||
name="host"
|
name="host"
|
||||||
label="Host"
|
label="Host"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -45,8 +57,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Port is required', "Must be a number", "Must be greater than 0 ", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Port is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be greater than 0 ',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="port"
|
name="port"
|
||||||
label="Port"
|
label="Port"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -58,7 +80,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isPath']}
|
validators={['required', 'isPath']}
|
||||||
errorMessages={['Base is required', "Not a valid Path"]}
|
errorMessages={['Base is required', 'Not a valid Path']}
|
||||||
name="base"
|
name="base"
|
||||||
label="Base"
|
label="Base"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -87,7 +109,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
name="client_id"
|
name="client_id"
|
||||||
label="Client ID (optional)"
|
label="Client ID"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.client_id}
|
value={data.client_id}
|
||||||
@@ -95,8 +117,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Keep alive is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:1',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Keep alive is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be greater than 0',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="keep_alive"
|
name="keep_alive"
|
||||||
label="Keep Alive (seconds)"
|
label="Keep Alive (seconds)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -106,13 +138,15 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
onChange={handleValueChange('keep_alive')}
|
onChange={handleValueChange('keep_alive')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<SelectValidator name="mqtt_qos"
|
<SelectValidator
|
||||||
|
name="mqtt_qos"
|
||||||
label="QoS"
|
label="QoS"
|
||||||
value={data.mqtt_qos}
|
value={data.mqtt_qos}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('mqtt_qos')}
|
onChange={handleValueChange('mqtt_qos')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
|
>
|
||||||
<MenuItem value={0}>0 (default)</MenuItem>
|
<MenuItem value={0}>0 (default)</MenuItem>
|
||||||
<MenuItem value={1}>1</MenuItem>
|
<MenuItem value={1}>1</MenuItem>
|
||||||
<MenuItem value={2}>2</MenuItem>
|
<MenuItem value={2}>2</MenuItem>
|
||||||
@@ -125,7 +159,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value="clean_session"
|
value="clean_session"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Clean Session"
|
label="Set Clean Session"
|
||||||
/>
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
@@ -135,43 +169,36 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value="mqtt_retain"
|
value="mqtt_retain"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Retain Flag"
|
label="Use Retain Flag"
|
||||||
/>
|
/>
|
||||||
<br></br>
|
<br></br>
|
||||||
<Typography variant="h6" color="primary" >
|
<Typography variant="h6" color="primary">
|
||||||
Formatting
|
Formatting
|
||||||
</Typography>
|
</Typography>
|
||||||
<BlockFormControlLabel
|
<SelectValidator
|
||||||
control={
|
name="nested_format"
|
||||||
<Checkbox
|
label="Topic/Payload Format"
|
||||||
checked={data.nested_format}
|
value={data.nested_format}
|
||||||
onChange={handleValueChange('nested_format')}
|
|
||||||
value="nested_format"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Nested format (Thermostat & Mixer only)"
|
|
||||||
/>
|
|
||||||
<SelectValidator name="dallas_format"
|
|
||||||
label="Dallas Sensor Payload Grouping"
|
|
||||||
value={data.dallas_format}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('dallas_format')}
|
onChange={handleValueChange('nested_format')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
<MenuItem value={1}>by Sensor ID</MenuItem>
|
>
|
||||||
<MenuItem value={2}>by Number</MenuItem>
|
<MenuItem value={1}>Nested on a single topic</MenuItem>
|
||||||
|
<MenuItem value={2}>As individual topics</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
<SelectValidator name="bool_format"
|
<SelectValidator
|
||||||
label="Boolean Format"
|
name="subscribe_format"
|
||||||
value={data.bool_format}
|
label="Subscribe Format"
|
||||||
|
value={data.subscribe_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('bool_format')}
|
onChange={handleValueChange('subscribe_format')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
>
|
||||||
<MenuItem value={2}>true/false</MenuItem>
|
<MenuItem value={0}>General device topic</MenuItem>
|
||||||
<MenuItem value={3}>1/0</MenuItem>
|
<MenuItem value={1}>Individual topics, main heating circuit</MenuItem>
|
||||||
<MenuItem value={4}>"ON"/"OFF"</MenuItem>
|
<MenuItem value={2}>Individual topics, all heating circuits</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
@@ -181,28 +208,40 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value="ha_enabled"
|
value="ha_enabled"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Home Assistant MQTT Discovery"
|
label="Use Home Assistant MQTT Discovery"
|
||||||
/>
|
/>
|
||||||
{ data.ha_enabled &&
|
{data.ha_enabled && (
|
||||||
<SelectValidator name="ha_climate_format"
|
<SelectValidator
|
||||||
|
name="ha_climate_format"
|
||||||
label="Thermostat Room Temperature"
|
label="Thermostat Room Temperature"
|
||||||
value={data.ha_climate_format}
|
value={data.ha_climate_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('ha_climate_format')}
|
onChange={handleValueChange('ha_climate_format')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
<MenuItem value={1}>use Current temperature (default)</MenuItem>
|
>
|
||||||
<MenuItem value={2}>use Setpoint temperature</MenuItem>
|
<MenuItem value={1}>Use Current temperature</MenuItem>
|
||||||
<MenuItem value={3}>Fix to 0</MenuItem>
|
<MenuItem value={2}>Use Setpoint temperature</MenuItem>
|
||||||
|
<MenuItem value={3}>Always set to 0</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
}
|
)}
|
||||||
<br></br>
|
<br></br>
|
||||||
<Typography variant="h6" color="primary" >
|
<Typography variant="h6" color="primary">
|
||||||
Publish Intervals
|
Publish Intervals
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_boiler"
|
name="publish_time_boiler"
|
||||||
label="Boiler Publish Interval (seconds, 0=on change)"
|
label="Boiler Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -213,8 +252,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_thermostat"
|
name="publish_time_thermostat"
|
||||||
label="Thermostat Publish Interval (seconds, 0=on change)"
|
label="Thermostat Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -225,8 +274,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_solar"
|
name="publish_time_solar"
|
||||||
label="Solar Publish Interval (seconds, 0=on change)"
|
label="Solar Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -237,8 +296,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_mixer"
|
name="publish_time_mixer"
|
||||||
label="Mixer Publish Interval (seconds, 0=on change)"
|
label="Mixer Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -249,8 +318,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_sensor"
|
name="publish_time_sensor"
|
||||||
label="Sensors Publish Interval (seconds, 0=on change)"
|
label="Sensors Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -261,8 +340,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Publish time is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or greater',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
name="publish_time_other"
|
name="publish_time_other"
|
||||||
label="All other Modules Publish Interval (seconds, 0=on change)"
|
label="All other Modules Publish Interval (seconds, 0=on change)"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -273,7 +362,12 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Theme } from "@material-ui/core";
|
import { Theme } from '@material-ui/core';
|
||||||
import { MqttStatus, MqttDisconnectReason } from "./types";
|
import { MqttStatus, MqttDisconnectReason } from './types';
|
||||||
|
|
||||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
export const mqttStatusHighlight = (
|
||||||
|
{ enabled, connected }: MqttStatus,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
}
|
}
|
||||||
@@ -9,48 +12,48 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
|
|||||||
return theme.palette.success.main;
|
return theme.palette.success.main;
|
||||||
}
|
}
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return "Not enabled";
|
return 'Not enabled';
|
||||||
}
|
}
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return "Connected";
|
return 'Connected';
|
||||||
}
|
}
|
||||||
return "Disconnected";
|
return 'Disconnected';
|
||||||
}
|
};
|
||||||
|
|
||||||
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||||
switch (disconnect_reason) {
|
switch (disconnect_reason) {
|
||||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||||
return "TCP disconnected";
|
return 'TCP disconnected';
|
||||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||||
return "Unacceptable protocol version";
|
return 'Unacceptable protocol version';
|
||||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||||
return "Client ID rejected";
|
return 'Client ID rejected';
|
||||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||||
return "Server unavailable";
|
return 'Server unavailable';
|
||||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||||
return "Malformed credentials";
|
return 'Malformed credentials';
|
||||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||||
return "Not authorized";
|
return 'Not authorized';
|
||||||
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||||
return "Device out of memory";
|
return 'Device out of memory';
|
||||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||||
return "Server fingerprint invalid";
|
return 'Server fingerprint invalid';
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
export const mqttPublishHighlight = (
|
||||||
|
{ mqtt_fails }: MqttStatus,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
|
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||||
|
|
||||||
if (mqtt_fails === 0)
|
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||||
return theme.palette.success.main;
|
|
||||||
|
|
||||||
if (mqtt_fails < 10)
|
|
||||||
return theme.palette.warning.main;
|
|
||||||
|
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { MQTT_STATUS_ENDPOINT } from '../api';
|
import { MQTT_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import MqttStatusForm from './MqttStatusForm';
|
import MqttStatusForm from './MqttStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { MqttStatus } from './types';
|
|||||||
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
||||||
|
|
||||||
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,10 +23,10 @@ class MqttStatusController extends Component<MqttStatusControllerProps> {
|
|||||||
<SectionContent title="MQTT Status">
|
<SectionContent title="MQTT Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <MqttStatusForm {...formProps} />}
|
render={(formProps) => <MqttStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,39 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
import ReportIcon from '@material-ui/icons/Report';
|
import ReportIcon from '@material-ui/icons/Report';
|
||||||
import SpeakerNotesOffIcon from "@material-ui/icons/SpeakerNotesOff";
|
import SpeakerNotesOffIcon from '@material-ui/icons/SpeakerNotesOff';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
import {
|
||||||
import { mqttStatusHighlight, mqttStatus, mqttPublishHighlight, disconnectReason } from './MqttStatus';
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
HighlightAvatar
|
||||||
|
} from '../components';
|
||||||
|
import {
|
||||||
|
mqttStatusHighlight,
|
||||||
|
mqttStatus,
|
||||||
|
mqttPublishHighlight,
|
||||||
|
disconnectReason
|
||||||
|
} from './MqttStatus';
|
||||||
import { MqttStatus } from './types';
|
import { MqttStatus } from './types';
|
||||||
|
|
||||||
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
||||||
|
|
||||||
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||||
|
|
||||||
renderConnectionStatus() {
|
renderConnectionStatus() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
if (data.connected) {
|
if (data.connected) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -29,16 +45,16 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<HighlightAvatar color={mqttPublishHighlight(data, theme)}>
|
<HighlightAvatar color={mqttPublishHighlight(data, theme)}>
|
||||||
<SpeakerNotesOffIcon />
|
<SpeakerNotesOffIcon />
|
||||||
</HighlightAvatar>
|
</HighlightAvatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="MQTT Publish Errors"
|
primary="MQTT Publish Errors"
|
||||||
secondary={data.mqtt_fails}
|
secondary={data.mqtt_fails}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,7 +66,10 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
<ReportIcon />
|
<ReportIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
|
<ListItemText
|
||||||
|
primary="Disconnect Reason"
|
||||||
|
secondary={disconnectReason(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -58,7 +77,7 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -78,18 +97,20 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(MqttStatusForm);
|
export default withTheme(MqttStatusForm);
|
||||||
|
|||||||
@@ -34,11 +34,10 @@ export interface MqttSettings {
|
|||||||
publish_time_mixer: number;
|
publish_time_mixer: number;
|
||||||
publish_time_other: number;
|
publish_time_other: number;
|
||||||
publish_time_sensor: number;
|
publish_time_sensor: number;
|
||||||
dallas_format: number;
|
|
||||||
bool_format: number;
|
|
||||||
mqtt_qos: number;
|
mqtt_qos: number;
|
||||||
mqtt_retain: boolean;
|
mqtt_retain: boolean;
|
||||||
ha_enabled: boolean;
|
ha_enabled: boolean;
|
||||||
ha_climate_format: number;
|
ha_climate_format: number;
|
||||||
nested_format: boolean;
|
nested_format: number;
|
||||||
|
subscribe_format: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import NetworkStatusController from './NetworkStatusController';
|
import NetworkStatusController from './NetworkStatusController';
|
||||||
import NetworkSettingsController from './NetworkSettingsController';
|
import NetworkSettingsController from './NetworkSettingsController';
|
||||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
import {
|
||||||
|
NetworkConnectionContext,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
} from './NetworkConnectionContext';
|
||||||
|
|
||||||
import { WiFiNetwork } from './types';
|
import { WiFiNetwork } from './types';
|
||||||
|
|
||||||
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnectionContextValue> {
|
class NetworkConnection extends Component<
|
||||||
|
NetworkConnectionProps,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
> {
|
||||||
constructor(props: NetworkConnectionProps) {
|
constructor(props: NetworkConnectionProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -28,13 +37,13 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
|||||||
selectNetwork = (network: WiFiNetwork) => {
|
selectNetwork = (network: WiFiNetwork) => {
|
||||||
this.setState({ selectedNetwork: network });
|
this.setState({ selectedNetwork: network });
|
||||||
this.props.history.push('/network/settings');
|
this.props.history.push('/network/settings');
|
||||||
}
|
};
|
||||||
|
|
||||||
deselectNetwork = () => {
|
deselectNetwork = () => {
|
||||||
this.setState({ selectedNetwork: undefined });
|
this.setState({ selectedNetwork: undefined });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
handleTabChange = (path: string) => {
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,20 +52,44 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
|||||||
return (
|
return (
|
||||||
<NetworkConnectionContext.Provider value={this.state}>
|
<NetworkConnectionContext.Provider value={this.state}>
|
||||||
<MenuAppBar sectionTitle="Network Connection">
|
<MenuAppBar sectionTitle="Network Connection">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/network/status" label="Network Status" />
|
<Tab value="/network/status" label="Network Status" />
|
||||||
<Tab value="/network/scan" label="Scan Networks" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
<Tab value="/network/settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
|
value="/network/scan"
|
||||||
|
label="Scan WiFi Networks"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="/network/settings"
|
||||||
|
label="Network Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/network/status" component={NetworkStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/network/scan" component={WiFiNetworkScanner} />
|
exact
|
||||||
<AuthenticatedRoute exact path="/network/settings" component={NetworkSettingsController} />
|
path="/network/status"
|
||||||
|
component={NetworkStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/scan"
|
||||||
|
component={WiFiNetworkScanner}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/settings"
|
||||||
|
component={NetworkSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/network/status" />
|
<Redirect to="/network/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
</NetworkConnectionContext.Provider>
|
</NetworkConnectionContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface NetworkConnectionContextValue {
|
|||||||
deselectNetwork: () => void;
|
deselectNetwork: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue
|
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue;
|
||||||
export const NetworkConnectionContext = React.createContext(
|
export const NetworkConnectionContext = React.createContext(
|
||||||
NetworkConnectionContextDefaultValue
|
NetworkConnectionContextDefaultValue
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import NetworkSettingsForm from './NetworkSettingsForm';
|
import NetworkSettingsForm from './NetworkSettingsForm';
|
||||||
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
||||||
import { NetworkSettings } from './types';
|
import { NetworkSettings } from './types';
|
||||||
@@ -8,7 +13,6 @@ import { NetworkSettings } from './types';
|
|||||||
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
||||||
|
|
||||||
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -18,12 +22,14 @@ class NetworkSettingsController extends Component<NetworkSettingsControllerProps
|
|||||||
<SectionContent title="Network Settings">
|
<SectionContent title="Network Settings">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NetworkSettingsForm {...formProps} />}
|
render={(formProps) => <NetworkSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NETWORK_SETTINGS_ENDPOINT, NetworkSettingsController);
|
export default restController(
|
||||||
|
NETWORK_SETTINGS_ENDPOINT,
|
||||||
|
NetworkSettingsController
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { TextValidator, SelectValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Checkbox, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction } from '@material-ui/core';
|
import {
|
||||||
|
Checkbox,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
import Avatar from '@material-ui/core/Avatar';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
@@ -9,34 +16,47 @@ import LockIcon from '@material-ui/icons/Lock';
|
|||||||
import LockOpenIcon from '@material-ui/icons/LockOpen';
|
import LockOpenIcon from '@material-ui/icons/LockOpen';
|
||||||
import DeleteIcon from '@material-ui/icons/Delete';
|
import DeleteIcon from '@material-ui/icons/Delete';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
|
||||||
|
|
||||||
import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
PasswordValidator,
|
||||||
|
BlockFormControlLabel,
|
||||||
|
FormActions,
|
||||||
|
FormButton
|
||||||
|
} from '../components';
|
||||||
import { isIP, isHostname, optional } from '../validators';
|
import { isIP, isHostname, optional } from '../validators';
|
||||||
|
|
||||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
import {
|
||||||
|
NetworkConnectionContext,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
} from './NetworkConnectionContext';
|
||||||
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
||||||
import { NetworkSettings } from './types';
|
import { NetworkSettings } from './types';
|
||||||
|
|
||||||
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
||||||
|
|
||||||
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||||
|
|
||||||
static contextType = NetworkConnectionContext;
|
static contextType = NetworkConnectionContext;
|
||||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||||
|
|
||||||
constructor(props: NetworkStatusFormProps, context: NetworkConnectionContextValue) {
|
constructor(
|
||||||
|
props: NetworkStatusFormProps,
|
||||||
|
context: NetworkConnectionContextValue
|
||||||
|
) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { selectedNetwork } = context;
|
const { selectedNetwork } = context;
|
||||||
if (selectedNetwork) {
|
if (selectedNetwork) {
|
||||||
const networkSettings: NetworkSettings = {
|
const networkSettings: NetworkSettings = {
|
||||||
ssid: selectedNetwork.ssid,
|
ssid: selectedNetwork.ssid,
|
||||||
password: "",
|
password: '',
|
||||||
hostname: props.data.hostname,
|
hostname: props.data.hostname,
|
||||||
ethernet_profile: 0,
|
|
||||||
static_ip_config: false,
|
static_ip_config: false,
|
||||||
}
|
enableIPv6: false,
|
||||||
|
bandwidth20: false,
|
||||||
|
tx_power: 20,
|
||||||
|
nosleep: false
|
||||||
|
};
|
||||||
props.setData(networkSettings);
|
props.setData(networkSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +70,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
deselectNetworkAndLoadData = () => {
|
deselectNetworkAndLoadData = () => {
|
||||||
this.context.deselectNetwork();
|
this.context.deselectNetwork();
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.context.deselectNetwork();
|
this.context.deselectNetwork();
|
||||||
@@ -61,41 +81,51 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
||||||
{
|
{selectedNetwork ? (
|
||||||
selectedNetwork ?
|
<List>
|
||||||
<List>
|
<ListItem>
|
||||||
<ListItem>
|
<ListItemAvatar>
|
||||||
<ListItemAvatar>
|
<Avatar>
|
||||||
<Avatar>
|
{isNetworkOpen(selectedNetwork) ? (
|
||||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
<LockOpenIcon />
|
||||||
</Avatar>
|
) : (
|
||||||
</ListItemAvatar>
|
<LockIcon />
|
||||||
<ListItemText
|
)}
|
||||||
primary={selectedNetwork.ssid}
|
</Avatar>
|
||||||
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
|
</ListItemAvatar>
|
||||||
/>
|
<ListItemText
|
||||||
<ListItemSecondaryAction>
|
primary={selectedNetwork.ssid}
|
||||||
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
secondary={
|
||||||
<DeleteIcon />
|
'Security: ' +
|
||||||
</IconButton>
|
networkSecurityMode(selectedNetwork) +
|
||||||
</ListItemSecondaryAction>
|
', Ch: ' +
|
||||||
</ListItem>
|
selectedNetwork.channel
|
||||||
</List>
|
}
|
||||||
:
|
/>
|
||||||
<TextValidator
|
<ListItemSecondaryAction>
|
||||||
validators={['matchRegexp:^.{0,32}$']}
|
<IconButton
|
||||||
errorMessages={['SSID must be 32 characters or less']}
|
aria-label="Manual Config"
|
||||||
name="ssid"
|
onClick={deselectNetwork}
|
||||||
label="SSID"
|
>
|
||||||
fullWidth
|
<DeleteIcon />
|
||||||
variant="outlined"
|
</IconButton>
|
||||||
value={data.ssid}
|
</ListItemSecondaryAction>
|
||||||
onChange={handleValueChange('ssid')}
|
</ListItem>
|
||||||
margin="normal"
|
</List>
|
||||||
/>
|
) : (
|
||||||
}
|
<TextValidator
|
||||||
{
|
validators={['matchRegexp:^.{0,32}$']}
|
||||||
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
|
errorMessages={['SSID must be 32 characters or less']}
|
||||||
|
name="ssid"
|
||||||
|
label="SSID (leave blank to disable WiFi)"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.ssid}
|
||||||
|
onChange={handleValueChange('ssid')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['matchRegexp:^.{0,64}$']}
|
validators={['matchRegexp:^.{0,64}$']}
|
||||||
errorMessages={['Password must be 64 characters or less']}
|
errorMessages={['Password must be 64 characters or less']}
|
||||||
@@ -107,10 +137,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
onChange={handleValueChange('password')}
|
onChange={handleValueChange('password')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isHostname']}
|
validators={['required', 'isHostname']}
|
||||||
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
errorMessages={['Hostname is required', 'Not a valid hostname']}
|
||||||
name="hostname"
|
name="hostname"
|
||||||
label="Hostname"
|
label="Hostname"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -119,29 +149,64 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
onChange={handleValueChange('hostname')}
|
onChange={handleValueChange('hostname')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<SelectValidator name="ems_bus_id"
|
<TextValidator
|
||||||
label="Ethernet Profile"
|
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:20']}
|
||||||
value={data.ethernet_profile}
|
errorMessages={[
|
||||||
|
'Tx Power is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be greater than 0dBm ',
|
||||||
|
'Max value is 20dBm'
|
||||||
|
]}
|
||||||
|
name="tx_power"
|
||||||
|
label="WiFi Tx Power (dBm)"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('ethernet_profile')}
|
value={data.tx_power}
|
||||||
margin="normal">
|
type="number"
|
||||||
<MenuItem value={0}>None (wifi only)</MenuItem>
|
onChange={handleValueChange('tx_power')}
|
||||||
<MenuItem value={1}>Profile 1 (LAN8720)</MenuItem>
|
margin="normal"
|
||||||
<MenuItem value={2}>Profile 2 (TLK110)</MenuItem>
|
/>
|
||||||
</SelectValidator>
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="enableIPv6"
|
||||||
|
checked={data.enableIPv6}
|
||||||
|
onChange={handleValueChange('enableIPv6')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable IPv6 support"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="bandwidth20"
|
||||||
|
checked={data.bandwidth20}
|
||||||
|
onChange={handleValueChange('bandwidth20')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Use Lower WiFi Bandwidth"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="nosleep"
|
||||||
|
checked={data.nosleep}
|
||||||
|
onChange={handleValueChange('nosleep')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Disable WiFi Sleep Mode"
|
||||||
|
/>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value="static_ip_config"
|
value="static_ip_config"
|
||||||
checked={data.static_ip_config}
|
checked={data.static_ip_config}
|
||||||
onChange={handleValueChange("static_ip_config")}
|
onChange={handleValueChange('static_ip_config')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Static IP Config"
|
label="Use Static IPs"
|
||||||
/>
|
/>
|
||||||
{
|
{data.static_ip_config && (
|
||||||
data.static_ip_config &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
@@ -167,7 +232,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
errorMessages={[
|
||||||
|
'Subnet mask is required',
|
||||||
|
'Must be an IP address'
|
||||||
|
]}
|
||||||
name="subnet_mask"
|
name="subnet_mask"
|
||||||
label="Subnet"
|
label="Subnet"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -199,9 +267,14 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -2,12 +2,21 @@ import { Theme } from '@material-ui/core';
|
|||||||
import { NetworkStatus, NetworkConnectionStatus } from './types';
|
import { NetworkStatus, NetworkConnectionStatus } from './types';
|
||||||
|
|
||||||
export const isConnected = ({ status }: NetworkStatus) => {
|
export const isConnected = ({ status }: NetworkStatus) => {
|
||||||
return ((status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED) || (status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED));
|
return (
|
||||||
}
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||||
|
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isWiFi = ({ status }: NetworkStatus) => (status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED)
|
export const isWiFi = ({ status }: NetworkStatus) =>
|
||||||
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||||
|
export const isEthernet = ({ status }: NetworkStatus) =>
|
||||||
|
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||||
|
|
||||||
export const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
|
export const networkStatusHighlight = (
|
||||||
|
{ status }: NetworkStatus,
|
||||||
|
theme: Theme
|
||||||
|
) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
@@ -22,27 +31,27 @@ export const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme)
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const networkStatus = ({ status }: NetworkStatus) => {
|
export const networkStatus = ({ status }: NetworkStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||||
return "Inactive";
|
return 'Inactive';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
return "Idle";
|
return 'Idle';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||||
return "No SSID Available";
|
return 'No SSID Available';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||||
return "Connected (WiFi)";
|
return 'Connected (WiFi)';
|
||||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||||
return "Connected (Ethernet)";
|
return 'Connected (Ethernet)';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||||
return "Connection Failed";
|
return 'Connection Failed';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||||
return "Connection Lost";
|
return 'Connection Lost';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
return "Disconnected";
|
return 'Disconnected';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import NetworkStatusForm from './NetworkStatusForm';
|
import NetworkStatusForm from './NetworkStatusForm';
|
||||||
import { NETWORK_STATUS_ENDPOINT } from '../api';
|
import { NETWORK_STATUS_ENDPOINT } from '../api';
|
||||||
import { NetworkStatus } from './types';
|
import { NetworkStatus } from './types';
|
||||||
@@ -8,7 +13,6 @@ import { NetworkStatus } from './types';
|
|||||||
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
||||||
|
|
||||||
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -18,12 +22,11 @@ class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
|||||||
<SectionContent title="Network Status">
|
<SectionContent title="Network Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NetworkStatusForm {...formProps} />}
|
render={(formProps) => <NetworkStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
||||||
|
|||||||
@@ -1,45 +1,79 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import DNSIcon from '@material-ui/icons/Dns';
|
import DNSIcon from '@material-ui/icons/Dns';
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
import WifiIcon from '@material-ui/icons/Wifi';
|
||||||
|
import RouterIcon from '@material-ui/icons/Router';
|
||||||
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
|
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
|
||||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
import {
|
||||||
import { networkStatus, networkStatusHighlight, isConnected, isWiFi } from './NetworkStatus';
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
HighlightAvatar
|
||||||
|
} from '../components';
|
||||||
|
import {
|
||||||
|
networkStatus,
|
||||||
|
networkStatusHighlight,
|
||||||
|
isConnected,
|
||||||
|
isWiFi,
|
||||||
|
isEthernet
|
||||||
|
} from './NetworkStatus';
|
||||||
import { NetworkStatus } from './types';
|
import { NetworkStatus } from './types';
|
||||||
|
|
||||||
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
||||||
|
|
||||||
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||||
|
|
||||||
dnsServers(status: NetworkStatus) {
|
dnsServers(status: NetworkStatus) {
|
||||||
if (!status.dns_ip_1) {
|
if (!status.dns_ip_1) {
|
||||||
return "none";
|
return 'none';
|
||||||
}
|
}
|
||||||
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
|
if (!status.dns_ip_2 || status.dns_ip_2 === '0.0.0.0') {
|
||||||
|
return status.dns_ip_1;
|
||||||
|
}
|
||||||
|
return status.dns_ip_1 + ', ' + status.dns_ip_2;
|
||||||
|
}
|
||||||
|
IPs(status: NetworkStatus) {
|
||||||
|
if (
|
||||||
|
!status.local_ipv6 ||
|
||||||
|
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000'
|
||||||
|
) {
|
||||||
|
return status.local_ip;
|
||||||
|
}
|
||||||
|
if (!status.local_ip || status.local_ip === '0.0.0.0') {
|
||||||
|
return status.local_ipv6;
|
||||||
|
}
|
||||||
|
return status.local_ip + ', ' + status.local_ipv6;
|
||||||
}
|
}
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<HighlightAvatar color={networkStatusHighlight(data, theme)}>
|
<HighlightAvatar color={networkStatusHighlight(data, theme)}>
|
||||||
<WifiIcon />
|
{isWiFi(data) && <WifiIcon />}
|
||||||
|
{isEthernet(data) && <RouterIcon />}
|
||||||
</HighlightAvatar>
|
</HighlightAvatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
{
|
{isWiFi(data) && (
|
||||||
isWiFi(data) &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
@@ -51,14 +85,14 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
{ isConnected(data) &&
|
{isConnected(data) && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>IP</Avatar>
|
<Avatar>IP</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="IP Address" secondary={data.local_ip} />
|
<ListItemText primary="IP Address" secondary={this.IPs(data)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -67,14 +101,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
<DeviceHubIcon />
|
<DeviceHubIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
<ListItemText
|
||||||
|
primary="MAC Address"
|
||||||
|
secondary={data.mac_address}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>#</Avatar>
|
<Avatar>#</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Subnet Mask" secondary={data.subnet_mask} />
|
<ListItemText
|
||||||
|
primary="Subnet Mask"
|
||||||
|
secondary={data.subnet_mask}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -83,7 +123,10 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
<SettingsInputComponentIcon />
|
<SettingsInputComponentIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
|
<ListItemText
|
||||||
|
primary="Gateway IP"
|
||||||
|
secondary={data.gateway_ip || 'none'}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -92,11 +135,14 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
<DNSIcon />
|
<DNSIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="DNS Server IP" secondary={this.dnsServers(data)} />
|
<ListItemText
|
||||||
|
primary="DNS Server IP"
|
||||||
|
secondary={this.dnsServers(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,18 +150,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(NetworkStatusForm);
|
export default withTheme(NetworkStatusForm);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
import { createStyles, WithStyles, Theme, withStyles, Typography, LinearProgress } from '@material-ui/core';
|
import {
|
||||||
|
createStyles,
|
||||||
|
WithStyles,
|
||||||
|
Theme,
|
||||||
|
withStyles,
|
||||||
|
Typography,
|
||||||
|
LinearProgress
|
||||||
|
} from '@material-ui/core';
|
||||||
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
|
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
|
||||||
|
|
||||||
import { FormActions, FormButton, SectionContent } from '../components';
|
import { FormActions, FormButton, SectionContent } from '../components';
|
||||||
@@ -11,9 +18,9 @@ import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
|
|||||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||||
import { WiFiNetworkList, WiFiNetwork } from './types';
|
import { WiFiNetworkList, WiFiNetwork } from './types';
|
||||||
|
|
||||||
const NUM_POLLS = 10
|
const NUM_POLLS = 10;
|
||||||
const POLLING_FREQUENCY = 500
|
const POLLING_FREQUENCY = 500;
|
||||||
const RETRY_EXCEPTION_TYPE = "retry"
|
const RETRY_EXCEPTION_TYPE = 'retry';
|
||||||
|
|
||||||
interface WiFiNetworkScannerState {
|
interface WiFiNetworkScannerState {
|
||||||
scanningForNetworks: boolean;
|
scanningForNetworks: boolean;
|
||||||
@@ -21,28 +28,31 @@ interface WiFiNetworkScannerState {
|
|||||||
networkList?: WiFiNetworkList;
|
networkList?: WiFiNetworkList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
scanningSettings: {
|
createStyles({
|
||||||
margin: theme.spacing(0.5),
|
scanningSettings: {
|
||||||
},
|
margin: theme.spacing(0.5)
|
||||||
scanningSettingsDetails: {
|
},
|
||||||
margin: theme.spacing(4),
|
scanningSettingsDetails: {
|
||||||
textAlign: "center"
|
margin: theme.spacing(4),
|
||||||
},
|
textAlign: 'center'
|
||||||
scanningProgress: {
|
},
|
||||||
margin: theme.spacing(4),
|
scanningProgress: {
|
||||||
textAlign: "center"
|
margin: theme.spacing(4),
|
||||||
}
|
textAlign: 'center'
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
||||||
|
|
||||||
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
|
class WiFiNetworkScanner extends Component<
|
||||||
|
WiFiNetworkScannerProps,
|
||||||
pollCount: number = 0;
|
WiFiNetworkScannerState
|
||||||
|
> {
|
||||||
|
pollCount = 0;
|
||||||
|
|
||||||
state: WiFiNetworkScannerState = {
|
state: WiFiNetworkScannerState = {
|
||||||
scanningForNetworks: false,
|
scanningForNetworks: false
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -54,23 +64,36 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
if (!scanningForNetworks) {
|
if (!scanningForNetworks) {
|
||||||
this.scanNetworks();
|
this.scanNetworks();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
scanNetworks() {
|
scanNetworks() {
|
||||||
this.pollCount = 0;
|
this.pollCount = 0;
|
||||||
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
|
this.setState({
|
||||||
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
scanningForNetworks: true,
|
||||||
if (response.status === 202) {
|
networkList: undefined,
|
||||||
this.schedulePollTimeout();
|
errorMessage: undefined
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw Error("Scanning for networks returned unexpected response code: " + response.status);
|
|
||||||
}).catch(error => {
|
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
|
||||||
});
|
});
|
||||||
|
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 202) {
|
||||||
|
this.schedulePollTimeout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw Error(
|
||||||
|
'Scanning for networks returned unexpected response code: ' +
|
||||||
|
response.status
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
scanningForNetworks: false,
|
||||||
|
networkList: undefined,
|
||||||
|
errorMessage: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
schedulePollTimeout() {
|
schedulePollTimeout() {
|
||||||
@@ -80,21 +103,20 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
retryError() {
|
retryError() {
|
||||||
return {
|
return {
|
||||||
name: RETRY_EXCEPTION_TYPE,
|
name: RETRY_EXCEPTION_TYPE,
|
||||||
message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
|
message:
|
||||||
|
'Network list not ready, will retry in ' + POLLING_FREQUENCY + 'ms.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
|
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
|
||||||
if (network1.rssi < network2.rssi)
|
if (network1.rssi < network2.rssi) return 1;
|
||||||
return 1;
|
if (network1.rssi > network2.rssi) return -1;
|
||||||
if (network1.rssi > network2.rssi)
|
|
||||||
return -1;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pollNetworkList = () => {
|
pollNetworkList = () => {
|
||||||
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -103,24 +125,34 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
this.schedulePollTimeout();
|
this.schedulePollTimeout();
|
||||||
throw this.retryError();
|
throw this.retryError();
|
||||||
} else {
|
} else {
|
||||||
throw Error("Device did not return network list in timely manner.");
|
throw Error('Device did not return network list in timely manner.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw Error("Device returned unexpected response code: " + response.status);
|
throw Error(
|
||||||
|
'Device returned unexpected response code: ' + response.status
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(json => {
|
.then((json) => {
|
||||||
json.networks.sort(this.compareNetworks)
|
json.networks.sort(this.compareNetworks);
|
||||||
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
|
this.setState({
|
||||||
|
scanningForNetworks: false,
|
||||||
|
networkList: json,
|
||||||
|
errorMessage: undefined
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||||
variant: 'error',
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
scanningForNetworks: false,
|
||||||
|
networkList: undefined,
|
||||||
|
errorMessage: error.message
|
||||||
});
|
});
|
||||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
renderNetworkScanner() {
|
renderNetworkScanner() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
@@ -144,9 +176,7 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return <WiFiNetworkSelector networkList={networkList} />;
|
||||||
<WiFiNetworkSelector networkList={networkList} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -155,14 +185,19 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
<SectionContent title="Network Scanner">
|
<SectionContent title="Network Scanner">
|
||||||
{this.renderNetworkScanner()}
|
{this.renderNetworkScanner()}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<PermScanWifiIcon />} variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}>
|
<FormButton
|
||||||
|
startIcon={<PermScanWifiIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.requestNetworkScan}
|
||||||
|
disabled={scanningForNetworks}
|
||||||
|
>
|
||||||
Scan again…
|
Scan again…
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { Avatar, Badge } from '@material-ui/core';
|
import { Avatar, Badge } from '@material-ui/core';
|
||||||
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
import WifiIcon from '@material-ui/icons/Wifi';
|
||||||
import LockIcon from '@material-ui/icons/Lock';
|
import LockIcon from '@material-ui/icons/Lock';
|
||||||
@@ -16,13 +22,16 @@ interface WiFiNetworkSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
||||||
|
|
||||||
static contextType = NetworkConnectionContext;
|
static contextType = NetworkConnectionContext;
|
||||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||||
|
|
||||||
renderNetwork = (network: WiFiNetwork) => {
|
renderNetwork = (network: WiFiNetwork) => {
|
||||||
return (
|
return (
|
||||||
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
|
<ListItem
|
||||||
|
key={network.bssid}
|
||||||
|
button
|
||||||
|
onClick={() => this.context.selectNetwork(network)}
|
||||||
|
>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
||||||
@@ -30,25 +39,27 @@ class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={network.ssid}
|
primary={network.ssid}
|
||||||
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
|
secondary={
|
||||||
|
'Security: ' +
|
||||||
|
networkSecurityMode(network) +
|
||||||
|
', Ch: ' +
|
||||||
|
network.channel
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Badge badgeContent={network.rssi + "db"}>
|
<Badge badgeContent={network.rssi + 'db'}>
|
||||||
<WifiIcon />
|
<WifiIcon />
|
||||||
</Badge>
|
</Badge>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>{this.props.networkList.networks.map(this.renderNetwork)}</List>
|
||||||
{this.props.networkList.networks.map(this.renderNetwork)}
|
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WiFiNetworkSelector;
|
export default WiFiNetworkSelector;
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { WiFiNetwork, WiFiEncryptionType } from "./types";
|
import { WiFiNetwork, WiFiEncryptionType } from './types';
|
||||||
|
|
||||||
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) => encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
|
||||||
|
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
||||||
|
|
||||||
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
||||||
switch (encryption_type) {
|
switch (encryption_type) {
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
||||||
return "WEP";
|
return 'WEP';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
||||||
return "WPA";
|
return 'WPA';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
||||||
return "WPA2";
|
return 'WPA2';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
||||||
return "WPA/WPA2";
|
return 'WPA/WPA2';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
||||||
return "WPA2 Enterprise";
|
return 'WPA2 Enterprise';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
||||||
return "None";
|
return 'None';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export enum WiFiEncryptionType {
|
|||||||
export interface NetworkStatus {
|
export interface NetworkStatus {
|
||||||
status: NetworkConnectionStatus;
|
status: NetworkConnectionStatus;
|
||||||
local_ip: string;
|
local_ip: string;
|
||||||
|
local_ipv6: string;
|
||||||
mac_address: string;
|
mac_address: string;
|
||||||
rssi: number;
|
rssi: number;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
@@ -36,8 +37,11 @@ export interface NetworkSettings {
|
|||||||
ssid: string;
|
ssid: string;
|
||||||
password: string;
|
password: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
ethernet_profile: number;
|
|
||||||
static_ip_config: boolean;
|
static_ip_config: boolean;
|
||||||
|
enableIPv6: boolean;
|
||||||
|
bandwidth20: boolean;
|
||||||
|
nosleep: boolean;
|
||||||
|
tx_power: number;
|
||||||
local_ip?: string;
|
local_ip?: string;
|
||||||
gateway_ip?: string;
|
gateway_ip?: string;
|
||||||
subnet_mask?: string;
|
subnet_mask?: string;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { NTP_SETTINGS_ENDPOINT } from '../api';
|
import { NTP_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import NTPSettingsForm from './NTPSettingsForm';
|
import NTPSettingsForm from './NTPSettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { NTPSettings } from './types';
|
|||||||
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
||||||
|
|
||||||
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
|||||||
<SectionContent title="NTP Settings" titleGutter>
|
<SectionContent title="NTP Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NTPSettingsForm {...formProps} />}
|
render={(formProps) => <NTPSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
import {
|
||||||
|
TextValidator,
|
||||||
|
ValidatorForm,
|
||||||
|
SelectValidator
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Checkbox, MenuItem } from '@material-ui/core';
|
import { Checkbox, MenuItem } from '@material-ui/core';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
BlockFormControlLabel
|
||||||
|
} from '../components';
|
||||||
import { isIP, isHostname, or } from '../validators';
|
import { isIP, isHostname, or } from '../validators';
|
||||||
|
|
||||||
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
||||||
@@ -13,7 +22,6 @@ import { NTPSettings } from './types';
|
|||||||
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
||||||
|
|
||||||
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
}
|
}
|
||||||
@@ -25,7 +33,7 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
tz_label: event.target.value,
|
tz_label: event.target.value,
|
||||||
tz_format: TIME_ZONES[event.target.value]
|
tz_format: TIME_ZONES[event.target.value]
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
@@ -43,7 +51,10 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIPOrHostname']}
|
validators={['required', 'isIPOrHostname']}
|
||||||
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
|
errorMessages={[
|
||||||
|
'Server is required',
|
||||||
|
'Not a valid IP address or hostname'
|
||||||
|
]}
|
||||||
name="server"
|
name="server"
|
||||||
label="Server"
|
label="Server"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -68,7 +79,12 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
{timeZoneSelectItems()}
|
{timeZoneSelectItems()}
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Theme } from "@material-ui/core";
|
import { Theme } from '@material-ui/core';
|
||||||
import { NTPStatus, NTPSyncStatus } from "./types";
|
import { NTPStatus, NTPSyncStatus } from './types';
|
||||||
|
|
||||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
export const isNtpActive = ({ status }: NTPStatus) =>
|
||||||
|
status === NTPSyncStatus.NTP_ACTIVE;
|
||||||
|
|
||||||
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -12,15 +13,15 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ntpStatus = ({ status }: NTPStatus) => {
|
export const ntpStatus = ({ status }: NTPStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NTPSyncStatus.NTP_INACTIVE:
|
case NTPSyncStatus.NTP_INACTIVE:
|
||||||
return "Inactive";
|
return 'Inactive';
|
||||||
case NTPSyncStatus.NTP_ACTIVE:
|
case NTPSyncStatus.NTP_ACTIVE:
|
||||||
return "Active";
|
return 'Active';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
import { NTP_STATUS_ENDPOINT } from '../api';
|
import { NTP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import NTPStatusForm from './NTPStatusForm';
|
import NTPStatusForm from './NTPStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { NTPStatus } from './types';
|
|||||||
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
||||||
|
|
||||||
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class NTPStatusController extends Component<NTPStatusControllerProps> {
|
|||||||
<SectionContent title="NTP Status">
|
<SectionContent title="NTP Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NTPStatusForm {...formProps} />}
|
render={(formProps) => <NTPStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
|
import {
|
||||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText,
|
||||||
|
Button
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Box,
|
||||||
|
TextField
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
@@ -13,12 +28,22 @@ import RefreshIcon from '@material-ui/icons/Refresh';
|
|||||||
|
|
||||||
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
||||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
||||||
import { formatDuration, formatDateTime, formatLocalDateTime } from './TimeFormat';
|
import {
|
||||||
|
formatDuration,
|
||||||
|
formatDateTime,
|
||||||
|
formatLocalDateTime
|
||||||
|
} from './TimeFormat';
|
||||||
import { NTPStatus, Time } from './types';
|
import { NTPStatus, Time } from './types';
|
||||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import {
|
||||||
|
redirectingAuthorizedFetch,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
import { TIME_ENDPOINT } from '../api';
|
import { TIME_ENDPOINT } from '../api';
|
||||||
|
|
||||||
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
|
type NTPStatusFormProps = RestFormProps<NTPStatus> &
|
||||||
|
WithTheme &
|
||||||
|
AuthenticatedContextProps;
|
||||||
|
|
||||||
interface NTPStatusFormState {
|
interface NTPStatusFormState {
|
||||||
settingTime: boolean;
|
settingTime: boolean;
|
||||||
@@ -27,7 +52,6 @@ interface NTPStatusFormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||||
|
|
||||||
constructor(props: NTPStatusFormProps) {
|
constructor(props: NTPStatusFormProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -41,20 +65,20 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
localTime: event.target.value
|
localTime: event.target.value
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
openSetTime = () => {
|
openSetTime = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
localTime: formatLocalDateTime(new Date()),
|
localTime: formatLocalDateTime(new Date()),
|
||||||
settingTime: true
|
settingTime: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
closeSetTime = () => {
|
closeSetTime = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
settingTime: false
|
settingTime: false
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
createTime = (): Time => ({
|
createTime = (): Time => ({
|
||||||
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
||||||
@@ -62,37 +86,48 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
|
|
||||||
configureTime = () => {
|
configureTime = () => {
|
||||||
this.setState({ processing: true });
|
this.setState({ processing: true });
|
||||||
redirectingAuthorizedFetch(TIME_ENDPOINT,
|
redirectingAuthorizedFetch(TIME_ENDPOINT, {
|
||||||
{
|
method: 'POST',
|
||||||
method: 'POST',
|
body: JSON.stringify(this.createTime()),
|
||||||
body: JSON.stringify(this.createTime()),
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
}
|
})
|
||||||
})
|
.then((response) => {
|
||||||
.then(response => {
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
|
this.props.enqueueSnackbar('Time set successfully', {
|
||||||
this.setState({ processing: false, settingTime: false }, this.props.loadData);
|
variant: 'success'
|
||||||
|
});
|
||||||
|
this.setState(
|
||||||
|
{ processing: false, settingTime: false },
|
||||||
|
this.props.loadData
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw Error("Error setting time, status code: " + response.status);
|
throw Error('Error setting time, status code: ' + response.status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
|
this.props.enqueueSnackbar(
|
||||||
|
error.message || 'Problem setting the time',
|
||||||
|
{ variant: 'error' }
|
||||||
|
);
|
||||||
this.setState({ processing: false, settingTime: false });
|
this.setState({ processing: false, settingTime: false });
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
renderSetTimeDialog() {
|
renderSetTimeDialog() {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.settingTime}
|
open={this.state.settingTime}
|
||||||
onClose={this.closeSetTime}
|
onClose={this.closeSetTime}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
>
|
>
|
||||||
<DialogTitle>Set Time</DialogTitle>
|
<DialogTitle>Set Time</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
|
<Box mb={2}>
|
||||||
|
Enter local date and time below to set the device's time.
|
||||||
|
</Box>
|
||||||
<TextField
|
<TextField
|
||||||
label="Local Time"
|
label="Local Time"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
@@ -102,24 +137,35 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true,
|
shrink: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.closeSetTime}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<AccessTimeIcon />} variant="contained" onClick={this.configureTime} disabled={this.state.processing} color="primary" autoFocus>
|
<Button
|
||||||
|
startIcon={<AccessTimeIcon />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.configureTime}
|
||||||
|
disabled={this.state.processing}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
Set Time
|
Set Time
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
const me = this.props.authenticatedContext.me;
|
const me = this.props.authenticatedContext.me;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -152,7 +198,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<AccessTimeIcon />
|
<AccessTimeIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
|
<ListItemText
|
||||||
|
primary="Local Time"
|
||||||
|
secondary={formatDateTime(data.local_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -161,7 +210,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<SwapVerticalCircleIcon />
|
<SwapVerticalCircleIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
|
<ListItemText
|
||||||
|
primary="UTC Time"
|
||||||
|
secondary={formatDateTime(data.utc_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -170,19 +222,32 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<AvTimerIcon />
|
<AvTimerIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Uptime" secondary={formatDuration(data.uptime)} />
|
<ListItemText
|
||||||
|
primary="Uptime"
|
||||||
|
secondary={formatDuration(data.uptime)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1} padding={1}>
|
<Box flexGrow={1} padding={1}>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</Box>
|
</Box>
|
||||||
{me.admin && !isNtpActive(data) && (
|
{me.admin && !isNtpActive(data) && (
|
||||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||||
<Button onClick={this.openSetTime} variant="contained" color="primary" startIcon={<AccessTimeIcon />}>
|
<Button
|
||||||
|
onClick={this.openSetTime}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<AccessTimeIcon />}
|
||||||
|
>
|
||||||
Set Time
|
Set Time
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import NTPStatusController from './NTPStatusController';
|
import NTPStatusController from './NTPStatusController';
|
||||||
@@ -12,8 +16,7 @@ import NTPSettingsController from './NTPSettingsController';
|
|||||||
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class NetworkTime extends Component<NetworkTimeProps> {
|
class NetworkTime extends Component<NetworkTimeProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,19 +24,34 @@ class NetworkTime extends Component<NetworkTimeProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Network Time">
|
<MenuAppBar sectionTitle="Network Time">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value="/ntp/status" label="NTP Status" />
|
<Tab value="/ntp/status" label="NTP Status" />
|
||||||
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
|
value="/ntp/settings"
|
||||||
|
label="NTP Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/ntp/status" component={NTPStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/ntp/settings" component={NTPSettingsController} />
|
exact
|
||||||
|
path="/ntp/status"
|
||||||
|
component={NTPStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/ntp/settings"
|
||||||
|
component={NTPSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/ntp/status" />
|
<Redirect to="/ntp/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticatedContext(NetworkTime)
|
export default withAuthenticatedContext(NetworkTime);
|
||||||
|
|||||||
@@ -1,479 +1,480 @@
|
|||||||
import React from 'react';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
type TimeZones = {
|
type TimeZones = {
|
||||||
[name: string]: string
|
[name: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TIME_ZONES: TimeZones = {
|
export const TIME_ZONES: TimeZones = {
|
||||||
"Africa/Abidjan": "GMT0",
|
'Africa/Abidjan': 'GMT0',
|
||||||
"Africa/Accra": "GMT0",
|
'Africa/Accra': 'GMT0',
|
||||||
"Africa/Addis_Ababa": "EAT-3",
|
'Africa/Addis_Ababa': 'EAT-3',
|
||||||
"Africa/Algiers": "CET-1",
|
'Africa/Algiers': 'CET-1',
|
||||||
"Africa/Asmara": "EAT-3",
|
'Africa/Asmara': 'EAT-3',
|
||||||
"Africa/Bamako": "GMT0",
|
'Africa/Bamako': 'GMT0',
|
||||||
"Africa/Bangui": "WAT-1",
|
'Africa/Bangui': 'WAT-1',
|
||||||
"Africa/Banjul": "GMT0",
|
'Africa/Banjul': 'GMT0',
|
||||||
"Africa/Bissau": "GMT0",
|
'Africa/Bissau': 'GMT0',
|
||||||
"Africa/Blantyre": "CAT-2",
|
'Africa/Blantyre': 'CAT-2',
|
||||||
"Africa/Brazzaville": "WAT-1",
|
'Africa/Brazzaville': 'WAT-1',
|
||||||
"Africa/Bujumbura": "CAT-2",
|
'Africa/Bujumbura': 'CAT-2',
|
||||||
"Africa/Cairo": "EET-2",
|
'Africa/Cairo': 'EET-2',
|
||||||
"Africa/Casablanca": "UNK-1",
|
'Africa/Casablanca': 'UNK-1',
|
||||||
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Africa/Conakry": "GMT0",
|
'Africa/Conakry': 'GMT0',
|
||||||
"Africa/Dakar": "GMT0",
|
'Africa/Dakar': 'GMT0',
|
||||||
"Africa/Dar_es_Salaam": "EAT-3",
|
'Africa/Dar_es_Salaam': 'EAT-3',
|
||||||
"Africa/Djibouti": "EAT-3",
|
'Africa/Djibouti': 'EAT-3',
|
||||||
"Africa/Douala": "WAT-1",
|
'Africa/Douala': 'WAT-1',
|
||||||
"Africa/El_Aaiun": "UNK-1",
|
'Africa/El_Aaiun': 'UNK-1',
|
||||||
"Africa/Freetown": "GMT0",
|
'Africa/Freetown': 'GMT0',
|
||||||
"Africa/Gaborone": "CAT-2",
|
'Africa/Gaborone': 'CAT-2',
|
||||||
"Africa/Harare": "CAT-2",
|
'Africa/Harare': 'CAT-2',
|
||||||
"Africa/Johannesburg": "SAST-2",
|
'Africa/Johannesburg': 'SAST-2',
|
||||||
"Africa/Juba": "EAT-3",
|
'Africa/Juba': 'EAT-3',
|
||||||
"Africa/Kampala": "EAT-3",
|
'Africa/Kampala': 'EAT-3',
|
||||||
"Africa/Khartoum": "CAT-2",
|
'Africa/Khartoum': 'CAT-2',
|
||||||
"Africa/Kigali": "CAT-2",
|
'Africa/Kigali': 'CAT-2',
|
||||||
"Africa/Kinshasa": "WAT-1",
|
'Africa/Kinshasa': 'WAT-1',
|
||||||
"Africa/Lagos": "WAT-1",
|
'Africa/Lagos': 'WAT-1',
|
||||||
"Africa/Libreville": "WAT-1",
|
'Africa/Libreville': 'WAT-1',
|
||||||
"Africa/Lome": "GMT0",
|
'Africa/Lome': 'GMT0',
|
||||||
"Africa/Luanda": "WAT-1",
|
'Africa/Luanda': 'WAT-1',
|
||||||
"Africa/Lubumbashi": "CAT-2",
|
'Africa/Lubumbashi': 'CAT-2',
|
||||||
"Africa/Lusaka": "CAT-2",
|
'Africa/Lusaka': 'CAT-2',
|
||||||
"Africa/Malabo": "WAT-1",
|
'Africa/Malabo': 'WAT-1',
|
||||||
"Africa/Maputo": "CAT-2",
|
'Africa/Maputo': 'CAT-2',
|
||||||
"Africa/Maseru": "SAST-2",
|
'Africa/Maseru': 'SAST-2',
|
||||||
"Africa/Mbabane": "SAST-2",
|
'Africa/Mbabane': 'SAST-2',
|
||||||
"Africa/Mogadishu": "EAT-3",
|
'Africa/Mogadishu': 'EAT-3',
|
||||||
"Africa/Monrovia": "GMT0",
|
'Africa/Monrovia': 'GMT0',
|
||||||
"Africa/Nairobi": "EAT-3",
|
'Africa/Nairobi': 'EAT-3',
|
||||||
"Africa/Ndjamena": "WAT-1",
|
'Africa/Ndjamena': 'WAT-1',
|
||||||
"Africa/Niamey": "WAT-1",
|
'Africa/Niamey': 'WAT-1',
|
||||||
"Africa/Nouakchott": "GMT0",
|
'Africa/Nouakchott': 'GMT0',
|
||||||
"Africa/Ouagadougou": "GMT0",
|
'Africa/Ouagadougou': 'GMT0',
|
||||||
"Africa/Porto-Novo": "WAT-1",
|
'Africa/Porto-Novo': 'WAT-1',
|
||||||
"Africa/Sao_Tome": "GMT0",
|
'Africa/Sao_Tome': 'GMT0',
|
||||||
"Africa/Tripoli": "EET-2",
|
'Africa/Tripoli': 'EET-2',
|
||||||
"Africa/Tunis": "CET-1",
|
'Africa/Tunis': 'CET-1',
|
||||||
"Africa/Windhoek": "CAT-2",
|
'Africa/Windhoek': 'CAT-2',
|
||||||
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
|
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
|
||||||
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Anguilla": "AST4",
|
'America/Anguilla': 'AST4',
|
||||||
"America/Antigua": "AST4",
|
'America/Antigua': 'AST4',
|
||||||
"America/Araguaina": "UNK3",
|
'America/Araguaina': 'UNK3',
|
||||||
"America/Argentina/Buenos_Aires": "UNK3",
|
'America/Argentina/Buenos_Aires': 'UNK3',
|
||||||
"America/Argentina/Catamarca": "UNK3",
|
'America/Argentina/Catamarca': 'UNK3',
|
||||||
"America/Argentina/Cordoba": "UNK3",
|
'America/Argentina/Cordoba': 'UNK3',
|
||||||
"America/Argentina/Jujuy": "UNK3",
|
'America/Argentina/Jujuy': 'UNK3',
|
||||||
"America/Argentina/La_Rioja": "UNK3",
|
'America/Argentina/La_Rioja': 'UNK3',
|
||||||
"America/Argentina/Mendoza": "UNK3",
|
'America/Argentina/Mendoza': 'UNK3',
|
||||||
"America/Argentina/Rio_Gallegos": "UNK3",
|
'America/Argentina/Rio_Gallegos': 'UNK3',
|
||||||
"America/Argentina/Salta": "UNK3",
|
'America/Argentina/Salta': 'UNK3',
|
||||||
"America/Argentina/San_Juan": "UNK3",
|
'America/Argentina/San_Juan': 'UNK3',
|
||||||
"America/Argentina/San_Luis": "UNK3",
|
'America/Argentina/San_Luis': 'UNK3',
|
||||||
"America/Argentina/Tucuman": "UNK3",
|
'America/Argentina/Tucuman': 'UNK3',
|
||||||
"America/Argentina/Ushuaia": "UNK3",
|
'America/Argentina/Ushuaia': 'UNK3',
|
||||||
"America/Aruba": "AST4",
|
'America/Aruba': 'AST4',
|
||||||
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
|
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
|
||||||
"America/Atikokan": "EST5",
|
'America/Atikokan': 'EST5',
|
||||||
"America/Bahia": "UNK3",
|
'America/Bahia': 'UNK3',
|
||||||
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Barbados": "AST4",
|
'America/Barbados': 'AST4',
|
||||||
"America/Belem": "UNK3",
|
'America/Belem': 'UNK3',
|
||||||
"America/Belize": "CST6",
|
'America/Belize': 'CST6',
|
||||||
"America/Blanc-Sablon": "AST4",
|
'America/Blanc-Sablon': 'AST4',
|
||||||
"America/Boa_Vista": "UNK4",
|
'America/Boa_Vista': 'UNK4',
|
||||||
"America/Bogota": "UNK5",
|
'America/Bogota': 'UNK5',
|
||||||
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Campo_Grande": "UNK4",
|
'America/Campo_Grande': 'UNK4',
|
||||||
"America/Cancun": "EST5",
|
'America/Cancun': 'EST5',
|
||||||
"America/Caracas": "UNK4",
|
'America/Caracas': 'UNK4',
|
||||||
"America/Cayenne": "UNK3",
|
'America/Cayenne': 'UNK3',
|
||||||
"America/Cayman": "EST5",
|
'America/Cayman': 'EST5',
|
||||||
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
|
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
|
||||||
"America/Costa_Rica": "CST6",
|
'America/Costa_Rica': 'CST6',
|
||||||
"America/Creston": "MST7",
|
'America/Creston': 'MST7',
|
||||||
"America/Cuiaba": "UNK4",
|
'America/Cuiaba': 'UNK4',
|
||||||
"America/Curacao": "AST4",
|
'America/Curacao': 'AST4',
|
||||||
"America/Danmarkshavn": "GMT0",
|
'America/Danmarkshavn': 'GMT0',
|
||||||
"America/Dawson": "MST7",
|
'America/Dawson': 'MST7',
|
||||||
"America/Dawson_Creek": "MST7",
|
'America/Dawson_Creek': 'MST7',
|
||||||
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Dominica": "AST4",
|
'America/Dominica': 'AST4',
|
||||||
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Eirunepe": "UNK5",
|
'America/Eirunepe': 'UNK5',
|
||||||
"America/El_Salvador": "CST6",
|
'America/El_Salvador': 'CST6',
|
||||||
"America/Fort_Nelson": "MST7",
|
'America/Fort_Nelson': 'MST7',
|
||||||
"America/Fortaleza": "UNK3",
|
'America/Fortaleza': 'UNK3',
|
||||||
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
|
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
|
||||||
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Grenada": "AST4",
|
'America/Grenada': 'AST4',
|
||||||
"America/Guadeloupe": "AST4",
|
'America/Guadeloupe': 'AST4',
|
||||||
"America/Guatemala": "CST6",
|
'America/Guatemala': 'CST6',
|
||||||
"America/Guayaquil": "UNK5",
|
'America/Guayaquil': 'UNK5',
|
||||||
"America/Guyana": "UNK4",
|
'America/Guyana': 'UNK4',
|
||||||
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
|
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
|
||||||
"America/Hermosillo": "MST7",
|
'America/Hermosillo': 'MST7',
|
||||||
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Indiana/Knox': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Petersburg': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Indiana/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Vevay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Winamac': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Jamaica": "EST5",
|
'America/Jamaica': 'EST5',
|
||||||
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Kentucky/Louisville': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Kralendijk": "AST4",
|
'America/Kralendijk': 'AST4',
|
||||||
"America/La_Paz": "UNK4",
|
'America/La_Paz': 'UNK4',
|
||||||
"America/Lima": "UNK5",
|
'America/Lima': 'UNK5',
|
||||||
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Lower_Princes": "AST4",
|
'America/Lower_Princes': 'AST4',
|
||||||
"America/Maceio": "UNK3",
|
'America/Maceio': 'UNK3',
|
||||||
"America/Managua": "CST6",
|
'America/Managua': 'CST6',
|
||||||
"America/Manaus": "UNK4",
|
'America/Manaus': 'UNK4',
|
||||||
"America/Marigot": "AST4",
|
'America/Marigot': 'AST4',
|
||||||
"America/Martinique": "AST4",
|
'America/Martinique': 'AST4',
|
||||||
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
|
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
|
||||||
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
|
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
|
||||||
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Moncton': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Montevideo": "UNK3",
|
'America/Montevideo': 'UNK3',
|
||||||
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Montserrat": "AST4",
|
'America/Montserrat': 'AST4',
|
||||||
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
|
'America/New_York': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Noronha": "UNK2",
|
'America/Noronha': 'UNK2',
|
||||||
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
|
'America/North_Dakota/Beulah': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
|
'America/North_Dakota/Center': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
|
'America/North_Dakota/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Panama": "EST5",
|
'America/Panama': 'EST5',
|
||||||
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Paramaribo": "UNK3",
|
'America/Paramaribo': 'UNK3',
|
||||||
"America/Phoenix": "MST7",
|
'America/Phoenix': 'MST7',
|
||||||
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Port_of_Spain": "AST4",
|
'America/Port_of_Spain': 'AST4',
|
||||||
"America/Porto_Velho": "UNK4",
|
'America/Porto_Velho': 'UNK4',
|
||||||
"America/Puerto_Rico": "AST4",
|
'America/Puerto_Rico': 'AST4',
|
||||||
"America/Punta_Arenas": "UNK3",
|
'America/Punta_Arenas': 'UNK3',
|
||||||
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Recife": "UNK3",
|
'America/Recife': 'UNK3',
|
||||||
"America/Regina": "CST6",
|
'America/Regina': 'CST6',
|
||||||
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Rio_Branco": "UNK5",
|
'America/Rio_Branco': 'UNK5',
|
||||||
"America/Santarem": "UNK3",
|
'America/Santarem': 'UNK3',
|
||||||
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
|
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
|
||||||
"America/Santo_Domingo": "AST4",
|
'America/Santo_Domingo': 'AST4',
|
||||||
"America/Sao_Paulo": "UNK3",
|
'America/Sao_Paulo': 'UNK3',
|
||||||
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||||
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/St_Barthelemy": "AST4",
|
'America/St_Barthelemy': 'AST4',
|
||||||
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
|
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
|
||||||
"America/St_Kitts": "AST4",
|
'America/St_Kitts': 'AST4',
|
||||||
"America/St_Lucia": "AST4",
|
'America/St_Lucia': 'AST4',
|
||||||
"America/St_Thomas": "AST4",
|
'America/St_Thomas': 'AST4',
|
||||||
"America/St_Vincent": "AST4",
|
'America/St_Vincent': 'AST4',
|
||||||
"America/Swift_Current": "CST6",
|
'America/Swift_Current': 'CST6',
|
||||||
"America/Tegucigalpa": "CST6",
|
'America/Tegucigalpa': 'CST6',
|
||||||
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Thunder_Bay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Tortola": "AST4",
|
'America/Tortola': 'AST4',
|
||||||
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Whitehorse": "MST7",
|
'America/Whitehorse': 'MST7',
|
||||||
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"Antarctica/Casey": "UNK-8",
|
'Antarctica/Casey': 'UNK-8',
|
||||||
"Antarctica/Davis": "UNK-7",
|
'Antarctica/Davis': 'UNK-7',
|
||||||
"Antarctica/DumontDUrville": "UNK-10",
|
'Antarctica/DumontDUrville': 'UNK-10',
|
||||||
"Antarctica/Macquarie": "UNK-11",
|
'Antarctica/Macquarie': 'UNK-11',
|
||||||
"Antarctica/Mawson": "UNK-5",
|
'Antarctica/Mawson': 'UNK-5',
|
||||||
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||||
"Antarctica/Palmer": "UNK3",
|
'Antarctica/Palmer': 'UNK3',
|
||||||
"Antarctica/Rothera": "UNK3",
|
'Antarctica/Rothera': 'UNK3',
|
||||||
"Antarctica/Syowa": "UNK-3",
|
'Antarctica/Syowa': 'UNK-3',
|
||||||
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
|
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
|
||||||
"Antarctica/Vostok": "UNK-6",
|
'Antarctica/Vostok': 'UNK-6',
|
||||||
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Asia/Aden": "UNK-3",
|
'Asia/Aden': 'UNK-3',
|
||||||
"Asia/Almaty": "UNK-6",
|
'Asia/Almaty': 'UNK-6',
|
||||||
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
|
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
|
||||||
"Asia/Anadyr": "UNK-12",
|
'Asia/Anadyr': 'UNK-12',
|
||||||
"Asia/Aqtau": "UNK-5",
|
'Asia/Aqtau': 'UNK-5',
|
||||||
"Asia/Aqtobe": "UNK-5",
|
'Asia/Aqtobe': 'UNK-5',
|
||||||
"Asia/Ashgabat": "UNK-5",
|
'Asia/Ashgabat': 'UNK-5',
|
||||||
"Asia/Atyrau": "UNK-5",
|
'Asia/Atyrau': 'UNK-5',
|
||||||
"Asia/Baghdad": "UNK-3",
|
'Asia/Baghdad': 'UNK-3',
|
||||||
"Asia/Bahrain": "UNK-3",
|
'Asia/Bahrain': 'UNK-3',
|
||||||
"Asia/Baku": "UNK-4",
|
'Asia/Baku': 'UNK-4',
|
||||||
"Asia/Bangkok": "UNK-7",
|
'Asia/Bangkok': 'UNK-7',
|
||||||
"Asia/Barnaul": "UNK-7",
|
'Asia/Barnaul': 'UNK-7',
|
||||||
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
|
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
|
||||||
"Asia/Bishkek": "UNK-6",
|
'Asia/Bishkek': 'UNK-6',
|
||||||
"Asia/Brunei": "UNK-8",
|
'Asia/Brunei': 'UNK-8',
|
||||||
"Asia/Chita": "UNK-9",
|
'Asia/Chita': 'UNK-9',
|
||||||
"Asia/Choibalsan": "UNK-8",
|
'Asia/Choibalsan': 'UNK-8',
|
||||||
"Asia/Colombo": "UNK-5:30",
|
'Asia/Colombo': 'UNK-5:30',
|
||||||
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
|
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
|
||||||
"Asia/Dhaka": "UNK-6",
|
'Asia/Dhaka': 'UNK-6',
|
||||||
"Asia/Dili": "UNK-9",
|
'Asia/Dili': 'UNK-9',
|
||||||
"Asia/Dubai": "UNK-4",
|
'Asia/Dubai': 'UNK-4',
|
||||||
"Asia/Dushanbe": "UNK-5",
|
'Asia/Dushanbe': 'UNK-5',
|
||||||
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Asia/Famagusta': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
'Asia/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||||
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
'Asia/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||||
"Asia/Ho_Chi_Minh": "UNK-7",
|
'Asia/Ho_Chi_Minh': 'UNK-7',
|
||||||
"Asia/Hong_Kong": "HKT-8",
|
'Asia/Hong_Kong': 'HKT-8',
|
||||||
"Asia/Hovd": "UNK-7",
|
'Asia/Hovd': 'UNK-7',
|
||||||
"Asia/Irkutsk": "UNK-8",
|
'Asia/Irkutsk': 'UNK-8',
|
||||||
"Asia/Jakarta": "WIB-7",
|
'Asia/Jakarta': 'WIB-7',
|
||||||
"Asia/Jayapura": "WIT-9",
|
'Asia/Jayapura': 'WIT-9',
|
||||||
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
|
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
|
||||||
"Asia/Kabul": "UNK-4:30",
|
'Asia/Kabul': 'UNK-4:30',
|
||||||
"Asia/Kamchatka": "UNK-12",
|
'Asia/Kamchatka': 'UNK-12',
|
||||||
"Asia/Karachi": "PKT-5",
|
'Asia/Karachi': 'PKT-5',
|
||||||
"Asia/Kathmandu": "UNK-5:45",
|
'Asia/Kathmandu': 'UNK-5:45',
|
||||||
"Asia/Khandyga": "UNK-9",
|
'Asia/Khandyga': 'UNK-9',
|
||||||
"Asia/Kolkata": "IST-5:30",
|
'Asia/Kolkata': 'IST-5:30',
|
||||||
"Asia/Krasnoyarsk": "UNK-7",
|
'Asia/Krasnoyarsk': 'UNK-7',
|
||||||
"Asia/Kuala_Lumpur": "UNK-8",
|
'Asia/Kuala_Lumpur': 'UNK-8',
|
||||||
"Asia/Kuching": "UNK-8",
|
'Asia/Kuching': 'UNK-8',
|
||||||
"Asia/Kuwait": "UNK-3",
|
'Asia/Kuwait': 'UNK-3',
|
||||||
"Asia/Macau": "CST-8",
|
'Asia/Macau': 'CST-8',
|
||||||
"Asia/Magadan": "UNK-11",
|
'Asia/Magadan': 'UNK-11',
|
||||||
"Asia/Makassar": "WITA-8",
|
'Asia/Makassar': 'WITA-8',
|
||||||
"Asia/Manila": "PST-8",
|
'Asia/Manila': 'PST-8',
|
||||||
"Asia/Muscat": "UNK-4",
|
'Asia/Muscat': 'UNK-4',
|
||||||
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Asia/Novokuznetsk": "UNK-7",
|
'Asia/Novokuznetsk': 'UNK-7',
|
||||||
"Asia/Novosibirsk": "UNK-7",
|
'Asia/Novosibirsk': 'UNK-7',
|
||||||
"Asia/Omsk": "UNK-6",
|
'Asia/Omsk': 'UNK-6',
|
||||||
"Asia/Oral": "UNK-5",
|
'Asia/Oral': 'UNK-5',
|
||||||
"Asia/Phnom_Penh": "UNK-7",
|
'Asia/Phnom_Penh': 'UNK-7',
|
||||||
"Asia/Pontianak": "WIB-7",
|
'Asia/Pontianak': 'WIB-7',
|
||||||
"Asia/Pyongyang": "KST-9",
|
'Asia/Pyongyang': 'KST-9',
|
||||||
"Asia/Qatar": "UNK-3",
|
'Asia/Qatar': 'UNK-3',
|
||||||
"Asia/Qyzylorda": "UNK-5",
|
'Asia/Qyzylorda': 'UNK-5',
|
||||||
"Asia/Riyadh": "UNK-3",
|
'Asia/Riyadh': 'UNK-3',
|
||||||
"Asia/Sakhalin": "UNK-11",
|
'Asia/Sakhalin': 'UNK-11',
|
||||||
"Asia/Samarkand": "UNK-5",
|
'Asia/Samarkand': 'UNK-5',
|
||||||
"Asia/Seoul": "KST-9",
|
'Asia/Seoul': 'KST-9',
|
||||||
"Asia/Shanghai": "CST-8",
|
'Asia/Shanghai': 'CST-8',
|
||||||
"Asia/Singapore": "UNK-8",
|
'Asia/Singapore': 'UNK-8',
|
||||||
"Asia/Srednekolymsk": "UNK-11",
|
'Asia/Srednekolymsk': 'UNK-11',
|
||||||
"Asia/Taipei": "CST-8",
|
'Asia/Taipei': 'CST-8',
|
||||||
"Asia/Tashkent": "UNK-5",
|
'Asia/Tashkent': 'UNK-5',
|
||||||
"Asia/Tbilisi": "UNK-4",
|
'Asia/Tbilisi': 'UNK-4',
|
||||||
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
|
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
|
||||||
"Asia/Thimphu": "UNK-6",
|
'Asia/Thimphu': 'UNK-6',
|
||||||
"Asia/Tokyo": "JST-9",
|
'Asia/Tokyo': 'JST-9',
|
||||||
"Asia/Tomsk": "UNK-7",
|
'Asia/Tomsk': 'UNK-7',
|
||||||
"Asia/Ulaanbaatar": "UNK-8",
|
'Asia/Ulaanbaatar': 'UNK-8',
|
||||||
"Asia/Urumqi": "UNK-6",
|
'Asia/Urumqi': 'UNK-6',
|
||||||
"Asia/Ust-Nera": "UNK-10",
|
'Asia/Ust-Nera': 'UNK-10',
|
||||||
"Asia/Vientiane": "UNK-7",
|
'Asia/Vientiane': 'UNK-7',
|
||||||
"Asia/Vladivostok": "UNK-10",
|
'Asia/Vladivostok': 'UNK-10',
|
||||||
"Asia/Yakutsk": "UNK-9",
|
'Asia/Yakutsk': 'UNK-9',
|
||||||
"Asia/Yangon": "UNK-6:30",
|
'Asia/Yangon': 'UNK-6:30',
|
||||||
"Asia/Yekaterinburg": "UNK-5",
|
'Asia/Yekaterinburg': 'UNK-5',
|
||||||
"Asia/Yerevan": "UNK-4",
|
'Asia/Yerevan': 'UNK-4',
|
||||||
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||||
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
|
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Cape_Verde": "UNK1",
|
'Atlantic/Cape_Verde': 'UNK1',
|
||||||
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Reykjavik": "GMT0",
|
'Atlantic/Reykjavik': 'GMT0',
|
||||||
"Atlantic/South_Georgia": "UNK2",
|
'Atlantic/South_Georgia': 'UNK2',
|
||||||
"Atlantic/St_Helena": "GMT0",
|
'Atlantic/St_Helena': 'GMT0',
|
||||||
"Atlantic/Stanley": "UNK3",
|
'Atlantic/Stanley': 'UNK3',
|
||||||
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Brisbane": "AEST-10",
|
'Australia/Brisbane': 'AEST-10',
|
||||||
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
'Australia/Broken_Hill': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Darwin": "ACST-9:30",
|
'Australia/Darwin': 'ACST-9:30',
|
||||||
"Australia/Eucla": "UNK-8:45",
|
'Australia/Eucla': 'UNK-8:45',
|
||||||
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Lindeman": "AEST-10",
|
'Australia/Lindeman': 'AEST-10',
|
||||||
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
|
'Australia/Lord_Howe': 'UNK-10:30UNK-11,M10.1.0,M4.1.0',
|
||||||
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Perth": "AWST-8",
|
'Australia/Perth': 'AWST-8',
|
||||||
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Etc/GMT": "GMT0",
|
'Etc/GMT': 'GMT0',
|
||||||
"Etc/GMT+0": "GMT0",
|
'Etc/GMT+0': 'GMT0',
|
||||||
"Etc/GMT+1": "UNK1",
|
'Etc/GMT+1': 'UNK1',
|
||||||
"Etc/GMT+10": "UNK10",
|
'Etc/GMT+10': 'UNK10',
|
||||||
"Etc/GMT+11": "UNK11",
|
'Etc/GMT+11': 'UNK11',
|
||||||
"Etc/GMT+12": "UNK12",
|
'Etc/GMT+12': 'UNK12',
|
||||||
"Etc/GMT+2": "UNK2",
|
'Etc/GMT+2': 'UNK2',
|
||||||
"Etc/GMT+3": "UNK3",
|
'Etc/GMT+3': 'UNK3',
|
||||||
"Etc/GMT+4": "UNK4",
|
'Etc/GMT+4': 'UNK4',
|
||||||
"Etc/GMT+5": "UNK5",
|
'Etc/GMT+5': 'UNK5',
|
||||||
"Etc/GMT+6": "UNK6",
|
'Etc/GMT+6': 'UNK6',
|
||||||
"Etc/GMT+7": "UNK7",
|
'Etc/GMT+7': 'UNK7',
|
||||||
"Etc/GMT+8": "UNK8",
|
'Etc/GMT+8': 'UNK8',
|
||||||
"Etc/GMT+9": "UNK9",
|
'Etc/GMT+9': 'UNK9',
|
||||||
"Etc/GMT-0": "GMT0",
|
'Etc/GMT-0': 'GMT0',
|
||||||
"Etc/GMT-1": "UNK-1",
|
'Etc/GMT-1': 'UNK-1',
|
||||||
"Etc/GMT-10": "UNK-10",
|
'Etc/GMT-10': 'UNK-10',
|
||||||
"Etc/GMT-11": "UNK-11",
|
'Etc/GMT-11': 'UNK-11',
|
||||||
"Etc/GMT-12": "UNK-12",
|
'Etc/GMT-12': 'UNK-12',
|
||||||
"Etc/GMT-13": "UNK-13",
|
'Etc/GMT-13': 'UNK-13',
|
||||||
"Etc/GMT-14": "UNK-14",
|
'Etc/GMT-14': 'UNK-14',
|
||||||
"Etc/GMT-2": "UNK-2",
|
'Etc/GMT-2': 'UNK-2',
|
||||||
"Etc/GMT-3": "UNK-3",
|
'Etc/GMT-3': 'UNK-3',
|
||||||
"Etc/GMT-4": "UNK-4",
|
'Etc/GMT-4': 'UNK-4',
|
||||||
"Etc/GMT-5": "UNK-5",
|
'Etc/GMT-5': 'UNK-5',
|
||||||
"Etc/GMT-6": "UNK-6",
|
'Etc/GMT-6': 'UNK-6',
|
||||||
"Etc/GMT-7": "UNK-7",
|
'Etc/GMT-7': 'UNK-7',
|
||||||
"Etc/GMT-8": "UNK-8",
|
'Etc/GMT-8': 'UNK-8',
|
||||||
"Etc/GMT-9": "UNK-9",
|
'Etc/GMT-9': 'UNK-9',
|
||||||
"Etc/GMT0": "GMT0",
|
'Etc/GMT0': 'GMT0',
|
||||||
"Etc/Greenwich": "GMT0",
|
'Etc/Greenwich': 'GMT0',
|
||||||
"Etc/UCT": "UTC0",
|
'Etc/UCT': 'UTC0',
|
||||||
"Etc/UTC": "UTC0",
|
'Etc/UTC': 'UTC0',
|
||||||
"Etc/Universal": "UTC0",
|
'Etc/Universal': 'UTC0',
|
||||||
"Etc/Zulu": "UTC0",
|
'Etc/Zulu': 'UTC0',
|
||||||
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Andorra': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Astrakhan": "UNK-4",
|
'Europe/Astrakhan': 'UNK-4',
|
||||||
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Berlin': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Busingen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
|
'Europe/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
|
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
|
||||||
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Helsinki': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Istanbul": "UNK-3",
|
'Europe/Istanbul': 'UNK-3',
|
||||||
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Kaliningrad": "EET-2",
|
'Europe/Kaliningrad': 'EET-2',
|
||||||
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Kirov": "UNK-3",
|
'Europe/Kirov': 'UNK-3',
|
||||||
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Madrid': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Minsk": "UNK-3",
|
'Europe/Minsk': 'UNK-3',
|
||||||
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Moscow": "MSK-3",
|
'Europe/Moscow': 'MSK-3',
|
||||||
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Paris': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Samara": "UNK-4",
|
'Europe/Samara': 'UNK-4',
|
||||||
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Sarajevo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Saratov": "UNK-4",
|
'Europe/Saratov': 'UNK-4',
|
||||||
"Europe/Simferopol": "MSK-3",
|
'Europe/Simferopol': 'MSK-3',
|
||||||
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Skopje': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Ulyanovsk": "UNK-4",
|
'Europe/Ulyanovsk': 'UNK-4',
|
||||||
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Uzhgorod': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Vatican': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Volgograd": "UNK-4",
|
'Europe/Volgograd': 'UNK-4',
|
||||||
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Zagreb': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Indian/Antananarivo": "EAT-3",
|
'Indian/Antananarivo': 'EAT-3',
|
||||||
"Indian/Chagos": "UNK-6",
|
'Indian/Chagos': 'UNK-6',
|
||||||
"Indian/Christmas": "UNK-7",
|
'Indian/Christmas': 'UNK-7',
|
||||||
"Indian/Cocos": "UNK-6:30",
|
'Indian/Cocos': 'UNK-6:30',
|
||||||
"Indian/Comoro": "EAT-3",
|
'Indian/Comoro': 'EAT-3',
|
||||||
"Indian/Kerguelen": "UNK-5",
|
'Indian/Kerguelen': 'UNK-5',
|
||||||
"Indian/Mahe": "UNK-4",
|
'Indian/Mahe': 'UNK-4',
|
||||||
"Indian/Maldives": "UNK-5",
|
'Indian/Maldives': 'UNK-5',
|
||||||
"Indian/Mauritius": "UNK-4",
|
'Indian/Mauritius': 'UNK-4',
|
||||||
"Indian/Mayotte": "EAT-3",
|
'Indian/Mayotte': 'EAT-3',
|
||||||
"Indian/Reunion": "UNK-4",
|
'Indian/Reunion': 'UNK-4',
|
||||||
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
|
'Pacific/Apia': 'UNK-13UNK,M9.5.0/3,M4.1.0/4',
|
||||||
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
'Pacific/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||||
"Pacific/Bougainville": "UNK-11",
|
'Pacific/Bougainville': 'UNK-11',
|
||||||
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
|
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
|
||||||
"Pacific/Chuuk": "UNK-10",
|
'Pacific/Chuuk': 'UNK-10',
|
||||||
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
|
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
|
||||||
"Pacific/Efate": "UNK-11",
|
'Pacific/Efate': 'UNK-11',
|
||||||
"Pacific/Enderbury": "UNK-13",
|
'Pacific/Enderbury': 'UNK-13',
|
||||||
"Pacific/Fakaofo": "UNK-13",
|
'Pacific/Fakaofo': 'UNK-13',
|
||||||
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
|
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
|
||||||
"Pacific/Funafuti": "UNK-12",
|
'Pacific/Funafuti': 'UNK-12',
|
||||||
"Pacific/Galapagos": "UNK6",
|
'Pacific/Galapagos': 'UNK6',
|
||||||
"Pacific/Gambier": "UNK9",
|
'Pacific/Gambier': 'UNK9',
|
||||||
"Pacific/Guadalcanal": "UNK-11",
|
'Pacific/Guadalcanal': 'UNK-11',
|
||||||
"Pacific/Guam": "ChST-10",
|
'Pacific/Guam': 'ChST-10',
|
||||||
"Pacific/Honolulu": "HST10",
|
'Pacific/Honolulu': 'HST10',
|
||||||
"Pacific/Kiritimati": "UNK-14",
|
'Pacific/Kiritimati': 'UNK-14',
|
||||||
"Pacific/Kosrae": "UNK-11",
|
'Pacific/Kosrae': 'UNK-11',
|
||||||
"Pacific/Kwajalein": "UNK-12",
|
'Pacific/Kwajalein': 'UNK-12',
|
||||||
"Pacific/Majuro": "UNK-12",
|
'Pacific/Majuro': 'UNK-12',
|
||||||
"Pacific/Marquesas": "UNK9:30",
|
'Pacific/Marquesas': 'UNK9:30',
|
||||||
"Pacific/Midway": "SST11",
|
'Pacific/Midway': 'SST11',
|
||||||
"Pacific/Nauru": "UNK-12",
|
'Pacific/Nauru': 'UNK-12',
|
||||||
"Pacific/Niue": "UNK11",
|
'Pacific/Niue': 'UNK11',
|
||||||
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
|
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
|
||||||
"Pacific/Noumea": "UNK-11",
|
'Pacific/Noumea': 'UNK-11',
|
||||||
"Pacific/Pago_Pago": "SST11",
|
'Pacific/Pago_Pago': 'SST11',
|
||||||
"Pacific/Palau": "UNK-9",
|
'Pacific/Palau': 'UNK-9',
|
||||||
"Pacific/Pitcairn": "UNK8",
|
'Pacific/Pitcairn': 'UNK8',
|
||||||
"Pacific/Pohnpei": "UNK-11",
|
'Pacific/Pohnpei': 'UNK-11',
|
||||||
"Pacific/Port_Moresby": "UNK-10",
|
'Pacific/Port_Moresby': 'UNK-10',
|
||||||
"Pacific/Rarotonga": "UNK10",
|
'Pacific/Rarotonga': 'UNK10',
|
||||||
"Pacific/Saipan": "ChST-10",
|
'Pacific/Saipan': 'ChST-10',
|
||||||
"Pacific/Tahiti": "UNK10",
|
'Pacific/Tahiti': 'UNK10',
|
||||||
"Pacific/Tarawa": "UNK-12",
|
'Pacific/Tarawa': 'UNK-12',
|
||||||
"Pacific/Tongatapu": "UNK-13",
|
'Pacific/Tongatapu': 'UNK-13',
|
||||||
"Pacific/Wake": "UNK-12",
|
'Pacific/Wake': 'UNK-12',
|
||||||
"Pacific/Wallis": "UNK-12"
|
'Pacific/Wallis': 'UNK-12'
|
||||||
}
|
};
|
||||||
|
|
||||||
export function selectedTimeZone(label: string, format: string) {
|
export function selectedTimeZone(label: string, format: string) {
|
||||||
return TIME_ZONES[label] === format ? label : undefined;
|
return TIME_ZONES[label] === format ? label : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeZoneSelectItems() {
|
export function timeZoneSelectItems() {
|
||||||
return Object.keys(TIME_ZONES).map(label => (
|
return Object.keys(TIME_ZONES).map((label) => (
|
||||||
<MenuItem key={label} value={label}>{label}</MenuItem>
|
<MenuItem key={label} value={label}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,29 @@
|
|||||||
import parseMilliseconds from 'parse-ms';
|
import parseMilliseconds from 'parse-ms';
|
||||||
|
|
||||||
const LOCALE_FORMAT = new Intl.DateTimeFormat(
|
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
||||||
[...window.navigator.languages],
|
day: 'numeric',
|
||||||
{
|
month: 'short',
|
||||||
day: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
hour: 'numeric',
|
||||||
year: 'numeric',
|
minute: 'numeric',
|
||||||
hour: 'numeric',
|
second: 'numeric',
|
||||||
minute: 'numeric',
|
hour12: false
|
||||||
second: 'numeric',
|
});
|
||||||
hour12: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const formatDateTime = (dateTime: string) => {
|
export const formatDateTime = (dateTime: string) => {
|
||||||
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
|
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
|
||||||
}
|
};
|
||||||
|
|
||||||
export const formatLocalDateTime = (date: Date) => {
|
export const formatLocalDateTime = (date: Date) => {
|
||||||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.substr(0, 19);
|
.substr(0, 19);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const formatDuration = (duration: number) => {
|
export const formatDuration = (duration: number) => {
|
||||||
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
||||||
var formatted = '';
|
let formatted = '';
|
||||||
if (days) {
|
if (days) {
|
||||||
formatted += pluralize(days, 'day');
|
formatted += pluralize(days, 'day');
|
||||||
}
|
}
|
||||||
@@ -40,6 +37,7 @@ export const formatDuration = (duration: number) => {
|
|||||||
formatted += pluralize(seconds, 'second');
|
formatted += pluralize(seconds, 'second');
|
||||||
}
|
}
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
};
|
||||||
|
|
||||||
const pluralize = (count: number, noun: string, suffix: string = 's') => ` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||||
|
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||||
|
|||||||
24
interface/src/project/EMSESPBoardProfiles.tsx
Normal file
24
interface/src/project/EMSESPBoardProfiles.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
|
type BoardProfiles = {
|
||||||
|
[name: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BOARD_PROFILES: BoardProfiles = {
|
||||||
|
S32: 'BBQKees Gateway S32',
|
||||||
|
E32: 'BBQKees Gateway E32',
|
||||||
|
NODEMCU: 'NodeMCU 32S',
|
||||||
|
'MH-ET': 'MH-ET Live D1 Mini',
|
||||||
|
LOLIN: 'Lolin D32',
|
||||||
|
OLIMEX: 'Olimex ESP32-EVB',
|
||||||
|
TLK110: 'Generic Ethernet (TLK110)',
|
||||||
|
LAN8720: 'Generic Ethernet (LAN8720)'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function boardProfileSelectItems() {
|
||||||
|
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||||
|
<MenuItem key={code} value={code}>
|
||||||
|
{BOARD_PROFILES[code]}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -8,34 +8,47 @@ import { MenuAppBar } from '../components';
|
|||||||
import { AuthenticatedRoute } from '../authentication';
|
import { AuthenticatedRoute } from '../authentication';
|
||||||
|
|
||||||
import EMSESPStatusController from './EMSESPStatusController';
|
import EMSESPStatusController from './EMSESPStatusController';
|
||||||
import EMSESPDevicesController from './EMSESPDevicesController';
|
import EMSESPDataController from './EMSESPDataController';
|
||||||
import EMSESPHelp from './EMSESPHelp';
|
import EMSESPHelp from './EMSESPHelp';
|
||||||
|
|
||||||
class EMSESP extends Component<RouteComponentProps> {
|
class EMSESP extends Component<RouteComponentProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Dashboard">
|
<MenuAppBar sectionTitle="Dashboard">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
<Tab value={`/${PROJECT_PATH}/devices`} label="Devices & Sensors" />
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
|
<Tab value={`/${PROJECT_PATH}/data`} label="Devices & Sensors" />
|
||||||
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
||||||
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/devices`} component={EMSESPDevicesController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
|
exact
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/help`} component={EMSESPHelp} />
|
path={`/${PROJECT_PATH}/data`}
|
||||||
<Redirect to={`/${PROJECT_PATH}/devices`} />
|
component={EMSESPDataController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/status`}
|
||||||
|
component={EMSESPStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/help`}
|
||||||
|
component={EMSESPHelp}
|
||||||
|
/>
|
||||||
|
<Redirect to={`/${PROJECT_PATH}/data`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EMSESP;
|
export default EMSESP;
|
||||||
|
|||||||
35
interface/src/project/EMSESPDataController.tsx
Normal file
35
interface/src/project/EMSESPDataController.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
|
import EMSESPDataForm from './EMSESPDataForm';
|
||||||
|
import { EMSESPData } from './EMSESPtypes';
|
||||||
|
|
||||||
|
export const EMSESP_DATA_ENDPOINT = ENDPOINT_ROOT + 'data';
|
||||||
|
|
||||||
|
type EMSESPDataControllerProps = RestControllerProps<EMSESPData>;
|
||||||
|
|
||||||
|
class EMSESPDataController extends Component<EMSESPDataControllerProps> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="Devices & Sensors">
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={(formProps) => <EMSESPDataForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(EMSESP_DATA_ENDPOINT, EMSESPDataController);
|
||||||
677
interface/src/project/EMSESPDataForm.tsx
Normal file
677
interface/src/project/EMSESPDataForm.tsx
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
import parseMilliseconds from 'parse-ms';
|
||||||
|
|
||||||
|
import { Decoder } from '@msgpack/msgpack';
|
||||||
|
const decoder = new Decoder();
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableContainer,
|
||||||
|
withWidth,
|
||||||
|
WithWidthProps,
|
||||||
|
isWidthDown,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Box,
|
||||||
|
Dialog,
|
||||||
|
Typography
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
import ListIcon from '@material-ui/icons/List';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
redirectingAuthorizedFetch,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
|
|
||||||
|
import { RestFormProps, FormButton, extractEventValue } from '../components';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EMSESPData,
|
||||||
|
EMSESPDeviceData,
|
||||||
|
Device,
|
||||||
|
DeviceValue,
|
||||||
|
DeviceValueUOM,
|
||||||
|
DeviceValueUOM_s,
|
||||||
|
Sensor
|
||||||
|
} from './EMSESPtypes';
|
||||||
|
|
||||||
|
import ValueForm from './ValueForm';
|
||||||
|
import SensorForm from './SensorForm';
|
||||||
|
|
||||||
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
|
|
||||||
|
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices';
|
||||||
|
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData';
|
||||||
|
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue';
|
||||||
|
export const WRITE_SENSOR_ENDPOINT = ENDPOINT_ROOT + 'writeSensor';
|
||||||
|
|
||||||
|
const StyledTableCell = withStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
head: {
|
||||||
|
backgroundColor: theme.palette.common.black,
|
||||||
|
color: theme.palette.common.white
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)(TableCell);
|
||||||
|
|
||||||
|
const CustomTooltip = withStyles((theme: Theme) => ({
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: theme.shadows[1],
|
||||||
|
fontSize: 11,
|
||||||
|
border: '1px solid #dadde9'
|
||||||
|
}
|
||||||
|
}))(Tooltip);
|
||||||
|
|
||||||
|
function compareDevices(a: Device, b: Device) {
|
||||||
|
if (a.type < b.type) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.type > b.type) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EMSESPDataFormState {
|
||||||
|
confirmScanDevices: boolean;
|
||||||
|
processing: boolean;
|
||||||
|
deviceData?: EMSESPDeviceData;
|
||||||
|
selectedDevice?: number;
|
||||||
|
edit_devicevalue?: DeviceValue;
|
||||||
|
edit_Sensor?: Sensor;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EMSESPDataFormProps = RestFormProps<EMSESPData> &
|
||||||
|
AuthenticatedContextProps &
|
||||||
|
WithWidthProps;
|
||||||
|
|
||||||
|
export const formatDuration = (duration_min: number) => {
|
||||||
|
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
|
||||||
|
let formatted = '';
|
||||||
|
if (days) {
|
||||||
|
formatted += pluralize(days, 'day');
|
||||||
|
}
|
||||||
|
if (hours) {
|
||||||
|
formatted += pluralize(hours, 'hour');
|
||||||
|
}
|
||||||
|
if (minutes) {
|
||||||
|
formatted += pluralize(minutes, 'minute');
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||||
|
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||||
|
|
||||||
|
function formatValue(value: any, uom: number, digit: number) {
|
||||||
|
switch (uom) {
|
||||||
|
case DeviceValueUOM.HOURS:
|
||||||
|
return value ? formatDuration(value * 60) : '0 hours';
|
||||||
|
case DeviceValueUOM.MINUTES:
|
||||||
|
return value ? formatDuration(value) : '0 minutes';
|
||||||
|
case DeviceValueUOM.NONE:
|
||||||
|
case DeviceValueUOM.LIST:
|
||||||
|
return value;
|
||||||
|
case DeviceValueUOM.NUM:
|
||||||
|
return new Intl.NumberFormat().format(value);
|
||||||
|
case DeviceValueUOM.BOOLEAN:
|
||||||
|
return value ? 'on' : 'off';
|
||||||
|
case DeviceValueUOM.DEGREES:
|
||||||
|
return (
|
||||||
|
new Intl.NumberFormat(undefined, {
|
||||||
|
minimumFractionDigits: digit
|
||||||
|
}).format(value) +
|
||||||
|
' ' +
|
||||||
|
DeviceValueUOM_s[uom]
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EMSESPDataForm extends Component<
|
||||||
|
EMSESPDataFormProps,
|
||||||
|
EMSESPDataFormState
|
||||||
|
> {
|
||||||
|
state: EMSESPDataFormState = {
|
||||||
|
confirmScanDevices: false,
|
||||||
|
processing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDeviceValueChange = (name: keyof DeviceValue) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
this.setState({
|
||||||
|
edit_devicevalue: {
|
||||||
|
...this.state.edit_devicevalue!,
|
||||||
|
[name]: extractEventValue(event)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelEditingDeviceValue = () => {
|
||||||
|
this.setState({ edit_devicevalue: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
doneEditingDeviceValue = () => {
|
||||||
|
const { edit_devicevalue, selectedDevice } = this.state;
|
||||||
|
|
||||||
|
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: selectedDevice,
|
||||||
|
devicevalue: edit_devicevalue
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.props.enqueueSnackbar('Write command sent to device', {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
|
this.handleRowClick(selectedDevice);
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
this.props.enqueueSnackbar('Write command failed', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
this.props.enqueueSnackbar('Write access denied', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (edit_devicevalue) {
|
||||||
|
this.setState({ edit_devicevalue: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand = (dv: DeviceValue) => {
|
||||||
|
this.setState({ edit_devicevalue: dv });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSensorChange = (name: keyof Sensor) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
this.setState({
|
||||||
|
edit_Sensor: {
|
||||||
|
...this.state.edit_Sensor!,
|
||||||
|
[name]: extractEventValue(event)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelEditingSensor = () => {
|
||||||
|
this.setState({ edit_Sensor: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
doneEditingSensor = () => {
|
||||||
|
const { edit_Sensor } = this.state;
|
||||||
|
|
||||||
|
redirectingAuthorizedFetch(WRITE_SENSOR_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
// because input field with type=number doens't like negative values, force it here
|
||||||
|
sensor: {
|
||||||
|
no: edit_Sensor?.no,
|
||||||
|
id: edit_Sensor?.id,
|
||||||
|
temp: edit_Sensor?.temp,
|
||||||
|
offset: Number(edit_Sensor?.offset)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.props.enqueueSnackbar('Sensor updated', {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
|
this.props.loadData();
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
this.props.enqueueSnackbar('Sensor change failed', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
this.props.enqueueSnackbar('Write access denied', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (edit_Sensor) {
|
||||||
|
this.setState({ edit_Sensor: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sendSensor = (sn: Sensor) => {
|
||||||
|
this.setState({ edit_Sensor: sn });
|
||||||
|
};
|
||||||
|
|
||||||
|
noDevices = () => {
|
||||||
|
return this.props.data.devices.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
noSensors = () => {
|
||||||
|
return this.props.data.sensors.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
noDeviceData = () => {
|
||||||
|
return (this.state.deviceData?.data || []).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderDeviceItems() {
|
||||||
|
const { width, data } = this.props;
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
EMS Devices
|
||||||
|
</Typography>
|
||||||
|
<p></p>
|
||||||
|
{!this.noDevices() && (
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{data.devices.sort(compareDevices).map((device) => (
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
key={device.id}
|
||||||
|
onClick={() => this.handleRowClick(device.id)}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<CustomTooltip
|
||||||
|
title={
|
||||||
|
'DeviceID:0x' +
|
||||||
|
(
|
||||||
|
'00' + device.deviceid.toString(16).toUpperCase()
|
||||||
|
).slice(-2) +
|
||||||
|
' ProductID:' +
|
||||||
|
device.productid +
|
||||||
|
' Version:' +
|
||||||
|
device.version
|
||||||
|
}
|
||||||
|
placement="right-end"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<ListIcon />}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{device.type}
|
||||||
|
</Button>
|
||||||
|
</CustomTooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{device.brand + ' ' + device.name}{' '}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
{this.noDevices() && (
|
||||||
|
<Box
|
||||||
|
bgcolor="error.main"
|
||||||
|
color="error.contrastText"
|
||||||
|
p={2}
|
||||||
|
mt={2}
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
No EMS devices found. Check the connections and for possible Tx
|
||||||
|
errors.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSensorItems() {
|
||||||
|
const { data } = this.props;
|
||||||
|
const me = this.props.authenticatedContext.me;
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<p></p>
|
||||||
|
<Typography variant="h6" color="primary" paragraph>
|
||||||
|
Sensors
|
||||||
|
</Typography>
|
||||||
|
{!this.noSensors() && (
|
||||||
|
<Table size="small" padding="default">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<StyledTableCell
|
||||||
|
padding="checkbox"
|
||||||
|
style={{ width: 18 }}
|
||||||
|
></StyledTableCell>
|
||||||
|
<StyledTableCell>Sensor #</StyledTableCell>
|
||||||
|
<StyledTableCell align="left">ID / Name</StyledTableCell>
|
||||||
|
<StyledTableCell align="right">Temperature</StyledTableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data.sensors.map((sensorData) => (
|
||||||
|
<TableRow key={sensorData.no} hover>
|
||||||
|
<TableCell padding="checkbox" style={{ width: 18 }}>
|
||||||
|
{me.admin && (
|
||||||
|
<CustomTooltip title="edit" placement="left-end">
|
||||||
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
size="small"
|
||||||
|
aria-label="Edit"
|
||||||
|
onClick={() => this.sendSensor(sensorData)}
|
||||||
|
>
|
||||||
|
<EditIcon color="primary" fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</CustomTooltip>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{sensorData.no}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="left">{sensorData.id}</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{formatValue(sensorData.temp, DeviceValueUOM.DEGREES, 1)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
{this.noSensors() && (
|
||||||
|
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<i>no connected Dallas temperature sensors were detected</i>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAnalog() {
|
||||||
|
const { data } = this.props;
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
{data.analog > 0 && (
|
||||||
|
<Table size="small" padding="default">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<StyledTableCell>Sensortype</StyledTableCell>
|
||||||
|
<StyledTableCell align="right">Value</StyledTableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
Analog Input
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
{formatValue(data.analog, DeviceValueUOM.MV, 0)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderScanDevicesDialog() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={this.state.confirmScanDevices}
|
||||||
|
onClose={this.onScanDevicesRejected}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
|
>
|
||||||
|
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
Are you sure you want to start a scan on the EMS bus for all new
|
||||||
|
devices?
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.onScanDevicesRejected}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.onScanDevicesConfirmed}
|
||||||
|
disabled={this.state.processing}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Start Scan
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onScanDevices = () => {
|
||||||
|
this.setState({ confirmScanDevices: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onScanDevicesRejected = () => {
|
||||||
|
this.setState({ confirmScanDevices: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onScanDevicesConfirmed = () => {
|
||||||
|
this.setState({ processing: true });
|
||||||
|
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.props.enqueueSnackbar('Device scan is starting...', {
|
||||||
|
variant: 'info'
|
||||||
|
});
|
||||||
|
this.setState({ processing: false, confirmScanDevices: false });
|
||||||
|
} else {
|
||||||
|
throw Error('Invalid status code: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
this.setState({ processing: false, confirmScanDevices: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRowClick = (device: any) => {
|
||||||
|
this.setState({ selectedDevice: device, deviceData: undefined });
|
||||||
|
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ id: device }),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.arrayBuffer();
|
||||||
|
}
|
||||||
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
|
})
|
||||||
|
.then((arrayBuffer) => {
|
||||||
|
const json: any = decoder.decode(arrayBuffer);
|
||||||
|
this.setState({ deviceData: json });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
error.message || 'Problem getting device data',
|
||||||
|
{ variant: 'error' }
|
||||||
|
);
|
||||||
|
this.setState({ deviceData: undefined });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderDeviceData() {
|
||||||
|
const { deviceData } = this.state;
|
||||||
|
const { width } = this.props;
|
||||||
|
const me = this.props.authenticatedContext.me;
|
||||||
|
|
||||||
|
if (this.noDevices()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<p></p>
|
||||||
|
<Box bgcolor="info.main" p={1} mt={1} mb={1}>
|
||||||
|
<Typography variant="body1" color="initial">
|
||||||
|
{deviceData.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{!this.noDeviceData() && (
|
||||||
|
<TableContainer>
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||||
|
>
|
||||||
|
<TableHead></TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{deviceData.data.map((item, i) => (
|
||||||
|
<TableRow hover key={i}>
|
||||||
|
<TableCell padding="checkbox" style={{ width: 18 }}>
|
||||||
|
{item.c && me.admin && (
|
||||||
|
<CustomTooltip
|
||||||
|
title="change value"
|
||||||
|
placement="left-end"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
size="small"
|
||||||
|
aria-label="Edit"
|
||||||
|
onClick={() => this.sendCommand(item)}
|
||||||
|
>
|
||||||
|
<EditIcon color="primary" fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</CustomTooltip>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell padding="none" component="th" scope="row">
|
||||||
|
{item.n}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell padding="none" align="right">
|
||||||
|
{formatValue(item.v, item.u, 0)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
{this.noDeviceData() && (
|
||||||
|
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<i>No data available for this device</i>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { edit_devicevalue, edit_Sensor } = this.state;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<br></br>
|
||||||
|
{this.renderDeviceItems()}
|
||||||
|
{this.renderDeviceData()}
|
||||||
|
{this.renderSensorItems()}
|
||||||
|
{this.renderAnalog()}
|
||||||
|
<br></br>
|
||||||
|
<Box display="flex" flexWrap="wrap">
|
||||||
|
<Box flexGrow={1} padding={1}>
|
||||||
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</FormButton>
|
||||||
|
</Box>
|
||||||
|
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||||
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.onScanDevices}
|
||||||
|
>
|
||||||
|
Scan Devices
|
||||||
|
</FormButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{this.renderScanDevicesDialog()}
|
||||||
|
{edit_devicevalue && (
|
||||||
|
<ValueForm
|
||||||
|
devicevalue={edit_devicevalue}
|
||||||
|
onDoneEditing={this.doneEditingDeviceValue}
|
||||||
|
onCancelEditing={this.cancelEditingDeviceValue}
|
||||||
|
handleValueChange={this.handleDeviceValueChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{edit_Sensor && (
|
||||||
|
<SensorForm
|
||||||
|
sensor={edit_Sensor}
|
||||||
|
onDoneEditing={this.doneEditingSensor}
|
||||||
|
onCancelEditing={this.cancelEditingSensor}
|
||||||
|
handleSensorChange={this.handleSensorChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticatedContext(withWidth()(EMSESPDataForm));
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
|
||||||
import { ENDPOINT_ROOT } from '../api';
|
|
||||||
import EMSESPDevicesForm from './EMSESPDevicesForm';
|
|
||||||
import { EMSESPDevices } from './EMSESPtypes';
|
|
||||||
|
|
||||||
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices";
|
|
||||||
|
|
||||||
type EMSESPDevicesControllerProps = RestControllerProps<EMSESPDevices>;
|
|
||||||
|
|
||||||
class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<SectionContent title="Devices & Sensors">
|
|
||||||
<RestFormLoader
|
|
||||||
{...this.props}
|
|
||||||
render={formProps => <EMSESPDevicesForm {...formProps} />}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restController(EMSESP_DEVICES_ENDPOINT, EMSESPDevicesController);
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
import React, { Component, Fragment } from "react";
|
|
||||||
import { withStyles, Theme, createStyles } from "@material-ui/core/styles";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableContainer,
|
|
||||||
withWidth,
|
|
||||||
WithWidthProps,
|
|
||||||
isWidthDown,
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Box,
|
|
||||||
Dialog,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
|
|
||||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
|
||||||
import ListIcon from "@material-ui/icons/List";
|
|
||||||
|
|
||||||
import {
|
|
||||||
redirectingAuthorizedFetch,
|
|
||||||
withAuthenticatedContext,
|
|
||||||
AuthenticatedContextProps,
|
|
||||||
} from "../authentication";
|
|
||||||
|
|
||||||
import { RestFormProps, FormButton } from "../components";
|
|
||||||
|
|
||||||
import { EMSESPDevices, EMSESPDeviceData, Device } from "./EMSESPtypes";
|
|
||||||
|
|
||||||
import { ENDPOINT_ROOT } from "../api";
|
|
||||||
|
|
||||||
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
|
|
||||||
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
|
|
||||||
|
|
||||||
const StyledTableCell = withStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
head: {
|
|
||||||
backgroundColor: theme.palette.common.black,
|
|
||||||
color: theme.palette.common.white,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)(TableCell);
|
|
||||||
|
|
||||||
function compareDevices(a: Device, b: Device) {
|
|
||||||
if (a.type < b.type) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.type > b.type) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EMSESPDevicesFormState {
|
|
||||||
confirmScanDevices: boolean;
|
|
||||||
processing: boolean;
|
|
||||||
deviceData?: EMSESPDeviceData;
|
|
||||||
}
|
|
||||||
|
|
||||||
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> &
|
|
||||||
AuthenticatedContextProps &
|
|
||||||
WithWidthProps;
|
|
||||||
|
|
||||||
function formatTemp(t: string) {
|
|
||||||
if (t == null) {
|
|
||||||
return "(not available)";
|
|
||||||
}
|
|
||||||
return t + " °C";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUnit(u: string) {
|
|
||||||
if (u == null) {
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
return " " + u;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EMSESPDevicesForm extends Component<
|
|
||||||
EMSESPDevicesFormProps,
|
|
||||||
EMSESPDevicesFormState
|
|
||||||
> {
|
|
||||||
state: EMSESPDevicesFormState = {
|
|
||||||
confirmScanDevices: false,
|
|
||||||
processing: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
noDevices = () => {
|
|
||||||
return this.props.data.devices.length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
noSensors = () => {
|
|
||||||
return this.props.data.sensors.length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
noDeviceData = () => {
|
|
||||||
return (this.state.deviceData?.data || []).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
createDeviceItems() {
|
|
||||||
const { width, data } = this.props;
|
|
||||||
return (
|
|
||||||
<TableContainer>
|
|
||||||
<Typography variant="h6" color="primary">
|
|
||||||
EMS Devices
|
|
||||||
</Typography>
|
|
||||||
<p></p>
|
|
||||||
{!this.noDevices() && (
|
|
||||||
<Table
|
|
||||||
size="small"
|
|
||||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
|
||||||
>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<StyledTableCell>Type</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Brand</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Model</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Device ID</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Product ID</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">Version</StyledTableCell>
|
|
||||||
<StyledTableCell></StyledTableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{data.devices.sort(compareDevices).map((device) => (
|
|
||||||
<TableRow
|
|
||||||
hover
|
|
||||||
key={device.id}
|
|
||||||
onClick={() => this.handleRowClick(device.id)}
|
|
||||||
>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
<Tooltip
|
|
||||||
title="click for details..."
|
|
||||||
arrow
|
|
||||||
placement="right-end"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
startIcon={<ListIcon />}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
{device.type}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">{device.brand}</TableCell>
|
|
||||||
<TableCell align="center">{device.name}</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
0x
|
|
||||||
{("00" + device.deviceid.toString(16).toUpperCase()).slice(
|
|
||||||
-2
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">{device.productid}</TableCell>
|
|
||||||
<TableCell align="center">{device.version}</TableCell>
|
|
||||||
<TableCell></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
{this.noDevices() && (
|
|
||||||
<Box
|
|
||||||
bgcolor="error.main"
|
|
||||||
color="error.contrastText"
|
|
||||||
p={2}
|
|
||||||
mt={2}
|
|
||||||
mb={2}
|
|
||||||
>
|
|
||||||
<Typography variant="body1">
|
|
||||||
No EMS devices found. Check the connections and for possible Tx
|
|
||||||
errors.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
createSensorItems() {
|
|
||||||
const { data } = this.props;
|
|
||||||
return (
|
|
||||||
<TableContainer>
|
|
||||||
<p></p>
|
|
||||||
<Typography variant="h6" color="primary" paragraph>
|
|
||||||
Dallas Sensors
|
|
||||||
</Typography>
|
|
||||||
{!this.noSensors() && (
|
|
||||||
<Table size="small" padding="default">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<StyledTableCell>Sensor #</StyledTableCell>
|
|
||||||
<StyledTableCell align="center">ID</StyledTableCell>
|
|
||||||
<StyledTableCell align="right">Temperature</StyledTableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{data.sensors.map((sensorData) => (
|
|
||||||
<TableRow key={sensorData.no}>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
{sensorData.no}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">{sensorData.id}</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
{formatTemp(sensorData.temp)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
{this.noSensors() && (
|
|
||||||
<Box color="warning.main" p={0} mt={0} mb={0}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
<i>no external temperature sensors were detected</i>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderScanDevicesDialog() {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={this.state.confirmScanDevices}
|
|
||||||
onClose={this.onScanDevicesRejected}
|
|
||||||
>
|
|
||||||
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
Are you sure you want to initiate a scan on the EMS bus for all new
|
|
||||||
devices?
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={this.onScanDevicesRejected}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
variant="contained"
|
|
||||||
onClick={this.onScanDevicesConfirmed}
|
|
||||||
disabled={this.state.processing}
|
|
||||||
color="primary"
|
|
||||||
autoFocus
|
|
||||||
>
|
|
||||||
Start Scan
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onScanDevices = () => {
|
|
||||||
this.setState({ confirmScanDevices: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onScanDevicesRejected = () => {
|
|
||||||
this.setState({ confirmScanDevices: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onScanDevicesConfirmed = () => {
|
|
||||||
this.setState({ processing: true });
|
|
||||||
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
this.props.enqueueSnackbar("Device scan is starting...", {
|
|
||||||
variant: "info",
|
|
||||||
});
|
|
||||||
this.setState({ processing: false, confirmScanDevices: false });
|
|
||||||
} else {
|
|
||||||
throw Error("Invalid status code: " + response.status);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.props.enqueueSnackbar(error.message || "Problem with scan", {
|
|
||||||
variant: "error",
|
|
||||||
});
|
|
||||||
this.setState({ processing: false, confirmScanDevices: false });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRowClick = (id: any) => {
|
|
||||||
this.setState({ deviceData: undefined });
|
|
||||||
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ id: id }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.json();
|
|
||||||
// this.setState({ errorMessage: undefined }, this.props.loadData);
|
|
||||||
}
|
|
||||||
throw Error("Unexpected response code: " + response.status);
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
this.setState({ deviceData: json });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.props.enqueueSnackbar(
|
|
||||||
error.message || "Problem getting device data",
|
|
||||||
{ variant: "error" }
|
|
||||||
);
|
|
||||||
this.setState({ deviceData: undefined });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderDeviceData() {
|
|
||||||
const { deviceData } = this.state;
|
|
||||||
const { width } = this.props;
|
|
||||||
|
|
||||||
if (this.noDevices()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deviceData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<p></p>
|
|
||||||
<Box bgcolor="info.main" p={1} mt={1} mb={1}>
|
|
||||||
<Typography variant="body1" color="initial">
|
|
||||||
{deviceData.name}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
{!this.noDeviceData() && (
|
|
||||||
<TableContainer>
|
|
||||||
<Table
|
|
||||||
size="small"
|
|
||||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
|
||||||
>
|
|
||||||
<TableHead></TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{deviceData.data.map((item, i) => {
|
|
||||||
if (i % 3) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<TableRow key={i}>
|
|
||||||
<TableCell component="th" scope="row">{deviceData.data[i + 2]}</TableCell>
|
|
||||||
<TableCell align="right">{deviceData.data[i]}{formatUnit(deviceData.data[i + 1])}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)}
|
|
||||||
{this.noDeviceData() && (
|
|
||||||
<Box color="warning.main" p={0} mt={0} mb={0}>
|
|
||||||
<Typography variant="body1">
|
|
||||||
<i>No data available for this device</i>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Fragment >
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<br></br>
|
|
||||||
{this.createDeviceItems()}
|
|
||||||
{this.renderDeviceData()}
|
|
||||||
{this.createSensorItems()}
|
|
||||||
<br></br>
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
|
||||||
<Box flexGrow={1} padding={1}>
|
|
||||||
<FormButton
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
onClick={this.props.loadData}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</FormButton>
|
|
||||||
</Box>
|
|
||||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
|
||||||
<FormButton
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={this.onScanDevices}
|
|
||||||
>
|
|
||||||
Scan Devices
|
|
||||||
</FormButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{this.renderScanDevicesDialog()}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withAuthenticatedContext(withWidth()(EMSESPDevicesForm));
|
|
||||||
@@ -1,85 +1,131 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Typography, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@material-ui/core';
|
import {
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
Link,
|
||||||
|
ListItemAvatar
|
||||||
|
} from '@material-ui/core';
|
||||||
import { SectionContent } from '../components';
|
import { SectionContent } from '../components';
|
||||||
|
|
||||||
import CommentIcon from "@material-ui/icons/CommentTwoTone";
|
import CommentIcon from '@material-ui/icons/CommentTwoTone';
|
||||||
import MenuBookIcon from "@material-ui/icons/MenuBookTwoTone";
|
import MenuBookIcon from '@material-ui/icons/MenuBookTwoTone';
|
||||||
import GitHubIcon from "@material-ui/icons/GitHub";
|
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||||
import StarIcon from "@material-ui/icons/Star";
|
import StarIcon from '@material-ui/icons/Star';
|
||||||
import ImportExportIcon from "@material-ui/icons/ImportExport";
|
import DownloadIcon from '@material-ui/icons/GetApp';
|
||||||
import BugReportIcon from "@material-ui/icons/BugReportTwoTone";
|
|
||||||
|
|
||||||
export const WebAPISystemSettings = window.location.origin + "/api?device=system&cmd=settings";
|
import { FormButton } from '../components';
|
||||||
export const WebAPISystemInfo = window.location.origin + "/api?device=system&cmd=info";
|
|
||||||
|
import { API_ENDPOINT_ROOT } from '../api';
|
||||||
|
|
||||||
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
|
|
||||||
class EMSESPHelp extends Component {
|
class EMSESPHelp extends Component {
|
||||||
|
onDownload = (endpoint: string) => {
|
||||||
|
redirectingAuthorizedFetch(API_ENDPOINT_ROOT + 'system/' + endpoint)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error(
|
||||||
|
'Device returned unexpected response code: ' + response.status
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const filename = 'emsesp_system_' + endpoint + '.txt';
|
||||||
|
a.href = URL.createObjectURL(
|
||||||
|
new Blob([JSON.stringify(json, null, 2)], {
|
||||||
|
type: 'text/plain'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
a.setAttribute('download', filename);
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SectionContent title='EMS-ESP Help' titleGutter>
|
<SectionContent title="EMS-ESP Help" titleGutter>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<MenuBookIcon />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText>
|
||||||
|
For help and information on the latest updates visit the{' '}
|
||||||
|
<Link href="https://emsesp.github.io/docs" color="primary">
|
||||||
|
{'online documentation'}
|
||||||
|
</Link>
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<List>
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<CommentIcon />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText>
|
||||||
|
For live community chat join our{' '}
|
||||||
|
<Link href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||||
|
{'Discord'} server
|
||||||
|
</Link>
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<MenuBookIcon />
|
<GitHubIcon />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
For the latest news and updates go to the <Link href="https://emsesp.github.io/docs" color="primary">{'official documentation'} website</Link>
|
To report an issue or request a feature go to{' '}
|
||||||
</ListItemText>
|
<Link
|
||||||
</ListItem>
|
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{'GitHub'}
|
||||||
|
</Link>
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
|
||||||
<ListItem>
|
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||||
<ListItemAvatar>
|
<FormButton
|
||||||
<CommentIcon />
|
startIcon={<DownloadIcon />}
|
||||||
</ListItemAvatar>
|
variant="contained"
|
||||||
<ListItemText>
|
color="primary"
|
||||||
For live community chat join our <Link href="https://discord.gg/3J3GgnzpyT" color="primary">{'Discord'} server</Link>
|
onClick={() => this.onDownload('info')}
|
||||||
</ListItemText>
|
>
|
||||||
</ListItem>
|
download system info
|
||||||
|
</FormButton>
|
||||||
<ListItem>
|
<FormButton
|
||||||
<ListItemAvatar>
|
startIcon={<DownloadIcon />}
|
||||||
<GitHubIcon />
|
variant="contained"
|
||||||
</ListItemAvatar>
|
color="primary"
|
||||||
<ListItemText>
|
onClick={() => this.onDownload('settings')}
|
||||||
To report an issue or feature request go to <Link href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">{'click here'}</Link>
|
>
|
||||||
</ListItemText>
|
download all settings
|
||||||
</ListItem>
|
</FormButton>
|
||||||
|
</Box>
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<ImportExportIcon />
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText>
|
|
||||||
To list your system settings <Link target="_blank" href={WebAPISystemSettings} color="primary">{'click here'}</Link>
|
|
||||||
</ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<BugReportIcon />
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText>
|
|
||||||
To create a report of the current EMS-ESP status <Link target="_blank" href={WebAPISystemInfo} color="primary">{'click here'}</Link>
|
|
||||||
</ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
|
|
||||||
</List>
|
|
||||||
|
|
||||||
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
|
|
||||||
<Typography variant="h6">
|
|
||||||
EMS-ESP is free and open-source.
|
|
||||||
<br></br>Please consider supporting this project by giving it a <StarIcon style={{ color: '#fdff3a' }} /> on our <Link href="https://github.com/emsesp/EMS-ESP32" color="primary">{'GitHub page'}</Link>.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<br></br>
|
|
||||||
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
EMS-ESP is free and open-source.
|
||||||
|
<br></br>Please consider supporting this project by giving it a{' '}
|
||||||
|
<StarIcon style={{ color: '#fdff3a' }} /> on our{' '}
|
||||||
|
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
|
||||||
|
{'GitHub page'}
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<br></br>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EMSESPHelp;
|
export default EMSESPHelp;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -10,26 +10,31 @@ import { AuthenticatedRoute } from '../authentication';
|
|||||||
import EMSESPSettingsController from './EMSESPSettingsController';
|
import EMSESPSettingsController from './EMSESPSettingsController';
|
||||||
|
|
||||||
class EMSESP extends Component<RouteComponentProps> {
|
class EMSESP extends Component<RouteComponentProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Settings">
|
<MenuAppBar sectionTitle="Settings">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
<Tabs
|
||||||
|
value={this.props.match.url}
|
||||||
|
onChange={(e, path) => this.handleTabChange(path)}
|
||||||
|
variant="fullWidth"
|
||||||
|
>
|
||||||
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
|
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/settings`} component={EMSESPSettingsController} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/settings`}
|
||||||
|
component={EMSESPSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to={`/${PROJECT_PATH}/settings`} />
|
<Redirect to={`/${PROJECT_PATH}/settings`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EMSESP;
|
export default EMSESP;
|
||||||
|
|||||||
@@ -1,321 +1,39 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { ValidatorForm, TextValidator, SelectValidator } from 'react-material-ui-form-validator';
|
|
||||||
|
|
||||||
import { Checkbox, Typography, Box, Link } from '@material-ui/core';
|
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
|
||||||
|
|
||||||
import { ENDPOINT_ROOT } from '../api';
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, BlockFormControlLabel, SectionContent } from '../components';
|
import EMSESPSettingsForm from './EMSESPSettingsForm';
|
||||||
|
|
||||||
import { isIP, optional } from '../validators';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import { EMSESPSettings } from './EMSESPtypes';
|
import { EMSESPSettings } from './EMSESPtypes';
|
||||||
|
|
||||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings';
|
||||||
|
|
||||||
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
||||||
|
|
||||||
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
render() {
|
||||||
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<SectionContent title='EMS-ESP Settings' titleGutter>
|
|
||||||
<RestFormLoader
|
|
||||||
{...this.props}
|
|
||||||
render={props => (
|
|
||||||
<EMSESPSettingsControllerForm {...props} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
|
|
||||||
|
|
||||||
type EMSESPSettingsControllerFormProps = RestFormProps<EMSESPSettings>;
|
|
||||||
|
|
||||||
function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps) {
|
|
||||||
const { data, saveData, handleValueChange } = props;
|
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={saveData}>
|
<SectionContent title="" titleGutter>
|
||||||
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
|
<RestFormLoader
|
||||||
<Typography variant="body1">
|
{...this.props}
|
||||||
Change the default settings on this page. For help click <Link target="_blank" href="https://emsesp.github.io/docs/#/Configure-firmware?id=settings" color="primary">{'here'}</Link>.
|
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
|
||||||
</Typography>
|
/>
|
||||||
</Box>
|
</SectionContent>
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
EMS Bus
|
|
||||||
</Typography>
|
|
||||||
<SelectValidator name="tx_mode"
|
|
||||||
label="Tx Mode"
|
|
||||||
value={data.tx_mode}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
onChange={handleValueChange('tx_mode')}
|
|
||||||
margin="normal">
|
|
||||||
<MenuItem value={0}>0 - Off</MenuItem>
|
|
||||||
<MenuItem value={1}>1 - Default</MenuItem>
|
|
||||||
<MenuItem value={2}>2 - EMS+</MenuItem>
|
|
||||||
<MenuItem value={3}>3 - HT3</MenuItem>
|
|
||||||
<MenuItem value={4}>4 - Hardware</MenuItem>
|
|
||||||
</SelectValidator>
|
|
||||||
<SelectValidator name="ems_bus_id"
|
|
||||||
label="Bus ID"
|
|
||||||
value={data.ems_bus_id}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
onChange={handleValueChange('ems_bus_id')}
|
|
||||||
margin="normal">
|
|
||||||
<MenuItem value={0x0B}>Service Key (0x0B)</MenuItem>
|
|
||||||
<MenuItem value={0x0D}>Modem (0x0D)</MenuItem>
|
|
||||||
<MenuItem value={0x0A}>Terminal (0x0A)</MenuItem>
|
|
||||||
<MenuItem value={0x0F}>Time Module (0x0F)</MenuItem>
|
|
||||||
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
|
|
||||||
</SelectValidator>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
|
|
||||||
errorMessages={['Rx GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
|
|
||||||
name="rx_gpio"
|
|
||||||
label="Rx GPIO pin"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.rx_gpio}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('rx_gpio')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
|
|
||||||
errorMessages={['Tx GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
|
|
||||||
name="tx_gpio"
|
|
||||||
label="Tx GPIO pin"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.tx_gpio}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('tx_gpio')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:120']}
|
|
||||||
errorMessages={['Tx delay is required', "Must be a number", "Must be 0 or higher", "Max value is 120"]}
|
|
||||||
name="tx_delay"
|
|
||||||
label="Tx delayed start (seconds)"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.tx_delay}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('tx_delay')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
External Button
|
|
||||||
</Typography>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
|
|
||||||
errorMessages={['Button GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
|
|
||||||
name="pbutton_gpio"
|
|
||||||
label="Button GPIO pin"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.pbutton_gpio}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('pbutton_gpio')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
Dallas Sensor
|
|
||||||
</Typography>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
|
|
||||||
errorMessages={['Dallas GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
|
|
||||||
name="dallas_gpio"
|
|
||||||
label="Dallas GPIO pin (0=none)"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.dallas_gpio}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('dallas_gpio')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.dallas_parasite}
|
|
||||||
onChange={handleValueChange('dallas_parasite')}
|
|
||||||
value="dallas_parasite"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Dallas Parasite Mode"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
LED
|
|
||||||
</Typography>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:40']}
|
|
||||||
errorMessages={['LED GPIO is required', "Must be a number", "Must be 0 or higher", "Max value is 40"]}
|
|
||||||
name="led_gpio"
|
|
||||||
label="LED GPIO pin (0=none)"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.led_gpio}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('led_gpio')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.hide_led}
|
|
||||||
onChange={handleValueChange('hide_led')}
|
|
||||||
value="hide_led"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Invert/Hide LED"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
Shower
|
|
||||||
</Typography>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.shower_timer}
|
|
||||||
onChange={handleValueChange('shower_timer')}
|
|
||||||
value="shower_timer"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Shower Timer"
|
|
||||||
/>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.shower_alert}
|
|
||||||
onChange={handleValueChange('shower_alert')}
|
|
||||||
value="shower_alert"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Shower Alert"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
API
|
|
||||||
</Typography>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.api_enabled}
|
|
||||||
onChange={handleValueChange('api_enabled')}
|
|
||||||
value="api_enabled"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Allow WEB API to write commands"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
Syslog
|
|
||||||
</Typography>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.syslog_enabled}
|
|
||||||
onChange={handleValueChange('syslog_enabled')}
|
|
||||||
value="syslog_enabled"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Enable Syslog"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['isOptionalIP']}
|
|
||||||
errorMessages={["Not a valid IP address"]}
|
|
||||||
name="syslog_host"
|
|
||||||
label="Syslog IP"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.syslog_host}
|
|
||||||
onChange={handleValueChange('syslog_host')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
|
||||||
errorMessages={['Port is required', "Must be a number", "Must be greater than 0 ", "Max value is 65535"]}
|
|
||||||
name="syslog_port"
|
|
||||||
label="Syslog Port (default 514)"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.syslog_port}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('syslog_port')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<SelectValidator name="syslog_level"
|
|
||||||
label="Syslog Log Level"
|
|
||||||
value={data.syslog_level}
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
onChange={handleValueChange('syslog_level')}
|
|
||||||
margin="normal">
|
|
||||||
<MenuItem value={-1}>OFF</MenuItem>
|
|
||||||
<MenuItem value={3}>ERR</MenuItem>
|
|
||||||
<MenuItem value={5}>NOTICE</MenuItem>
|
|
||||||
<MenuItem value={6}>INFO</MenuItem>
|
|
||||||
<MenuItem value={7}>DEBUG</MenuItem>
|
|
||||||
<MenuItem value={8}>ALL</MenuItem>
|
|
||||||
</SelectValidator>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
|
||||||
errorMessages={['Syslog Mark is required', "Must be a number", "Must be 0 or higher", "Max value is 10"]}
|
|
||||||
name="syslog_mark_interval"
|
|
||||||
label="Syslog Mark Interval (seconds, 0=off)"
|
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
value={data.syslog_mark_interval}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('syslog_mark_interval')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.trace_raw}
|
|
||||||
onChange={handleValueChange('trace_raw')}
|
|
||||||
value="trace_raw"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Trace EMS telegrams in raw format"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<Typography variant="h6" color="primary" >
|
|
||||||
Analog Input
|
|
||||||
</Typography>
|
|
||||||
<BlockFormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={data.analog_enabled}
|
|
||||||
onChange={handleValueChange('analog_enabled')}
|
|
||||||
value="analog_enabled"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Enable ADC"
|
|
||||||
/>
|
|
||||||
<br></br>
|
|
||||||
<FormActions>
|
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
|
||||||
Save
|
|
||||||
</FormButton>
|
|
||||||
</FormActions>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default restController(
|
||||||
|
EMSESP_SETTINGS_ENDPOINT,
|
||||||
|
EMSESPSettingsController
|
||||||
|
);
|
||||||
|
|||||||
652
interface/src/project/EMSESPSettingsForm.tsx
Normal file
652
interface/src/project/EMSESPSettingsForm.tsx
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ValidatorForm,
|
||||||
|
TextValidator,
|
||||||
|
SelectValidator
|
||||||
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
Link,
|
||||||
|
withWidth,
|
||||||
|
WithWidthProps,
|
||||||
|
Grid
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
|
import {
|
||||||
|
redirectingAuthorizedFetch,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
BlockFormControlLabel
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
|
import { isIPv4, optional, isHostname, or } from '../validators';
|
||||||
|
|
||||||
|
import { EMSESPSettings } from './EMSESPtypes';
|
||||||
|
|
||||||
|
import { boardProfileSelectItems } from './EMSESPBoardProfiles';
|
||||||
|
|
||||||
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
|
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile';
|
||||||
|
|
||||||
|
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
|
||||||
|
AuthenticatedContextProps &
|
||||||
|
WithWidthProps;
|
||||||
|
|
||||||
|
interface EMSESPSettingsFormState {
|
||||||
|
processing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EMSESPSettingsForm extends Component<EMSESPSettingsFormProps> {
|
||||||
|
state: EMSESPSettingsFormState = {
|
||||||
|
processing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
ValidatorForm.addValidationRule(
|
||||||
|
'isOptionalIPorHost',
|
||||||
|
optional(or(isIPv4, isHostname))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const { data, setData } = this.props;
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
board_profile: event.target.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event.target.value === 'CUSTOM') return;
|
||||||
|
|
||||||
|
this.setState({ processing: true });
|
||||||
|
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ code: event.target.value }),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
this.props.enqueueSnackbar('Profile loaded', { variant: 'success' });
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
led_gpio: json.led_gpio,
|
||||||
|
dallas_gpio: json.dallas_gpio,
|
||||||
|
rx_gpio: json.rx_gpio,
|
||||||
|
tx_gpio: json.tx_gpio,
|
||||||
|
pbutton_gpio: json.pbutton_gpio,
|
||||||
|
board_profile: event.target.value
|
||||||
|
});
|
||||||
|
this.setState({ processing: false });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
error.message || 'Problem fetching board profile',
|
||||||
|
{ variant: 'warning' }
|
||||||
|
);
|
||||||
|
this.setState({ processing: false });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data, saveData, handleValueChange } = this.props;
|
||||||
|
return (
|
||||||
|
<ValidatorForm onSubmit={saveData}>
|
||||||
|
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<i>
|
||||||
|
Refer to the
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{' documentation'}
|
||||||
|
</Link>
|
||||||
|
for information on each setting
|
||||||
|
</i>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
EMS Bus
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justify="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
<Grid item xs={5}>
|
||||||
|
<SelectValidator
|
||||||
|
name="tx_mode"
|
||||||
|
label="Tx Mode"
|
||||||
|
value={data.tx_mode}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('tx_mode')}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>Off</MenuItem>
|
||||||
|
<MenuItem value={1}>EMS</MenuItem>
|
||||||
|
<MenuItem value={2}>EMS+</MenuItem>
|
||||||
|
<MenuItem value={3}>HT3</MenuItem>
|
||||||
|
<MenuItem value={4}>Hardware</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<SelectValidator
|
||||||
|
name="ems_bus_id"
|
||||||
|
label="Bus ID"
|
||||||
|
value={data.ems_bus_id}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('ems_bus_id')}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||||
|
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
|
||||||
|
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
|
||||||
|
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
|
||||||
|
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:120'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Tx delay is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or higher',
|
||||||
|
'Max value is 120'
|
||||||
|
]}
|
||||||
|
name="tx_delay"
|
||||||
|
label="Tx start delay (seconds)"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.tx_delay}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('tx_delay')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
Board Profile
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<i>
|
||||||
|
Select a pre-configured board layout to automatically set the GPIO
|
||||||
|
pins. Select "Custom..." to view or manually edit the values.
|
||||||
|
</i>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<SelectValidator
|
||||||
|
name="board_profile"
|
||||||
|
label="Board Profile"
|
||||||
|
value={data.board_profile}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={this.changeBoardProfile}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
{boardProfileSelectItems()}
|
||||||
|
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||||
|
Custom...
|
||||||
|
</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
|
||||||
|
{data.board_profile === 'CUSTOM' && (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justify="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:40',
|
||||||
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'GPIO is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or higher',
|
||||||
|
'Max value is 40',
|
||||||
|
'Not a valid GPIO'
|
||||||
|
]}
|
||||||
|
name="rx_gpio"
|
||||||
|
label="Rx GPIO"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.rx_gpio}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('rx_gpio')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:40',
|
||||||
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'GPIO is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or higher',
|
||||||
|
'Max value is 40',
|
||||||
|
'Not a valid GPIO'
|
||||||
|
]}
|
||||||
|
name="tx_gpio"
|
||||||
|
label="Tx GPIO"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.tx_gpio}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('tx_gpio')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:40',
|
||||||
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'GPIO is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or higher',
|
||||||
|
'Max value is 40',
|
||||||
|
'Not a valid GPIO'
|
||||||
|
]}
|
||||||
|
name="pbutton_gpio"
|
||||||
|
label="Button GPIO"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.pbutton_gpio}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('pbutton_gpio')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:40',
|
||||||
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'GPIO is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or higher',
|
||||||
|
'Max value is 40',
|
||||||
|
'Not a valid GPIO'
|
||||||
|
]}
|
||||||
|
name="dallas_gpio"
|
||||||
|
label="Dallas GPIO (0=none)"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.dallas_gpio}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('dallas_gpio')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:40',
|
||||||
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'GPIO is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or higher',
|
||||||
|
'Max value is 40',
|
||||||
|
'Not a valid GPIO'
|
||||||
|
]}
|
||||||
|
name="led_gpio"
|
||||||
|
label="LED GPIO (0=none)"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.led_gpio}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('led_gpio')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
General Options
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{data.led_gpio !== 0 && (
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.hide_led}
|
||||||
|
onChange={handleValueChange('hide_led')}
|
||||||
|
value="hide_led"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Hide LED"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.dallas_gpio !== 0 && (
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.dallas_parasite}
|
||||||
|
onChange={handleValueChange('dallas_parasite')}
|
||||||
|
value="dallas_parasite"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Use Dallas Sensor parasite power"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.analog_enabled}
|
||||||
|
onChange={handleValueChange('analog_enabled')}
|
||||||
|
value="analog_enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable ADC"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.low_clock}
|
||||||
|
onChange={handleValueChange('low_clock')}
|
||||||
|
value="low_clock"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Run at a lower CPU clock speed"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.notoken_api}
|
||||||
|
onChange={handleValueChange('notoken_api')}
|
||||||
|
value="notoken_api"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Bypass Access Token authorization on API calls"
|
||||||
|
/>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
direction="row"
|
||||||
|
justify="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.shower_timer}
|
||||||
|
onChange={handleValueChange('shower_timer')}
|
||||||
|
value="shower_timer"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable Shower Timer"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.shower_alert}
|
||||||
|
onChange={handleValueChange('shower_alert')}
|
||||||
|
value="shower_alert"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable Shower Alert"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
Formatting Options
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justify="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<SelectValidator
|
||||||
|
name="bool_format"
|
||||||
|
label="Boolean Format"
|
||||||
|
value={data.bool_format}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('bool_format')}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>"on"/"off"</MenuItem>
|
||||||
|
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
|
||||||
|
<MenuItem value={3}>true/false</MenuItem>
|
||||||
|
<MenuItem value={4}>1/0</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<SelectValidator
|
||||||
|
name="enum_format"
|
||||||
|
label="Enum Format"
|
||||||
|
value={data.enum_format}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('enum_format')}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>Text</MenuItem>
|
||||||
|
<MenuItem value={2}>Number</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<SelectValidator
|
||||||
|
name="dallas_format"
|
||||||
|
label="Dallas Sensor Format"
|
||||||
|
value={data.dallas_format}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('dallas_format')}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value={1}>ID</MenuItem>
|
||||||
|
<MenuItem value={2}>Number</MenuItem>
|
||||||
|
<MenuItem value={3}>Name</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
Syslog
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.syslog_enabled}
|
||||||
|
onChange={handleValueChange('syslog_enabled')}
|
||||||
|
value="syslog_enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable Syslog"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{data.syslog_enabled && (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={1}
|
||||||
|
direction="row"
|
||||||
|
justify="flex-start"
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
<Grid item xs={5}>
|
||||||
|
<TextValidator
|
||||||
|
validators={['isOptionalIPorHost']}
|
||||||
|
errorMessages={['Not a valid IPv4 address or hostname']}
|
||||||
|
name="syslog_host"
|
||||||
|
label="Host"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.syslog_host}
|
||||||
|
onChange={handleValueChange('syslog_host')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Port is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be greater than 0 ',
|
||||||
|
'Max value is 65535'
|
||||||
|
]}
|
||||||
|
name="syslog_port"
|
||||||
|
label="Port"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.syslog_port}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('syslog_port')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={5}>
|
||||||
|
<SelectValidator
|
||||||
|
name="syslog_level"
|
||||||
|
label="Log Level"
|
||||||
|
value={data.syslog_level}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('syslog_level')}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem value={-1}>OFF</MenuItem>
|
||||||
|
<MenuItem value={3}>ERR</MenuItem>
|
||||||
|
<MenuItem value={5}>NOTICE</MenuItem>
|
||||||
|
<MenuItem value={6}>INFO</MenuItem>
|
||||||
|
<MenuItem value={7}>DEBUG</MenuItem>
|
||||||
|
<MenuItem value={8}>ALL</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<TextValidator
|
||||||
|
validators={[
|
||||||
|
'required',
|
||||||
|
'isNumber',
|
||||||
|
'minNumber:0',
|
||||||
|
'maxNumber:65535'
|
||||||
|
]}
|
||||||
|
errorMessages={[
|
||||||
|
'Syslog Mark is required',
|
||||||
|
'Must be a number',
|
||||||
|
'Must be 0 or higher',
|
||||||
|
'Max value is 10'
|
||||||
|
]}
|
||||||
|
name="syslog_mark_interval"
|
||||||
|
label="Mark Interval (seconds, 0=off)"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.syslog_mark_interval}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('syslog_mark_interval')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.trace_raw}
|
||||||
|
onChange={handleValueChange('trace_raw')}
|
||||||
|
value="trace_raw"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Output EMS telegrams as hexadecimal bytes"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
<FormActions>
|
||||||
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</ValidatorForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticatedContext(withWidth()(EMSESPSettingsForm));
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Theme } from '@material-ui/core';
|
import { Theme } from '@material-ui/core';
|
||||||
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes';
|
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes';
|
||||||
|
|
||||||
export const isConnected = ({ status }: EMSESPStatus) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
export const isConnected = ({ status }: EMSESPStatus) =>
|
||||||
|
status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||||
|
|
||||||
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
@@ -15,26 +15,25 @@ export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
|||||||
default:
|
default:
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const busStatus = ({ status }: EMSESPStatus) => {
|
export const busStatus = ({ status }: EMSESPStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||||
return "Connected";
|
return 'Connected';
|
||||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||||
return "Tx Errors";
|
return 'Tx Errors';
|
||||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||||
return "Disconnected";
|
return 'Disconnected';
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const qualityHighlight = ( value: number, theme: Theme) => {
|
export const qualityHighlight = (value: number, theme: Theme) => {
|
||||||
if (value >= 95) {
|
if (value >= 95) {
|
||||||
return theme.palette.success.main;
|
return theme.palette.success.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
return theme.palette.error.main;
|
return theme.palette.error.main;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user