mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50459a23fe | ||
|
|
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 | ||
|
|
6fb8a4bbe9 | ||
|
|
5c605e15dd | ||
|
|
9983269662 | ||
|
|
d891c7a325 | ||
|
|
9771ea8f2d | ||
|
|
5cf41bdce0 | ||
|
|
f28fafed8d | ||
|
|
81e2c31dd3 |
@@ -2,7 +2,7 @@ Language: Cpp
|
||||
BasedOnStyle: LLVM
|
||||
UseTab: Never
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 220
|
||||
ColumnLimit: 160
|
||||
TabWidth: 4
|
||||
#BreakBeforeBraces: Custom
|
||||
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"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "dev"
|
||||
@@ -13,46 +14,41 @@ jobs:
|
||||
|
||||
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
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
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
|
||||
run: make
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Install pio
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
platformio update
|
||||
|
||||
- name: Build web
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
cd interface
|
||||
npm install
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
run: |
|
||||
platformio run -e ci
|
||||
|
||||
- name: Release
|
||||
- name: Create a GH Release
|
||||
id: "automatic_releases"
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
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"
|
||||
prerelease: true
|
||||
files: |
|
||||
|
||||
26
.github/workflows/tagged_release.yml
vendored
26
.github/workflows/tagged_release.yml
vendored
@@ -12,34 +12,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get build variables
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
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"
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
- name: Compile locally
|
||||
run: make
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Install pio
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
platformio update
|
||||
|
||||
- name: Build web
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
cd interface
|
||||
npm install
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,6 +25,6 @@ emsesp
|
||||
/data/www
|
||||
/lib/framework/WWWData.h
|
||||
/interface/build
|
||||
/interface/node_modules
|
||||
node_modules
|
||||
/interface/.eslintcache
|
||||
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -5,7 +5,53 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.0.1] March 30 2021
|
||||
# [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
|
||||
|
||||
@@ -63,9 +109,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Shower Alert (disabled for now)
|
||||
|
||||
## [3.0.0] March 18 2021
|
||||
# [3.0.0] March 18 2021
|
||||
|
||||
### Added
|
||||
## Added
|
||||
|
||||
- Power settings, disabling BLE and turning off Wifi sleep
|
||||
- Rx and Tx counts to Heartbeat MQTT payload
|
||||
@@ -82,7 +128,7 @@ 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
|
||||
- Syslog port selectable (#744)
|
||||
|
||||
### Fixed
|
||||
## Fixed
|
||||
|
||||
- telegrams matched to masterthermostat 0x18
|
||||
- multiple roomcontrollers
|
||||
@@ -93,7 +139,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- wrong position of values #723, #732
|
||||
- OTA Upload via Web on OSX
|
||||
|
||||
### Changed
|
||||
## Changed
|
||||
|
||||
- changed how telegram parameters are rendered for mqtt, console and web (#632)
|
||||
- split `show values` in smaller packages (edited)
|
||||
|
||||
111
README.md
111
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.
|
||||
|
||||
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/commits/main)
|
||||
[](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)
|
||||
[](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)
|
||||
|
||||
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://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%>
|
||||
|
||||
---
|
||||
|
||||
## **Features**
|
||||
# **Features**
|
||||
|
||||
- Compatible with both ESP8266 and ESP32
|
||||
- A multi-user secure web interface to change settings and monitor the data
|
||||
- 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/)
|
||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||
- 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**
|
||||
|
||||
## Web Interface
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| <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_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"> |
|
||||
|
||||
### Telnet Console:
|
||||
<img src="media/console.PNG" width=80% height=80%>
|
||||
## Telnet Console
|
||||
|
||||
### In Home Assistant:
|
||||
<img src="media/ha_lovelace.PNG" width=80% height=80%>
|
||||
<img src="media/console.png" width=80% height=80%>
|
||||
|
||||
## **Installing**
|
||||
## 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.
|
||||
|
||||
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)
|
||||
* [Building the firmware from source code and flashing manually](https://emsesp.github.io/docs/#/Building-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)
|
||||
|
||||
## **Support Information**
|
||||
# **Support Information**
|
||||
|
||||
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
|
||||
* [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)
|
||||
- [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)
|
||||
|
||||
### 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
|
||||
* [Search in Issues](https://github.com/emsesp/EMS-ESP32/issues): You might find an answer to your question by searching current or closed issues
|
||||
- [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
|
||||
|
||||
### Developer's Community
|
||||
## Developer's Community
|
||||
|
||||
* [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
|
||||
* [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
|
||||
- [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
|
||||
- [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)
|
||||
- testing new released features and report issues on your EMS equipment
|
||||
- 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...
|
||||
- **@MichaelDvP** for all his amazing contributions and patience. Specifically for the improved uart library, thermostat and mixer logic.
|
||||
- **@BBQKees** for his endless testing and building the awesome circuit boards
|
||||
- **@rjwats** for his [esp8266-react](https://github.com/rjwats/esp8266-react) framework that provides the new Web UI
|
||||
- **@nomis** for his core [console](https://github.com/nomis/mcu-uuid-console), telnet and syslog core libraries
|
||||
- plus everyone else providing suggestions, PRs and the odd donation that keeps this project open source. Thanks!
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for JSON
|
||||
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||
|
||||
## **License**
|
||||
# **License**
|
||||
|
||||
This program is licensed under GPL-3.0
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# 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
|
||||
# with -DENABLE_CORS
|
||||
# Change the IP address to that of your ESP device to enable local development of the UI
|
||||
|
||||
# my Wifi
|
||||
#REACT_APP_HTTP_ROOT=http://10.10.10.101
|
||||
#REACT_APP_WEB_SOCKET_ROOT=ws://10.10.10.101
|
||||
# REACT_APP_HTTP_ROOT=http://localhost:3000
|
||||
# REACT_APP_WEB_SOCKET_ROOT=ws://localhost:3000
|
||||
|
||||
# my Ethernet
|
||||
REACT_APP_HTTP_ROOT=http://192.168.1.134
|
||||
REACT_APP_WEB_SOCKET_ROOT=ws://http://192.168.1.134
|
||||
|
||||
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 ProgmemGenerator = require('./progmem-generator.js');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = function override(config, env) {
|
||||
if (env === "production") {
|
||||
// rename the ouput file, we need it's path to be short, for SPIFFS
|
||||
const hosted = process.env.REACT_APP_HOSTED;
|
||||
|
||||
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.chunkFilename = 'js/[id].[chunkhash:4].js';
|
||||
|
||||
// take out the manifest and service worker plugins
|
||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin));
|
||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW));
|
||||
config.plugins = config.plugins.filter(
|
||||
(plugin) => !(plugin instanceof ManifestPlugin)
|
||||
);
|
||||
config.plugins = config.plugins.filter(
|
||||
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW)
|
||||
);
|
||||
|
||||
// shorten css filenames
|
||||
const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin);
|
||||
miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css";
|
||||
miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css";
|
||||
const miniCssExtractPlugin = config.plugins.find(
|
||||
(plugin) => plugin instanceof MiniCssExtractPlugin
|
||||
);
|
||||
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
|
||||
miniCssExtractPlugin.options.chunkFilename =
|
||||
'css/[id].[contenthash:4].c.css';
|
||||
|
||||
// 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
|
||||
config.plugins.push(new CompressionPlugin({
|
||||
filename: "[path].gz[query]",
|
||||
algorithm: "gzip",
|
||||
config.plugins.push(
|
||||
new CompressionPlugin({
|
||||
filename: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: /\.(js)$/,
|
||||
deleteOriginalAssets: true
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
11439
interface/package-lock.json
generated
11439
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",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@msgpack/msgpack": "^2.7.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^12.20.4",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.1",
|
||||
"@types/node": "^15.0.1",
|
||||
"@types/react": "^17.0.4",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@types/react-material-ui-form-validator": "^2.1.0",
|
||||
"@types/react-router": "^5.1.12",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"compression-webpack-plugin": "^4.0.0",
|
||||
"@types/react-router": "^5.1.13",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"compression-webpack-plugin": "^5.0.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"express": "^4.17.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.29",
|
||||
"notistack": "^1.0.5",
|
||||
"parse-ms": "^2.1.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-dropzone": "^11.3.1",
|
||||
"mime-types": "^2.1.30",
|
||||
"notistack": "^1.0.6",
|
||||
"parse-ms": "^3.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
"react-form-validator-core": "^1.1.1",
|
||||
"react-material-ui-form-validator": "^2.1.4",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"sockette": "^2.0.6",
|
||||
"typescript": "4.0.5",
|
||||
"typescript": "4.2.4",
|
||||
"zlib": "^1.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"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": {
|
||||
"extends": "react-app"
|
||||
@@ -51,6 +59,13 @@
|
||||
]
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
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 mime = require('mime-types');
|
||||
|
||||
const ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
|
||||
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||
|
||||
function getFilesSync(dir, files = []) {
|
||||
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
|
||||
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||
const entryPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
getFilesSync(entryPath, files);
|
||||
} else {
|
||||
files.push(entryPath);
|
||||
}
|
||||
})
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -25,13 +31,17 @@ function cleanAndOpen(path) {
|
||||
if (existsSync(path)) {
|
||||
unlinkSync(path);
|
||||
}
|
||||
return createWriteStream(path, { flags: "w+" });
|
||||
return createWriteStream(path, { flags: 'w+' });
|
||||
}
|
||||
|
||||
class ProgmemGenerator {
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -41,30 +51,34 @@ class ProgmemGenerator {
|
||||
(compilation, callback) => {
|
||||
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
||||
const fileInfo = [];
|
||||
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
||||
const writeStream = cleanAndOpen(
|
||||
resolve(compilation.options.context, outputPath)
|
||||
);
|
||||
try {
|
||||
const writeIncludes = () => {
|
||||
writeStream.write(includes);
|
||||
}
|
||||
};
|
||||
|
||||
const writeFile = (relativeFilePath, buffer) => {
|
||||
const variable = "ESP_REACT_DATA_" + fileInfo.length;
|
||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||
const mimeType = mime.lookup(relativeFilePath);
|
||||
var size = 0;
|
||||
writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
|
||||
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
||||
const zipBuffer = zlib.gzipSync(buffer);
|
||||
zipBuffer.forEach((b) => {
|
||||
if (!(size % bytesPerLine)) {
|
||||
writeStream.write("\n");
|
||||
writeStream.write('\n');
|
||||
writeStream.write(indent);
|
||||
}
|
||||
writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
|
||||
writeStream.write(
|
||||
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ','
|
||||
);
|
||||
size++;
|
||||
});
|
||||
if (size % bytesPerLine) {
|
||||
writeStream.write("\n");
|
||||
writeStream.write('\n');
|
||||
}
|
||||
writeStream.write("};\n\n");
|
||||
writeStream.write('};\n\n');
|
||||
fileInfo.push({
|
||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||
mimeType,
|
||||
@@ -84,25 +98,37 @@ class ProgmemGenerator {
|
||||
// process assets
|
||||
const { assets } = compilation;
|
||||
Object.keys(assets).forEach((relativeFilePath) => {
|
||||
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
||||
writeFile(
|
||||
relativeFilePath,
|
||||
coherseToBuffer(assets[relativeFilePath].source())
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const generateWWWClass = () => {
|
||||
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
||||
|
||||
class WWWData {
|
||||
${indent}public:
|
||||
${indent.repeat(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
|
||||
)}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)}}
|
||||
};
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const writeWWWClass = () => {
|
||||
writeStream.write(generateWWWClass());
|
||||
}
|
||||
};
|
||||
|
||||
writeIncludes();
|
||||
writeFiles();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name":"EMS-ESP",
|
||||
"icons":[
|
||||
"name": "EMS-ESP",
|
||||
"icons": [
|
||||
{
|
||||
"src":"/app/icon.png",
|
||||
"sizes":"48x48 72x72 96x96 128x128 256x256"
|
||||
"src": "/app/icon.png",
|
||||
"sizes": "48x48 72x72 96x96 128x128 256x256"
|
||||
}
|
||||
],
|
||||
"start_url":"/",
|
||||
"display":"fullscreen",
|
||||
"orientation":"any"
|
||||
"start_url": "/",
|
||||
"display": "fullscreen",
|
||||
"orientation": "any"
|
||||
}
|
||||
|
||||
@@ -3,20 +3,26 @@
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Roboto Light'), local('Roboto-Light'), 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;
|
||||
src: local('Roboto Light'), local('Roboto-Light'),
|
||||
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-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Roboto'), local('Roboto-Regular'), 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;
|
||||
src: local('Roboto'), local('Roboto-Regular'),
|
||||
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-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Roboto Medium'), local('Roboto-Medium'), 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;
|
||||
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||
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="/" />;
|
||||
|
||||
class App extends Component {
|
||||
|
||||
notistackRef: RefObject<any> = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
@@ -23,21 +22,29 @@ class App extends Component {
|
||||
|
||||
onClickDismiss = (key: string | number | undefined) => () => {
|
||||
this.notistackRef.current.closeSnackbar(key);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CustomMuiTheme>
|
||||
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
<SnackbarProvider
|
||||
autoHideDuration={3000}
|
||||
maxSnack={3}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
ref={this.notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}>
|
||||
)}
|
||||
>
|
||||
<FeaturesWrapper>
|
||||
<Switch>
|
||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
||||
<Route
|
||||
exact
|
||||
path="/unauthorized"
|
||||
component={unauthorizedRedirect}
|
||||
/>
|
||||
<Route component={AppRouting} />
|
||||
</Switch>
|
||||
</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 { 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> {
|
||||
|
||||
componentDidMount() {
|
||||
Authentication.clearLoginRedirect();
|
||||
}
|
||||
@@ -35,9 +35,17 @@ class AppRouting extends Component<WithFeaturesProps> {
|
||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||
)}
|
||||
{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} />
|
||||
{features.ntp && (
|
||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||
@@ -52,7 +60,7 @@ class AppRouting extends Component<WithFeaturesProps> {
|
||||
<Redirect to={getDefaultRoute(features)} />
|
||||
</Switch>
|
||||
</AuthenticationWrapper>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
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';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
type: "dark",
|
||||
type: 'dark',
|
||||
primary: {
|
||||
main: '#33bfff',
|
||||
main: '#33bfff'
|
||||
},
|
||||
secondary: {
|
||||
main: '#3d5afe',
|
||||
main: '#3d5afe'
|
||||
},
|
||||
info: {
|
||||
main: blueGrey[500]
|
||||
@@ -29,7 +33,6 @@ const theme = createMuiTheme({
|
||||
});
|
||||
|
||||
export default class CustomMuiTheme extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StylesProvider>
|
||||
@@ -40,5 +43,4 @@ export default class CustomMuiTheme extends Component {
|
||||
</StylesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,53 +2,63 @@ import React, { Component } from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
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 ForwardIcon from '@material-ui/icons/Forward';
|
||||
|
||||
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
||||
import {PasswordValidator} from './components';
|
||||
import {
|
||||
withAuthenticationContext,
|
||||
AuthenticationContextProps
|
||||
} from './authentication/AuthenticationContext';
|
||||
import { PasswordValidator } from './components';
|
||||
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
signInPage: {
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
margin: "auto",
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
margin: 'auto',
|
||||
padding: theme.spacing(2),
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
maxWidth: theme.breakpoints.values.sm
|
||||
},
|
||||
signInPanel: {
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
padding: theme.spacing(2),
|
||||
paddingTop: "200px",
|
||||
paddingTop: '200px',
|
||||
backgroundImage: 'url("/app/icon.png")',
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
||||
backgroundSize: "auto 150px",
|
||||
width: "100%"
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50% ' + theme.spacing(2) + 'px',
|
||||
backgroundSize: 'auto 150px',
|
||||
width: '100%'
|
||||
},
|
||||
extendedIcon: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
marginRight: theme.spacing(0.5)
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
marginTop: theme.spacing(2)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
||||
type SignInProps = WithSnackbarProps &
|
||||
WithStyles<typeof styles> &
|
||||
AuthenticationContextProps;
|
||||
|
||||
interface SignInState {
|
||||
username: string,
|
||||
password: string,
|
||||
processing: boolean
|
||||
username: string;
|
||||
password: string;
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
class SignIn extends Component<SignInProps, SignInState> {
|
||||
|
||||
constructor(props: SignInProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -60,10 +70,10 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
|
||||
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const { name, value } = event.currentTarget;
|
||||
this.setState(prevState => ({
|
||||
this.setState((prevState) => ({
|
||||
...prevState,
|
||||
[name]: value,
|
||||
}))
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
@@ -77,20 +87,21 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else if (response.status === 401) {
|
||||
throw Error("Invalid credentials.");
|
||||
throw Error('Invalid credentials.');
|
||||
} else {
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
}
|
||||
}).then(json => {
|
||||
})
|
||||
.then((json) => {
|
||||
authenticationContext.signIn(json.access_token);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(error.message, {
|
||||
variant: 'warning',
|
||||
variant: 'warning'
|
||||
});
|
||||
this.setState({ processing: false });
|
||||
});
|
||||
@@ -116,8 +127,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
autoCapitalize: "none",
|
||||
autoCorrect: "off",
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off'
|
||||
}}
|
||||
/>
|
||||
<PasswordValidator
|
||||
@@ -132,7 +143,13 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
onChange={this.updateInputElement}
|
||||
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} />
|
||||
Sign In
|
||||
</Fab>
|
||||
@@ -141,7 +158,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
</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) => {
|
||||
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 {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
|
||||
import APSettingsForm from './APSettingsForm';
|
||||
import { APSettings } from './types';
|
||||
@@ -9,7 +14,6 @@ import { APSettings } from './types';
|
||||
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
||||
|
||||
class APSettingsController extends Component<APSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class APSettingsController extends Component<APSettingsControllerProps> {
|
||||
<SectionContent title="Access Point Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <APSettingsForm {...formProps} />}
|
||||
render={(formProps) => <APSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
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 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 { APSettings, APProvisionMode } from './types';
|
||||
@@ -13,7 +22,6 @@ import { isIP } from '../validators';
|
||||
type APSettingsFormProps = RestFormProps<APSettings>;
|
||||
|
||||
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIP', isIP);
|
||||
}
|
||||
@@ -22,23 +30,29 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
const { data, handleValueChange, saveData } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
||||
<SelectValidator name="provision_mode"
|
||||
<SelectValidator
|
||||
name="provision_mode"
|
||||
label="Provide Access Point…"
|
||||
value={data.provision_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('provision_mode')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When Network Disconnected</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
|
||||
When Network Disconnected
|
||||
</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||
</SelectValidator>
|
||||
{
|
||||
isAPEnabled(data) &&
|
||||
{isAPEnabled(data) && (
|
||||
<Fragment>
|
||||
<TextValidator
|
||||
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"
|
||||
label="Access Point SSID"
|
||||
fullWidth
|
||||
@@ -49,7 +63,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
/>
|
||||
<PasswordValidator
|
||||
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"
|
||||
label="Access Point Password"
|
||||
fullWidth
|
||||
@@ -71,7 +88,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
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"
|
||||
label="Gateway"
|
||||
fullWidth
|
||||
@@ -82,7 +102,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
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"
|
||||
label="Subnet"
|
||||
fullWidth
|
||||
@@ -92,9 +115,14 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { APStatus, APNetworkStatus } from "./types";
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { APStatus, APNetworkStatus } from './types';
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
@@ -12,17 +12,17 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const apStatus = ({ status }: APStatus) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return "Active";
|
||||
return 'Active';
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return "Inactive";
|
||||
return 'Inactive';
|
||||
case APNetworkStatus.LINGERING:
|
||||
return "Lingering until idle";
|
||||
return 'Lingering until idle';
|
||||
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 APStatusForm from './APStatusForm';
|
||||
@@ -9,7 +14,6 @@ import { APStatus } from './types';
|
||||
type APStatusControllerProps = RestControllerProps<APStatus>;
|
||||
|
||||
class APStatusController extends Component<APStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,10 +23,10 @@ class APStatusController extends Component<APStatusControllerProps> {
|
||||
<SectionContent title="Access Point Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <APStatusForm {...formProps} />}
|
||||
render={(formProps) => <APStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
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 DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
import ComputerIcon from '@material-ui/icons/Computer';
|
||||
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 { APStatus } from './types';
|
||||
|
||||
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
||||
|
||||
class APStatusForm extends Component<APStatusFormProps> {
|
||||
|
||||
createListItems() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
@@ -61,18 +72,20 @@ class APStatusForm extends Component<APStatusFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withTheme(APStatusForm);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
AuthenticatedContextProps,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import APSettingsController from './APSettingsController';
|
||||
@@ -12,8 +16,7 @@ import APStatusController from './APStatusController';
|
||||
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class AccessPoint extends Component<AccessPointProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -21,17 +24,33 @@ class AccessPoint extends Component<AccessPointProps> {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<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/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/ap/settings"
|
||||
label="Access Point Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
|
||||
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ap/status"
|
||||
component={APStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ap/settings"
|
||||
component={APSettingsController}
|
||||
/>
|
||||
<Redirect to="/ap/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { ENDPOINT_ROOT } from './Env';
|
||||
|
||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
|
||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
|
||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
|
||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
|
||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "networkSettings";
|
||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + "networkStatus";
|
||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
|
||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
|
||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
|
||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
|
||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
|
||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features';
|
||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus';
|
||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings';
|
||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time';
|
||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings';
|
||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus';
|
||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks';
|
||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks';
|
||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings';
|
||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus';
|
||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings';
|
||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware';
|
||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings';
|
||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus';
|
||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus';
|
||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn';
|
||||
export const VERIFY_AUTHORIZATION_ENDPOINT =
|
||||
ENDPOINT_ROOT + 'verifyAuthorization';
|
||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings';
|
||||
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken';
|
||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart';
|
||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||
|
||||
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
|
||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");
|
||||
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
|
||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
|
||||
export const EVENT_SOURCE_ROOT = calculateEndpointRoot('/es/');
|
||||
|
||||
function calculateEndpointRoot(endpointPath: string) {
|
||||
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
||||
@@ -10,7 +11,7 @@ function calculateEndpointRoot(endpointPath: string) {
|
||||
return httpRoot + endpointPath;
|
||||
}
|
||||
const location = window.location;
|
||||
return location.protocol + "//" + location.host + endpointPath;
|
||||
return location.protocol + '//' + location.host + endpointPath;
|
||||
}
|
||||
|
||||
function calculateWebSocketRoot(webSocketPath: string) {
|
||||
@@ -19,6 +20,6 @@ function calculateWebSocketRoot(webSocketPath: string) {
|
||||
return webSocketRoot + webSocketPath;
|
||||
}
|
||||
const location = window.location;
|
||||
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return webProtocol + "//" + location.host + webSocketPath;
|
||||
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return webProtocol + '//' + location.host + webSocketPath;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './Env'
|
||||
export * from './Endpoints'
|
||||
export * from './Env';
|
||||
export * from './Endpoints';
|
||||
|
||||
@@ -1,42 +1,56 @@
|
||||
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 * 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, WithSnackbarProps, AuthenticationContextProps {
|
||||
component: ChildComponent;
|
||||
interface AuthenticatedRouteProps
|
||||
extends RouteProps,
|
||||
WithSnackbarProps,
|
||||
AuthenticationContextProps {
|
||||
component:
|
||||
| React.ComponentType<RouteComponentProps<any>>
|
||||
| React.ComponentType<any>;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||
|
||||
render() {
|
||||
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
||||
const {
|
||||
enqueueSnackbar,
|
||||
authenticationContext,
|
||||
component: Component,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { location } = this.props;
|
||||
const renderComponent: RenderComponent = (props) => {
|
||||
if (authenticationContext.me) {
|
||||
return (
|
||||
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
|
||||
<AuthenticatedContext.Provider
|
||||
value={authenticationContext as AuthenticatedContextValue}
|
||||
>
|
||||
<Component {...props} />
|
||||
</AuthenticatedContext.Provider>
|
||||
);
|
||||
}
|
||||
Authentication.storeLoginRedirect(location);
|
||||
enqueueSnackbar("Please sign in to continue.", { variant: 'info' });
|
||||
return (
|
||||
<Redirect to='/' />
|
||||
);
|
||||
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
|
||||
return <Redirect to="/" />;
|
||||
};
|
||||
return <Route {...rest} render={renderComponent} />;
|
||||
}
|
||||
return (
|
||||
<Route {...rest} render={renderComponent} />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
||||
|
||||
@@ -27,7 +27,9 @@ export function clearLoginRedirect() {
|
||||
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 signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||
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);
|
||||
if (accessToken) {
|
||||
params = params || {};
|
||||
params.credentials = 'include';
|
||||
params.headers = {
|
||||
...params.headers,
|
||||
"Authorization": 'Bearer ' + accessToken
|
||||
Authorization: 'Bearer ' + accessToken
|
||||
};
|
||||
}
|
||||
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
|
||||
* for a single file upload and takes care of adding the Authroization header and redirecting on
|
||||
* authroization errors as we do for normal fetch operations.
|
||||
* for a single file upload and takes care of adding the Authorization header and redirecting on
|
||||
* 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) => {
|
||||
xhr.open("POST", url, true);
|
||||
xhr.open('POST', url, true);
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
|
||||
}
|
||||
xhr.upload.onprogress = onProgress;
|
||||
xhr.onload = function () {
|
||||
if (xhr.status === 401 || xhr.status === 403) {
|
||||
history.push("/unauthorized");
|
||||
history.push('/unauthorized');
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
|
||||
xhr.onerror = function () {
|
||||
reject(new DOMException('Error', 'UploadError'));
|
||||
};
|
||||
xhr.onabort = function () {
|
||||
@@ -87,17 +97,22 @@ 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) => {
|
||||
authorizedFetch(url, params).then(response => {
|
||||
authorizedFetch(url, params)
|
||||
.then((response) => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
history.push("/unauthorized");
|
||||
history.push('/unauthorized');
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
}).catch(error => {
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as React from "react";
|
||||
import * as React from 'react';
|
||||
|
||||
export interface Me {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
version: string; // proddy added
|
||||
}
|
||||
|
||||
export interface AuthenticationContextValue {
|
||||
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
|
||||
me?: Me;
|
||||
}
|
||||
|
||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
|
||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
||||
export const AuthenticationContext = React.createContext(
|
||||
AuthenticationContextDefaultValue
|
||||
);
|
||||
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
|
||||
authenticationContext: AuthenticationContextValue;
|
||||
}
|
||||
|
||||
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
|
||||
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
|
||||
export function withAuthenticationContext<T extends AuthenticationContextProps>(
|
||||
Component: React.ComponentType<T>
|
||||
) {
|
||||
return class extends React.Component<
|
||||
Omit<T, keyof AuthenticationContextProps>
|
||||
> {
|
||||
render() {
|
||||
return (
|
||||
<AuthenticationContext.Consumer>
|
||||
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
|
||||
{(authenticationContext) => (
|
||||
<Component
|
||||
{...(this.props as T)}
|
||||
authenticationContext={authenticationContext}
|
||||
/>
|
||||
)}
|
||||
</AuthenticationContext.Consumer>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
||||
me: Me;
|
||||
}
|
||||
|
||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
|
||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
|
||||
export const AuthenticatedContext = React.createContext(
|
||||
AuthenticatedContextDefaultValue
|
||||
);
|
||||
@@ -47,12 +55,21 @@ export interface AuthenticatedContextProps {
|
||||
authenticatedContext: AuthenticatedContextValue;
|
||||
}
|
||||
|
||||
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
|
||||
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
|
||||
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
|
||||
Component: React.ComponentType<T>
|
||||
) {
|
||||
return class extends React.Component<
|
||||
Omit<T, keyof AuthenticatedContextProps>
|
||||
> {
|
||||
render() {
|
||||
return (
|
||||
<AuthenticatedContext.Consumer>
|
||||
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
|
||||
{(authenticatedContext) => (
|
||||
<Component
|
||||
{...(this.props as T)}
|
||||
authenticatedContext={authenticatedContext}
|
||||
/>
|
||||
)}
|
||||
</AuthenticatedContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@ import * as React from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
import history from '../history'
|
||||
import history from '../history';
|
||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||
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 { 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 {
|
||||
context: AuthenticationContextValue;
|
||||
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
|
||||
|
||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||
|
||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
||||
|
||||
class AuthenticationWrapper extends React.Component<
|
||||
AuthenticationWrapperProps,
|
||||
AuthenticationWrapperState
|
||||
> {
|
||||
constructor(props: AuthenticationWrapperProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
context: {
|
||||
refresh: this.refresh,
|
||||
signIn: this.signIn,
|
||||
signOut: this.signOut,
|
||||
signOut: this.signOut
|
||||
},
|
||||
initialized: false
|
||||
};
|
||||
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
|
||||
{this.state.initialized
|
||||
? this.renderContent()
|
||||
: this.renderContentLoading()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
}
|
||||
|
||||
renderContentLoading() {
|
||||
return (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||
// return;
|
||||
// }
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||
.then(response => {
|
||||
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
||||
}).catch(error => {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
||||
variant: 'error',
|
||||
.then((response) => {
|
||||
const me =
|
||||
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me }
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me: undefined }
|
||||
});
|
||||
this.props.enqueueSnackbar(
|
||||
'Error verifying authorization: ' + error.message,
|
||||
{
|
||||
variant: 'error'
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||
}
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me: undefined }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
signIn = (accessToken: string) => {
|
||||
try {
|
||||
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||
const me: Me = decodeMeJWT(accessToken);
|
||||
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) {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||
throw new Error("Failed to parse JWT " + err.message);
|
||||
}
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me: undefined }
|
||||
});
|
||||
throw new Error('Failed to parse JWT ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
signOut = () => {
|
||||
getStorage().removeItem(ACCESS_TOKEN);
|
||||
@@ -101,10 +127,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
me: undefined
|
||||
}
|
||||
});
|
||||
this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
|
||||
this.props.enqueueSnackbar('You have signed out', { variant: 'success' });
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
||||
export default withFeatures(withSnackbar(AuthenticationWrapper));
|
||||
|
||||
@@ -1,29 +1,46 @@
|
||||
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 { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||
|
||||
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
|
||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||
interface UnauthenticatedRouteProps
|
||||
extends RouteProps,
|
||||
AuthenticationContextProps,
|
||||
WithFeaturesProps {
|
||||
component:
|
||||
| React.ComponentType<RouteComponentProps<any>>
|
||||
| React.ComponentType<any>;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||
|
||||
public render() {
|
||||
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
||||
const {
|
||||
authenticationContext,
|
||||
component: Component,
|
||||
features,
|
||||
...rest
|
||||
} = this.props;
|
||||
const renderComponent: RenderComponent = (props) => {
|
||||
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 { makeStyles } from '@material-ui/styles';
|
||||
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
|
||||
import WarningIcon from "@material-ui/icons/Warning"
|
||||
import { Paper, Typography, Box, CssBaseline } from '@material-ui/core';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
|
||||
const styles = makeStyles(
|
||||
{
|
||||
const styles = makeStyles({
|
||||
siteErrorPage: {
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column"
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
siteErrorPagePanel: {
|
||||
textAlign: "center",
|
||||
padding: "280px 0 40px 0",
|
||||
textAlign: 'center',
|
||||
padding: '280px 0 40px 0',
|
||||
backgroundImage: 'url("/app/icon.png")',
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "50% 40px",
|
||||
backgroundSize: "200px auto",
|
||||
width: "100%",
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50% 40px',
|
||||
backgroundSize: '200px auto',
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
interface ApplicationErrorProps {
|
||||
error?: string;
|
||||
@@ -33,27 +31,29 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
|
||||
<div className={classes.siteErrorPage}>
|
||||
<CssBaseline />
|
||||
<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" />
|
||||
<Box ml={2}>
|
||||
<Typography variant="h4">
|
||||
Application error
|
||||
</Typography>
|
||||
<Typography variant="h4">Application error</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Failed to configure the application, please refresh to try again.
|
||||
</Typography>
|
||||
{error &&
|
||||
(
|
||||
{error && (
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Error: {error}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ApplicationError;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { FC } from "react";
|
||||
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
|
||||
import { FC } from 'react';
|
||||
import { FormControlLabel, FormControlLabelProps } from '@material-ui/core';
|
||||
|
||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||
<div>
|
||||
<FormControlLabel {...props} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
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 }) => ({
|
||||
color: theme.palette.getContrastText(theme.palette.error.main),
|
||||
backgroundColor: theme.palette.error.main,
|
||||
'&: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 }) => ({
|
||||
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 }) => ({
|
||||
margin: theme.spacing(0, 1),
|
||||
'&:last-child': {
|
||||
marginRight: 0,
|
||||
marginRight: 0
|
||||
},
|
||||
'&: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 { makeStyles, createStyles } from '@material-ui/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
fullScreenLoading: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
flexDirection: "column"
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
progress: {
|
||||
margin: theme.spacing(4),
|
||||
margin: theme.spacing(4)
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const FullScreenLoading = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.fullScreenLoading}>
|
||||
<CircularProgress className={classes.progress} size={100} />
|
||||
<Typography variant="h4">
|
||||
Loading…
|
||||
</Typography>
|
||||
<Typography variant="h4">Loading…</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FullScreenLoading;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Avatar, makeStyles } from "@material-ui/core";
|
||||
import React, { FC } from "react";
|
||||
import { Avatar, makeStyles } from '@material-ui/core';
|
||||
import { FC } from 'react';
|
||||
|
||||
interface HighlightAvatarProps {
|
||||
color: string;
|
||||
@@ -13,11 +13,7 @@ const useStyles = makeStyles({
|
||||
|
||||
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
||||
const classes = useStyles(props);
|
||||
return (
|
||||
<Avatar className={classes.root}>
|
||||
{props.children}
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
return <Avatar className={classes.root}>{props.children}</Avatar>;
|
||||
};
|
||||
|
||||
export default HighlightAvatar;
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
import React, { RefObject, Fragment } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Drawer, 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 {
|
||||
Drawer,
|
||||
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 { 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 AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
||||
@@ -19,20 +46,24 @@ import MenuIcon from '@material-ui/icons/Menu';
|
||||
|
||||
import ProjectMenu from '../project/ProjectMenu';
|
||||
import { PROJECT_NAME } from '../api';
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||
|
||||
const drawerWidth = 290;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
display: 'flex'
|
||||
},
|
||||
drawer: {
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
},
|
||||
flexShrink: 0
|
||||
}
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1
|
||||
@@ -40,8 +71,8 @@ const styles = (theme: Theme) => createStyles({
|
||||
appBar: {
|
||||
marginLeft: drawerWidth,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
},
|
||||
width: `calc(100% - ${drawerWidth}px)`
|
||||
}
|
||||
},
|
||||
toolbarImage: {
|
||||
[theme.breakpoints.up('xs')]: {
|
||||
@@ -51,44 +82,48 @@ const styles = (theme: Theme) => createStyles({
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
height: 36,
|
||||
marginRight: theme.spacing(3)
|
||||
},
|
||||
}
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
toolbar: theme.mixins.toolbar,
|
||||
drawerPaper: {
|
||||
width: drawerWidth,
|
||||
width: drawerWidth
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1
|
||||
},
|
||||
authMenu: {
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
maxWidth: 400,
|
||||
maxWidth: 400
|
||||
},
|
||||
authMenuActions: {
|
||||
padding: theme.spacing(2),
|
||||
"& > * + *": {
|
||||
marginLeft: theme.spacing(2),
|
||||
'& > * + *': {
|
||||
marginLeft: theme.spacing(2)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
interface MenuAppBarState {
|
||||
mobileOpen: 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;
|
||||
}
|
||||
|
||||
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
|
||||
constructor(props: MenuAppBarProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -101,38 +136,48 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
|
||||
handleToggle = () => {
|
||||
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
this.setState({ authMenuOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerToggle = () => {
|
||||
this.setState({ mobileOpen: !this.state.mobileOpen });
|
||||
};
|
||||
|
||||
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 path = this.props.match.url;
|
||||
const drawer = (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<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>
|
||||
|
||||
<Typography variant="h6" color="textPrimary">
|
||||
{PROJECT_NAME}
|
||||
</Typography>
|
||||
|
||||
<Typography align="right" variant="caption" color="textPrimary">
|
||||
v{authenticatedContext.me.version}
|
||||
</Typography>
|
||||
|
||||
<Divider absolute />
|
||||
</Toolbar>
|
||||
|
||||
@@ -144,20 +189,35 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
)}
|
||||
|
||||
<List>
|
||||
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/network/"
|
||||
selected={path.startsWith('/network/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<WifiIcon />
|
||||
<SettingsEthernetIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Network Connection" />
|
||||
</ListItem>
|
||||
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/ap/"
|
||||
selected={path.startsWith('/ap/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsInputAntennaIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Access Point" />
|
||||
</ListItem>
|
||||
{features.ntp && (
|
||||
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/ntp/"
|
||||
selected={path.startsWith('/ntp/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AccessTimeIcon />
|
||||
</ListItemIcon>
|
||||
@@ -165,7 +225,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
</ListItem>
|
||||
)}
|
||||
{features.mqtt && (
|
||||
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/mqtt/"
|
||||
selected={path.startsWith('/mqtt/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DeviceHubIcon />
|
||||
</ListItemIcon>
|
||||
@@ -173,14 +238,25 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
</ListItem>
|
||||
)}
|
||||
{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>
|
||||
<LockIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Security" />
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
|
||||
<ListItem
|
||||
to="/system/"
|
||||
selected={path.startsWith('/system/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
@@ -201,7 +277,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
>
|
||||
<AccountCircleIcon />
|
||||
</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}>
|
||||
<Card id="menu-list-grow">
|
||||
<CardContent>
|
||||
@@ -212,13 +293,27 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
<AccountCircleIcon />
|
||||
</Avatar>
|
||||
</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>
|
||||
</List>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<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>
|
||||
</Card>
|
||||
</ClickAwayListener>
|
||||
@@ -239,7 +334,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
className={classes.title}
|
||||
>
|
||||
{sectionTitle}
|
||||
</Typography>
|
||||
{features.security && userMenu}
|
||||
@@ -253,10 +353,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
open={mobileOpen}
|
||||
onClose={this.handleDrawerToggle}
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
paper: classes.drawerPaper
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
keepMounted: true
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
@@ -265,7 +365,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
<Hidden smDown implementation="css">
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
paper: classes.drawerPaper
|
||||
}}
|
||||
variant="permanent"
|
||||
open
|
||||
@@ -285,10 +385,6 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
|
||||
export default withRouter(
|
||||
withTheme(
|
||||
withFeatures(
|
||||
withAuthenticatedContext(
|
||||
withStyles(styles)(MenuAppBar)
|
||||
)
|
||||
)
|
||||
withFeatures(withAuthenticatedContext(withStyles(styles)(MenuAppBar)))
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
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 { InputAdornment, IconButton } from '@material-ui/core';
|
||||
import {Visibility,VisibilityOff } from '@material-ui/icons';
|
||||
import { Visibility, VisibilityOff } from '@material-ui/icons';
|
||||
|
||||
const styles = createStyles({
|
||||
input: {
|
||||
"&::-ms-reveal": {
|
||||
display: "none"
|
||||
'&::-ms-reveal': {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
||||
type PasswordValidatorProps = WithStyles<typeof styles> &
|
||||
Exclude<ValidatorComponentProps, 'type' | 'InputProps'>;
|
||||
|
||||
interface PasswordValidatorState {
|
||||
showPassword: boolean;
|
||||
}
|
||||
|
||||
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
||||
|
||||
class PasswordValidator extends React.Component<
|
||||
PasswordValidatorProps,
|
||||
PasswordValidatorState
|
||||
> {
|
||||
state = {
|
||||
showPassword: false
|
||||
};
|
||||
@@ -29,7 +35,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
||||
this.setState({
|
||||
showPassword: !this.state.showPassword
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, ...rest } = this.props;
|
||||
@@ -39,7 +45,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
InputProps={{
|
||||
classes,
|
||||
endAdornment:
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="Toggle password visibility"
|
||||
@@ -48,11 +54,11 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withStyles(styles)(PasswordValidator);
|
||||
|
||||
@@ -4,7 +4,9 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
|
||||
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;
|
||||
saveData: () => void;
|
||||
@@ -15,16 +17,18 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
export const extractEventValue = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
switch (event.target.type) {
|
||||
case "number":
|
||||
case 'number':
|
||||
return event.target.valueAsNumber;
|
||||
case "checkbox":
|
||||
case 'checkbox':
|
||||
return event.target.checked;
|
||||
default:
|
||||
return event.target.value
|
||||
return event.target.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface RestControllerState<D> {
|
||||
data?: D;
|
||||
@@ -32,10 +36,15 @@ interface RestControllerState<D> {
|
||||
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(
|
||||
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> = {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
@@ -43,12 +52,15 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
||||
};
|
||||
|
||||
setData = (data: D, callback?: () => void) => {
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
data,
|
||||
loading: false,
|
||||
errorMessage: undefined
|
||||
}, callback);
|
||||
}
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
loadData = () => {
|
||||
this.setState({
|
||||
@@ -56,19 +68,24 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
||||
loading: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
redirectingAuthorizedFetch(endpointUrl).then(response => {
|
||||
redirectingAuthorizedFetch(endpointUrl)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}).then(json => {
|
||||
this.setState({ data: json, loading: false })
|
||||
}).catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
this.setState({ data: json, loading: false });
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage = error.message || 'Unknown error';
|
||||
this.props.enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ data: undefined, loading: false, errorMessage });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
saveData = () => {
|
||||
this.setState({ loading: true });
|
||||
@@ -78,36 +95,47 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => {
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}).then(json => {
|
||||
this.props.enqueueSnackbar("Update successful.", { variant: 'success' });
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
this.props.enqueueSnackbar('Update successful.', {
|
||||
variant: 'success'
|
||||
});
|
||||
this.setState({ data: json, loading: false });
|
||||
}).catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
|
||||
})
|
||||
.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) };
|
||||
this.setState({ data });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <RestController
|
||||
return (
|
||||
<RestController
|
||||
{...this.state}
|
||||
{...this.props as P}
|
||||
{...(this.props as P)}
|
||||
handleValueChange={this.handleValueChange}
|
||||
setData={this.setData}
|
||||
saveData={this.saveData}
|
||||
loadData={this.loadData}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,20 +8,23 @@ import { RestControllerProps } from '.';
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
loadingSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
margin: theme.spacing(0.5)
|
||||
},
|
||||
loadingSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
textAlign: 'center'
|
||||
},
|
||||
button: {
|
||||
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> {
|
||||
render: (props: RestFormProps<D>) => JSX.Element;
|
||||
@@ -46,7 +49,12 @@ export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
|
||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={loadData}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
content: {
|
||||
padding: theme.spacing(2),
|
||||
margin: theme.spacing(3),
|
||||
margin: theme.spacing(3)
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
interface SectionContentProps {
|
||||
title: string;
|
||||
titleGutter?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const SectionContent: React.FC<SectionContentProps> = (props) => {
|
||||
const { children, title, titleGutter } = props;
|
||||
const { children, title, titleGutter, id } = props;
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.content}>
|
||||
<Paper id={id} className={classes.content}>
|
||||
<Typography variant="h6" gutterBottom={titleGutter}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
@@ -4,13 +4,20 @@ import { useDropzone, DropzoneState } from 'react-dropzone';
|
||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
||||
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 {
|
||||
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) => {
|
||||
if (props.isDragAccept) {
|
||||
@@ -23,9 +30,10 @@ const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
||||
return theme.palette.info.main;
|
||||
}
|
||||
return theme.palette.grey[700];
|
||||
}
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
dropzone: {
|
||||
padding: theme.spacing(8, 2),
|
||||
borderWidth: 2,
|
||||
@@ -33,11 +41,14 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
borderStyle: 'dashed',
|
||||
color: theme.palette.grey[700],
|
||||
transition: 'border .24s ease-in-out',
|
||||
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
|
||||
cursor: (props: SingleUploadStyleProps) =>
|
||||
props.uploading ? 'default' : 'pointer',
|
||||
width: '100%',
|
||||
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
|
||||
borderColor: (props: SingleUploadStyleProps) =>
|
||||
getBorderColor(theme, props)
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
export interface SingleUploadProps {
|
||||
onDrop: (acceptedFiles: File[]) => void;
|
||||
@@ -47,26 +58,44 @@ export interface SingleUploadProps {
|
||||
progress?: ProgressEvent;
|
||||
}
|
||||
|
||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
|
||||
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
|
||||
const SingleUpload: FC<SingleUploadProps> = ({
|
||||
onDrop,
|
||||
onCancel,
|
||||
accept,
|
||||
uploading,
|
||||
progress
|
||||
}) => {
|
||||
const dropzoneState = useDropzone({
|
||||
onDrop,
|
||||
accept,
|
||||
disabled: uploading,
|
||||
multiple: false
|
||||
});
|
||||
const { getRootProps, getInputProps } = dropzoneState;
|
||||
const classes = useStyles({ ...dropzoneState, uploading });
|
||||
|
||||
|
||||
const renderProgressText = () => {
|
||||
if (uploading) {
|
||||
if (progress?.lengthComputable) {
|
||||
return `Uploading: ${progressPercentage(progress)}%`;
|
||||
}
|
||||
return "Uploading\u2026";
|
||||
}
|
||||
return "Drop file or click here";
|
||||
return 'Uploading\u2026';
|
||||
}
|
||||
return 'Drop file or click here';
|
||||
};
|
||||
|
||||
const renderProgress = (progress?: ProgressEvent) => (
|
||||
<LinearProgress
|
||||
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
|
||||
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
|
||||
variant={
|
||||
!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 })}>
|
||||
<input {...getInputProps()} />
|
||||
<Box flexDirection="column" display="flex" alignItems="center">
|
||||
<CloudUploadIcon fontSize='large' />
|
||||
<Typography variant="h6">
|
||||
{renderProgressText()}
|
||||
</Typography>
|
||||
<CloudUploadIcon fontSize="large" />
|
||||
<Typography variant="h6">{renderProgressText()}</Typography>
|
||||
{uploading && (
|
||||
<Fragment>
|
||||
<Box width="100%" p={2}>
|
||||
{renderProgress(progress)}
|
||||
</Box>
|
||||
<Button startIcon={<CancelIcon />} variant="contained" color="secondary" onClick={onCancel}>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Fragment>
|
||||
@@ -91,6 +123,6 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SingleUpload;
|
||||
|
||||
@@ -7,7 +7,9 @@ import { addAccessTokenParameter } from '../authentication';
|
||||
import { extractEventValue } from '.';
|
||||
|
||||
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;
|
||||
saveData: () => void;
|
||||
@@ -25,8 +27,8 @@ interface WebSocketControllerState<D> {
|
||||
}
|
||||
|
||||
enum WebSocketMessageType {
|
||||
ID = "id",
|
||||
PAYLOAD = "payload"
|
||||
ID = 'id',
|
||||
PAYLOAD = 'payload'
|
||||
}
|
||||
|
||||
interface WebSocketIdMessage {
|
||||
@@ -40,21 +42,32 @@ interface WebSocketPayloadMessage<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(
|
||||
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
|
||||
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
|
||||
class extends React.Component<
|
||||
Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps,
|
||||
WebSocketControllerState<D>
|
||||
> {
|
||||
constructor(
|
||||
props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps
|
||||
) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
||||
onmessage: this.onMessage,
|
||||
onopen: this.onOpen,
|
||||
onclose: this.onClose,
|
||||
onclose: this.onClose
|
||||
}),
|
||||
connected: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -64,37 +77,42 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
||||
onMessage = (event: MessageEvent) => {
|
||||
const rawData = event.data;
|
||||
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>) => {
|
||||
const { clientId, data } = this.state;
|
||||
|
||||
switch (message.type) {
|
||||
case WebSocketMessageType.ID:
|
||||
this.setState({ clientId: message.id });
|
||||
break;
|
||||
case WebSocketMessageType.PAYLOAD:
|
||||
const { clientId, data } = this.state;
|
||||
if (clientId && (!data || clientId !== message.origin_id)) {
|
||||
this.setState(
|
||||
{ data: message.payload }
|
||||
);
|
||||
this.setState({ data: message.payload });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onOpen = () => {
|
||||
this.setState({ connected: true });
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({ connected: false, clientId: undefined, data: undefined });
|
||||
}
|
||||
this.setState({
|
||||
connected: false,
|
||||
clientId: undefined,
|
||||
data: undefined
|
||||
});
|
||||
};
|
||||
|
||||
setData = (data: D, callback?: () => void) => {
|
||||
this.setState({ data }, callback);
|
||||
}
|
||||
};
|
||||
|
||||
saveData = throttle(() => {
|
||||
const { ws, connected, data } = this.state;
|
||||
@@ -106,28 +124,35 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
||||
saveDataAndClear = throttle(() => {
|
||||
const { ws, connected, data } = this.state;
|
||||
if (connected) {
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
data: undefined
|
||||
}, () => ws.json(data));
|
||||
},
|
||||
() => ws.json(data)
|
||||
);
|
||||
}
|
||||
}, 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) };
|
||||
this.setState({ data });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <WebSocketController
|
||||
{...this.props as P}
|
||||
return (
|
||||
<WebSocketController
|
||||
{...(this.props as P)}
|
||||
handleValueChange={this.handleValueChange}
|
||||
setData={this.setData}
|
||||
saveData={this.saveData}
|
||||
saveDataAndClear={this.saveDataAndClear}
|
||||
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 { LinearProgress, Typography } from '@material-ui/core';
|
||||
|
||||
@@ -8,22 +6,27 @@ import { WebSocketControllerProps } from '.';
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
loadingSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
margin: theme.spacing(0.5)
|
||||
},
|
||||
loadingSettingsDetails: {
|
||||
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> {
|
||||
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 classes = useStyles();
|
||||
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 './WebSocketController';
|
||||
|
||||
export * from './WindowSize';
|
||||
|
||||
@@ -5,21 +5,26 @@ export interface FeaturesContextValue {
|
||||
features: Features;
|
||||
}
|
||||
|
||||
const FeaturesContextDefaultValue = {} as FeaturesContextValue
|
||||
export const FeaturesContext = React.createContext(
|
||||
FeaturesContextDefaultValue
|
||||
);
|
||||
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
|
||||
export const FeaturesContext = React.createContext(FeaturesContextDefaultValue);
|
||||
|
||||
export interface WithFeaturesProps {
|
||||
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>> {
|
||||
render() {
|
||||
return (
|
||||
<FeaturesContext.Consumer>
|
||||
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
|
||||
{(featuresContext) => (
|
||||
<Component
|
||||
{...(this.props as T)}
|
||||
features={featuresContext.features}
|
||||
/>
|
||||
)}
|
||||
</FeaturesContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { Features } from './types';
|
||||
import { FeaturesContext } from './FeaturesContext';
|
||||
@@ -9,10 +9,9 @@ import { FEATURES_ENDPOINT } from '../api';
|
||||
interface FeaturesWrapperState {
|
||||
features?: Features;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||
|
||||
state: FeaturesWrapperState = {};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -21,41 +20,39 @@ class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||
|
||||
fetchFeaturesDetails = () => {
|
||||
fetch(FEATURES_ENDPOINT)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw Error("Unexpected status code: " + response.status);
|
||||
throw Error('Unexpected status code: ' + response.status);
|
||||
}
|
||||
}).then(features => {
|
||||
})
|
||||
.then((features) => {
|
||||
this.setState({ features });
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.setState({ error: error.message });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { features, error } = this.state;
|
||||
if (features) {
|
||||
return (
|
||||
<FeaturesContext.Provider value={{
|
||||
<FeaturesContext.Provider
|
||||
value={{
|
||||
features
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</FeaturesContext.Provider>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<ApplicationError error={error} />
|
||||
);
|
||||
return <ApplicationError error={error} />;
|
||||
}
|
||||
return (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FeaturesWrapper;
|
||||
|
||||
@@ -2,4 +2,4 @@ import { createBrowserHistory } from 'history';
|
||||
|
||||
export default createBrowserHistory({
|
||||
/* pass a configuration object here if needed */
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6,8 +6,9 @@ import { Router } from 'react-router';
|
||||
|
||||
import App from './App';
|
||||
|
||||
render((
|
||||
render(
|
||||
<Router history={history}>
|
||||
<App/>
|
||||
</Router>
|
||||
), document.getElementById("root"))
|
||||
<App />
|
||||
</Router>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
AuthenticatedContextProps,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
import MqttStatusController from './MqttStatusController';
|
||||
import MqttSettingsController from './MqttSettingsController';
|
||||
@@ -11,8 +15,7 @@ import MqttSettingsController from './MqttSettingsController';
|
||||
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class Mqtt extends Component<MqttProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -20,17 +23,33 @@ class Mqtt extends Component<MqttProps> {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<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/settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/mqtt/settings"
|
||||
label="MQTT Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
|
||||
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/mqtt/status"
|
||||
component={MqttStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/mqtt/settings"
|
||||
component={MqttSettingsController}
|
||||
/>
|
||||
<Redirect to="/mqtt/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
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 MqttSettingsForm from './MqttSettingsForm';
|
||||
@@ -9,7 +14,6 @@ import { MqttSettings } from './types';
|
||||
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
||||
|
||||
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
||||
<SectionContent title="MQTT Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <MqttSettingsForm {...formProps} />}
|
||||
render={(formProps) => <MqttSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
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 SaveIcon from '@material-ui/icons/Save';
|
||||
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 { MqttSettings } from './types';
|
||||
@@ -13,7 +23,6 @@ import { MqttSettings } from './types';
|
||||
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
||||
|
||||
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||
ValidatorForm.addValidationRule('isPath', isPath);
|
||||
@@ -35,7 +44,10 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
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"
|
||||
label="Host"
|
||||
fullWidth
|
||||
@@ -45,8 +57,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
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"]}
|
||||
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="port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
@@ -58,7 +80,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isPath']}
|
||||
errorMessages={['Base is required', "Not a valid Path"]}
|
||||
errorMessages={['Base is required', 'Not a valid Path']}
|
||||
name="base"
|
||||
label="Base"
|
||||
fullWidth
|
||||
@@ -95,8 +117,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
|
||||
errorMessages={['Keep alive is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
|
||||
validators={[
|
||||
'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"
|
||||
label="Keep Alive (seconds)"
|
||||
fullWidth
|
||||
@@ -106,13 +138,15 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
onChange={handleValueChange('keep_alive')}
|
||||
margin="normal"
|
||||
/>
|
||||
<SelectValidator name="mqtt_qos"
|
||||
<SelectValidator
|
||||
name="mqtt_qos"
|
||||
label="QoS"
|
||||
value={data.mqtt_qos}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('mqtt_qos')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>0 (default)</MenuItem>
|
||||
<MenuItem value={1}>1</MenuItem>
|
||||
<MenuItem value={2}>2</MenuItem>
|
||||
@@ -138,48 +172,56 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
label="Retain Flag"
|
||||
/>
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary" >
|
||||
<Typography variant="h6" color="primary">
|
||||
Formatting
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.nested_format}
|
||||
<SelectValidator
|
||||
name="nested_format"
|
||||
label="Topic/Payload Format"
|
||||
value={data.nested_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('nested_format')}
|
||||
value="nested_format"
|
||||
/>
|
||||
}
|
||||
label="Nested format (Thermostat & Mixer only)"
|
||||
/>
|
||||
<SelectValidator name="dallas_format"
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>nested on a single topic</MenuItem>
|
||||
<MenuItem value={2}>as individual topics</MenuItem>
|
||||
</SelectValidator>
|
||||
<SelectValidator
|
||||
name="dallas_format"
|
||||
label="Dallas Sensor Payload Grouping"
|
||||
value={data.dallas_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('dallas_format')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>by Sensor ID</MenuItem>
|
||||
<MenuItem value={2}>by Number</MenuItem>
|
||||
</SelectValidator>
|
||||
<SelectValidator name="bool_format"
|
||||
<SelectValidator
|
||||
name="bool_format"
|
||||
label="Boolean Format"
|
||||
value={data.bool_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('bool_format')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
||||
<MenuItem value={2}>true/false</MenuItem>
|
||||
<MenuItem value={3}>1/0</MenuItem>
|
||||
<MenuItem value={4}>"ON"/"OFF"</MenuItem>
|
||||
</SelectValidator>
|
||||
<SelectValidator name="subscribe_format"
|
||||
<SelectValidator
|
||||
name="subscribe_format"
|
||||
label="Subscribe Format"
|
||||
value={data.subscribe_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('subscribe_format')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>general device topic</MenuItem>
|
||||
<MenuItem value={1}>individual topics, main heating circuit</MenuItem>
|
||||
<MenuItem value={2}>individual topics, all heating circuits</MenuItem>
|
||||
@@ -192,28 +234,40 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
value="ha_enabled"
|
||||
/>
|
||||
}
|
||||
label="Home Assistant MQTT Discovery"
|
||||
label="Use Home Assistant MQTT Discovery"
|
||||
/>
|
||||
{ data.ha_enabled &&
|
||||
<SelectValidator name="ha_climate_format"
|
||||
{data.ha_enabled && (
|
||||
<SelectValidator
|
||||
name="ha_climate_format"
|
||||
label="Thermostat Room Temperature"
|
||||
value={data.ha_climate_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
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={3}>Fix to 0</MenuItem>
|
||||
</SelectValidator>
|
||||
}
|
||||
)}
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary" >
|
||||
<Typography variant="h6" color="primary">
|
||||
Publish Intervals
|
||||
</Typography>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
||||
validators={[
|
||||
'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"
|
||||
label="Boiler Publish Interval (seconds, 0=on change)"
|
||||
fullWidth
|
||||
@@ -224,8 +278,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
||||
validators={[
|
||||
'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"
|
||||
label="Thermostat Publish Interval (seconds, 0=on change)"
|
||||
fullWidth
|
||||
@@ -236,8 +300,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
||||
validators={[
|
||||
'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"
|
||||
label="Solar Publish Interval (seconds, 0=on change)"
|
||||
fullWidth
|
||||
@@ -248,8 +322,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
||||
validators={[
|
||||
'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"
|
||||
label="Mixer Publish Interval (seconds, 0=on change)"
|
||||
fullWidth
|
||||
@@ -260,8 +344,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
||||
validators={[
|
||||
'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"
|
||||
label="Sensors Publish Interval (seconds, 0=on change)"
|
||||
fullWidth
|
||||
@@ -272,8 +366,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
|
||||
errorMessages={['Publish time is required', "Must be a number", "Must be 0 or greater", "Max value is 65535"]}
|
||||
validators={[
|
||||
'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"
|
||||
label="All other Modules Publish Interval (seconds, 0=on change)"
|
||||
fullWidth
|
||||
@@ -284,7 +388,12 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { MqttStatus, MqttDisconnectReason } from "./types";
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { MqttStatus, MqttDisconnectReason } from './types';
|
||||
|
||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||
export const mqttStatusHighlight = (
|
||||
{ enabled, connected }: MqttStatus,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (!enabled) {
|
||||
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.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
||||
if (!enabled) {
|
||||
return "Not enabled";
|
||||
return 'Not enabled';
|
||||
}
|
||||
if (connected) {
|
||||
return "Connected";
|
||||
return 'Connected';
|
||||
}
|
||||
return "Disconnected";
|
||||
}
|
||||
return 'Disconnected';
|
||||
};
|
||||
|
||||
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||
switch (disconnect_reason) {
|
||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||
return "TCP disconnected";
|
||||
return 'TCP disconnected';
|
||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
return "Unacceptable protocol version";
|
||||
return 'Unacceptable protocol version';
|
||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||
return "Client ID rejected";
|
||||
return 'Client ID rejected';
|
||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||
return "Server unavailable";
|
||||
return 'Server unavailable';
|
||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||
return "Malformed credentials";
|
||||
return 'Malformed credentials';
|
||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||
return "Not authorized";
|
||||
return 'Not authorized';
|
||||
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||
return "Device out of memory";
|
||||
return 'Device out of memory';
|
||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||
return "Server fingerprint invalid";
|
||||
return 'Server fingerprint invalid';
|
||||
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)
|
||||
return theme.palette.success.main;
|
||||
|
||||
if (mqtt_fails < 10)
|
||||
return theme.palette.warning.main;
|
||||
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
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 MqttStatusForm from './MqttStatusForm';
|
||||
@@ -9,7 +14,6 @@ import { MqttStatus } from './types';
|
||||
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
||||
|
||||
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,10 +23,10 @@ class MqttStatusController extends Component<MqttStatusControllerProps> {
|
||||
<SectionContent title="MQTT Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <MqttStatusForm {...formProps} />}
|
||||
render={(formProps) => <MqttStatusForm {...formProps} />}
|
||||
/>
|
||||
</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 { 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 RefreshIcon from '@material-ui/icons/Refresh';
|
||||
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 { mqttStatusHighlight, mqttStatus, mqttPublishHighlight, disconnectReason } from './MqttStatus';
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
import {
|
||||
mqttStatusHighlight,
|
||||
mqttStatus,
|
||||
mqttPublishHighlight,
|
||||
disconnectReason
|
||||
} from './MqttStatus';
|
||||
import { MqttStatus } from './types';
|
||||
|
||||
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
||||
|
||||
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
|
||||
renderConnectionStatus() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
if (data.connected) {
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -50,7 +66,10 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
<ReportIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
|
||||
<ListItemText
|
||||
primary="Disconnect Reason"
|
||||
secondary={disconnectReason(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</Fragment>
|
||||
@@ -58,7 +77,7 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
}
|
||||
|
||||
createListItems() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
@@ -78,18 +97,20 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withTheme(MqttStatusForm);
|
||||
|
||||
@@ -40,6 +40,6 @@ export interface MqttSettings {
|
||||
mqtt_retain: boolean;
|
||||
ha_enabled: boolean;
|
||||
ha_climate_format: number;
|
||||
nested_format: boolean;
|
||||
nested_format: number;
|
||||
subscribe_format: number;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import NetworkStatusController from './NetworkStatusController';
|
||||
import NetworkSettingsController from './NetworkSettingsController';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
||||
import {
|
||||
NetworkConnectionContext,
|
||||
NetworkConnectionContextValue
|
||||
} from './NetworkConnectionContext';
|
||||
|
||||
import { WiFiNetwork } from './types';
|
||||
|
||||
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnectionContextValue> {
|
||||
|
||||
class NetworkConnection extends Component<
|
||||
NetworkConnectionProps,
|
||||
NetworkConnectionContextValue
|
||||
> {
|
||||
constructor(props: NetworkConnectionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -28,13 +37,13 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
||||
selectNetwork = (network: WiFiNetwork) => {
|
||||
this.setState({ selectedNetwork: network });
|
||||
this.props.history.push('/network/settings');
|
||||
}
|
||||
};
|
||||
|
||||
deselectNetwork = () => {
|
||||
this.setState({ selectedNetwork: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -43,20 +52,44 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
||||
return (
|
||||
<NetworkConnectionContext.Provider value={this.state}>
|
||||
<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/scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="/network/settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/network/scan"
|
||||
label="Scan WiFi Networks"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
<Tab
|
||||
value="/network/settings"
|
||||
label="Network Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/network/status" component={NetworkStatusController} />
|
||||
<AuthenticatedRoute exact path="/network/scan" component={WiFiNetworkScanner} />
|
||||
<AuthenticatedRoute exact path="/network/settings" component={NetworkSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/network/status"
|
||||
component={NetworkStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/network/scan"
|
||||
component={WiFiNetworkScanner}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/network/settings"
|
||||
component={NetworkSettingsController}
|
||||
/>
|
||||
<Redirect to="/network/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
</NetworkConnectionContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface NetworkConnectionContextValue {
|
||||
deselectNetwork: () => void;
|
||||
}
|
||||
|
||||
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue
|
||||
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue;
|
||||
export const NetworkConnectionContext = React.createContext(
|
||||
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 { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
||||
import { NetworkSettings } from './types';
|
||||
@@ -8,7 +13,6 @@ import { NetworkSettings } from './types';
|
||||
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
||||
|
||||
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -18,12 +22,14 @@ class NetworkSettingsController extends Component<NetworkSettingsControllerProps
|
||||
<SectionContent title="Network Settings">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NetworkSettingsForm {...formProps} />}
|
||||
render={(formProps) => <NetworkSettingsForm {...formProps} />}
|
||||
/>
|
||||
</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 { 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 IconButton from '@material-ui/core/IconButton';
|
||||
@@ -10,31 +17,42 @@ import LockOpenIcon from '@material-ui/icons/LockOpen';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
|
||||
import {
|
||||
RestFormProps,
|
||||
PasswordValidator,
|
||||
BlockFormControlLabel,
|
||||
FormActions,
|
||||
FormButton
|
||||
} from '../components';
|
||||
import { isIP, isHostname, optional } from '../validators';
|
||||
|
||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
||||
import {
|
||||
NetworkConnectionContext,
|
||||
NetworkConnectionContextValue
|
||||
} from './NetworkConnectionContext';
|
||||
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
||||
import { NetworkSettings } from './types';
|
||||
|
||||
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
||||
|
||||
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
|
||||
static contextType = NetworkConnectionContext;
|
||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||
|
||||
constructor(props: NetworkStatusFormProps, context: NetworkConnectionContextValue) {
|
||||
constructor(
|
||||
props: NetworkStatusFormProps,
|
||||
context: NetworkConnectionContextValue
|
||||
) {
|
||||
super(props);
|
||||
|
||||
const { selectedNetwork } = context;
|
||||
if (selectedNetwork) {
|
||||
const networkSettings: NetworkSettings = {
|
||||
ssid: selectedNetwork.ssid,
|
||||
password: "",
|
||||
password: '',
|
||||
hostname: props.data.hostname,
|
||||
static_ip_config: false,
|
||||
}
|
||||
static_ip_config: false
|
||||
};
|
||||
props.setData(networkSettings);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +66,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
deselectNetworkAndLoadData = () => {
|
||||
this.context.deselectNetwork();
|
||||
this.props.loadData();
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.context.deselectNetwork();
|
||||
@@ -59,27 +77,38 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
const { data, handleValueChange, saveData } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
||||
{
|
||||
selectedNetwork ?
|
||||
{selectedNetwork ? (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
||||
{isNetworkOpen(selectedNetwork) ? (
|
||||
<LockOpenIcon />
|
||||
) : (
|
||||
<LockIcon />
|
||||
)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={selectedNetwork.ssid}
|
||||
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
|
||||
secondary={
|
||||
'Security: ' +
|
||||
networkSecurityMode(selectedNetwork) +
|
||||
', Ch: ' +
|
||||
selectedNetwork.channel
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
||||
<IconButton
|
||||
aria-label="Manual Config"
|
||||
onClick={deselectNetwork}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</List>
|
||||
:
|
||||
) : (
|
||||
<TextValidator
|
||||
validators={['matchRegexp:^.{0,32}$']}
|
||||
errorMessages={['SSID must be 32 characters or less']}
|
||||
@@ -91,9 +120,8 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
onChange={handleValueChange('ssid')}
|
||||
margin="normal"
|
||||
/>
|
||||
}
|
||||
{
|
||||
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
|
||||
)}
|
||||
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||
<PasswordValidator
|
||||
validators={['matchRegexp:^.{0,64}$']}
|
||||
errorMessages={['Password must be 64 characters or less']}
|
||||
@@ -105,10 +133,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
onChange={handleValueChange('password')}
|
||||
margin="normal"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<TextValidator
|
||||
validators={['required', 'isHostname']}
|
||||
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
||||
errorMessages={['Hostname is required', 'Not a valid hostname']}
|
||||
name="hostname"
|
||||
label="Hostname"
|
||||
fullWidth
|
||||
@@ -122,13 +150,12 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
<Checkbox
|
||||
value="static_ip_config"
|
||||
checked={data.static_ip_config}
|
||||
onChange={handleValueChange("static_ip_config")}
|
||||
onChange={handleValueChange('static_ip_config')}
|
||||
/>
|
||||
}
|
||||
label="Static IP Config"
|
||||
/>
|
||||
{
|
||||
data.static_ip_config &&
|
||||
{data.static_ip_config && (
|
||||
<Fragment>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
@@ -154,7 +181,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
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"
|
||||
label="Subnet"
|
||||
fullWidth
|
||||
@@ -186,9 +216,14 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -2,12 +2,21 @@ import { Theme } from '@material-ui/core';
|
||||
import { NetworkStatus, NetworkConnectionStatus } from './types';
|
||||
|
||||
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) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
@@ -22,27 +31,27 @@ export const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme)
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const networkStatus = ({ status }: NetworkStatus) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return "Inactive";
|
||||
return 'Inactive';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return "Idle";
|
||||
return 'Idle';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return "No SSID Available";
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return "Connected (WiFi)";
|
||||
return 'Connected (WiFi)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return "Connected (Ethernet)";
|
||||
return 'Connected (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return "Connection Failed";
|
||||
return 'Connection Failed';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return "Connection Lost";
|
||||
return 'Connection Lost';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return "Disconnected";
|
||||
return 'Disconnected';
|
||||
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 { NETWORK_STATUS_ENDPOINT } from '../api';
|
||||
import { NetworkStatus } from './types';
|
||||
@@ -8,7 +13,6 @@ import { NetworkStatus } from './types';
|
||||
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
||||
|
||||
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -18,12 +22,11 @@ class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
||||
<SectionContent title="Network Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NetworkStatusForm {...formProps} />}
|
||||
render={(formProps) => <NetworkStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
||||
|
||||
@@ -1,45 +1,64 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Component, Fragment } from 'react';
|
||||
|
||||
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 WifiIcon from '@material-ui/icons/Wifi';
|
||||
import RouterIcon from '@material-ui/icons/Router';
|
||||
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
|
||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
||||
import { networkStatus, networkStatusHighlight, isConnected, isWiFi } from './NetworkStatus';
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
import {
|
||||
networkStatus,
|
||||
networkStatusHighlight,
|
||||
isConnected,
|
||||
isWiFi,
|
||||
isEthernet
|
||||
} from './NetworkStatus';
|
||||
import { NetworkStatus } from './types';
|
||||
|
||||
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
||||
|
||||
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
|
||||
dnsServers(status: NetworkStatus) {
|
||||
if (!status.dns_ip_1) {
|
||||
return "none";
|
||||
return 'none';
|
||||
}
|
||||
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
|
||||
}
|
||||
|
||||
createListItems() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<HighlightAvatar color={networkStatusHighlight(data, theme)}>
|
||||
<WifiIcon />
|
||||
{isWiFi(data) && <WifiIcon />}
|
||||
{isEthernet(data) && <RouterIcon />}
|
||||
</HighlightAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Status" secondary={networkStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{
|
||||
isWiFi(data) &&
|
||||
{isWiFi(data) && (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
@@ -51,8 +70,8 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</Fragment>
|
||||
}
|
||||
{ isConnected(data) &&
|
||||
)}
|
||||
{isConnected(data) && (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
@@ -67,14 +86,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
||||
<ListItemText
|
||||
primary="MAC Address"
|
||||
secondary={data.mac_address}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>#</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Subnet Mask" secondary={data.subnet_mask} />
|
||||
<ListItemText
|
||||
primary="Subnet Mask"
|
||||
secondary={data.subnet_mask}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -83,7 +108,10 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
<SettingsInputComponentIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Gateway IP" secondary={data.gateway_ip || "none"} />
|
||||
<ListItemText
|
||||
primary="Gateway IP"
|
||||
secondary={data.gateway_ip || 'none'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -92,11 +120,14 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
<DNSIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="DNS Server IP" secondary={this.dnsServers(data)} />
|
||||
<ListItemText
|
||||
primary="DNS Server IP"
|
||||
secondary={this.dnsServers(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -104,18 +135,20 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withTheme(NetworkStatusForm);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
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 { FormActions, FormButton, SectionContent } from '../components';
|
||||
@@ -11,9 +18,9 @@ import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
|
||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||
import { WiFiNetworkList, WiFiNetwork } from './types';
|
||||
|
||||
const NUM_POLLS = 10
|
||||
const POLLING_FREQUENCY = 500
|
||||
const RETRY_EXCEPTION_TYPE = "retry"
|
||||
const NUM_POLLS = 10;
|
||||
const POLLING_FREQUENCY = 500;
|
||||
const RETRY_EXCEPTION_TYPE = 'retry';
|
||||
|
||||
interface WiFiNetworkScannerState {
|
||||
scanningForNetworks: boolean;
|
||||
@@ -21,28 +28,31 @@ interface WiFiNetworkScannerState {
|
||||
networkList?: WiFiNetworkList;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
scanningSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
margin: theme.spacing(0.5)
|
||||
},
|
||||
scanningSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
textAlign: 'center'
|
||||
},
|
||||
scanningProgress: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
||||
|
||||
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
|
||||
|
||||
pollCount: number = 0;
|
||||
class WiFiNetworkScanner extends Component<
|
||||
WiFiNetworkScannerProps,
|
||||
WiFiNetworkScannerState
|
||||
> {
|
||||
pollCount = 0;
|
||||
|
||||
state: WiFiNetworkScannerState = {
|
||||
scanningForNetworks: false,
|
||||
scanningForNetworks: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -54,22 +64,35 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
if (!scanningForNetworks) {
|
||||
this.scanNetworks();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scanNetworks() {
|
||||
this.pollCount = 0;
|
||||
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
|
||||
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
||||
this.setState({
|
||||
scanningForNetworks: true,
|
||||
networkList: undefined,
|
||||
errorMessage: undefined
|
||||
});
|
||||
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',
|
||||
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
|
||||
});
|
||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,21 +103,20 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
retryError() {
|
||||
return {
|
||||
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) {
|
||||
if (network1.rssi < network2.rssi)
|
||||
return 1;
|
||||
if (network1.rssi > network2.rssi)
|
||||
return -1;
|
||||
if (network1.rssi < network2.rssi) return 1;
|
||||
if (network1.rssi > network2.rssi) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
pollNetworkList = () => {
|
||||
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
@@ -103,24 +125,34 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
this.schedulePollTimeout();
|
||||
throw this.retryError();
|
||||
} 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 => {
|
||||
json.networks.sort(this.compareNetworks)
|
||||
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
|
||||
.then((json) => {
|
||||
json.networks.sort(this.compareNetworks);
|
||||
this.setState({
|
||||
scanningForNetworks: false,
|
||||
networkList: json,
|
||||
errorMessage: undefined
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
||||
variant: 'error',
|
||||
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
||||
}
|
||||
this.setState({
|
||||
scanningForNetworks: false,
|
||||
networkList: undefined,
|
||||
errorMessage: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
renderNetworkScanner() {
|
||||
const { classes } = this.props;
|
||||
@@ -144,9 +176,7 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<WiFiNetworkSelector networkList={networkList} />
|
||||
);
|
||||
return <WiFiNetworkSelector networkList={networkList} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -155,14 +185,19 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
<SectionContent title="Network Scanner">
|
||||
{this.renderNetworkScanner()}
|
||||
<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…
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
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 LockIcon from '@material-ui/icons/Lock';
|
||||
@@ -16,13 +22,16 @@ interface WiFiNetworkSelectorProps {
|
||||
}
|
||||
|
||||
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
||||
|
||||
static contextType = NetworkConnectionContext;
|
||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||
|
||||
renderNetwork = (network: WiFiNetwork) => {
|
||||
return (
|
||||
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
|
||||
<ListItem
|
||||
key={network.bssid}
|
||||
button
|
||||
onClick={() => this.context.selectNetwork(network)}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
||||
@@ -30,25 +39,27 @@ class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={network.ssid}
|
||||
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
|
||||
secondary={
|
||||
'Security: ' +
|
||||
networkSecurityMode(network) +
|
||||
', Ch: ' +
|
||||
network.channel
|
||||
}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent={network.rssi + "db"}>
|
||||
<Badge badgeContent={network.rssi + 'db'}>
|
||||
<WifiIcon />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<List>
|
||||
{this.props.networkList.networks.map(this.renderNetwork)}
|
||||
</List>
|
||||
<List>{this.props.networkList.networks.map(this.renderNetwork)}</List>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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) => {
|
||||
switch (encryption_type) {
|
||||
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
||||
return "WEP";
|
||||
return 'WEP';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
||||
return "WPA";
|
||||
return 'WPA';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
||||
return "WPA2";
|
||||
return 'WPA2';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
||||
return "WPA/WPA2";
|
||||
return 'WPA/WPA2';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
||||
return "WPA2 Enterprise";
|
||||
return 'WPA2 Enterprise';
|
||||
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
||||
return "None";
|
||||
return 'None';
|
||||
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_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import NTPSettingsForm from './NTPSettingsForm';
|
||||
@@ -9,7 +14,6 @@ import { NTPSettings } from './types';
|
||||
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
||||
|
||||
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||
<SectionContent title="NTP Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NTPSettingsForm {...formProps} />}
|
||||
render={(formProps) => <NTPSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
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 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 { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
||||
@@ -13,7 +22,6 @@ import { NTPSettings } from './types';
|
||||
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
||||
|
||||
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||
}
|
||||
@@ -25,7 +33,7 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
tz_label: event.target.value,
|
||||
tz_format: TIME_ZONES[event.target.value]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, handleValueChange, saveData } = this.props;
|
||||
@@ -43,7 +51,10 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
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"
|
||||
label="Server"
|
||||
fullWidth
|
||||
@@ -68,7 +79,12 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
{timeZoneSelectItems()}
|
||||
</SelectValidator>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { NTPStatus, NTPSyncStatus } from "./types";
|
||||
import { Theme } from '@material-ui/core';
|
||||
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) => {
|
||||
switch (status) {
|
||||
@@ -12,15 +13,15 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ntpStatus = ({ status }: NTPStatus) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return "Inactive";
|
||||
return 'Inactive';
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return "Active";
|
||||
return 'Active';
|
||||
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 NTPStatusForm from './NTPStatusForm';
|
||||
@@ -9,7 +14,6 @@ import { NTPStatus } from './types';
|
||||
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
||||
|
||||
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||
<SectionContent title="NTP Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NTPStatusForm {...formProps} />}
|
||||
render={(formProps) => <NTPStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
|
||||
import {
|
||||
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 AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||
@@ -13,12 +28,22 @@ import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
||||
import { formatDuration, formatDateTime, formatLocalDateTime } from './TimeFormat';
|
||||
import {
|
||||
formatDuration,
|
||||
formatDateTime,
|
||||
formatLocalDateTime
|
||||
} from './TimeFormat';
|
||||
import { NTPStatus, Time } from './types';
|
||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
import { TIME_ENDPOINT } from '../api';
|
||||
|
||||
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
|
||||
type NTPStatusFormProps = RestFormProps<NTPStatus> &
|
||||
WithTheme &
|
||||
AuthenticatedContextProps;
|
||||
|
||||
interface NTPStatusFormState {
|
||||
settingTime: boolean;
|
||||
@@ -27,7 +52,6 @@ interface NTPStatusFormState {
|
||||
}
|
||||
|
||||
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
|
||||
constructor(props: NTPStatusFormProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -41,20 +65,20 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
this.setState({
|
||||
localTime: event.target.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
openSetTime = () => {
|
||||
this.setState({
|
||||
localTime: formatLocalDateTime(new Date()),
|
||||
settingTime: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
closeSetTime = () => {
|
||||
this.setState({
|
||||
settingTime: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
createTime = (): Time => ({
|
||||
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
||||
@@ -62,37 +86,48 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
|
||||
configureTime = () => {
|
||||
this.setState({ processing: true });
|
||||
redirectingAuthorizedFetch(TIME_ENDPOINT,
|
||||
{
|
||||
redirectingAuthorizedFetch(TIME_ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.createTime()),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
|
||||
this.setState({ processing: false, settingTime: false }, this.props.loadData);
|
||||
this.props.enqueueSnackbar('Time set successfully', {
|
||||
variant: 'success'
|
||||
});
|
||||
this.setState(
|
||||
{ processing: false, settingTime: false },
|
||||
this.props.loadData
|
||||
);
|
||||
} else {
|
||||
throw Error("Error setting time, status code: " + response.status);
|
||||
throw Error('Error setting time, status code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || 'Problem setting the time',
|
||||
{ variant: 'error' }
|
||||
);
|
||||
this.setState({ processing: false, settingTime: false });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderSetTimeDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.settingTime}
|
||||
onClose={this.closeSetTime}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle>Set Time</DialogTitle>
|
||||
<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
|
||||
label="Local Time"
|
||||
type="datetime-local"
|
||||
@@ -102,24 +137,35 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={this.closeSetTime}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</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
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
const me = this.props.authenticatedContext.me;
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -152,7 +198,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
<AccessTimeIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
|
||||
<ListItemText
|
||||
primary="Local Time"
|
||||
secondary={formatDateTime(data.local_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -161,7 +210,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
<SwapVerticalCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
|
||||
<ListItemText
|
||||
primary="UTC Time"
|
||||
secondary={formatDateTime(data.utc_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -170,19 +222,32 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
<AvTimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Uptime" secondary={formatDuration(data.uptime)} />
|
||||
<ListItemText
|
||||
primary="Uptime"
|
||||
secondary={formatDuration(data.uptime)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<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
|
||||
</FormButton>
|
||||
</Box>
|
||||
{me.admin && !isNtpActive(data) && (
|
||||
<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
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import NTPStatusController from './NTPStatusController';
|
||||
@@ -12,8 +16,7 @@ import NTPSettingsController from './NTPSettingsController';
|
||||
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class NetworkTime extends Component<NetworkTimeProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -21,19 +24,34 @@ class NetworkTime extends Component<NetworkTimeProps> {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<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/settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/ntp/settings"
|
||||
label="NTP Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/ntp/status" component={NTPStatusController} />
|
||||
<AuthenticatedRoute exact path="/ntp/settings" component={NTPSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ntp/status"
|
||||
component={NTPStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ntp/settings"
|
||||
component={NTPSettingsController}
|
||||
/>
|
||||
<Redirect to="/ntp/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(NetworkTime)
|
||||
export default withAuthenticatedContext(NetworkTime);
|
||||
|
||||
@@ -1,479 +1,480 @@
|
||||
import React from 'react';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
type TimeZones = {
|
||||
[name: string]: string
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
export const TIME_ZONES: TimeZones = {
|
||||
"Africa/Abidjan": "GMT0",
|
||||
"Africa/Accra": "GMT0",
|
||||
"Africa/Addis_Ababa": "EAT-3",
|
||||
"Africa/Algiers": "CET-1",
|
||||
"Africa/Asmara": "EAT-3",
|
||||
"Africa/Bamako": "GMT0",
|
||||
"Africa/Bangui": "WAT-1",
|
||||
"Africa/Banjul": "GMT0",
|
||||
"Africa/Bissau": "GMT0",
|
||||
"Africa/Blantyre": "CAT-2",
|
||||
"Africa/Brazzaville": "WAT-1",
|
||||
"Africa/Bujumbura": "CAT-2",
|
||||
"Africa/Cairo": "EET-2",
|
||||
"Africa/Casablanca": "UNK-1",
|
||||
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Africa/Conakry": "GMT0",
|
||||
"Africa/Dakar": "GMT0",
|
||||
"Africa/Dar_es_Salaam": "EAT-3",
|
||||
"Africa/Djibouti": "EAT-3",
|
||||
"Africa/Douala": "WAT-1",
|
||||
"Africa/El_Aaiun": "UNK-1",
|
||||
"Africa/Freetown": "GMT0",
|
||||
"Africa/Gaborone": "CAT-2",
|
||||
"Africa/Harare": "CAT-2",
|
||||
"Africa/Johannesburg": "SAST-2",
|
||||
"Africa/Juba": "EAT-3",
|
||||
"Africa/Kampala": "EAT-3",
|
||||
"Africa/Khartoum": "CAT-2",
|
||||
"Africa/Kigali": "CAT-2",
|
||||
"Africa/Kinshasa": "WAT-1",
|
||||
"Africa/Lagos": "WAT-1",
|
||||
"Africa/Libreville": "WAT-1",
|
||||
"Africa/Lome": "GMT0",
|
||||
"Africa/Luanda": "WAT-1",
|
||||
"Africa/Lubumbashi": "CAT-2",
|
||||
"Africa/Lusaka": "CAT-2",
|
||||
"Africa/Malabo": "WAT-1",
|
||||
"Africa/Maputo": "CAT-2",
|
||||
"Africa/Maseru": "SAST-2",
|
||||
"Africa/Mbabane": "SAST-2",
|
||||
"Africa/Mogadishu": "EAT-3",
|
||||
"Africa/Monrovia": "GMT0",
|
||||
"Africa/Nairobi": "EAT-3",
|
||||
"Africa/Ndjamena": "WAT-1",
|
||||
"Africa/Niamey": "WAT-1",
|
||||
"Africa/Nouakchott": "GMT0",
|
||||
"Africa/Ouagadougou": "GMT0",
|
||||
"Africa/Porto-Novo": "WAT-1",
|
||||
"Africa/Sao_Tome": "GMT0",
|
||||
"Africa/Tripoli": "EET-2",
|
||||
"Africa/Tunis": "CET-1",
|
||||
"Africa/Windhoek": "CAT-2",
|
||||
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
|
||||
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Anguilla": "AST4",
|
||||
"America/Antigua": "AST4",
|
||||
"America/Araguaina": "UNK3",
|
||||
"America/Argentina/Buenos_Aires": "UNK3",
|
||||
"America/Argentina/Catamarca": "UNK3",
|
||||
"America/Argentina/Cordoba": "UNK3",
|
||||
"America/Argentina/Jujuy": "UNK3",
|
||||
"America/Argentina/La_Rioja": "UNK3",
|
||||
"America/Argentina/Mendoza": "UNK3",
|
||||
"America/Argentina/Rio_Gallegos": "UNK3",
|
||||
"America/Argentina/Salta": "UNK3",
|
||||
"America/Argentina/San_Juan": "UNK3",
|
||||
"America/Argentina/San_Luis": "UNK3",
|
||||
"America/Argentina/Tucuman": "UNK3",
|
||||
"America/Argentina/Ushuaia": "UNK3",
|
||||
"America/Aruba": "AST4",
|
||||
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
|
||||
"America/Atikokan": "EST5",
|
||||
"America/Bahia": "UNK3",
|
||||
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Barbados": "AST4",
|
||||
"America/Belem": "UNK3",
|
||||
"America/Belize": "CST6",
|
||||
"America/Blanc-Sablon": "AST4",
|
||||
"America/Boa_Vista": "UNK4",
|
||||
"America/Bogota": "UNK5",
|
||||
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Campo_Grande": "UNK4",
|
||||
"America/Cancun": "EST5",
|
||||
"America/Caracas": "UNK4",
|
||||
"America/Cayenne": "UNK3",
|
||||
"America/Cayman": "EST5",
|
||||
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Costa_Rica": "CST6",
|
||||
"America/Creston": "MST7",
|
||||
"America/Cuiaba": "UNK4",
|
||||
"America/Curacao": "AST4",
|
||||
"America/Danmarkshavn": "GMT0",
|
||||
"America/Dawson": "MST7",
|
||||
"America/Dawson_Creek": "MST7",
|
||||
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Dominica": "AST4",
|
||||
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Eirunepe": "UNK5",
|
||||
"America/El_Salvador": "CST6",
|
||||
"America/Fort_Nelson": "MST7",
|
||||
"America/Fortaleza": "UNK3",
|
||||
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
|
||||
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Grenada": "AST4",
|
||||
"America/Guadeloupe": "AST4",
|
||||
"America/Guatemala": "CST6",
|
||||
"America/Guayaquil": "UNK5",
|
||||
"America/Guyana": "UNK4",
|
||||
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
|
||||
"America/Hermosillo": "MST7",
|
||||
"America/Indiana/Indianapolis": "EST5EDT,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/Petersburg": "EST5EDT,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/Vincennes": "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/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Jamaica": "EST5",
|
||||
"America/Juneau": "AKST9AKDT,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/Kralendijk": "AST4",
|
||||
"America/La_Paz": "UNK4",
|
||||
"America/Lima": "UNK5",
|
||||
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Lower_Princes": "AST4",
|
||||
"America/Maceio": "UNK3",
|
||||
"America/Managua": "CST6",
|
||||
"America/Manaus": "UNK4",
|
||||
"America/Marigot": "AST4",
|
||||
"America/Martinique": "AST4",
|
||||
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Miquelon": "UNK3UNK,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/Montevideo": "UNK3",
|
||||
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Montserrat": "AST4",
|
||||
"America/Nassau": "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/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Noronha": "UNK2",
|
||||
"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/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Panama": "EST5",
|
||||
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Paramaribo": "UNK3",
|
||||
"America/Phoenix": "MST7",
|
||||
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Port_of_Spain": "AST4",
|
||||
"America/Porto_Velho": "UNK4",
|
||||
"America/Puerto_Rico": "AST4",
|
||||
"America/Punta_Arenas": "UNK3",
|
||||
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Recife": "UNK3",
|
||||
"America/Regina": "CST6",
|
||||
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rio_Branco": "UNK5",
|
||||
"America/Santarem": "UNK3",
|
||||
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
|
||||
"America/Santo_Domingo": "AST4",
|
||||
"America/Sao_Paulo": "UNK3",
|
||||
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/St_Barthelemy": "AST4",
|
||||
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
|
||||
"America/St_Kitts": "AST4",
|
||||
"America/St_Lucia": "AST4",
|
||||
"America/St_Thomas": "AST4",
|
||||
"America/St_Vincent": "AST4",
|
||||
"America/Swift_Current": "CST6",
|
||||
"America/Tegucigalpa": "CST6",
|
||||
"America/Thule": "AST4ADT,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/Toronto": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tortola": "AST4",
|
||||
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Whitehorse": "MST7",
|
||||
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"Antarctica/Casey": "UNK-8",
|
||||
"Antarctica/Davis": "UNK-7",
|
||||
"Antarctica/DumontDUrville": "UNK-10",
|
||||
"Antarctica/Macquarie": "UNK-11",
|
||||
"Antarctica/Mawson": "UNK-5",
|
||||
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Antarctica/Palmer": "UNK3",
|
||||
"Antarctica/Rothera": "UNK3",
|
||||
"Antarctica/Syowa": "UNK-3",
|
||||
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
|
||||
"Antarctica/Vostok": "UNK-6",
|
||||
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Asia/Aden": "UNK-3",
|
||||
"Asia/Almaty": "UNK-6",
|
||||
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
|
||||
"Asia/Anadyr": "UNK-12",
|
||||
"Asia/Aqtau": "UNK-5",
|
||||
"Asia/Aqtobe": "UNK-5",
|
||||
"Asia/Ashgabat": "UNK-5",
|
||||
"Asia/Atyrau": "UNK-5",
|
||||
"Asia/Baghdad": "UNK-3",
|
||||
"Asia/Bahrain": "UNK-3",
|
||||
"Asia/Baku": "UNK-4",
|
||||
"Asia/Bangkok": "UNK-7",
|
||||
"Asia/Barnaul": "UNK-7",
|
||||
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
|
||||
"Asia/Bishkek": "UNK-6",
|
||||
"Asia/Brunei": "UNK-8",
|
||||
"Asia/Chita": "UNK-9",
|
||||
"Asia/Choibalsan": "UNK-8",
|
||||
"Asia/Colombo": "UNK-5:30",
|
||||
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
|
||||
"Asia/Dhaka": "UNK-6",
|
||||
"Asia/Dili": "UNK-9",
|
||||
"Asia/Dubai": "UNK-4",
|
||||
"Asia/Dushanbe": "UNK-5",
|
||||
"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/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Ho_Chi_Minh": "UNK-7",
|
||||
"Asia/Hong_Kong": "HKT-8",
|
||||
"Asia/Hovd": "UNK-7",
|
||||
"Asia/Irkutsk": "UNK-8",
|
||||
"Asia/Jakarta": "WIB-7",
|
||||
"Asia/Jayapura": "WIT-9",
|
||||
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
|
||||
"Asia/Kabul": "UNK-4:30",
|
||||
"Asia/Kamchatka": "UNK-12",
|
||||
"Asia/Karachi": "PKT-5",
|
||||
"Asia/Kathmandu": "UNK-5:45",
|
||||
"Asia/Khandyga": "UNK-9",
|
||||
"Asia/Kolkata": "IST-5:30",
|
||||
"Asia/Krasnoyarsk": "UNK-7",
|
||||
"Asia/Kuala_Lumpur": "UNK-8",
|
||||
"Asia/Kuching": "UNK-8",
|
||||
"Asia/Kuwait": "UNK-3",
|
||||
"Asia/Macau": "CST-8",
|
||||
"Asia/Magadan": "UNK-11",
|
||||
"Asia/Makassar": "WITA-8",
|
||||
"Asia/Manila": "PST-8",
|
||||
"Asia/Muscat": "UNK-4",
|
||||
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Novokuznetsk": "UNK-7",
|
||||
"Asia/Novosibirsk": "UNK-7",
|
||||
"Asia/Omsk": "UNK-6",
|
||||
"Asia/Oral": "UNK-5",
|
||||
"Asia/Phnom_Penh": "UNK-7",
|
||||
"Asia/Pontianak": "WIB-7",
|
||||
"Asia/Pyongyang": "KST-9",
|
||||
"Asia/Qatar": "UNK-3",
|
||||
"Asia/Qyzylorda": "UNK-5",
|
||||
"Asia/Riyadh": "UNK-3",
|
||||
"Asia/Sakhalin": "UNK-11",
|
||||
"Asia/Samarkand": "UNK-5",
|
||||
"Asia/Seoul": "KST-9",
|
||||
"Asia/Shanghai": "CST-8",
|
||||
"Asia/Singapore": "UNK-8",
|
||||
"Asia/Srednekolymsk": "UNK-11",
|
||||
"Asia/Taipei": "CST-8",
|
||||
"Asia/Tashkent": "UNK-5",
|
||||
"Asia/Tbilisi": "UNK-4",
|
||||
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
|
||||
"Asia/Thimphu": "UNK-6",
|
||||
"Asia/Tokyo": "JST-9",
|
||||
"Asia/Tomsk": "UNK-7",
|
||||
"Asia/Ulaanbaatar": "UNK-8",
|
||||
"Asia/Urumqi": "UNK-6",
|
||||
"Asia/Ust-Nera": "UNK-10",
|
||||
"Asia/Vientiane": "UNK-7",
|
||||
"Asia/Vladivostok": "UNK-10",
|
||||
"Asia/Yakutsk": "UNK-9",
|
||||
"Asia/Yangon": "UNK-6:30",
|
||||
"Asia/Yekaterinburg": "UNK-5",
|
||||
"Asia/Yerevan": "UNK-4",
|
||||
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Cape_Verde": "UNK1",
|
||||
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Reykjavik": "GMT0",
|
||||
"Atlantic/South_Georgia": "UNK2",
|
||||
"Atlantic/St_Helena": "GMT0",
|
||||
"Atlantic/Stanley": "UNK3",
|
||||
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Brisbane": "AEST-10",
|
||||
"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/Darwin": "ACST-9:30",
|
||||
"Australia/Eucla": "UNK-8:45",
|
||||
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Lindeman": "AEST-10",
|
||||
"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/Perth": "AWST-8",
|
||||
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Etc/GMT": "GMT0",
|
||||
"Etc/GMT+0": "GMT0",
|
||||
"Etc/GMT+1": "UNK1",
|
||||
"Etc/GMT+10": "UNK10",
|
||||
"Etc/GMT+11": "UNK11",
|
||||
"Etc/GMT+12": "UNK12",
|
||||
"Etc/GMT+2": "UNK2",
|
||||
"Etc/GMT+3": "UNK3",
|
||||
"Etc/GMT+4": "UNK4",
|
||||
"Etc/GMT+5": "UNK5",
|
||||
"Etc/GMT+6": "UNK6",
|
||||
"Etc/GMT+7": "UNK7",
|
||||
"Etc/GMT+8": "UNK8",
|
||||
"Etc/GMT+9": "UNK9",
|
||||
"Etc/GMT-0": "GMT0",
|
||||
"Etc/GMT-1": "UNK-1",
|
||||
"Etc/GMT-10": "UNK-10",
|
||||
"Etc/GMT-11": "UNK-11",
|
||||
"Etc/GMT-12": "UNK-12",
|
||||
"Etc/GMT-13": "UNK-13",
|
||||
"Etc/GMT-14": "UNK-14",
|
||||
"Etc/GMT-2": "UNK-2",
|
||||
"Etc/GMT-3": "UNK-3",
|
||||
"Etc/GMT-4": "UNK-4",
|
||||
"Etc/GMT-5": "UNK-5",
|
||||
"Etc/GMT-6": "UNK-6",
|
||||
"Etc/GMT-7": "UNK-7",
|
||||
"Etc/GMT-8": "UNK-8",
|
||||
"Etc/GMT-9": "UNK-9",
|
||||
"Etc/GMT0": "GMT0",
|
||||
"Etc/Greenwich": "GMT0",
|
||||
"Etc/UCT": "UTC0",
|
||||
"Etc/UTC": "UTC0",
|
||||
"Etc/Universal": "UTC0",
|
||||
"Etc/Zulu": "UTC0",
|
||||
"Europe/Amsterdam": "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/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Belgrade": "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/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Budapest": "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/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
|
||||
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"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/Istanbul": "UNK-3",
|
||||
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Kaliningrad": "EET-2",
|
||||
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Kirov": "UNK-3",
|
||||
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Luxembourg": "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/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Minsk": "UNK-3",
|
||||
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Moscow": "MSK-3",
|
||||
"Europe/Oslo": "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/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Samara": "UNK-4",
|
||||
"Europe/San_Marino": "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/Simferopol": "MSK-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/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Ulyanovsk": "UNK-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/Vatican": "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/Volgograd": "UNK-4",
|
||||
"Europe/Warsaw": "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/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Indian/Antananarivo": "EAT-3",
|
||||
"Indian/Chagos": "UNK-6",
|
||||
"Indian/Christmas": "UNK-7",
|
||||
"Indian/Cocos": "UNK-6:30",
|
||||
"Indian/Comoro": "EAT-3",
|
||||
"Indian/Kerguelen": "UNK-5",
|
||||
"Indian/Mahe": "UNK-4",
|
||||
"Indian/Maldives": "UNK-5",
|
||||
"Indian/Mauritius": "UNK-4",
|
||||
"Indian/Mayotte": "EAT-3",
|
||||
"Indian/Reunion": "UNK-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/Bougainville": "UNK-11",
|
||||
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
|
||||
"Pacific/Chuuk": "UNK-10",
|
||||
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
|
||||
"Pacific/Efate": "UNK-11",
|
||||
"Pacific/Enderbury": "UNK-13",
|
||||
"Pacific/Fakaofo": "UNK-13",
|
||||
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
|
||||
"Pacific/Funafuti": "UNK-12",
|
||||
"Pacific/Galapagos": "UNK6",
|
||||
"Pacific/Gambier": "UNK9",
|
||||
"Pacific/Guadalcanal": "UNK-11",
|
||||
"Pacific/Guam": "ChST-10",
|
||||
"Pacific/Honolulu": "HST10",
|
||||
"Pacific/Kiritimati": "UNK-14",
|
||||
"Pacific/Kosrae": "UNK-11",
|
||||
"Pacific/Kwajalein": "UNK-12",
|
||||
"Pacific/Majuro": "UNK-12",
|
||||
"Pacific/Marquesas": "UNK9:30",
|
||||
"Pacific/Midway": "SST11",
|
||||
"Pacific/Nauru": "UNK-12",
|
||||
"Pacific/Niue": "UNK11",
|
||||
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
|
||||
"Pacific/Noumea": "UNK-11",
|
||||
"Pacific/Pago_Pago": "SST11",
|
||||
"Pacific/Palau": "UNK-9",
|
||||
"Pacific/Pitcairn": "UNK8",
|
||||
"Pacific/Pohnpei": "UNK-11",
|
||||
"Pacific/Port_Moresby": "UNK-10",
|
||||
"Pacific/Rarotonga": "UNK10",
|
||||
"Pacific/Saipan": "ChST-10",
|
||||
"Pacific/Tahiti": "UNK10",
|
||||
"Pacific/Tarawa": "UNK-12",
|
||||
"Pacific/Tongatapu": "UNK-13",
|
||||
"Pacific/Wake": "UNK-12",
|
||||
"Pacific/Wallis": "UNK-12"
|
||||
}
|
||||
'Africa/Abidjan': 'GMT0',
|
||||
'Africa/Accra': 'GMT0',
|
||||
'Africa/Addis_Ababa': 'EAT-3',
|
||||
'Africa/Algiers': 'CET-1',
|
||||
'Africa/Asmara': 'EAT-3',
|
||||
'Africa/Bamako': 'GMT0',
|
||||
'Africa/Bangui': 'WAT-1',
|
||||
'Africa/Banjul': 'GMT0',
|
||||
'Africa/Bissau': 'GMT0',
|
||||
'Africa/Blantyre': 'CAT-2',
|
||||
'Africa/Brazzaville': 'WAT-1',
|
||||
'Africa/Bujumbura': 'CAT-2',
|
||||
'Africa/Cairo': 'EET-2',
|
||||
'Africa/Casablanca': 'UNK-1',
|
||||
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Africa/Conakry': 'GMT0',
|
||||
'Africa/Dakar': 'GMT0',
|
||||
'Africa/Dar_es_Salaam': 'EAT-3',
|
||||
'Africa/Djibouti': 'EAT-3',
|
||||
'Africa/Douala': 'WAT-1',
|
||||
'Africa/El_Aaiun': 'UNK-1',
|
||||
'Africa/Freetown': 'GMT0',
|
||||
'Africa/Gaborone': 'CAT-2',
|
||||
'Africa/Harare': 'CAT-2',
|
||||
'Africa/Johannesburg': 'SAST-2',
|
||||
'Africa/Juba': 'EAT-3',
|
||||
'Africa/Kampala': 'EAT-3',
|
||||
'Africa/Khartoum': 'CAT-2',
|
||||
'Africa/Kigali': 'CAT-2',
|
||||
'Africa/Kinshasa': 'WAT-1',
|
||||
'Africa/Lagos': 'WAT-1',
|
||||
'Africa/Libreville': 'WAT-1',
|
||||
'Africa/Lome': 'GMT0',
|
||||
'Africa/Luanda': 'WAT-1',
|
||||
'Africa/Lubumbashi': 'CAT-2',
|
||||
'Africa/Lusaka': 'CAT-2',
|
||||
'Africa/Malabo': 'WAT-1',
|
||||
'Africa/Maputo': 'CAT-2',
|
||||
'Africa/Maseru': 'SAST-2',
|
||||
'Africa/Mbabane': 'SAST-2',
|
||||
'Africa/Mogadishu': 'EAT-3',
|
||||
'Africa/Monrovia': 'GMT0',
|
||||
'Africa/Nairobi': 'EAT-3',
|
||||
'Africa/Ndjamena': 'WAT-1',
|
||||
'Africa/Niamey': 'WAT-1',
|
||||
'Africa/Nouakchott': 'GMT0',
|
||||
'Africa/Ouagadougou': 'GMT0',
|
||||
'Africa/Porto-Novo': 'WAT-1',
|
||||
'Africa/Sao_Tome': 'GMT0',
|
||||
'Africa/Tripoli': 'EET-2',
|
||||
'Africa/Tunis': 'CET-1',
|
||||
'Africa/Windhoek': 'CAT-2',
|
||||
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
|
||||
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Anguilla': 'AST4',
|
||||
'America/Antigua': 'AST4',
|
||||
'America/Araguaina': 'UNK3',
|
||||
'America/Argentina/Buenos_Aires': 'UNK3',
|
||||
'America/Argentina/Catamarca': 'UNK3',
|
||||
'America/Argentina/Cordoba': 'UNK3',
|
||||
'America/Argentina/Jujuy': 'UNK3',
|
||||
'America/Argentina/La_Rioja': 'UNK3',
|
||||
'America/Argentina/Mendoza': 'UNK3',
|
||||
'America/Argentina/Rio_Gallegos': 'UNK3',
|
||||
'America/Argentina/Salta': 'UNK3',
|
||||
'America/Argentina/San_Juan': 'UNK3',
|
||||
'America/Argentina/San_Luis': 'UNK3',
|
||||
'America/Argentina/Tucuman': 'UNK3',
|
||||
'America/Argentina/Ushuaia': 'UNK3',
|
||||
'America/Aruba': 'AST4',
|
||||
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
|
||||
'America/Atikokan': 'EST5',
|
||||
'America/Bahia': 'UNK3',
|
||||
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Barbados': 'AST4',
|
||||
'America/Belem': 'UNK3',
|
||||
'America/Belize': 'CST6',
|
||||
'America/Blanc-Sablon': 'AST4',
|
||||
'America/Boa_Vista': 'UNK4',
|
||||
'America/Bogota': 'UNK5',
|
||||
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Campo_Grande': 'UNK4',
|
||||
'America/Cancun': 'EST5',
|
||||
'America/Caracas': 'UNK4',
|
||||
'America/Cayenne': 'UNK3',
|
||||
'America/Cayman': 'EST5',
|
||||
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
|
||||
'America/Costa_Rica': 'CST6',
|
||||
'America/Creston': 'MST7',
|
||||
'America/Cuiaba': 'UNK4',
|
||||
'America/Curacao': 'AST4',
|
||||
'America/Danmarkshavn': 'GMT0',
|
||||
'America/Dawson': 'MST7',
|
||||
'America/Dawson_Creek': 'MST7',
|
||||
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Dominica': 'AST4',
|
||||
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Eirunepe': 'UNK5',
|
||||
'America/El_Salvador': 'CST6',
|
||||
'America/Fort_Nelson': 'MST7',
|
||||
'America/Fortaleza': 'UNK3',
|
||||
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
|
||||
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Grenada': 'AST4',
|
||||
'America/Guadeloupe': 'AST4',
|
||||
'America/Guatemala': 'CST6',
|
||||
'America/Guayaquil': 'UNK5',
|
||||
'America/Guyana': 'UNK4',
|
||||
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
|
||||
'America/Hermosillo': 'MST7',
|
||||
'America/Indiana/Indianapolis': 'EST5EDT,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/Petersburg': 'EST5EDT,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/Vincennes': '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/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Jamaica': 'EST5',
|
||||
'America/Juneau': 'AKST9AKDT,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/Kralendijk': 'AST4',
|
||||
'America/La_Paz': 'UNK4',
|
||||
'America/Lima': 'UNK5',
|
||||
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Lower_Princes': 'AST4',
|
||||
'America/Maceio': 'UNK3',
|
||||
'America/Managua': 'CST6',
|
||||
'America/Manaus': 'UNK4',
|
||||
'America/Marigot': 'AST4',
|
||||
'America/Martinique': 'AST4',
|
||||
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
|
||||
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Miquelon': 'UNK3UNK,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/Montevideo': 'UNK3',
|
||||
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Montserrat': 'AST4',
|
||||
'America/Nassau': '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/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Noronha': 'UNK2',
|
||||
'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/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Panama': 'EST5',
|
||||
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Paramaribo': 'UNK3',
|
||||
'America/Phoenix': 'MST7',
|
||||
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Port_of_Spain': 'AST4',
|
||||
'America/Porto_Velho': 'UNK4',
|
||||
'America/Puerto_Rico': 'AST4',
|
||||
'America/Punta_Arenas': 'UNK3',
|
||||
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Recife': 'UNK3',
|
||||
'America/Regina': 'CST6',
|
||||
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Rio_Branco': 'UNK5',
|
||||
'America/Santarem': 'UNK3',
|
||||
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
|
||||
'America/Santo_Domingo': 'AST4',
|
||||
'America/Sao_Paulo': 'UNK3',
|
||||
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/St_Barthelemy': 'AST4',
|
||||
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
|
||||
'America/St_Kitts': 'AST4',
|
||||
'America/St_Lucia': 'AST4',
|
||||
'America/St_Thomas': 'AST4',
|
||||
'America/St_Vincent': 'AST4',
|
||||
'America/Swift_Current': 'CST6',
|
||||
'America/Tegucigalpa': 'CST6',
|
||||
'America/Thule': 'AST4ADT,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/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Tortola': 'AST4',
|
||||
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Whitehorse': 'MST7',
|
||||
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'Antarctica/Casey': 'UNK-8',
|
||||
'Antarctica/Davis': 'UNK-7',
|
||||
'Antarctica/DumontDUrville': 'UNK-10',
|
||||
'Antarctica/Macquarie': 'UNK-11',
|
||||
'Antarctica/Mawson': 'UNK-5',
|
||||
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||
'Antarctica/Palmer': 'UNK3',
|
||||
'Antarctica/Rothera': 'UNK3',
|
||||
'Antarctica/Syowa': 'UNK-3',
|
||||
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
|
||||
'Antarctica/Vostok': 'UNK-6',
|
||||
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Asia/Aden': 'UNK-3',
|
||||
'Asia/Almaty': 'UNK-6',
|
||||
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
|
||||
'Asia/Anadyr': 'UNK-12',
|
||||
'Asia/Aqtau': 'UNK-5',
|
||||
'Asia/Aqtobe': 'UNK-5',
|
||||
'Asia/Ashgabat': 'UNK-5',
|
||||
'Asia/Atyrau': 'UNK-5',
|
||||
'Asia/Baghdad': 'UNK-3',
|
||||
'Asia/Bahrain': 'UNK-3',
|
||||
'Asia/Baku': 'UNK-4',
|
||||
'Asia/Bangkok': 'UNK-7',
|
||||
'Asia/Barnaul': 'UNK-7',
|
||||
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
|
||||
'Asia/Bishkek': 'UNK-6',
|
||||
'Asia/Brunei': 'UNK-8',
|
||||
'Asia/Chita': 'UNK-9',
|
||||
'Asia/Choibalsan': 'UNK-8',
|
||||
'Asia/Colombo': 'UNK-5:30',
|
||||
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
|
||||
'Asia/Dhaka': 'UNK-6',
|
||||
'Asia/Dili': 'UNK-9',
|
||||
'Asia/Dubai': 'UNK-4',
|
||||
'Asia/Dushanbe': 'UNK-5',
|
||||
'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/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||
'Asia/Ho_Chi_Minh': 'UNK-7',
|
||||
'Asia/Hong_Kong': 'HKT-8',
|
||||
'Asia/Hovd': 'UNK-7',
|
||||
'Asia/Irkutsk': 'UNK-8',
|
||||
'Asia/Jakarta': 'WIB-7',
|
||||
'Asia/Jayapura': 'WIT-9',
|
||||
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
|
||||
'Asia/Kabul': 'UNK-4:30',
|
||||
'Asia/Kamchatka': 'UNK-12',
|
||||
'Asia/Karachi': 'PKT-5',
|
||||
'Asia/Kathmandu': 'UNK-5:45',
|
||||
'Asia/Khandyga': 'UNK-9',
|
||||
'Asia/Kolkata': 'IST-5:30',
|
||||
'Asia/Krasnoyarsk': 'UNK-7',
|
||||
'Asia/Kuala_Lumpur': 'UNK-8',
|
||||
'Asia/Kuching': 'UNK-8',
|
||||
'Asia/Kuwait': 'UNK-3',
|
||||
'Asia/Macau': 'CST-8',
|
||||
'Asia/Magadan': 'UNK-11',
|
||||
'Asia/Makassar': 'WITA-8',
|
||||
'Asia/Manila': 'PST-8',
|
||||
'Asia/Muscat': 'UNK-4',
|
||||
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Asia/Novokuznetsk': 'UNK-7',
|
||||
'Asia/Novosibirsk': 'UNK-7',
|
||||
'Asia/Omsk': 'UNK-6',
|
||||
'Asia/Oral': 'UNK-5',
|
||||
'Asia/Phnom_Penh': 'UNK-7',
|
||||
'Asia/Pontianak': 'WIB-7',
|
||||
'Asia/Pyongyang': 'KST-9',
|
||||
'Asia/Qatar': 'UNK-3',
|
||||
'Asia/Qyzylorda': 'UNK-5',
|
||||
'Asia/Riyadh': 'UNK-3',
|
||||
'Asia/Sakhalin': 'UNK-11',
|
||||
'Asia/Samarkand': 'UNK-5',
|
||||
'Asia/Seoul': 'KST-9',
|
||||
'Asia/Shanghai': 'CST-8',
|
||||
'Asia/Singapore': 'UNK-8',
|
||||
'Asia/Srednekolymsk': 'UNK-11',
|
||||
'Asia/Taipei': 'CST-8',
|
||||
'Asia/Tashkent': 'UNK-5',
|
||||
'Asia/Tbilisi': 'UNK-4',
|
||||
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
|
||||
'Asia/Thimphu': 'UNK-6',
|
||||
'Asia/Tokyo': 'JST-9',
|
||||
'Asia/Tomsk': 'UNK-7',
|
||||
'Asia/Ulaanbaatar': 'UNK-8',
|
||||
'Asia/Urumqi': 'UNK-6',
|
||||
'Asia/Ust-Nera': 'UNK-10',
|
||||
'Asia/Vientiane': 'UNK-7',
|
||||
'Asia/Vladivostok': 'UNK-10',
|
||||
'Asia/Yakutsk': 'UNK-9',
|
||||
'Asia/Yangon': 'UNK-6:30',
|
||||
'Asia/Yekaterinburg': 'UNK-5',
|
||||
'Asia/Yerevan': 'UNK-4',
|
||||
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Cape_Verde': 'UNK1',
|
||||
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Reykjavik': 'GMT0',
|
||||
'Atlantic/South_Georgia': 'UNK2',
|
||||
'Atlantic/St_Helena': 'GMT0',
|
||||
'Atlantic/Stanley': 'UNK3',
|
||||
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Brisbane': 'AEST-10',
|
||||
'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/Darwin': 'ACST-9:30',
|
||||
'Australia/Eucla': 'UNK-8:45',
|
||||
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Lindeman': 'AEST-10',
|
||||
'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/Perth': 'AWST-8',
|
||||
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Etc/GMT': 'GMT0',
|
||||
'Etc/GMT+0': 'GMT0',
|
||||
'Etc/GMT+1': 'UNK1',
|
||||
'Etc/GMT+10': 'UNK10',
|
||||
'Etc/GMT+11': 'UNK11',
|
||||
'Etc/GMT+12': 'UNK12',
|
||||
'Etc/GMT+2': 'UNK2',
|
||||
'Etc/GMT+3': 'UNK3',
|
||||
'Etc/GMT+4': 'UNK4',
|
||||
'Etc/GMT+5': 'UNK5',
|
||||
'Etc/GMT+6': 'UNK6',
|
||||
'Etc/GMT+7': 'UNK7',
|
||||
'Etc/GMT+8': 'UNK8',
|
||||
'Etc/GMT+9': 'UNK9',
|
||||
'Etc/GMT-0': 'GMT0',
|
||||
'Etc/GMT-1': 'UNK-1',
|
||||
'Etc/GMT-10': 'UNK-10',
|
||||
'Etc/GMT-11': 'UNK-11',
|
||||
'Etc/GMT-12': 'UNK-12',
|
||||
'Etc/GMT-13': 'UNK-13',
|
||||
'Etc/GMT-14': 'UNK-14',
|
||||
'Etc/GMT-2': 'UNK-2',
|
||||
'Etc/GMT-3': 'UNK-3',
|
||||
'Etc/GMT-4': 'UNK-4',
|
||||
'Etc/GMT-5': 'UNK-5',
|
||||
'Etc/GMT-6': 'UNK-6',
|
||||
'Etc/GMT-7': 'UNK-7',
|
||||
'Etc/GMT-8': 'UNK-8',
|
||||
'Etc/GMT-9': 'UNK-9',
|
||||
'Etc/GMT0': 'GMT0',
|
||||
'Etc/Greenwich': 'GMT0',
|
||||
'Etc/UCT': 'UTC0',
|
||||
'Etc/UTC': 'UTC0',
|
||||
'Etc/Universal': 'UTC0',
|
||||
'Etc/Zulu': 'UTC0',
|
||||
'Europe/Amsterdam': '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/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Belgrade': '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/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Budapest': '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/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
|
||||
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'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/Istanbul': 'UNK-3',
|
||||
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Kaliningrad': 'EET-2',
|
||||
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Kirov': 'UNK-3',
|
||||
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Luxembourg': '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/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Minsk': 'UNK-3',
|
||||
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Moscow': 'MSK-3',
|
||||
'Europe/Oslo': '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/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Samara': 'UNK-4',
|
||||
'Europe/San_Marino': '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/Simferopol': 'MSK-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/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Ulyanovsk': 'UNK-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/Vatican': '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/Volgograd': 'UNK-4',
|
||||
'Europe/Warsaw': '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/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Indian/Antananarivo': 'EAT-3',
|
||||
'Indian/Chagos': 'UNK-6',
|
||||
'Indian/Christmas': 'UNK-7',
|
||||
'Indian/Cocos': 'UNK-6:30',
|
||||
'Indian/Comoro': 'EAT-3',
|
||||
'Indian/Kerguelen': 'UNK-5',
|
||||
'Indian/Mahe': 'UNK-4',
|
||||
'Indian/Maldives': 'UNK-5',
|
||||
'Indian/Mauritius': 'UNK-4',
|
||||
'Indian/Mayotte': 'EAT-3',
|
||||
'Indian/Reunion': 'UNK-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/Bougainville': 'UNK-11',
|
||||
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
|
||||
'Pacific/Chuuk': 'UNK-10',
|
||||
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
|
||||
'Pacific/Efate': 'UNK-11',
|
||||
'Pacific/Enderbury': 'UNK-13',
|
||||
'Pacific/Fakaofo': 'UNK-13',
|
||||
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
|
||||
'Pacific/Funafuti': 'UNK-12',
|
||||
'Pacific/Galapagos': 'UNK6',
|
||||
'Pacific/Gambier': 'UNK9',
|
||||
'Pacific/Guadalcanal': 'UNK-11',
|
||||
'Pacific/Guam': 'ChST-10',
|
||||
'Pacific/Honolulu': 'HST10',
|
||||
'Pacific/Kiritimati': 'UNK-14',
|
||||
'Pacific/Kosrae': 'UNK-11',
|
||||
'Pacific/Kwajalein': 'UNK-12',
|
||||
'Pacific/Majuro': 'UNK-12',
|
||||
'Pacific/Marquesas': 'UNK9:30',
|
||||
'Pacific/Midway': 'SST11',
|
||||
'Pacific/Nauru': 'UNK-12',
|
||||
'Pacific/Niue': 'UNK11',
|
||||
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
|
||||
'Pacific/Noumea': 'UNK-11',
|
||||
'Pacific/Pago_Pago': 'SST11',
|
||||
'Pacific/Palau': 'UNK-9',
|
||||
'Pacific/Pitcairn': 'UNK8',
|
||||
'Pacific/Pohnpei': 'UNK-11',
|
||||
'Pacific/Port_Moresby': 'UNK-10',
|
||||
'Pacific/Rarotonga': 'UNK10',
|
||||
'Pacific/Saipan': 'ChST-10',
|
||||
'Pacific/Tahiti': 'UNK10',
|
||||
'Pacific/Tarawa': 'UNK-12',
|
||||
'Pacific/Tongatapu': 'UNK-13',
|
||||
'Pacific/Wake': 'UNK-12',
|
||||
'Pacific/Wallis': 'UNK-12'
|
||||
};
|
||||
|
||||
export function selectedTimeZone(label: string, format: string) {
|
||||
return TIME_ZONES[label] === format ? label : undefined;
|
||||
}
|
||||
|
||||
export function timeZoneSelectItems() {
|
||||
return Object.keys(TIME_ZONES).map(label => (
|
||||
<MenuItem key={label} value={label}>{label}</MenuItem>
|
||||
return Object.keys(TIME_ZONES).map((label) => (
|
||||
<MenuItem key={label} value={label}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import parseMilliseconds from 'parse-ms';
|
||||
|
||||
const LOCALE_FORMAT = new Intl.DateTimeFormat(
|
||||
[...window.navigator.languages],
|
||||
{
|
||||
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
@@ -10,23 +8,22 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat(
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: false
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const formatDateTime = (dateTime: string) => {
|
||||
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
|
||||
}
|
||||
};
|
||||
|
||||
export const formatLocalDateTime = (date: Date) => {
|
||||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||
.toISOString()
|
||||
.slice(0, -1)
|
||||
.substr(0, 19);
|
||||
}
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: number) => {
|
||||
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
||||
var formatted = '';
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += pluralize(days, 'day');
|
||||
}
|
||||
@@ -40,6 +37,7 @@ export const formatDuration = (duration: number) => {
|
||||
formatted += pluralize(seconds, 'second');
|
||||
}
|
||||
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 : ''} `;
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
type BoardProfiles = {
|
||||
[name: string]: string
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
export const BOARD_PROFILES: BoardProfiles = {
|
||||
"S32": "BBQKees Gateway S32",
|
||||
"E32": "BBQKees Gateway E32",
|
||||
"NODEMCU": "NodeMCU 32S",
|
||||
"MT-ET": "MT-ET Live D1 Mini",
|
||||
"LOLIN": "Lolin D32",
|
||||
"OLIMEX": "Olimex ESP32-EVB",
|
||||
"TLK110": "Generic Ethernet (TLK110)",
|
||||
"LAN8720": "Generic Ethernet (LAN8720)"
|
||||
}
|
||||
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>
|
||||
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 { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
@@ -12,30 +12,46 @@ import EMSESPDevicesController from './EMSESPDevicesController';
|
||||
import EMSESPHelp from './EMSESPHelp';
|
||||
|
||||
class EMSESP extends Component<RouteComponentProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Dashboard">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value={`/${PROJECT_PATH}/devices`} label="Devices & Sensors" />
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab
|
||||
value={`/${PROJECT_PATH}/devices`}
|
||||
label="Devices & Sensors"
|
||||
/>
|
||||
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
||||
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/devices`} component={EMSESPDevicesController} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/help`} component={EMSESPHelp} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/devices`}
|
||||
component={EMSESPDevicesController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/status`}
|
||||
component={EMSESPStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/help`}
|
||||
component={EMSESPHelp}
|
||||
/>
|
||||
<Redirect to={`/${PROJECT_PATH}/devices`} />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EMSESP;
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
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";
|
||||
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">
|
||||
<SectionContent title="Devices & Sensors">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <EMSESPDevicesForm {...formProps} />}
|
||||
render={(formProps) => <EMSESPDevicesForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,83 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { withStyles, Theme, createStyles } from "@material-ui/core/styles";
|
||||
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";
|
||||
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 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 } from "../components";
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
import { EMSESPDevices, EMSESPDeviceData, Device } from "./EMSESPtypes";
|
||||
import { RestFormProps, FormButton, extractEventValue } from '../components';
|
||||
|
||||
import { ENDPOINT_ROOT } from "../api";
|
||||
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
|
||||
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
|
||||
import {
|
||||
EMSESPDevices,
|
||||
EMSESPDeviceData,
|
||||
Device,
|
||||
DeviceValue,
|
||||
DeviceValueUOM,
|
||||
DeviceValueUOM_s
|
||||
} from './EMSESPtypes';
|
||||
|
||||
import ValueForm from './ValueForm';
|
||||
|
||||
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';
|
||||
|
||||
const StyledTableCell = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
head: {
|
||||
backgroundColor: theme.palette.common.black,
|
||||
color: theme.palette.common.white,
|
||||
color: theme.palette.common.white
|
||||
},
|
||||
body: {
|
||||
fontSize: 14,
|
||||
},
|
||||
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;
|
||||
@@ -44,30 +92,118 @@ interface EMSESPDevicesFormState {
|
||||
confirmScanDevices: boolean;
|
||||
processing: boolean;
|
||||
deviceData?: EMSESPDeviceData;
|
||||
selectedDevice?: number;
|
||||
edit_devicevalue?: DeviceValue;
|
||||
}
|
||||
|
||||
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> &
|
||||
AuthenticatedContextProps &
|
||||
WithWidthProps;
|
||||
|
||||
function formatTemp(t: string) {
|
||||
if (t == null) {
|
||||
return "n/a";
|
||||
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) {
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDuration(value * 60) : '0 hours';
|
||||
case DeviceValueUOM.MINUTES:
|
||||
return value ? formatDuration(value) : '0 minutes';
|
||||
case DeviceValueUOM.NONE:
|
||||
return value;
|
||||
case DeviceValueUOM.NUM:
|
||||
return new Intl.NumberFormat().format(value);
|
||||
case DeviceValueUOM.BOOLEAN:
|
||||
return value ? 'on' : 'off';
|
||||
default:
|
||||
return (
|
||||
new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]
|
||||
);
|
||||
}
|
||||
return t + " °C";
|
||||
}
|
||||
|
||||
function formatUnit(u: string) {
|
||||
if (u == null) {
|
||||
return u;
|
||||
}
|
||||
return " " + u;
|
||||
}
|
||||
|
||||
class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesFormState> {
|
||||
class EMSESPDevicesForm extends Component<
|
||||
EMSESPDevicesFormProps,
|
||||
EMSESPDevicesFormState
|
||||
> {
|
||||
state: EMSESPDevicesFormState = {
|
||||
confirmScanDevices: false,
|
||||
processing: false,
|
||||
processing: false
|
||||
};
|
||||
|
||||
handleValueChange = (name: keyof DeviceValue) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({
|
||||
edit_devicevalue: {
|
||||
...this.state.edit_devicevalue!,
|
||||
[name]: extractEventValue(event)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cancelEditingValue = () => {
|
||||
this.setState({ edit_devicevalue: undefined });
|
||||
};
|
||||
|
||||
doneEditingValue = () => {
|
||||
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'
|
||||
});
|
||||
} 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 });
|
||||
};
|
||||
|
||||
noDevices = () => {
|
||||
@@ -93,30 +229,27 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
{!this.noDevices() && (
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
||||
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)}
|
||||
onClick={() => this.handleRowClick(device)}
|
||||
>
|
||||
<TableCell component="th" scope="row">
|
||||
<Tooltip
|
||||
title="click for details..."
|
||||
arrow
|
||||
<TableCell>
|
||||
<CustomTooltip
|
||||
title={
|
||||
'DeviceID:0x' +
|
||||
(
|
||||
'00' + device.deviceid.toString(16).toUpperCase()
|
||||
).slice(-2) +
|
||||
' ProductID:' +
|
||||
device.productid +
|
||||
' Version:' +
|
||||
device.version
|
||||
}
|
||||
placement="right-end"
|
||||
>
|
||||
<Button
|
||||
@@ -126,19 +259,11 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
>
|
||||
{device.type}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</CustomTooltip>
|
||||
</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 align="right">
|
||||
{device.brand + ' ' + device.name}{' '}
|
||||
</TableCell>
|
||||
<TableCell align="center">{device.productid}</TableCell>
|
||||
<TableCell align="center">{device.version}</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -187,7 +312,7 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
</TableCell>
|
||||
<TableCell align="center">{sensorData.id}</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatTemp(sensorData.temp)}
|
||||
{formatValue(sensorData.temp, DeviceValueUOM.DEGREES)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -210,10 +335,12 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
<Dialog
|
||||
open={this.state.confirmScanDevices}
|
||||
onClose={this.onScanDevicesRejected}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
Are you sure you want to initiate a scan on the EMS bus for all new
|
||||
Are you sure you want to start a scan on the EMS bus for all new
|
||||
devices?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -252,44 +379,45 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar("Device scan is starting...", {
|
||||
variant: "info",
|
||||
this.props.enqueueSnackbar('Device scan is starting...', {
|
||||
variant: 'info'
|
||||
});
|
||||
this.setState({ processing: false, confirmScanDevices: false });
|
||||
} else {
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(error.message || "Problem with scan", {
|
||||
variant: "error",
|
||||
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ processing: false, confirmScanDevices: false });
|
||||
});
|
||||
};
|
||||
|
||||
handleRowClick = (id: any) => {
|
||||
this.setState({ deviceData: undefined });
|
||||
handleRowClick = (device: any) => {
|
||||
this.setState({ selectedDevice: device.id, deviceData: undefined });
|
||||
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id: id }),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: device.id }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
throw Error("Unexpected response code: " + response.status);
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
.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" }
|
||||
error.message || 'Problem getting device data',
|
||||
{ variant: 'error' }
|
||||
);
|
||||
this.setState({ deviceData: undefined });
|
||||
});
|
||||
@@ -298,6 +426,7 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
renderDeviceData() {
|
||||
const { deviceData } = this.state;
|
||||
const { width } = this.props;
|
||||
const me = this.props.authenticatedContext.me;
|
||||
|
||||
if (this.noDevices()) {
|
||||
return;
|
||||
@@ -319,22 +448,37 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
<TableContainer>
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
||||
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>
|
||||
{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)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
})}
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -346,11 +490,12 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Fragment >
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { edit_devicevalue } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<br></br>
|
||||
@@ -373,7 +518,6 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={this.onScanDevices}
|
||||
>
|
||||
Scan Devices
|
||||
@@ -381,6 +525,14 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
</Box>
|
||||
</Box>
|
||||
{this.renderScanDevicesDialog()}
|
||||
{edit_devicevalue && (
|
||||
<ValueForm
|
||||
devicevalue={edit_devicevalue}
|
||||
onDoneEditing={this.doneEditingValue}
|
||||
onCancelEditing={this.cancelEditingValue}
|
||||
handleValueChange={this.handleValueChange}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
import React, { 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 CommentIcon from "@material-ui/icons/CommentTwoTone";
|
||||
import MenuBookIcon from "@material-ui/icons/MenuBookTwoTone";
|
||||
import GitHubIcon from "@material-ui/icons/GitHub";
|
||||
import StarIcon from "@material-ui/icons/Star";
|
||||
import ImportExportIcon from "@material-ui/icons/ImportExport";
|
||||
import BugReportIcon from "@material-ui/icons/BugReportTwoTone";
|
||||
import CommentIcon from '@material-ui/icons/CommentTwoTone';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBookTwoTone';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import ImportExportIcon from '@material-ui/icons/ImportExport';
|
||||
import BugReportIcon from '@material-ui/icons/BugReportTwoTone';
|
||||
|
||||
export const WebAPISystemSettings = window.location.origin + "/api?device=system&cmd=settings";
|
||||
export const WebAPISystemInfo = window.location.origin + "/api?device=system&cmd=info";
|
||||
export const WebAPISystemSettings =
|
||||
window.location.origin + '/api/system/settings';
|
||||
export const WebAPISystemInfo = window.location.origin + '/api/system/info';
|
||||
|
||||
class EMSESPHelp extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title='EMS-ESP Help' titleGutter>
|
||||
|
||||
<SectionContent title="EMS-ESP Help" titleGutter>
|
||||
<List>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<MenuBookIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For the latest news and updates go to the <Link href="https://emsesp.github.io/docs" color="primary">{'official documentation'} website</Link>
|
||||
For the latest news and updates go to the{' '}
|
||||
<Link href="https://emsesp.github.io/docs" color="primary">
|
||||
{'official documentation'} website
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
@@ -34,7 +43,10 @@ class EMSESPHelp extends Component {
|
||||
<CommentIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For live community chat join our <Link href="https://discord.gg/3J3GgnzpyT" color="primary">{'Discord'} server</Link>
|
||||
For live community chat join our{' '}
|
||||
<Link href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||
{'Discord'} server
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
@@ -43,7 +55,13 @@ class EMSESPHelp extends Component {
|
||||
<GitHubIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
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>
|
||||
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>
|
||||
</ListItem>
|
||||
|
||||
@@ -52,34 +70,41 @@ class EMSESPHelp extends Component {
|
||||
<ImportExportIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
To list your system settings <Link target="_blank" href={WebAPISystemSettings} color="primary">{'click here'}</Link>
|
||||
To export 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>
|
||||
To export the current status of EMS-ESP{' '}
|
||||
<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>.
|
||||
<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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
@@ -10,26 +10,31 @@ import { AuthenticatedRoute } from '../authentication';
|
||||
import EMSESPSettingsController from './EMSESPSettingsController';
|
||||
|
||||
class EMSESP extends Component<RouteComponentProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<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" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/settings`} component={EMSESPSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/settings`}
|
||||
component={EMSESPSettingsController}
|
||||
/>
|
||||
<Redirect to={`/${PROJECT_PATH}/settings`} />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EMSESP;
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
// import { Container } from '@material-ui/core';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPSettingsForm from './EMSESPSettingsForm';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
|
||||
import { EMSESPSettings } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings';
|
||||
|
||||
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
||||
|
||||
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
// <Container maxWidth="md" disableGutters>
|
||||
<SectionContent title='' titleGutter>
|
||||
<SectionContent title="" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => (
|
||||
<EMSESPSettingsForm {...formProps} />
|
||||
)}
|
||||
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
// </Container>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
|
||||
export default restController(
|
||||
EMSESP_SETTINGS_ENDPOINT,
|
||||
EMSESPSettingsController
|
||||
);
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
import React from 'react';
|
||||
import { ValidatorForm, TextValidator, SelectValidator } from 'react-material-ui-form-validator';
|
||||
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 { Checkbox, Typography, Box, Link, withWidth, WithWidthProps } from '@material-ui/core';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication";
|
||||
|
||||
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
BlockFormControlLabel
|
||||
} from '../components';
|
||||
|
||||
import { isIP, optional } from '../validators';
|
||||
|
||||
@@ -17,20 +38,21 @@ import { EMSESPSettings } from './EMSESPtypes';
|
||||
|
||||
import { boardProfileSelectItems } from './EMSESPBoardProfiles';
|
||||
|
||||
import { ENDPOINT_ROOT } from "../api";
|
||||
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile';
|
||||
|
||||
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> & AuthenticatedContextProps & WithWidthProps;
|
||||
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
|
||||
AuthenticatedContextProps &
|
||||
WithWidthProps;
|
||||
|
||||
interface EMSESPSettingsFormState {
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
|
||||
class EMSESPSettingsForm extends Component<EMSESPSettingsFormProps> {
|
||||
state: EMSESPSettingsFormState = {
|
||||
processing: false
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
|
||||
@@ -43,25 +65,24 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
board_profile: event.target.value
|
||||
});
|
||||
|
||||
if (event.target.value === "CUSTOM")
|
||||
return;
|
||||
if (event.target.value === 'CUSTOM') return;
|
||||
|
||||
this.setState({ processing: true });
|
||||
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code: event.target.value }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Unexpected response code: " + response.status);
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
this.props.enqueueSnackbar("Profile loaded", { variant: 'success' });
|
||||
this.props.enqueueSnackbar('Profile loaded', { variant: 'success' });
|
||||
setData({
|
||||
...data,
|
||||
led_gpio: json.led_gpio,
|
||||
@@ -75,8 +96,8 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || "Problem fetching board profile",
|
||||
{ variant: "warning" }
|
||||
error.message || 'Problem fetching board profile',
|
||||
{ variant: 'warning' }
|
||||
);
|
||||
this.setState({ processing: false });
|
||||
});
|
||||
@@ -88,24 +109,40 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
<ValidatorForm onSubmit={saveData}>
|
||||
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
|
||||
<Typography variant="body1">
|
||||
Modify any of the EMS-ESP settings here. For help refer to the <Link target="_blank" href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings" color="primary">{'online documentation'}</Link>.
|
||||
Adjust any of the EMS-ESP settings here. For help refer to the{' '}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
|
||||
color="primary"
|
||||
>
|
||||
{'online documentation'}
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary" >
|
||||
<Typography variant="h6" color="primary">
|
||||
EMS Bus
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={1} direction="row" justify="flex-start" alignItems="flex-start">
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={5}>
|
||||
<SelectValidator name="tx_mode"
|
||||
<SelectValidator
|
||||
name="tx_mode"
|
||||
label="Tx Mode"
|
||||
value={data.tx_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('tx_mode')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>Off</MenuItem>
|
||||
<MenuItem value={1}>EMS</MenuItem>
|
||||
<MenuItem value={2}>EMS+</MenuItem>
|
||||
@@ -114,24 +151,36 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<SelectValidator name="ems_bus_id"
|
||||
<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>
|
||||
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"]}
|
||||
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
|
||||
@@ -145,33 +194,58 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</Grid>
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary" >
|
||||
<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, or set your own custom configuration</i>
|
||||
<i>
|
||||
Select a pre-configured board layout to automatically set the GPIO
|
||||
pins, or set your own custom configuration
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<SelectValidator name="board_profile"
|
||||
<SelectValidator
|
||||
name="board_profile"
|
||||
label="Board Profile"
|
||||
value={data.board_profile}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={this.changeBoardProfile}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
{boardProfileSelectItems()}
|
||||
<MenuItem key={"CUSTOM"} value={"CUSTOM"}>Custom...</MenuItem>
|
||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||
Custom...
|
||||
</MenuItem>
|
||||
</SelectValidator>
|
||||
|
||||
{ (data.board_profile === "CUSTOM") &&
|
||||
<Grid container spacing={1} direction="row" justify="flex-start" alignItems="flex-start">
|
||||
{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"]}
|
||||
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
|
||||
@@ -184,8 +258,20 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</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"]}
|
||||
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
|
||||
@@ -198,8 +284,20 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</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"]}
|
||||
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
|
||||
@@ -212,8 +310,20 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</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"]}
|
||||
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
|
||||
@@ -226,8 +336,20 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</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"]}
|
||||
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
|
||||
@@ -239,14 +361,27 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
)}
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary" >
|
||||
<Typography variant="h6" color="primary">
|
||||
Options
|
||||
</Typography>
|
||||
|
||||
{ data.dallas_gpio !== 0 &&
|
||||
{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
|
||||
@@ -257,52 +392,17 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
}
|
||||
label="Enable Dallas parasite mode"
|
||||
/>
|
||||
}
|
||||
{ data.led_gpio !== 0 &&
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.hide_led}
|
||||
onChange={handleValueChange('hide_led')}
|
||||
value="hide_led"
|
||||
/>
|
||||
}
|
||||
label = "Hide LED"
|
||||
/>
|
||||
}
|
||||
|
||||
<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"
|
||||
checked={data.notoken_api}
|
||||
onChange={handleValueChange('notoken_api')}
|
||||
value="notoken_api"
|
||||
/>
|
||||
}
|
||||
label="Shower Timer"
|
||||
/>
|
||||
{/* <BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_alert}
|
||||
onChange={handleValueChange('shower_alert')}
|
||||
value="shower_alert"
|
||||
/>
|
||||
}
|
||||
label="Shower Alert"
|
||||
/> */}
|
||||
</Grid>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.api_enabled}
|
||||
onChange={handleValueChange('api_enabled')}
|
||||
value="api_enabled"
|
||||
/>
|
||||
}
|
||||
label="Enable API write commands"
|
||||
label="Bypass Access Token authorization on API calls"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
@@ -314,8 +414,37 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
}
|
||||
label="Enable ADC"
|
||||
/>
|
||||
<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" >
|
||||
<Typography variant="h6" color="primary">
|
||||
Syslog
|
||||
</Typography>
|
||||
|
||||
@@ -330,12 +459,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
label="Enable Syslog"
|
||||
/>
|
||||
|
||||
{ data.syslog_enabled &&
|
||||
<Grid container spacing={1} direction="row" justify="flex-start" alignItems="flex-start">
|
||||
{data.syslog_enabled && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={5}>
|
||||
<TextValidator
|
||||
validators={['isOptionalIP']}
|
||||
errorMessages={["Not a valid IP address"]}
|
||||
errorMessages={['Not a valid IP address']}
|
||||
name="syslog_host"
|
||||
label="IP"
|
||||
fullWidth
|
||||
@@ -347,8 +482,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</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"]}
|
||||
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
|
||||
@@ -360,13 +505,15 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<SelectValidator name="syslog_level"
|
||||
<SelectValidator
|
||||
name="syslog_level"
|
||||
label="Log Level"
|
||||
value={data.syslog_level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('syslog_level')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={-1}>OFF</MenuItem>
|
||||
<MenuItem value={3}>ERR</MenuItem>
|
||||
<MenuItem value={5}>NOTICE</MenuItem>
|
||||
@@ -377,8 +524,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
</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"]}
|
||||
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
|
||||
@@ -400,18 +557,22 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
label="Output EMS telegrams in raw format"
|
||||
/>
|
||||
</Grid>
|
||||
}
|
||||
)}
|
||||
|
||||
<br></br>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm >
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(withWidth()(EMSESPSettingsForm));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Theme } from '@material-ui/core';
|
||||
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) => {
|
||||
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.warning.main;
|
||||
@@ -15,26 +15,25 @@ export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const busStatus = ({ status }: EMSESPStatus) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return "Connected";
|
||||
return 'Connected';
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return "Tx Errors";
|
||||
return 'Tx Errors';
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return "Disconnected";
|
||||
return 'Disconnected';
|
||||
default:
|
||||
return "Unknown";
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const qualityHighlight = ( value: number, theme: Theme) => {
|
||||
export const qualityHighlight = (value: number, theme: Theme) => {
|
||||
if (value >= 95) {
|
||||
return theme.palette.success.main;
|
||||
}
|
||||
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPStatusForm from './EMSESPStatusForm';
|
||||
import { EMSESPStatus } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + "emsespStatus";
|
||||
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'emsespStatus';
|
||||
|
||||
type EMSESPStatusControllerProps = RestControllerProps<EMSESPStatus>;
|
||||
|
||||
class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="EMS Status">
|
||||
<SectionContent title="EMS Status" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <EMSESPStatusForm {...formProps} />}
|
||||
render={(formProps) => <EMSESPStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 {
|
||||
TableContainer,
|
||||
Table,
|
||||
@@ -13,35 +13,32 @@ import {
|
||||
ListItemText,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
isWidthDown,
|
||||
} from "@material-ui/core";
|
||||
isWidthDown
|
||||
} from '@material-ui/core';
|
||||
|
||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar,
|
||||
} from "../components";
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
|
||||
import {
|
||||
busStatus,
|
||||
busStatusHighlight,
|
||||
isConnected,
|
||||
} from "./EMSESPStatus";
|
||||
import { busStatus, busStatusHighlight, isConnected } from './EMSESPStatus';
|
||||
|
||||
import { EMSESPStatus } from "./EMSESPtypes";
|
||||
import { EMSESPStatus } from './EMSESPtypes';
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> & WithTheme & WithWidthProps;
|
||||
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> &
|
||||
WithTheme &
|
||||
WithWidthProps;
|
||||
|
||||
class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
|
||||
createListItems() {
|
||||
const { data, theme, width } = this.props;
|
||||
return (
|
||||
@@ -52,24 +49,30 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
<DeviceHubIcon />
|
||||
</HighlightAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Connection Status" secondary={busStatus(data)} />
|
||||
<ListItemText
|
||||
primary="Connection Status"
|
||||
secondary={busStatus(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
{isConnected(data) && (
|
||||
<TableContainer>
|
||||
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||
>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
# Telegrams Received
|
||||
</TableCell>
|
||||
<TableCell align="right">{formatNumber(data.rx_received)} (quality {data.rx_quality}%)
|
||||
<TableCell># Telegrams Received</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatNumber(data.rx_received)} (quality{' '}
|
||||
{data.rx_quality}%)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell >
|
||||
# Telegrams Sent
|
||||
</TableCell >
|
||||
<TableCell align="right">{formatNumber(data.tx_sent)} (quality {data.tx_quality}%)
|
||||
<TableCell># Telegrams Sent</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatNumber(data.tx_sent)} (quality {data.tx_quality}
|
||||
%)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
@@ -86,7 +89,11 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface EMSESPSettings {
|
||||
dallas_parasite: boolean;
|
||||
led_gpio: number;
|
||||
hide_led: boolean;
|
||||
api_enabled: boolean;
|
||||
notoken_api: boolean;
|
||||
analog_enabled: boolean;
|
||||
pbutton_gpio: number;
|
||||
trace_raw: boolean;
|
||||
@@ -58,7 +58,54 @@ export interface EMSESPDevices {
|
||||
sensors: Sensor[];
|
||||
}
|
||||
|
||||
export interface DeviceValue {
|
||||
v: any;
|
||||
u: number;
|
||||
n: string;
|
||||
c: string;
|
||||
}
|
||||
|
||||
export interface EMSESPDeviceData {
|
||||
name: string;
|
||||
data: string[];
|
||||
data: DeviceValue[];
|
||||
}
|
||||
|
||||
export enum DeviceValueUOM {
|
||||
NONE = 0,
|
||||
DEGREES,
|
||||
PERCENT,
|
||||
LMIN,
|
||||
KWH,
|
||||
WH,
|
||||
HOURS,
|
||||
MINUTES,
|
||||
UA,
|
||||
BAR,
|
||||
KW,
|
||||
W,
|
||||
KB,
|
||||
SECONDS,
|
||||
DBM,
|
||||
NUM,
|
||||
BOOLEAN
|
||||
}
|
||||
|
||||
export const DeviceValueUOM_s = [
|
||||
'',
|
||||
'°C',
|
||||
'%',
|
||||
'l/min',
|
||||
'kWh',
|
||||
'Wh',
|
||||
'hours',
|
||||
'minutes',
|
||||
'uA',
|
||||
'bar',
|
||||
'kW',
|
||||
'W',
|
||||
'KB',
|
||||
'seconds',
|
||||
'dBm',
|
||||
'number',
|
||||
'on/off'
|
||||
];
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import { Component } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
|
||||
|
||||
import TuneIcon from '@material-ui/icons/Tune';
|
||||
import DashboardIcon from "@material-ui/icons/Dashboard";
|
||||
import DashboardIcon from '@material-ui/icons/Dashboard';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
type ProjectProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
@@ -16,13 +19,28 @@ class ProjectMenu extends Component<ProjectProps> {
|
||||
const path = this.props.match.url;
|
||||
return (
|
||||
<List>
|
||||
<ListItem to='/ems-esp/' selected={path.startsWith('/ems-esp/status') || path.startsWith('/ems-esp/devices') || path.startsWith('/ems-esp/help')} button component={Link}>
|
||||
<ListItem
|
||||
to="/ems-esp/"
|
||||
selected={
|
||||
path.startsWith('/ems-esp/status') ||
|
||||
path.startsWith('/ems-esp/devices') ||
|
||||
path.startsWith('/ems-esp/help')
|
||||
}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DashboardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Dashboard" />
|
||||
</ListItem>
|
||||
<ListItem to='/ems-esp/settings' selected={path.startsWith('/ems-esp/settings')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||
<ListItem
|
||||
to="/ems-esp/settings"
|
||||
selected={path.startsWith('/ems-esp/settings')}
|
||||
button
|
||||
component={Link}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<TuneIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch } from 'react-router';
|
||||
|
||||
import { AuthenticatedRoute } from '../authentication';
|
||||
@@ -7,24 +7,32 @@ import EMSESPDashboard from './EMSESPDashboard';
|
||||
import EMSESPSettings from './EMSESPSettings';
|
||||
|
||||
class ProjectRouting extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/ems-esp/status/*" component={EMSESPDashboard} />
|
||||
<AuthenticatedRoute exact path="/ems-esp/settings" component={EMSESPSettings} />
|
||||
<AuthenticatedRoute exact path="/ems-esp/*" component={EMSESPDashboard} />
|
||||
{
|
||||
/*
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/status/*"
|
||||
component={EMSESPDashboard}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/settings"
|
||||
component={EMSESPSettings}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/*"
|
||||
component={EMSESPDashboard}
|
||||
/>
|
||||
{/*
|
||||
* The redirect below caters for the default project route and redirecting invalid paths.
|
||||
* The "to" property must match one of the routes above for this to work correctly.
|
||||
*/
|
||||
}
|
||||
*/}
|
||||
<Redirect to={`/ems-esp/status`} />
|
||||
</Switch>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ProjectRouting;
|
||||
|
||||
121
interface/src/project/ValueForm.tsx
Normal file
121
interface/src/project/ValueForm.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
Typography,
|
||||
FormHelperText,
|
||||
OutlinedInput,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
MenuItem
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
import { DeviceValue, DeviceValueUOM, DeviceValueUOM_s } from './EMSESPtypes';
|
||||
|
||||
interface ValueFormProps {
|
||||
devicevalue: DeviceValue;
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
handleValueChange: (
|
||||
data: keyof DeviceValue
|
||||
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
class ValueForm extends React.Component<ValueFormProps> {
|
||||
formRef: RefObject<any> = React.createRef();
|
||||
|
||||
submit = () => {
|
||||
this.formRef.current.submit();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
devicevalue,
|
||||
handleValueChange,
|
||||
onDoneEditing,
|
||||
onCancelEditing
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||
<Dialog
|
||||
maxWidth="xs"
|
||||
onClose={onCancelEditing}
|
||||
aria-labelledby="user-form-dialog-title"
|
||||
open
|
||||
>
|
||||
<DialogTitle id="user-form-dialog-title">Change Value</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{devicevalue.u !== DeviceValueUOM.BOOLEAN && (
|
||||
<OutlinedInput
|
||||
id="outlined-adornment-value"
|
||||
value={devicevalue.v}
|
||||
autoFocus
|
||||
fullWidth
|
||||
onChange={handleValueChange('v')}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
{DeviceValueUOM_s[devicevalue.u]}
|
||||
</InputAdornment>
|
||||
}
|
||||
aria-describedby="outlined-value-helper-text"
|
||||
inputProps={{
|
||||
'aria-label': 'value'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{devicevalue.u === DeviceValueUOM.BOOLEAN && (
|
||||
<TextField
|
||||
id="outlined-select-value"
|
||||
select
|
||||
value={devicevalue.v}
|
||||
autoFocus
|
||||
fullWidth
|
||||
onChange={handleValueChange('v')}
|
||||
variant="outlined"
|
||||
>
|
||||
<MenuItem value="true">on</MenuItem>
|
||||
<MenuItem value="false">off</MenuItem>
|
||||
</TextField>
|
||||
)}
|
||||
<FormHelperText id="outlined-value-helper-text">
|
||||
{devicevalue.n}
|
||||
</FormHelperText>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={4} mb={0}>
|
||||
<Typography variant="body2">
|
||||
<i>
|
||||
Note: it may take a few seconds before the change is
|
||||
registered with the EMS device.
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onCancelEditing}
|
||||
>
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={this.submit}
|
||||
>
|
||||
Done
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ValueForm;
|
||||
120
interface/src/security/GenerateToken.tsx
Normal file
120
interface/src/security/GenerateToken.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
TextField
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
import { GENERATE_TOKEN_ENDPOINT } from '../api';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
|
||||
interface GenerateTokenProps extends WithSnackbarProps {
|
||||
username: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface GenerateTokenState {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
class GenerateToken extends React.Component<
|
||||
GenerateTokenProps,
|
||||
GenerateTokenState
|
||||
> {
|
||||
state: GenerateTokenState = {};
|
||||
|
||||
componentDidMount() {
|
||||
const { username } = this.props;
|
||||
redirectingAuthorizedFetch(
|
||||
GENERATE_TOKEN_ENDPOINT + '?' + new URLSearchParams({ username }),
|
||||
{ method: 'GET' }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw Error('Error generating token: ' + response.status);
|
||||
}
|
||||
})
|
||||
.then((generatedToken) => {
|
||||
// console.log(generatedToken);
|
||||
this.setState({ token: generatedToken.token });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || 'Problem generating token',
|
||||
{ variant: 'error' }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClose, username } = this.props;
|
||||
const { token } = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
onClose={onClose}
|
||||
aria-labelledby="generate-token-dialog-title"
|
||||
open
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle id="generate-token-dialog-title">
|
||||
Token for: {username}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{token ? (
|
||||
<Fragment>
|
||||
<Box
|
||||
bgcolor="primary.main"
|
||||
color="primary.contrastText"
|
||||
p={2}
|
||||
mt={2}
|
||||
mb={2}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
The token below may be used to access the secured APIs, either
|
||||
as a Bearer authentication in the "Authorization" header or
|
||||
using the "access_token" query parameter.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mt={2} mb={2}>
|
||||
<TextField
|
||||
label="Token"
|
||||
multiline
|
||||
value={token}
|
||||
fullWidth
|
||||
contentEditable={false}
|
||||
/>
|
||||
</Box>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Box m={4} textAlign="center">
|
||||
<LinearProgress />
|
||||
<Typography variant="h6">Generating token…</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withSnackbar(GenerateToken);
|
||||
@@ -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 { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import ManageUsersForm from './ManageUsersForm';
|
||||
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
|
||||
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
|
||||
|
||||
class ManageUsersController extends Component<ManageUsersControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,14 @@ class ManageUsersController extends Component<ManageUsersControllerProps> {
|
||||
<SectionContent title="Manage Users" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <ManageUsersForm {...formProps} />}
|
||||
render={(formProps) => <ManageUsersForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
|
||||
export default restController(
|
||||
SECURITY_SETTINGS_ENDPOINT,
|
||||
ManageUsersController
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user