Deleted .idea/.gitignore, .idea/clion.iml, .idea/misc.xml, .idea/modules.xml, .idea/platformio.iml, .idea/serialmonitor_settings.xml, .idea/vcs.xml, .idea/watcherTasks.xml files
This commit is contained in:
		
							
								
								
									
										16
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
Language: Cpp
 | 
			
		||||
BasedOnStyle: Google
 | 
			
		||||
ColumnLimit: 120
 | 
			
		||||
AllowShortBlocksOnASingleLine: false
 | 
			
		||||
AllowShortFunctionsOnASingleLine: false
 | 
			
		||||
AllowShortIfStatementsOnASingleLine: false
 | 
			
		||||
AllowShortLoopsOnASingleLine: false
 | 
			
		||||
BinPackArguments: false
 | 
			
		||||
BinPackParameters: false
 | 
			
		||||
BreakConstructorInitializers: AfterColon
 | 
			
		||||
AllowAllParametersOfDeclarationOnNextLine: false
 | 
			
		||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
 | 
			
		||||
ExperimentalAutoDetectBinPacking: false
 | 
			
		||||
KeepEmptyLinesAtTheStartOfBlocks: false
 | 
			
		||||
DerivePointerAlignment: false
 | 
			
		||||
SortIncludes: false
 | 
			
		||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,9 @@
 | 
			
		||||
.pio
 | 
			
		||||
CMakeListsPrivate.txt
 | 
			
		||||
.clang_complete
 | 
			
		||||
.gcc-flags.json
 | 
			
		||||
*Thumbs.db
 | 
			
		||||
/data/www
 | 
			
		||||
/lib/framework/WWWData.h
 | 
			
		||||
/interface/build
 | 
			
		||||
/interface/node_modules
 | 
			
		||||
.vscode
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.gitlab-ci.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
image: nikolaik/python-nodejs:python3.9-nodejs15-slim
 | 
			
		||||
 | 
			
		||||
stages:
 | 
			
		||||
  - build
 | 
			
		||||
 | 
			
		||||
cache:
 | 
			
		||||
  paths:
 | 
			
		||||
    - "~/.platformio"
 | 
			
		||||
 | 
			
		||||
before_script:
 | 
			
		||||
  - "pip install -U platformio"
 | 
			
		||||
  - "platformio update"
 | 
			
		||||
 | 
			
		||||
build:
 | 
			
		||||
  stage: build
 | 
			
		||||
  script: "platformio run -e esp12e"
 | 
			
		||||
  artifacts:
 | 
			
		||||
    paths:
 | 
			
		||||
      - .pio/build/esp12e/*.bin
 | 
			
		||||
							
								
								
									
										2
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,2 +0,0 @@
 | 
			
		||||
# Default ignored files
 | 
			
		||||
/workspace.xml
 | 
			
		||||
							
								
								
									
										2
									
								
								.idea/clion.iml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/clion.iml
									
									
									
										generated
									
									
									
								
							@@ -1,2 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<module classpath="CMake" type="CPP_MODULE" version="4" />
 | 
			
		||||
							
								
								
									
										16
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							@@ -1,16 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
 | 
			
		||||
  <component name="CidrRootsConfiguration">
 | 
			
		||||
    <sourceRoots>
 | 
			
		||||
      <file path="$PROJECT_DIR$/src" />
 | 
			
		||||
    </sourceRoots>
 | 
			
		||||
    <libraryRoots>
 | 
			
		||||
      <file path="$PROJECT_DIR$/lib" />
 | 
			
		||||
      <file path="$PROJECT_DIR$/.pio/libdeps" />
 | 
			
		||||
    </libraryRoots>
 | 
			
		||||
    <excludeRoots>
 | 
			
		||||
      <file path="$PROJECT_DIR$/.pio" />
 | 
			
		||||
    </excludeRoots>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										9
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
								
							@@ -1,9 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="ProjectModuleManager">
 | 
			
		||||
    <modules>
 | 
			
		||||
      <module fileurl="file://$PROJECT_DIR$/.idea/clion.iml" filepath="$PROJECT_DIR$/.idea/clion.iml" />
 | 
			
		||||
      <module fileurl="file://$PROJECT_DIR$/.idea/platformio.iml" filepath="$PROJECT_DIR$/.idea/platformio.iml" />
 | 
			
		||||
    </modules>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										2
									
								
								.idea/platformio.iml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/platformio.iml
									
									
									
										generated
									
									
									
								
							@@ -1,2 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<module classpath="CMake" type="CPP_MODULE" version="4" />
 | 
			
		||||
							
								
								
									
										4
									
								
								.idea/serialmonitor_settings.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								.idea/serialmonitor_settings.xml
									
									
									
										generated
									
									
									
								
							@@ -1,4 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="SerialMonitorSettings" PortName="/dev/ttyUSB1" BaudRate="9600" LineEndingsIndex="0" ShowStatusWidget="false" />
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							@@ -1,6 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="VcsDirectoryMappings">
 | 
			
		||||
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										30
									
								
								.idea/watcherTasks.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								.idea/watcherTasks.xml
									
									
									
										generated
									
									
									
								
							@@ -1,30 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="ProjectTasksOptions">
 | 
			
		||||
    <TaskOptions isEnabled="true">
 | 
			
		||||
      <option name="arguments" value="-f -c clion init --ide clion" />
 | 
			
		||||
      <option name="checkSyntaxErrors" value="true" />
 | 
			
		||||
      <option name="description" />
 | 
			
		||||
      <option name="exitCodeBehavior" value="NEVER" />
 | 
			
		||||
      <option name="fileExtension" value="ini" />
 | 
			
		||||
      <option name="immediateSync" value="false" />
 | 
			
		||||
      <option name="name" value="Monitor platformio.ini" />
 | 
			
		||||
      <option name="output" value="" />
 | 
			
		||||
      <option name="outputFilters">
 | 
			
		||||
        <array>
 | 
			
		||||
          <FilterInfo>
 | 
			
		||||
            <option name="description" value="" />
 | 
			
		||||
            <option name="name" value="PIO Conf" />
 | 
			
		||||
            <option name="regExp" value="$FILE_PATH$:^platformio" />
 | 
			
		||||
          </FilterInfo>
 | 
			
		||||
        </array>
 | 
			
		||||
      </option>
 | 
			
		||||
      <option name="outputFromStdout" value="false" />
 | 
			
		||||
      <option name="program" value="/usr/local/bin/platformio" />
 | 
			
		||||
      <option name="scopeName" value="Project Files" />
 | 
			
		||||
      <option name="trackOnlyRoot" value="false" />
 | 
			
		||||
      <option name="workingDir" value="$PROJECT_DIR$" />
 | 
			
		||||
      <envs />
 | 
			
		||||
    </TaskOptions>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										19
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
language: python
 | 
			
		||||
python:
 | 
			
		||||
    - "3.8"
 | 
			
		||||
 | 
			
		||||
before_install:
 | 
			
		||||
  - nvm install 10.15.3
 | 
			
		||||
  - nvm use 10.15.3
 | 
			
		||||
 | 
			
		||||
sudo: false
 | 
			
		||||
cache:
 | 
			
		||||
    directories:
 | 
			
		||||
        - "~/.platformio"
 | 
			
		||||
 | 
			
		||||
install:
 | 
			
		||||
    - pip install -U platformio
 | 
			
		||||
    - platformio update
 | 
			
		||||
 | 
			
		||||
script:
 | 
			
		||||
    - platformio run -e esp12e -e node32s
 | 
			
		||||
							
								
								
									
										289
									
								
								CMakeListsPrivate.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								CMakeListsPrivate.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,289 @@
 | 
			
		||||
# !!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT AND USE
 | 
			
		||||
# https://docs.platformio.org/page/projectconf/section_env_build.html#build-flags
 | 
			
		||||
#
 | 
			
		||||
# If you need to override existing CMake configuration or add extra,
 | 
			
		||||
# please create `CMakeListsUser.txt` in the root of project.
 | 
			
		||||
# The `CMakeListsUser.txt` will not be overwritten by PlatformIO.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
set(CMAKE_CONFIGURATION_TYPES "esp12e;node32s;" CACHE STRING "Build Types reflect PlatformIO Environments" FORCE)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SET(CMAKE_C_COMPILER "$ENV{HOME}/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc")
 | 
			
		||||
SET(CMAKE_CXX_COMPILER "$ENV{HOME}/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-g++")
 | 
			
		||||
SET(CMAKE_CXX_FLAGS "-fno-rtti -std=c++11 -Os -mlongcalls -mtext-section-literals -falign-functions=4 -U__STRICT_ANSI__ -ffunction-sections -fdata-sections -fno-exceptions -Wall")
 | 
			
		||||
SET(CMAKE_C_FLAGS "-std=gnu99 -Wpointer-arith -Wno-implicit-function-declaration -Wl,-EL -fno-inline-functions -nostdlib -Os -mlongcalls -mtext-section-literals -falign-functions=4 -U__STRICT_ANSI__ -ffunction-sections -fdata-sections -fno-exceptions -Wall")
 | 
			
		||||
 | 
			
		||||
SET(CMAKE_C_STANDARD 99)
 | 
			
		||||
set(CMAKE_CXX_STANDARD 11)
 | 
			
		||||
 | 
			
		||||
if (CMAKE_BUILD_TYPE MATCHES "esp12e")
 | 
			
		||||
    add_definitions(-D'PLATFORMIO=50003')
 | 
			
		||||
    add_definitions(-D'ESP8266')
 | 
			
		||||
    add_definitions(-D'ARDUINO_ARCH_ESP8266')
 | 
			
		||||
    add_definitions(-D'ARDUINO_ESP8266_ESP12')
 | 
			
		||||
    add_definitions(-D'FACTORY_WIFI_SSID=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_WIFI_PASSWORD=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_SSID=\"ESP8266-React\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_PASSWORD=\"esp-react\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_LOCAL_IP=\"192.168.4.1\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_ADMIN_USERNAME=\"admin\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_ADMIN_PASSWORD=\"admin\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_GUEST_USERNAME=\"guest\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_GUEST_PASSWORD=\"guest\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_ENABLED=true')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/London\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_SERVER=\"time.google.com\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_OTA_PORT=8266')
 | 
			
		||||
    add_definitions(-D'FACTORY_OTA_PASSWORD=\"esp-react\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_OTA_ENABLED=true')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_ENABLED=false')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_HOST=\"test.mosquitto.org\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_PORT=1883')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_USERNAME=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_PASSWORD=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_KEEP_ALIVE=60')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_CLEAN_SESSION=true')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_MAX_TOPIC_LENGTH=128')
 | 
			
		||||
    add_definitions(-D'FT_PROJECT=1')
 | 
			
		||||
    add_definitions(-D'FT_SECURITY=0')
 | 
			
		||||
    add_definitions(-D'FT_MQTT=0')
 | 
			
		||||
    add_definitions(-D'FT_NTP=0')
 | 
			
		||||
    add_definitions(-D'FT_OTA=1')
 | 
			
		||||
    add_definitions(-D'FT_UPLOAD_FIRMWARE=1')
 | 
			
		||||
    add_definitions(-D'NO_GLOBAL_ARDUINOOTA')
 | 
			
		||||
    add_definitions(-D'ENABLE_CORS')
 | 
			
		||||
    add_definitions(-D'CORS_ORIGIN=\"http://localhost:3000\"')
 | 
			
		||||
    add_definitions(-D'PROGMEM_WWW')
 | 
			
		||||
    add_definitions(-D'F_CPU=160000000L')
 | 
			
		||||
    add_definitions(-D'__ets__')
 | 
			
		||||
    add_definitions(-D'ICACHE_FLASH')
 | 
			
		||||
    add_definitions(-D'ARDUINO=10805')
 | 
			
		||||
    add_definitions(-D'ARDUINO_BOARD=\"PLATFORMIO_ESP12E\"')
 | 
			
		||||
    add_definitions(-D'FLASHMODE_DIO')
 | 
			
		||||
    add_definitions(-D'LWIP_OPEN_SRC')
 | 
			
		||||
    add_definitions(-D'NONOSDK22x_190703=1')
 | 
			
		||||
    add_definitions(-D'TCP_MSS=536')
 | 
			
		||||
    add_definitions(-D'LWIP_FEATURES=1')
 | 
			
		||||
    add_definitions(-D'LWIP_IPV6=0')
 | 
			
		||||
    add_definitions(-D'VTABLES_IN_FLASH')
 | 
			
		||||
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/include")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/lib/framework")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ArduinoOTA")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266mDNS/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/DNSServer/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/LittleFS/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/esp12e/DHT/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Ticker/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/esp12e/AsyncMqttClient/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/esp12e/ESP Async WebServer/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Hash/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/esp12e/ESPAsyncTCP/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFi/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/esp12e/ArduinoJson/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/include")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/libc/xtensa-lx106-elf/include")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/cores/esp8266")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lwip2/include")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/variants/nodemcu")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/esp12e/ESPAsyncTCP@1.2.0/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/EEPROM")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266AVRISP/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPClient/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266HTTPUpdateServer/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266LLMNR")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266NetBIOS")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266SSDP")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266SdFat/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WebServer/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266WiFiMesh/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/ESP8266httpUpdate/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Ethernet/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/GDBStub/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SD/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SDFS/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SPI")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SPISlave/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Servo/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/SoftwareSerial/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/TFT_Touch_Shield_V2")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/Wire")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif8266/libraries/esp8266/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include/c++/4.8.2")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include/c++/4.8.2/xtensa-lx106-elf")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/lib/gcc/xtensa-lx106-elf/4.8.2/include-fixed")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/lib/gcc/xtensa-lx106-elf/4.8.2/include")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa/xtensa-lx106-elf/include")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/tool-unity")
 | 
			
		||||
 | 
			
		||||
    FILE(GLOB_RECURSE EXTRA_LIB_SOURCES
 | 
			
		||||
        ${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/esp12e/*.*
 | 
			
		||||
    )
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
if (CMAKE_BUILD_TYPE MATCHES "node32s")
 | 
			
		||||
    add_definitions(-D'PLATFORMIO=50003')
 | 
			
		||||
    add_definitions(-D'ARDUINO_Node32s')
 | 
			
		||||
    add_definitions(-D'FACTORY_WIFI_SSID=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_WIFI_PASSWORD=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_SSID=\"ESP8266-React\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_PASSWORD=\"esp-react\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_LOCAL_IP=\"192.168.4.1\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_ADMIN_USERNAME=\"admin\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_ADMIN_PASSWORD=\"admin\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_GUEST_USERNAME=\"guest\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_GUEST_PASSWORD=\"guest\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_ENABLED=true')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/London\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_NTP_SERVER=\"time.google.com\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_OTA_PORT=8266')
 | 
			
		||||
    add_definitions(-D'FACTORY_OTA_PASSWORD=\"esp-react\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_OTA_ENABLED=true')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_ENABLED=false')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_HOST=\"test.mosquitto.org\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_PORT=1883')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_USERNAME=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_PASSWORD=\"\"')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_KEEP_ALIVE=60')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_CLEAN_SESSION=true')
 | 
			
		||||
    add_definitions(-D'FACTORY_MQTT_MAX_TOPIC_LENGTH=128')
 | 
			
		||||
    add_definitions(-D'FT_PROJECT=1')
 | 
			
		||||
    add_definitions(-D'FT_SECURITY=0')
 | 
			
		||||
    add_definitions(-D'FT_MQTT=0')
 | 
			
		||||
    add_definitions(-D'FT_NTP=0')
 | 
			
		||||
    add_definitions(-D'FT_OTA=1')
 | 
			
		||||
    add_definitions(-D'FT_UPLOAD_FIRMWARE=1')
 | 
			
		||||
    add_definitions(-D'NO_GLOBAL_ARDUINOOTA')
 | 
			
		||||
    add_definitions(-D'ENABLE_CORS')
 | 
			
		||||
    add_definitions(-D'CORS_ORIGIN=\"http://localhost:3000\"')
 | 
			
		||||
    add_definitions(-D'PROGMEM_WWW')
 | 
			
		||||
    add_definitions(-D'ESP32')
 | 
			
		||||
    add_definitions(-D'ESP_PLATFORM')
 | 
			
		||||
    add_definitions(-D'F_CPU=240000000L')
 | 
			
		||||
    add_definitions(-D'HAVE_CONFIG_H')
 | 
			
		||||
    add_definitions(-D'MBEDTLS_CONFIG_FILE=\"mbedtls/esp_config.h\"')
 | 
			
		||||
    add_definitions(-D'ARDUINO=10805')
 | 
			
		||||
    add_definitions(-D'ARDUINO_ARCH_ESP32')
 | 
			
		||||
    add_definitions(-D'ARDUINO_VARIANT=\"esp32\"')
 | 
			
		||||
    add_definitions(-D'ARDUINO_BOARD=\"Node32s\"')
 | 
			
		||||
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/include")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/lib/framework")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/ArduinoOTA/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/ESPmDNS/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/Update/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/DNSServer/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/SPIFFS/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/Ticker/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/node32s/AsyncMqttClient/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/node32s/ESP Async WebServer/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/FS/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/node32s/AsyncTCP/src")
 | 
			
		||||
    include_directories("${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/node32s/ArduinoJson/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/config")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/app_trace")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/app_update")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/asio")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/bootloader_support")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/bt")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/coap")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/console")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/driver")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp-tls")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp32")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_adc_cal")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_event")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_http_client")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_http_server")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_https_ota")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp_ringbuf")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/ethernet")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/expat")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/fatfs")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freemodbus")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freertos")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/heap")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/idf_test")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/jsmn")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/json")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/libsodium")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/log")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/lwip")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mbedtls")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mdns")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/micro-ecc")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/mqtt")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/newlib")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/nghttp")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/nvs_flash")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/openssl")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/protobuf-c")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/protocomm")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/pthread")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/sdmmc")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/smartconfig_ack")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/soc")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/spi_flash")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/spiffs")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/tcp_transport")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/tcpip_adapter")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/ulp")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/vfs")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wear_levelling")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wifi_provisioning")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/wpa_supplicant")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/xtensa-debug-module")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp-face")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/esp32-camera")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/fb_gfx")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/cores/esp32")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/variants/esp32")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/AsyncUDP/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/AzureIoT/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/BLE/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/BluetoothSerial/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/EEPROM/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/ESP32/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/FFat/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/HTTPClient/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/HTTPUpdate/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/NetBIOS/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/Preferences/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/SD/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/SD_MMC/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/SPI/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/SimpleBLE/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/WebServer/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/WiFiClientSecure/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/framework-arduinoespressif32/libraries/Wire/src")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa32/xtensa-esp32-elf/include/c++/5.2.0")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa32/xtensa-esp32-elf/include/c++/5.2.0/xtensa-esp32-elf")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa32/lib/gcc/xtensa-esp32-elf/5.2.0/include-fixed")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa32/lib/gcc/xtensa-esp32-elf/5.2.0/include")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/toolchain-xtensa32/xtensa-esp32-elf/include")
 | 
			
		||||
    include_directories("$ENV{HOME}/.platformio/packages/tool-unity")
 | 
			
		||||
 | 
			
		||||
    FILE(GLOB_RECURSE EXTRA_LIB_SOURCES
 | 
			
		||||
        ${CMAKE_CURRENT_LIST_DIR}/.pio/libdeps/node32s/*.*
 | 
			
		||||
    )
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
FILE(GLOB_RECURSE SRC_LIST
 | 
			
		||||
    ${CMAKE_CURRENT_LIST_DIR}/src/*.*
 | 
			
		||||
    ${CMAKE_CURRENT_LIST_DIR}/lib/*.*
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
list(APPEND SRC_LIST ${EXTRA_LIB_SOURCES})
 | 
			
		||||
							
								
								
									
										165
									
								
								LICENSE.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								LICENSE.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
                   GNU LESSER GENERAL PUBLIC LICENSE
 | 
			
		||||
                       Version 3, 29 June 2007
 | 
			
		||||
 | 
			
		||||
 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 | 
			
		||||
 Everyone is permitted to copy and distribute verbatim copies
 | 
			
		||||
 of this license document, but changing it is not allowed.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  This version of the GNU Lesser General Public License incorporates
 | 
			
		||||
the terms and conditions of version 3 of the GNU General Public
 | 
			
		||||
License, supplemented by the additional permissions listed below.
 | 
			
		||||
 | 
			
		||||
  0. Additional Definitions.
 | 
			
		||||
 | 
			
		||||
  As used herein, "this License" refers to version 3 of the GNU Lesser
 | 
			
		||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
 | 
			
		||||
General Public License.
 | 
			
		||||
 | 
			
		||||
  "The Library" refers to a covered work governed by this License,
 | 
			
		||||
other than an Application or a Combined Work as defined below.
 | 
			
		||||
 | 
			
		||||
  An "Application" is any work that makes use of an interface provided
 | 
			
		||||
by the Library, but which is not otherwise based on the Library.
 | 
			
		||||
Defining a subclass of a class defined by the Library is deemed a mode
 | 
			
		||||
of using an interface provided by the Library.
 | 
			
		||||
 | 
			
		||||
  A "Combined Work" is a work produced by combining or linking an
 | 
			
		||||
Application with the Library.  The particular version of the Library
 | 
			
		||||
with which the Combined Work was made is also called the "Linked
 | 
			
		||||
Version".
 | 
			
		||||
 | 
			
		||||
  The "Minimal Corresponding Source" for a Combined Work means the
 | 
			
		||||
Corresponding Source for the Combined Work, excluding any source code
 | 
			
		||||
for portions of the Combined Work that, considered in isolation, are
 | 
			
		||||
based on the Application, and not on the Linked Version.
 | 
			
		||||
 | 
			
		||||
  The "Corresponding Application Code" for a Combined Work means the
 | 
			
		||||
object code and/or source code for the Application, including any data
 | 
			
		||||
and utility programs needed for reproducing the Combined Work from the
 | 
			
		||||
Application, but excluding the System Libraries of the Combined Work.
 | 
			
		||||
 | 
			
		||||
  1. Exception to Section 3 of the GNU GPL.
 | 
			
		||||
 | 
			
		||||
  You may convey a covered work under sections 3 and 4 of this License
 | 
			
		||||
without being bound by section 3 of the GNU GPL.
 | 
			
		||||
 | 
			
		||||
  2. Conveying Modified Versions.
 | 
			
		||||
 | 
			
		||||
  If you modify a copy of the Library, and, in your modifications, a
 | 
			
		||||
facility refers to a function or data to be supplied by an Application
 | 
			
		||||
that uses the facility (other than as an argument passed when the
 | 
			
		||||
facility is invoked), then you may convey a copy of the modified
 | 
			
		||||
version:
 | 
			
		||||
 | 
			
		||||
   a) under this License, provided that you make a good faith effort to
 | 
			
		||||
   ensure that, in the event an Application does not supply the
 | 
			
		||||
   function or data, the facility still operates, and performs
 | 
			
		||||
   whatever part of its purpose remains meaningful, or
 | 
			
		||||
 | 
			
		||||
   b) under the GNU GPL, with none of the additional permissions of
 | 
			
		||||
   this License applicable to that copy.
 | 
			
		||||
 | 
			
		||||
  3. Object Code Incorporating Material from Library Header Files.
 | 
			
		||||
 | 
			
		||||
  The object code form of an Application may incorporate material from
 | 
			
		||||
a header file that is part of the Library.  You may convey such object
 | 
			
		||||
code under terms of your choice, provided that, if the incorporated
 | 
			
		||||
material is not limited to numerical parameters, data structure
 | 
			
		||||
layouts and accessors, or small macros, inline functions and templates
 | 
			
		||||
(ten or fewer lines in length), you do both of the following:
 | 
			
		||||
 | 
			
		||||
   a) Give prominent notice with each copy of the object code that the
 | 
			
		||||
   Library is used in it and that the Library and its use are
 | 
			
		||||
   covered by this License.
 | 
			
		||||
 | 
			
		||||
   b) Accompany the object code with a copy of the GNU GPL and this license
 | 
			
		||||
   document.
 | 
			
		||||
 | 
			
		||||
  4. Combined Works.
 | 
			
		||||
 | 
			
		||||
  You may convey a Combined Work under terms of your choice that,
 | 
			
		||||
taken together, effectively do not restrict modification of the
 | 
			
		||||
portions of the Library contained in the Combined Work and reverse
 | 
			
		||||
engineering for debugging such modifications, if you also do each of
 | 
			
		||||
the following:
 | 
			
		||||
 | 
			
		||||
   a) Give prominent notice with each copy of the Combined Work that
 | 
			
		||||
   the Library is used in it and that the Library and its use are
 | 
			
		||||
   covered by this License.
 | 
			
		||||
 | 
			
		||||
   b) Accompany the Combined Work with a copy of the GNU GPL and this license
 | 
			
		||||
   document.
 | 
			
		||||
 | 
			
		||||
   c) For a Combined Work that displays copyright notices during
 | 
			
		||||
   execution, include the copyright notice for the Library among
 | 
			
		||||
   these notices, as well as a reference directing the user to the
 | 
			
		||||
   copies of the GNU GPL and this license document.
 | 
			
		||||
 | 
			
		||||
   d) Do one of the following:
 | 
			
		||||
 | 
			
		||||
       0) Convey the Minimal Corresponding Source under the terms of this
 | 
			
		||||
       License, and the Corresponding Application Code in a form
 | 
			
		||||
       suitable for, and under terms that permit, the user to
 | 
			
		||||
       recombine or relink the Application with a modified version of
 | 
			
		||||
       the Linked Version to produce a modified Combined Work, in the
 | 
			
		||||
       manner specified by section 6 of the GNU GPL for conveying
 | 
			
		||||
       Corresponding Source.
 | 
			
		||||
 | 
			
		||||
       1) Use a suitable shared library mechanism for linking with the
 | 
			
		||||
       Library.  A suitable mechanism is one that (a) uses at run time
 | 
			
		||||
       a copy of the Library already present on the user's computer
 | 
			
		||||
       system, and (b) will operate properly with a modified version
 | 
			
		||||
       of the Library that is interface-compatible with the Linked
 | 
			
		||||
       Version.
 | 
			
		||||
 | 
			
		||||
   e) Provide Installation Information, but only if you would otherwise
 | 
			
		||||
   be required to provide such information under section 6 of the
 | 
			
		||||
   GNU GPL, and only to the extent that such information is
 | 
			
		||||
   necessary to install and execute a modified version of the
 | 
			
		||||
   Combined Work produced by recombining or relinking the
 | 
			
		||||
   Application with a modified version of the Linked Version. (If
 | 
			
		||||
   you use option 4d0, the Installation Information must accompany
 | 
			
		||||
   the Minimal Corresponding Source and Corresponding Application
 | 
			
		||||
   Code. If you use option 4d1, you must provide the Installation
 | 
			
		||||
   Information in the manner specified by section 6 of the GNU GPL
 | 
			
		||||
   for conveying Corresponding Source.)
 | 
			
		||||
 | 
			
		||||
  5. Combined Libraries.
 | 
			
		||||
 | 
			
		||||
  You may place library facilities that are a work based on the
 | 
			
		||||
Library side by side in a single library together with other library
 | 
			
		||||
facilities that are not Applications and are not covered by this
 | 
			
		||||
License, and convey such a combined library under terms of your
 | 
			
		||||
choice, if you do both of the following:
 | 
			
		||||
 | 
			
		||||
   a) Accompany the combined library with a copy of the same work based
 | 
			
		||||
   on the Library, uncombined with any other library facilities,
 | 
			
		||||
   conveyed under the terms of this License.
 | 
			
		||||
 | 
			
		||||
   b) Give prominent notice with the combined library that part of it
 | 
			
		||||
   is a work based on the Library, and explaining where to find the
 | 
			
		||||
   accompanying uncombined form of the same work.
 | 
			
		||||
 | 
			
		||||
  6. Revised Versions of the GNU Lesser General Public License.
 | 
			
		||||
 | 
			
		||||
  The Free Software Foundation may publish revised and/or new versions
 | 
			
		||||
of the GNU Lesser General Public License from time to time. Such new
 | 
			
		||||
versions will be similar in spirit to the present version, but may
 | 
			
		||||
differ in detail to address new problems or concerns.
 | 
			
		||||
 | 
			
		||||
  Each version is given a distinguishing version number. If the
 | 
			
		||||
Library as you received it specifies that a certain numbered version
 | 
			
		||||
of the GNU Lesser General Public License "or any later version"
 | 
			
		||||
applies to it, you have the option of following the terms and
 | 
			
		||||
conditions either of that published version or of any later version
 | 
			
		||||
published by the Free Software Foundation. If the Library as you
 | 
			
		||||
received it does not specify a version number of the GNU Lesser
 | 
			
		||||
General Public License, you may choose any version of the GNU Lesser
 | 
			
		||||
General Public License ever published by the Free Software Foundation.
 | 
			
		||||
 | 
			
		||||
  If the Library as you received it specifies that a proxy can decide
 | 
			
		||||
whether future versions of the GNU Lesser General Public License shall
 | 
			
		||||
apply, that proxy's public statement of acceptance of any version is
 | 
			
		||||
permanent authorization for you to choose that version for the
 | 
			
		||||
Library.
 | 
			
		||||
							
								
								
									
										48
									
								
								factory_settings.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								factory_settings.ini
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
[factory_settings]
 | 
			
		||||
build_flags = 
 | 
			
		||||
    ; WiFi settings
 | 
			
		||||
    -D FACTORY_WIFI_SSID=\"\"
 | 
			
		||||
    -D FACTORY_WIFI_PASSWORD=\"\"
 | 
			
		||||
    ; if unspecified the devices hardware ID will be used
 | 
			
		||||
    ; -D FACTORY_WIFI_HOSTNAME=\"esp-react\"  
 | 
			
		||||
 | 
			
		||||
    ; Access point settings
 | 
			
		||||
    -D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED
 | 
			
		||||
    -D FACTORY_AP_SSID=\"ESP8266-React\" ; 1-64 characters
 | 
			
		||||
    -D FACTORY_AP_PASSWORD=\"esp-react\" ; 8-64 characters
 | 
			
		||||
    -D FACTORY_AP_LOCAL_IP=\"192.168.4.1\"
 | 
			
		||||
    -D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\"
 | 
			
		||||
    -D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\"
 | 
			
		||||
 | 
			
		||||
    ; User credentials for admin and guest user
 | 
			
		||||
    -D FACTORY_ADMIN_USERNAME=\"admin\"
 | 
			
		||||
    -D FACTORY_ADMIN_PASSWORD=\"admin\"
 | 
			
		||||
    -D FACTORY_GUEST_USERNAME=\"guest\"
 | 
			
		||||
    -D FACTORY_GUEST_PASSWORD=\"guest\"
 | 
			
		||||
 | 
			
		||||
    ; NTP settings
 | 
			
		||||
    -D FACTORY_NTP_ENABLED=true
 | 
			
		||||
    -D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/London\"
 | 
			
		||||
    -D FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\"
 | 
			
		||||
    -D FACTORY_NTP_SERVER=\"time.google.com\"
 | 
			
		||||
 | 
			
		||||
    ; OTA settings
 | 
			
		||||
    -D FACTORY_OTA_PORT=8266
 | 
			
		||||
    -D FACTORY_OTA_PASSWORD=\"esp-react\"
 | 
			
		||||
    -D FACTORY_OTA_ENABLED=true
 | 
			
		||||
 | 
			
		||||
    ; MQTT settings
 | 
			
		||||
    -D FACTORY_MQTT_ENABLED=false
 | 
			
		||||
    -D FACTORY_MQTT_HOST=\"test.mosquitto.org\"
 | 
			
		||||
    -D FACTORY_MQTT_PORT=1883
 | 
			
		||||
    -D FACTORY_MQTT_USERNAME=\"\"
 | 
			
		||||
    -D FACTORY_MQTT_PASSWORD=\"\"
 | 
			
		||||
    ; if unspecified the devices hardware ID will be used
 | 
			
		||||
    ;-D FACTORY_MQTT_CLIENT_ID=\"esp-react\"
 | 
			
		||||
    -D FACTORY_MQTT_KEEP_ALIVE=60
 | 
			
		||||
    -D FACTORY_MQTT_CLEAN_SESSION=true
 | 
			
		||||
    -D FACTORY_MQTT_MAX_TOPIC_LENGTH=128
 | 
			
		||||
 | 
			
		||||
    ; JWT Secret
 | 
			
		||||
    ; if unspecified the devices hardware ID will be used
 | 
			
		||||
    ; -D FACTORY_JWT_SECRET=\"esp8266-react\"
 | 
			
		||||
							
								
								
									
										8
									
								
								features.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								features.ini
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
[features]
 | 
			
		||||
build_flags = 
 | 
			
		||||
    -D FT_PROJECT=1
 | 
			
		||||
    -D FT_SECURITY=0
 | 
			
		||||
    -D FT_MQTT=0
 | 
			
		||||
    -D FT_NTP=0
 | 
			
		||||
    -D FT_OTA=1
 | 
			
		||||
    -D FT_UPLOAD_FIRMWARE=1
 | 
			
		||||
							
								
								
									
										5
									
								
								interface/.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								interface/.env
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# This is the name of your project. It appears on the sign-in page and in the menu bar.
 | 
			
		||||
REACT_APP_PROJECT_NAME=ESP8266 React
 | 
			
		||||
 | 
			
		||||
# This is the url path your project will be exposed under.
 | 
			
		||||
REACT_APP_PROJECT_PATH=project
 | 
			
		||||
							
								
								
									
										4
									
								
								interface/.env.development
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								interface/.env.development
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
# 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.
 | 
			
		||||
REACT_APP_HTTP_ROOT=http://192.168.0.230
 | 
			
		||||
REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.230
 | 
			
		||||
							
								
								
									
										1
									
								
								interface/.env.production
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								interface/.env.production
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
GENERATE_SOURCEMAP=false
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								interface/build/app/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/build/app/icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 8.7 KiB  | 
							
								
								
									
										12
									
								
								interface/build/app/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								interface/build/app/manifest.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name":"ESP8266 React",
 | 
			
		||||
  "icons":[
 | 
			
		||||
    {
 | 
			
		||||
      "src":"/app/icon.png",
 | 
			
		||||
      "sizes":"48x48 72x72 96x96 128x128 256x256"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "start_url":"/",
 | 
			
		||||
  "display":"fullscreen",
 | 
			
		||||
  "orientation":"any"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								interface/build/css/roboto.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								interface/build/css/roboto.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
/* Just supporting latin due to size constrains on the esp chip */
 | 
			
		||||
@font-face {
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
@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;
 | 
			
		||||
}
 | 
			
		||||
@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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								interface/build/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/build/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								interface/build/fonts/li.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/build/fonts/li.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								interface/build/fonts/me.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/build/fonts/me.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								interface/build/fonts/re.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/build/fonts/re.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								interface/build/js/1.b30b.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/build/js/1.b30b.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										37
									
								
								interface/config-overrides.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								interface/config-overrides.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
const ManifestPlugin = require('webpack-manifest-plugin');
 | 
			
		||||
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
 | 
			
		||||
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 embedded FS
 | 
			
		||||
    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));
 | 
			
		||||
 | 
			
		||||
    // 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";
 | 
			
		||||
 | 
			
		||||
    // build progmem data files
 | 
			
		||||
    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",
 | 
			
		||||
      test: /\.(js)$/,
 | 
			
		||||
      deleteOriginalAssets: true
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
  return config;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14652
									
								
								interface/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										14652
									
								
								interface/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										57
									
								
								interface/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								interface/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "esp8266-react",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@material-ui/core": "^4.11.0",
 | 
			
		||||
    "@material-ui/icons": "^4.9.1",
 | 
			
		||||
    "@types/jwt-decode": "^3.1.0",
 | 
			
		||||
    "@types/lodash": "^4.14.165",
 | 
			
		||||
    "@types/node": "^12.12.32",
 | 
			
		||||
    "@types/react": "^16.9.56",
 | 
			
		||||
    "@types/react-dom": "^16.9.9",
 | 
			
		||||
    "@types/react-material-ui-form-validator": "^2.1.0",
 | 
			
		||||
    "@types/react-router": "^5.1.8",
 | 
			
		||||
    "@types/react-router-dom": "^5.1.6",
 | 
			
		||||
    "compression-webpack-plugin": "^4.0.0",
 | 
			
		||||
    "jwt-decode": "^3.1.1",
 | 
			
		||||
    "lodash": "^4.17.20",
 | 
			
		||||
    "mime-types": "^2.1.27",
 | 
			
		||||
    "moment": "^2.29.1",
 | 
			
		||||
    "notistack": "^1.0.1",
 | 
			
		||||
    "react": "^16.14.0",
 | 
			
		||||
    "react-dom": "^16.14.0",
 | 
			
		||||
    "react-dropzone": "^11.2.4",
 | 
			
		||||
    "react-form-validator-core": "^1.0.0",
 | 
			
		||||
    "react-material-ui-form-validator": "^2.1.1",
 | 
			
		||||
    "react-router": "^5.2.0",
 | 
			
		||||
    "react-router-dom": "^5.2.0",
 | 
			
		||||
    "react-scripts": "3.4.4",
 | 
			
		||||
    "sockette": "^2.0.6",
 | 
			
		||||
    "typescript": "^4.0.2",
 | 
			
		||||
    "zlib": "^1.0.5"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "react-app-rewired start",
 | 
			
		||||
    "build": "react-app-rewired build",
 | 
			
		||||
    "eject": "react-scripts eject"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "extends": "react-app"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": {
 | 
			
		||||
    "production": [
 | 
			
		||||
      ">0.2%",
 | 
			
		||||
      "not dead",
 | 
			
		||||
      "not op_mini all"
 | 
			
		||||
    ],
 | 
			
		||||
    "development": [
 | 
			
		||||
      "last 1 chrome version",
 | 
			
		||||
      "last 1 firefox version",
 | 
			
		||||
      "last 1 safari version"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "react-app-rewired": "^2.1.6"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										122
									
								
								interface/progmem-generator.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								interface/progmem-generator.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
			
		||||
const { resolve, relative, sep } = require('path');
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
function getFilesSync(dir, files = []) {
 | 
			
		||||
  readdirSync(dir, { withFileTypes: true }).forEach(entry => {
 | 
			
		||||
    const entryPath = resolve(dir, entry.name);
 | 
			
		||||
    if (entry.isDirectory()) {
 | 
			
		||||
      getFilesSync(entryPath, files);
 | 
			
		||||
    } else {
 | 
			
		||||
      files.push(entryPath);
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  return files;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function coherseToBuffer(input) {
 | 
			
		||||
  return Buffer.isBuffer(input) ? input : Buffer.from(input);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cleanAndOpen(path) {
 | 
			
		||||
  if (existsSync(path)) {
 | 
			
		||||
    unlinkSync(path);
 | 
			
		||||
  }
 | 
			
		||||
  return createWriteStream(path, { flags: "w+" });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ProgmemGenerator {
 | 
			
		||||
 | 
			
		||||
  constructor(options = {}) {
 | 
			
		||||
    const { outputPath, bytesPerLine = 20, indent = "  ", includes = ARDUINO_INCLUDES } = options;
 | 
			
		||||
    this.options = { outputPath, bytesPerLine, indent, includes };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  apply(compiler) {
 | 
			
		||||
    compiler.hooks.emit.tapAsync(
 | 
			
		||||
      { name: 'ProgmemGenerator' },
 | 
			
		||||
      (compilation, callback) => {
 | 
			
		||||
        const { outputPath, bytesPerLine, indent, includes } = this.options;
 | 
			
		||||
        const fileInfo = [];
 | 
			
		||||
        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 mimeType = mime.lookup(relativeFilePath);
 | 
			
		||||
            var size = 0;
 | 
			
		||||
            writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
 | 
			
		||||
            const zipBuffer = zlib.gzipSync(buffer);
 | 
			
		||||
            zipBuffer.forEach((b) => {
 | 
			
		||||
              if (!(size % bytesPerLine)) {
 | 
			
		||||
                writeStream.write("\n");
 | 
			
		||||
                writeStream.write(indent);
 | 
			
		||||
              }
 | 
			
		||||
              writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
 | 
			
		||||
              size++;
 | 
			
		||||
            });
 | 
			
		||||
            if (size % bytesPerLine) {
 | 
			
		||||
              writeStream.write("\n");
 | 
			
		||||
            }
 | 
			
		||||
            writeStream.write("};\n\n");
 | 
			
		||||
            fileInfo.push({
 | 
			
		||||
              uri: '/' + relativeFilePath.replace(sep, '/'),
 | 
			
		||||
              mimeType,
 | 
			
		||||
              variable,
 | 
			
		||||
              size
 | 
			
		||||
            });
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          const writeFiles = () => {
 | 
			
		||||
            // process static files
 | 
			
		||||
            const buildPath = compilation.options.output.path;
 | 
			
		||||
            for (const filePath of getFilesSync(buildPath)) {
 | 
			
		||||
              const readStream = readFileSync(filePath);
 | 
			
		||||
              const relativeFilePath = relative(buildPath, filePath);
 | 
			
		||||
              writeFile(relativeFilePath, readStream);
 | 
			
		||||
            }
 | 
			
		||||
            // process assets
 | 
			
		||||
            const { assets } = compilation;
 | 
			
		||||
            Object.keys(assets).forEach((relativeFilePath) => {
 | 
			
		||||
              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)}}
 | 
			
		||||
};
 | 
			
		||||
`;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const writeWWWClass = () => {
 | 
			
		||||
            writeStream.write(generateWWWClass());
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          writeIncludes();
 | 
			
		||||
          writeFiles();
 | 
			
		||||
          writeWWWClass();
 | 
			
		||||
 | 
			
		||||
          writeStream.on('finish', () => {
 | 
			
		||||
            callback();
 | 
			
		||||
          });
 | 
			
		||||
        } finally {
 | 
			
		||||
          writeStream.end();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = ProgmemGenerator;
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								interface/public/app/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/app/icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 8.7 KiB  | 
							
								
								
									
										12
									
								
								interface/public/app/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								interface/public/app/manifest.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name":"ESP8266 React",
 | 
			
		||||
  "icons":[
 | 
			
		||||
    {
 | 
			
		||||
      "src":"/app/icon.png",
 | 
			
		||||
      "sizes":"48x48 72x72 96x96 128x128 256x256"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "start_url":"/",
 | 
			
		||||
  "display":"fullscreen",
 | 
			
		||||
  "orientation":"any"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								interface/public/css/roboto.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								interface/public/css/roboto.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
/* Just supporting latin due to size constrains on the esp chip */
 | 
			
		||||
@font-face {
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
@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;
 | 
			
		||||
}
 | 
			
		||||
@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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								interface/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								interface/public/fonts/li.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/fonts/li.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								interface/public/fonts/me.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/fonts/me.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								interface/public/fonts/re.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/fonts/re.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										16
									
								
								interface/public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								interface/public/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 | 
			
		||||
    <link rel="stylesheet" href="%PUBLIC_URL%/css/roboto.css">
 | 
			
		||||
    <link rel="manifest" href="%PUBLIC_URL%/app/manifest.json">
 | 
			
		||||
    <title>ESP8266 React</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>
 | 
			
		||||
      You need to enable JavaScript to run this app.
 | 
			
		||||
    </noscript>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										50
									
								
								interface/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								interface/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import React, { Component, RefObject } from 'react';
 | 
			
		||||
import { Redirect, Route, Switch } from 'react-router';
 | 
			
		||||
import { SnackbarProvider } from 'notistack';
 | 
			
		||||
 | 
			
		||||
import { IconButton } from '@material-ui/core';
 | 
			
		||||
import CloseIcon from '@material-ui/icons/Close';
 | 
			
		||||
 | 
			
		||||
import AppRouting from './AppRouting';
 | 
			
		||||
import CustomMuiTheme from './CustomMuiTheme';
 | 
			
		||||
import { PROJECT_NAME } from './api';
 | 
			
		||||
import FeaturesWrapper from './features/FeaturesWrapper';
 | 
			
		||||
 | 
			
		||||
// this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid.
 | 
			
		||||
const unauthorizedRedirect = () => <Redirect to="/" />;
 | 
			
		||||
 | 
			
		||||
class App extends Component {
 | 
			
		||||
 | 
			
		||||
  notistackRef: RefObject<any> = React.createRef();
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    document.title = PROJECT_NAME;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onClickDismiss = (key: string | number | undefined) => () => {
 | 
			
		||||
    this.notistackRef.current.closeSnackbar(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <CustomMuiTheme>
 | 
			
		||||
        <SnackbarProvider 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 component={AppRouting} />
 | 
			
		||||
            </Switch>
 | 
			
		||||
          </FeaturesWrapper>
 | 
			
		||||
        </SnackbarProvider>
 | 
			
		||||
      </CustomMuiTheme>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App
 | 
			
		||||
							
								
								
									
										60
									
								
								interface/src/AppRouting.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								interface/src/AppRouting.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
import { Switch, Redirect } from 'react-router';
 | 
			
		||||
 | 
			
		||||
import * as Authentication from './authentication/Authentication';
 | 
			
		||||
import AuthenticationWrapper from './authentication/AuthenticationWrapper';
 | 
			
		||||
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
 | 
			
		||||
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
 | 
			
		||||
 | 
			
		||||
import SignIn from './SignIn';
 | 
			
		||||
import ProjectRouting from './project/ProjectRouting';
 | 
			
		||||
import WiFiConnection from './wifi/WiFiConnection';
 | 
			
		||||
import AccessPoint from './ap/AccessPoint';
 | 
			
		||||
import NetworkTime from './ntp/NetworkTime';
 | 
			
		||||
import Security from './security/Security';
 | 
			
		||||
import System from './system/System';
 | 
			
		||||
 | 
			
		||||
import { PROJECT_PATH } from './api';
 | 
			
		||||
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}/` : "/wifi/";
 | 
			
		||||
 | 
			
		||||
class AppRouting extends Component<WithFeaturesProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    Authentication.clearLoginRedirect();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { features } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <AuthenticationWrapper>
 | 
			
		||||
        <Switch>
 | 
			
		||||
          {features.security && (
 | 
			
		||||
            <UnauthenticatedRoute exact path="/" component={SignIn} />
 | 
			
		||||
          )}
 | 
			
		||||
          {features.project && (
 | 
			
		||||
            <AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
 | 
			
		||||
          )}
 | 
			
		||||
          <AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
 | 
			
		||||
          <AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
 | 
			
		||||
          {features.ntp && (
 | 
			
		||||
          <AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
 | 
			
		||||
          )}
 | 
			
		||||
          {features.mqtt && (
 | 
			
		||||
            <AuthenticatedRoute exact path="/mqtt/*" component={Mqtt} />
 | 
			
		||||
          )}
 | 
			
		||||
          {features.security && (
 | 
			
		||||
            <AuthenticatedRoute exact path="/security/*" component={Security} />
 | 
			
		||||
          )}
 | 
			
		||||
          <AuthenticatedRoute exact path="/system/*" component={System} />
 | 
			
		||||
          <Redirect to={getDefaultRoute(features)} />
 | 
			
		||||
        </Switch>
 | 
			
		||||
      </AuthenticationWrapper>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withFeatures(AppRouting);
 | 
			
		||||
							
								
								
									
										39
									
								
								interface/src/CustomMuiTheme.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								interface/src/CustomMuiTheme.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import { CssBaseline } from '@material-ui/core';
 | 
			
		||||
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
 | 
			
		||||
import { blueGrey, indigo, orange, red, green } from '@material-ui/core/colors';
 | 
			
		||||
 | 
			
		||||
const theme = createMuiTheme({
 | 
			
		||||
  palette: {
 | 
			
		||||
    primary: indigo,
 | 
			
		||||
    secondary: blueGrey,
 | 
			
		||||
    info: {
 | 
			
		||||
      main: blueGrey[900]
 | 
			
		||||
    },
 | 
			
		||||
    warning: {
 | 
			
		||||
      main: orange[500]
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      main: red[500]
 | 
			
		||||
    },
 | 
			
		||||
    success: {
 | 
			
		||||
      main: green[500]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default class CustomMuiTheme extends Component {
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <StylesProvider>
 | 
			
		||||
        <MuiThemeProvider theme={theme}>
 | 
			
		||||
          <CssBaseline />
 | 
			
		||||
          {this.props.children}
 | 
			
		||||
        </MuiThemeProvider>
 | 
			
		||||
      </StylesProvider>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										147
									
								
								interface/src/SignIn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								interface/src/SignIn.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,147 @@
 | 
			
		||||
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 { 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 { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
 | 
			
		||||
 | 
			
		||||
const styles = (theme: Theme) => createStyles({
 | 
			
		||||
  signInPage: {
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    height: "100vh",
 | 
			
		||||
    margin: "auto",
 | 
			
		||||
    padding: theme.spacing(2),
 | 
			
		||||
    justifyContent: "center",
 | 
			
		||||
    flexDirection: "column",
 | 
			
		||||
    maxWidth: theme.breakpoints.values.sm
 | 
			
		||||
  },
 | 
			
		||||
  signInPanel: {
 | 
			
		||||
    textAlign: "center",
 | 
			
		||||
    padding: theme.spacing(2),
 | 
			
		||||
    paddingTop: "200px",
 | 
			
		||||
    backgroundImage: 'url("/app/icon.png")',
 | 
			
		||||
    backgroundRepeat: "no-repeat",
 | 
			
		||||
    backgroundPosition: "50% " + theme.spacing(2) + "px",
 | 
			
		||||
    backgroundSize: "auto 150px",
 | 
			
		||||
    width: "100%"
 | 
			
		||||
  },
 | 
			
		||||
  extendedIcon: {
 | 
			
		||||
    marginRight: theme.spacing(0.5),
 | 
			
		||||
  },
 | 
			
		||||
  button: {
 | 
			
		||||
    marginRight: theme.spacing(2),
 | 
			
		||||
    marginTop: theme.spacing(2),
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
 | 
			
		||||
 | 
			
		||||
interface SignInState {
 | 
			
		||||
  username: string,
 | 
			
		||||
  password: string,
 | 
			
		||||
  processing: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SignIn extends Component<SignInProps, SignInState> {
 | 
			
		||||
 | 
			
		||||
  constructor(props: SignInProps) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.state = {
 | 
			
		||||
      username: '',
 | 
			
		||||
      password: '',
 | 
			
		||||
      processing: false
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
 | 
			
		||||
    const { name, value } = event.currentTarget;
 | 
			
		||||
    this.setState(prevState => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      [name]: value,
 | 
			
		||||
    }))
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  onSubmit = () => {
 | 
			
		||||
    const { username, password } = this.state;
 | 
			
		||||
    const { authenticationContext } = this.props;
 | 
			
		||||
    this.setState({ processing: true });
 | 
			
		||||
    fetch(SIGN_IN_ENDPOINT, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: JSON.stringify({ username, password }),
 | 
			
		||||
      headers: new Headers({
 | 
			
		||||
        'Content-Type': 'application/json'
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
      .then(response => {
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
          return response.json();
 | 
			
		||||
        } else if (response.status === 401) {
 | 
			
		||||
          throw Error("Invalid credentials.");
 | 
			
		||||
        } else {
 | 
			
		||||
          throw Error("Invalid status code: " + response.status);
 | 
			
		||||
        }
 | 
			
		||||
      }).then(json => {
 | 
			
		||||
        authenticationContext.signIn(json.access_token);
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => {
 | 
			
		||||
        this.props.enqueueSnackbar(error.message, {
 | 
			
		||||
          variant: 'warning',
 | 
			
		||||
        });
 | 
			
		||||
        this.setState({ processing: false });
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { username, password, processing } = this.state;
 | 
			
		||||
    const { classes } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classes.signInPage}>
 | 
			
		||||
        <Paper className={classes.signInPanel}>
 | 
			
		||||
          <Typography variant="h4">{PROJECT_NAME}</Typography>
 | 
			
		||||
          <ValidatorForm onSubmit={this.onSubmit}>
 | 
			
		||||
            <TextValidator
 | 
			
		||||
              disabled={processing}
 | 
			
		||||
              validators={['required']}
 | 
			
		||||
              errorMessages={['Username is required']}
 | 
			
		||||
              name="username"
 | 
			
		||||
              label="Username"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              value={username}
 | 
			
		||||
              onChange={this.updateInputElement}
 | 
			
		||||
              margin="normal"
 | 
			
		||||
              inputProps={{
 | 
			
		||||
                autoCapitalize: "none",
 | 
			
		||||
                autoCorrect: "off",
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <PasswordValidator
 | 
			
		||||
              disabled={processing}
 | 
			
		||||
              validators={['required']}
 | 
			
		||||
              errorMessages={['Password is required']}
 | 
			
		||||
              name="password"
 | 
			
		||||
              label="Password"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              value={password}
 | 
			
		||||
              onChange={this.updateInputElement}
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
 | 
			
		||||
              <ForwardIcon className={classes.extendedIcon} />
 | 
			
		||||
              Sign In
 | 
			
		||||
            </Fab>
 | 
			
		||||
          </ValidatorForm>
 | 
			
		||||
        </Paper>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
 | 
			
		||||
							
								
								
									
										5
									
								
								interface/src/ap/APModes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								interface/src/ap/APModes.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import { APSettings, APProvisionMode } from "./types";
 | 
			
		||||
 | 
			
		||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
 | 
			
		||||
    return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								interface/src/ap/APSettingsController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								interface/src/ap/APSettingsController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import { AP_SETTINGS_ENDPOINT } from '../api';
 | 
			
		||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
 | 
			
		||||
 | 
			
		||||
import APSettingsForm from './APSettingsForm';
 | 
			
		||||
import { APSettings } from './types';
 | 
			
		||||
 | 
			
		||||
type APSettingsControllerProps = RestControllerProps<APSettings>;
 | 
			
		||||
 | 
			
		||||
class APSettingsController extends Component<APSettingsControllerProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <SectionContent title="Access Point Settings" titleGutter>
 | 
			
		||||
        <RestFormLoader
 | 
			
		||||
          {...this.props}
 | 
			
		||||
          render={formProps => <APSettingsForm {...formProps} />}
 | 
			
		||||
        />
 | 
			
		||||
      </SectionContent>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
 | 
			
		||||
							
								
								
									
										106
									
								
								interface/src/ap/APSettingsForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								interface/src/ap/APSettingsForm.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
import React, { Fragment } from 'react';
 | 
			
		||||
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 { isAPEnabled } from './APModes';
 | 
			
		||||
import { APSettings, APProvisionMode } from './types';
 | 
			
		||||
import { isIP } from '../validators';
 | 
			
		||||
 | 
			
		||||
type APSettingsFormProps = RestFormProps<APSettings>;
 | 
			
		||||
 | 
			
		||||
class APSettingsForm extends React.Component<APSettingsFormProps> {
 | 
			
		||||
 | 
			
		||||
  componentWillMount() {
 | 
			
		||||
    ValidatorForm.addValidationRule('isIP', isIP);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { data, handleValueChange, saveData } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <ValidatorForm onSubmit={saveData} ref="APSettingsForm">
 | 
			
		||||
        <SelectValidator name="provision_mode"
 | 
			
		||||
          label="Provide Access Point…"
 | 
			
		||||
          value={data.provision_mode}
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          onChange={handleValueChange('provision_mode')}
 | 
			
		||||
          margin="normal">
 | 
			
		||||
          <MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
 | 
			
		||||
          <MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
 | 
			
		||||
          <MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
 | 
			
		||||
        </SelectValidator>
 | 
			
		||||
        {
 | 
			
		||||
          isAPEnabled(data) &&
 | 
			
		||||
          <Fragment>
 | 
			
		||||
            <TextValidator
 | 
			
		||||
              validators={['required', 'matchRegexp:^.{1,32}$']}
 | 
			
		||||
              errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
 | 
			
		||||
              name="ssid"
 | 
			
		||||
              label="Access Point SSID"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              value={data.ssid}
 | 
			
		||||
              onChange={handleValueChange('ssid')}
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <PasswordValidator
 | 
			
		||||
              validators={['required', 'matchRegexp:^.{8,64}$']}
 | 
			
		||||
              errorMessages={['Access Point Password is required', 'Access Point Password must be 8-64 characters']}
 | 
			
		||||
              name="password"
 | 
			
		||||
              label="Access Point Password"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              value={data.password}
 | 
			
		||||
              onChange={handleValueChange('password')}
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <TextValidator
 | 
			
		||||
              validators={['required', 'isIP']}
 | 
			
		||||
              errorMessages={['Local IP is required', 'Must be an IP address']}
 | 
			
		||||
              name="local_ip"
 | 
			
		||||
              label="Local IP"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              value={data.local_ip}
 | 
			
		||||
              onChange={handleValueChange('local_ip')}
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <TextValidator
 | 
			
		||||
              validators={['required', 'isIP']}
 | 
			
		||||
              errorMessages={['Gateway IP is required', 'Must be an IP address']}
 | 
			
		||||
              name="gateway_ip"
 | 
			
		||||
              label="Gateway"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              value={data.gateway_ip}
 | 
			
		||||
              onChange={handleValueChange('gateway_ip')}
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <TextValidator
 | 
			
		||||
              validators={['required', 'isIP']}
 | 
			
		||||
              errorMessages={['Subnet mask is required', 'Must be an IP address']}
 | 
			
		||||
              name="subnet_mask"
 | 
			
		||||
              label="Subnet"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              value={data.subnet_mask}
 | 
			
		||||
              onChange={handleValueChange('subnet_mask')}
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
          </Fragment>
 | 
			
		||||
        }
 | 
			
		||||
        <FormActions>
 | 
			
		||||
          <FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
 | 
			
		||||
            Save
 | 
			
		||||
          </FormButton>
 | 
			
		||||
        </FormActions>
 | 
			
		||||
      </ValidatorForm>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default APSettingsForm;
 | 
			
		||||
							
								
								
									
										28
									
								
								interface/src/ap/APStatus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								interface/src/ap/APStatus.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import { Theme } from "@material-ui/core";
 | 
			
		||||
import { APStatus, APNetworkStatus } from "./types";
 | 
			
		||||
 | 
			
		||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case APNetworkStatus.ACTIVE:
 | 
			
		||||
      return theme.palette.success.main;
 | 
			
		||||
    case APNetworkStatus.INACTIVE:
 | 
			
		||||
      return theme.palette.info.main;
 | 
			
		||||
    case APNetworkStatus.LINGERING:
 | 
			
		||||
      return theme.palette.warning.main;
 | 
			
		||||
    default:
 | 
			
		||||
      return theme.palette.warning.main;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const apStatus = ({ status }: APStatus) => {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case APNetworkStatus.ACTIVE:
 | 
			
		||||
      return "Active";
 | 
			
		||||
    case APNetworkStatus.INACTIVE:
 | 
			
		||||
      return "Inactive";
 | 
			
		||||
    case APNetworkStatus.LINGERING:
 | 
			
		||||
      return "Lingering until idle";
 | 
			
		||||
    default:
 | 
			
		||||
      return "Unknown";
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										29
									
								
								interface/src/ap/APStatusController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								interface/src/ap/APStatusController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
 | 
			
		||||
import { AP_STATUS_ENDPOINT } from '../api';
 | 
			
		||||
 | 
			
		||||
import APStatusForm from './APStatusForm';
 | 
			
		||||
import { APStatus } from './types';
 | 
			
		||||
 | 
			
		||||
type APStatusControllerProps = RestControllerProps<APStatus>;
 | 
			
		||||
 | 
			
		||||
class APStatusController extends Component<APStatusControllerProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <SectionContent title="Access Point Status">
 | 
			
		||||
        <RestFormLoader
 | 
			
		||||
          {...this.props}
 | 
			
		||||
          render={formProps => <APStatusForm {...formProps} />}
 | 
			
		||||
        />
 | 
			
		||||
      </SectionContent>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default restController(AP_STATUS_ENDPOINT, APStatusController);
 | 
			
		||||
							
								
								
									
										78
									
								
								interface/src/ap/APStatusForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								interface/src/ap/APStatusForm.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
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 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 { apStatusHighlight, apStatus } from './APStatus';
 | 
			
		||||
import { APStatus } from './types';
 | 
			
		||||
 | 
			
		||||
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
 | 
			
		||||
 | 
			
		||||
class APStatusForm extends Component<APStatusFormProps> {
 | 
			
		||||
 | 
			
		||||
  createListItems() {
 | 
			
		||||
    const { data, theme } = this.props
 | 
			
		||||
    return (
 | 
			
		||||
      <Fragment>
 | 
			
		||||
        <ListItem>
 | 
			
		||||
          <ListItemAvatar>
 | 
			
		||||
            <HighlightAvatar color={apStatusHighlight(data, theme)}>
 | 
			
		||||
              <SettingsInputAntennaIcon />
 | 
			
		||||
            </HighlightAvatar>
 | 
			
		||||
          </ListItemAvatar>
 | 
			
		||||
          <ListItemText primary="Status" secondary={apStatus(data)} />
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <Divider variant="inset" component="li" />
 | 
			
		||||
        <ListItem>
 | 
			
		||||
          <ListItemAvatar>
 | 
			
		||||
            <Avatar>IP</Avatar>
 | 
			
		||||
          </ListItemAvatar>
 | 
			
		||||
          <ListItemText primary="IP Address" secondary={data.ip_address} />
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <Divider variant="inset" component="li" />
 | 
			
		||||
        <ListItem>
 | 
			
		||||
          <ListItemAvatar>
 | 
			
		||||
            <Avatar>
 | 
			
		||||
              <DeviceHubIcon />
 | 
			
		||||
            </Avatar>
 | 
			
		||||
          </ListItemAvatar>
 | 
			
		||||
          <ListItemText primary="MAC Address" secondary={data.mac_address} />
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <Divider variant="inset" component="li" />
 | 
			
		||||
        <ListItem>
 | 
			
		||||
          <ListItemAvatar>
 | 
			
		||||
            <Avatar>
 | 
			
		||||
              <ComputerIcon />
 | 
			
		||||
            </Avatar>
 | 
			
		||||
          </ListItemAvatar>
 | 
			
		||||
          <ListItemText primary="AP Clients" secondary={data.station_num} />
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <Divider variant="inset" component="li" />
 | 
			
		||||
      </Fragment>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <Fragment>
 | 
			
		||||
        <List>
 | 
			
		||||
          {this.createListItems()}
 | 
			
		||||
        </List>
 | 
			
		||||
        <FormActions>
 | 
			
		||||
          <FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
 | 
			
		||||
            Refresh
 | 
			
		||||
          </FormButton>
 | 
			
		||||
        </FormActions>
 | 
			
		||||
      </Fragment>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTheme(APStatusForm);
 | 
			
		||||
							
								
								
									
										38
									
								
								interface/src/ap/AccessPoint.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								interface/src/ap/AccessPoint.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
import React, { 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 { MenuAppBar } from '../components';
 | 
			
		||||
 | 
			
		||||
import APSettingsController from './APSettingsController';
 | 
			
		||||
import APStatusController from './APStatusController';
 | 
			
		||||
 | 
			
		||||
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
 | 
			
		||||
 | 
			
		||||
class AccessPoint extends Component<AccessPointProps> {
 | 
			
		||||
 | 
			
		||||
  handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
 | 
			
		||||
    this.props.history.push(path);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { authenticatedContext } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <MenuAppBar sectionTitle="Access Point">
 | 
			
		||||
        <Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
 | 
			
		||||
          <Tab value="/ap/status" label="Access Point Status" />
 | 
			
		||||
          <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} />
 | 
			
		||||
          <Redirect to="/ap/status" />
 | 
			
		||||
        </Switch>
 | 
			
		||||
      </MenuAppBar>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withAuthenticatedContext(AccessPoint);
 | 
			
		||||
							
								
								
									
										27
									
								
								interface/src/ap/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								interface/src/ap/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
export enum APProvisionMode {
 | 
			
		||||
  AP_MODE_ALWAYS = 0,
 | 
			
		||||
  AP_MODE_DISCONNECTED = 1,
 | 
			
		||||
  AP_NEVER = 2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum APNetworkStatus {
 | 
			
		||||
  ACTIVE = 0,
 | 
			
		||||
  INACTIVE = 1,
 | 
			
		||||
  LINGERING = 2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface APStatus {
 | 
			
		||||
  status: APNetworkStatus;
 | 
			
		||||
  ip_address: string;
 | 
			
		||||
  mac_address: string;
 | 
			
		||||
  station_num: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface APSettings {
 | 
			
		||||
  provision_mode: APProvisionMode;
 | 
			
		||||
  ssid: string;
 | 
			
		||||
  password: string;
 | 
			
		||||
  local_ip: string;
 | 
			
		||||
  gateway_ip: string;
 | 
			
		||||
  subnet_mask: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								interface/src/api/Endpoints.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								interface/src/api/Endpoints.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
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 WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "wifiSettings";
 | 
			
		||||
export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + "wifiStatus";
 | 
			
		||||
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";
 | 
			
		||||
							
								
								
									
										24
									
								
								interface/src/api/Env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								interface/src/api/Env.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
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/");
 | 
			
		||||
 | 
			
		||||
function calculateEndpointRoot(endpointPath: string) {
 | 
			
		||||
    const httpRoot = process.env.REACT_APP_HTTP_ROOT;
 | 
			
		||||
    if (httpRoot) {
 | 
			
		||||
        return httpRoot + endpointPath;
 | 
			
		||||
    }
 | 
			
		||||
    const location = window.location;
 | 
			
		||||
    return location.protocol + "//" + location.host + endpointPath;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function calculateWebSocketRoot(webSocketPath: string) {
 | 
			
		||||
    const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
 | 
			
		||||
    if (webSocketRoot) {
 | 
			
		||||
        return webSocketRoot + webSocketPath;
 | 
			
		||||
    }
 | 
			
		||||
    const location = window.location;
 | 
			
		||||
    const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
 | 
			
		||||
    return webProtocol + "//" + location.host + webSocketPath;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								interface/src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								interface/src/api/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
export * from './Env'
 | 
			
		||||
export * from './Endpoints'
 | 
			
		||||
							
								
								
									
										42
									
								
								interface/src/authentication/AuthenticatedRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								interface/src/authentication/AuthenticatedRoute.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
 | 
			
		||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
 | 
			
		||||
 | 
			
		||||
import * as Authentication from './Authentication';
 | 
			
		||||
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext } from './AuthenticationContext';
 | 
			
		||||
 | 
			
		||||
type ChildComponent = React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
 | 
			
		||||
 | 
			
		||||
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
 | 
			
		||||
  component: ChildComponent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
 | 
			
		||||
 | 
			
		||||
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    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 AuthenticatedContext}>
 | 
			
		||||
            <Component {...props} />
 | 
			
		||||
          </AuthenticatedContext.Provider>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      Authentication.storeLoginRedirect(location);
 | 
			
		||||
      enqueueSnackbar("Please sign in to continue.", { variant: 'info' });
 | 
			
		||||
      return (
 | 
			
		||||
        <Redirect to='/' />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <Route {...rest} render={renderComponent} />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
 | 
			
		||||
							
								
								
									
										114
									
								
								interface/src/authentication/Authentication.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								interface/src/authentication/Authentication.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
import * as H from 'history';
 | 
			
		||||
 | 
			
		||||
import history from '../history';
 | 
			
		||||
import { Features } from '../features/types';
 | 
			
		||||
import { getDefaultRoute } from '../AppRouting';
 | 
			
		||||
 | 
			
		||||
export const ACCESS_TOKEN = 'access_token';
 | 
			
		||||
export const SIGN_IN_PATHNAME = 'signInPathname';
 | 
			
		||||
export const SIGN_IN_SEARCH = 'signInSearch';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
 | 
			
		||||
 */
 | 
			
		||||
export function getStorage() {
 | 
			
		||||
  return localStorage || sessionStorage;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function storeLoginRedirect(location?: H.Location) {
 | 
			
		||||
  if (location) {
 | 
			
		||||
    getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
 | 
			
		||||
    getStorage().setItem(SIGN_IN_SEARCH, location.search);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function clearLoginRedirect() {
 | 
			
		||||
  getStorage().removeItem(SIGN_IN_PATHNAME);
 | 
			
		||||
  getStorage().removeItem(SIGN_IN_SEARCH);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
 | 
			
		||||
  const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
 | 
			
		||||
  const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
 | 
			
		||||
  clearLoginRedirect();
 | 
			
		||||
  return {
 | 
			
		||||
    pathname: signInPathname || getDefaultRoute(features),
 | 
			
		||||
    search: (signInPathname && signInSearch) || undefined
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wraps the normal fetch routene with one with provides the access token if present.
 | 
			
		||||
 */
 | 
			
		||||
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
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  return fetch(url, params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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.
 | 
			
		||||
 */
 | 
			
		||||
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);
 | 
			
		||||
    const accessToken = getStorage().getItem(ACCESS_TOKEN);
 | 
			
		||||
    if (accessToken) {
 | 
			
		||||
      xhr.withCredentials = true;
 | 
			
		||||
      xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
 | 
			
		||||
    }
 | 
			
		||||
    xhr.upload.onprogress = onProgress;
 | 
			
		||||
    xhr.onload = function () {
 | 
			
		||||
      if (xhr.status === 401 || xhr.status === 403) {
 | 
			
		||||
        history.push("/unauthorized");
 | 
			
		||||
      } else {
 | 
			
		||||
        resolve();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    xhr.onerror = function (event: ProgressEvent<EventTarget>) {
 | 
			
		||||
      reject(new DOMException('Error', 'UploadError'));
 | 
			
		||||
    };
 | 
			
		||||
    xhr.onabort = function () {
 | 
			
		||||
      reject(new DOMException('Aborted', 'AbortError'));
 | 
			
		||||
    };
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
    formData.append('file', file);
 | 
			
		||||
    xhr.send(formData);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wraps the normal fetch routene which redirects on 401 response.
 | 
			
		||||
 */
 | 
			
		||||
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
 | 
			
		||||
  return new Promise<Response>((resolve, reject) => {
 | 
			
		||||
    authorizedFetch(url, params).then(response => {
 | 
			
		||||
      if (response.status === 401 || response.status === 403) {
 | 
			
		||||
        history.push("/unauthorized");
 | 
			
		||||
      } else {
 | 
			
		||||
        resolve(response);
 | 
			
		||||
      }
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
      reject(error);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addAccessTokenParameter(url: string) {
 | 
			
		||||
  const accessToken = getStorage().getItem(ACCESS_TOKEN);
 | 
			
		||||
  if (!accessToken) {
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
  const parsedUrl = new URL(url);
 | 
			
		||||
  parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
 | 
			
		||||
  return parsedUrl.toString();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								interface/src/authentication/AuthenticationContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								interface/src/authentication/AuthenticationContext.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
export interface Me {
 | 
			
		||||
  username: string;
 | 
			
		||||
  admin: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthenticationContext {
 | 
			
		||||
  refresh: () => void;
 | 
			
		||||
  signIn: (accessToken: string) => void;
 | 
			
		||||
  signOut: () => void;
 | 
			
		||||
  me?: Me;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AuthenticationContextDefaultValue = {} as AuthenticationContext
 | 
			
		||||
export const AuthenticationContext = React.createContext(
 | 
			
		||||
  AuthenticationContextDefaultValue
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export interface AuthenticationContextProps {
 | 
			
		||||
  authenticationContext: AuthenticationContext;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.Consumer>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AuthenticatedContext extends AuthenticationContext {
 | 
			
		||||
  me: Me;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContext
 | 
			
		||||
export const AuthenticatedContext = React.createContext(
 | 
			
		||||
  AuthenticatedContextDefaultValue
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export interface AuthenticatedContextProps {
 | 
			
		||||
  authenticatedContext: AuthenticatedContext;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.Consumer>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								interface/src/authentication/AuthenticationWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								interface/src/authentication/AuthenticationWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
 | 
			
		||||
import jwtDecode from 'jwt-decode';
 | 
			
		||||
 | 
			
		||||
import history from '../history'
 | 
			
		||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
 | 
			
		||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
 | 
			
		||||
import { AuthenticationContext, Me } from './AuthenticationContext';
 | 
			
		||||
import FullScreenLoading from '../components/FullScreenLoading';
 | 
			
		||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
 | 
			
		||||
 | 
			
		||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
 | 
			
		||||
 | 
			
		||||
interface AuthenticationWrapperState {
 | 
			
		||||
  context: AuthenticationContext;
 | 
			
		||||
  initialized: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
 | 
			
		||||
 | 
			
		||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
 | 
			
		||||
 | 
			
		||||
  constructor(props: AuthenticationWrapperProps) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.state = {
 | 
			
		||||
      context: {
 | 
			
		||||
        refresh: this.refresh,
 | 
			
		||||
        signIn: this.signIn,
 | 
			
		||||
        signOut: this.signOut,
 | 
			
		||||
      },
 | 
			
		||||
      initialized: false
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.refresh();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <React.Fragment>
 | 
			
		||||
        {this.state.initialized ? this.renderContent() : this.renderContentLoading()}
 | 
			
		||||
      </React.Fragment>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderContent() {
 | 
			
		||||
    return (
 | 
			
		||||
      <AuthenticationContext.Provider value={this.state.context}>
 | 
			
		||||
        {this.props.children}
 | 
			
		||||
      </AuthenticationContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderContentLoading() {
 | 
			
		||||
    return (
 | 
			
		||||
      <FullScreenLoading />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  refresh = () => {
 | 
			
		||||
    if (!this.props.features.security) {
 | 
			
		||||
      this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    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',
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
      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' });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
 | 
			
		||||
      throw new Error("Failed to parse JWT " + err.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  signOut = () => {
 | 
			
		||||
    getStorage().removeItem(ACCESS_TOKEN);
 | 
			
		||||
    this.setState({
 | 
			
		||||
      context: {
 | 
			
		||||
        ...this.state.context,
 | 
			
		||||
        me: undefined
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
 | 
			
		||||
    history.push('/');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
 | 
			
		||||
							
								
								
									
										30
									
								
								interface/src/authentication/UnauthenticatedRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								interface/src/authentication/UnauthenticatedRoute.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
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>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
 | 
			
		||||
 | 
			
		||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
 | 
			
		||||
 | 
			
		||||
  public render() {
 | 
			
		||||
    const { authenticationContext, component: Component, features, ...rest } = this.props;
 | 
			
		||||
    const renderComponent: RenderComponent = (props) => {
 | 
			
		||||
      if (authenticationContext.me) {
 | 
			
		||||
        return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
 | 
			
		||||
      }
 | 
			
		||||
      return (<Component {...props} />);
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <Route {...rest} render={renderComponent} />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withFeatures(withAuthenticationContext(UnauthenticatedRoute));
 | 
			
		||||
							
								
								
									
										6
									
								
								interface/src/authentication/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								interface/src/authentication/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
export { default as AuthenticatedRoute } from './AuthenticatedRoute';
 | 
			
		||||
export { default as AuthenticationWrapper } from './AuthenticationWrapper';
 | 
			
		||||
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute';
 | 
			
		||||
 | 
			
		||||
export * from './Authentication';
 | 
			
		||||
export * from './AuthenticationContext';
 | 
			
		||||
							
								
								
									
										59
									
								
								interface/src/components/ApplicationError.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								interface/src/components/ApplicationError.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
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"
 | 
			
		||||
 | 
			
		||||
const styles = makeStyles(
 | 
			
		||||
  {
 | 
			
		||||
    siteErrorPage: {
 | 
			
		||||
      display: "flex",
 | 
			
		||||
      height: "100vh",
 | 
			
		||||
      justifyContent: "center",
 | 
			
		||||
      flexDirection: "column"
 | 
			
		||||
    },
 | 
			
		||||
    siteErrorPagePanel: {
 | 
			
		||||
      textAlign: "center",
 | 
			
		||||
      padding: "280px 0 40px 0",
 | 
			
		||||
      backgroundImage: 'url("/app/icon.png")',
 | 
			
		||||
      backgroundRepeat: "no-repeat",
 | 
			
		||||
      backgroundPosition: "50% 40px",
 | 
			
		||||
      backgroundSize: "200px auto",
 | 
			
		||||
      width: "100%",
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
interface ApplicationErrorProps {
 | 
			
		||||
  error?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
 | 
			
		||||
  const classes = styles();
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={classes.siteErrorPage}>
 | 
			
		||||
      <CssBaseline />
 | 
			
		||||
      <Paper className={classes.siteErrorPagePanel} elevation={10}>
 | 
			
		||||
        <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>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
        <Typography variant="subtitle1" gutterBottom>
 | 
			
		||||
          Failed to configure the application, please refresh to try again.
 | 
			
		||||
        </Typography>
 | 
			
		||||
        {error &&
 | 
			
		||||
          (
 | 
			
		||||
            <Typography variant="subtitle2" gutterBottom>
 | 
			
		||||
              Error: {error}
 | 
			
		||||
            </Typography>
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ApplicationError;
 | 
			
		||||
							
								
								
									
										10
									
								
								interface/src/components/BlockFormControlLabel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								interface/src/components/BlockFormControlLabel.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
import React, { FC } from "react";
 | 
			
		||||
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
 | 
			
		||||
 | 
			
		||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
 | 
			
		||||
  <div>
 | 
			
		||||
    <FormControlLabel {...props} />
 | 
			
		||||
  </div>
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export default BlockFormControlLabel;
 | 
			
		||||
							
								
								
									
										11
									
								
								interface/src/components/ErrorButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								interface/src/components/ErrorButton.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
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,
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export default ErrorButton;
 | 
			
		||||
							
								
								
									
										7
									
								
								interface/src/components/FormActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								interface/src/components/FormActions.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { styled, Box } from "@material-ui/core";
 | 
			
		||||
 | 
			
		||||
const FormActions = styled(Box)(({ theme }) => ({
 | 
			
		||||
  marginTop: theme.spacing(1)
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export default FormActions;
 | 
			
		||||
							
								
								
									
										13
									
								
								interface/src/components/FormButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								interface/src/components/FormButton.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import { Button, styled } from "@material-ui/core";
 | 
			
		||||
 | 
			
		||||
const FormButton = styled(Button)(({ theme }) => ({
 | 
			
		||||
  margin: theme.spacing(0, 1),
 | 
			
		||||
  '&:last-child': {
 | 
			
		||||
    marginRight: 0,
 | 
			
		||||
  },
 | 
			
		||||
  '&:first-child': {
 | 
			
		||||
    marginLeft: 0,
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export default FormButton;
 | 
			
		||||
							
								
								
									
										32
									
								
								interface/src/components/FullScreenLoading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								interface/src/components/FullScreenLoading.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
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({
 | 
			
		||||
  fullScreenLoading: {
 | 
			
		||||
    padding: theme.spacing(2),
 | 
			
		||||
    display: "flex",
 | 
			
		||||
    alignItems: "center",
 | 
			
		||||
    justifyContent: "center",
 | 
			
		||||
    height: "100vh",
 | 
			
		||||
    flexDirection: "column"
 | 
			
		||||
  },
 | 
			
		||||
  progress: {
 | 
			
		||||
    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>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default FullScreenLoading;
 | 
			
		||||
							
								
								
									
										23
									
								
								interface/src/components/HighlightAvatar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								interface/src/components/HighlightAvatar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { Avatar, makeStyles } from "@material-ui/core";
 | 
			
		||||
import React, { FC } from "react";
 | 
			
		||||
 | 
			
		||||
interface HighlightAvatarProps {
 | 
			
		||||
  color: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles({
 | 
			
		||||
  root: (props: HighlightAvatarProps) => ({
 | 
			
		||||
    backgroundColor: props.color
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
 | 
			
		||||
  const classes = useStyles(props);
 | 
			
		||||
  return (
 | 
			
		||||
    <Avatar className={classes.root}>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </Avatar>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default HighlightAvatar;
 | 
			
		||||
							
								
								
									
										286
									
								
								interface/src/components/MenuAppBar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								interface/src/components/MenuAppBar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,286 @@
 | 
			
		||||
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 { Card, CardContent, CardActions } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
 | 
			
		||||
 | 
			
		||||
import WifiIcon from '@material-ui/icons/Wifi';
 | 
			
		||||
import SettingsIcon from '@material-ui/icons/Settings';
 | 
			
		||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
 | 
			
		||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
 | 
			
		||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
 | 
			
		||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
 | 
			
		||||
import LockIcon from '@material-ui/icons/Lock';
 | 
			
		||||
import MenuIcon from '@material-ui/icons/Menu';
 | 
			
		||||
 | 
			
		||||
import ProjectMenu from '../project/ProjectMenu';
 | 
			
		||||
import { PROJECT_NAME } from '../api';
 | 
			
		||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
 | 
			
		||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
 | 
			
		||||
 | 
			
		||||
const drawerWidth = 290;
 | 
			
		||||
 | 
			
		||||
const styles = (theme: Theme) => createStyles({
 | 
			
		||||
  root: {
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
  },
 | 
			
		||||
  drawer: {
 | 
			
		||||
    [theme.breakpoints.up('md')]: {
 | 
			
		||||
      width: drawerWidth,
 | 
			
		||||
      flexShrink: 0,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  title: {
 | 
			
		||||
    flexGrow: 1
 | 
			
		||||
  },
 | 
			
		||||
  appBar: {
 | 
			
		||||
    marginLeft: drawerWidth,
 | 
			
		||||
    [theme.breakpoints.up('md')]: {
 | 
			
		||||
      width: `calc(100% - ${drawerWidth}px)`,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  toolbarImage: {
 | 
			
		||||
    [theme.breakpoints.up('xs')]: {
 | 
			
		||||
      height: 24,
 | 
			
		||||
      marginRight: theme.spacing(2)
 | 
			
		||||
    },
 | 
			
		||||
    [theme.breakpoints.up('sm')]: {
 | 
			
		||||
      height: 36,
 | 
			
		||||
      marginRight: theme.spacing(3)
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  menuButton: {
 | 
			
		||||
    marginRight: theme.spacing(2),
 | 
			
		||||
    [theme.breakpoints.up('md')]: {
 | 
			
		||||
      display: 'none',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  toolbar: theme.mixins.toolbar,
 | 
			
		||||
  drawerPaper: {
 | 
			
		||||
    width: drawerWidth,
 | 
			
		||||
  },
 | 
			
		||||
  content: {
 | 
			
		||||
    flexGrow: 1
 | 
			
		||||
  },
 | 
			
		||||
  authMenu: {
 | 
			
		||||
    zIndex: theme.zIndex.tooltip,
 | 
			
		||||
    maxWidth: 400,
 | 
			
		||||
  },
 | 
			
		||||
  authMenuActions: {
 | 
			
		||||
    padding: theme.spacing(2),
 | 
			
		||||
    "& > * + *": {
 | 
			
		||||
      marginLeft: theme.spacing(2),
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface MenuAppBarState {
 | 
			
		||||
  mobileOpen: boolean;
 | 
			
		||||
  authMenuOpen: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 = {
 | 
			
		||||
      mobileOpen: false,
 | 
			
		||||
      authMenuOpen: false
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  anchorRef: RefObject<HTMLButtonElement> = React.createRef();
 | 
			
		||||
 | 
			
		||||
  handleToggle = () => {
 | 
			
		||||
    this.setState({ authMenuOpen: !this.state.authMenuOpen });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClose = (event: React.MouseEvent<Document>) => {
 | 
			
		||||
    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 { 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} />
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Typography variant="h6" color="textPrimary">
 | 
			
		||||
            {PROJECT_NAME}
 | 
			
		||||
          </Typography>
 | 
			
		||||
          <Divider absolute />
 | 
			
		||||
        </Toolbar>
 | 
			
		||||
        {features.project && (
 | 
			
		||||
          <Fragment>
 | 
			
		||||
            <ProjectMenu />
 | 
			
		||||
            <Divider />
 | 
			
		||||
          </Fragment>
 | 
			
		||||
        )}
 | 
			
		||||
        <List>
 | 
			
		||||
          <ListItem to='/wifi/' selected={path.startsWith('/wifi/')} button component={Link}>
 | 
			
		||||
            <ListItemIcon>
 | 
			
		||||
              <WifiIcon />
 | 
			
		||||
            </ListItemIcon>
 | 
			
		||||
            <ListItemText primary="WiFi Connection" />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          <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}>
 | 
			
		||||
            <ListItemIcon>
 | 
			
		||||
              <AccessTimeIcon />
 | 
			
		||||
            </ListItemIcon>
 | 
			
		||||
            <ListItemText primary="Network Time" />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          )}
 | 
			
		||||
          {features.mqtt && (
 | 
			
		||||
            <ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
 | 
			
		||||
              <ListItemIcon>
 | 
			
		||||
                <DeviceHubIcon />
 | 
			
		||||
              </ListItemIcon>
 | 
			
		||||
              <ListItemText primary="MQTT" />
 | 
			
		||||
            </ListItem>
 | 
			
		||||
          )}
 | 
			
		||||
          {features.security && (
 | 
			
		||||
            <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} >
 | 
			
		||||
            <ListItemIcon>
 | 
			
		||||
              <SettingsIcon />
 | 
			
		||||
            </ListItemIcon>
 | 
			
		||||
            <ListItemText primary="System" />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        </List>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const userMenu = (
 | 
			
		||||
      <div>
 | 
			
		||||
        <IconButton
 | 
			
		||||
          ref={this.anchorRef}
 | 
			
		||||
          aria-owns={authMenuOpen ? 'menu-list-grow' : undefined}
 | 
			
		||||
          aria-haspopup="true"
 | 
			
		||||
          onClick={this.handleToggle}
 | 
			
		||||
          color="inherit"
 | 
			
		||||
        >
 | 
			
		||||
          <AccountCircleIcon />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
        <Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
 | 
			
		||||
          <ClickAwayListener onClickAway={this.handleClose}>
 | 
			
		||||
            <Card id="menu-list-grow">
 | 
			
		||||
              <CardContent>
 | 
			
		||||
                <List disablePadding>
 | 
			
		||||
                  <ListItem disableGutters>
 | 
			
		||||
                    <ListItemAvatar>
 | 
			
		||||
                      <Avatar>
 | 
			
		||||
                        <AccountCircleIcon />
 | 
			
		||||
                      </Avatar>
 | 
			
		||||
                    </ListItemAvatar>
 | 
			
		||||
                    <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>
 | 
			
		||||
              </CardActions>
 | 
			
		||||
            </Card>
 | 
			
		||||
          </ClickAwayListener>
 | 
			
		||||
        </Popper>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classes.root}>
 | 
			
		||||
        <AppBar position="fixed" className={classes.appBar} elevation={0}>
 | 
			
		||||
          <Toolbar>
 | 
			
		||||
            <IconButton
 | 
			
		||||
              color="inherit"
 | 
			
		||||
              aria-label="Open drawer"
 | 
			
		||||
              edge="start"
 | 
			
		||||
              onClick={this.handleDrawerToggle}
 | 
			
		||||
              className={classes.menuButton}
 | 
			
		||||
            >
 | 
			
		||||
              <MenuIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
            <Typography variant="h6" color="inherit" noWrap className={classes.title}>
 | 
			
		||||
              {sectionTitle}
 | 
			
		||||
            </Typography>
 | 
			
		||||
            {features.security && userMenu}
 | 
			
		||||
          </Toolbar>
 | 
			
		||||
        </AppBar>
 | 
			
		||||
        <nav className={classes.drawer}>
 | 
			
		||||
          <Hidden mdUp implementation="css">
 | 
			
		||||
            <Drawer
 | 
			
		||||
              variant="temporary"
 | 
			
		||||
              anchor={theme.direction === 'rtl' ? 'right' : 'left'}
 | 
			
		||||
              open={mobileOpen}
 | 
			
		||||
              onClose={this.handleDrawerToggle}
 | 
			
		||||
              classes={{
 | 
			
		||||
                paper: classes.drawerPaper,
 | 
			
		||||
              }}
 | 
			
		||||
              ModalProps={{
 | 
			
		||||
                keepMounted: true,
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {drawer}
 | 
			
		||||
            </Drawer>
 | 
			
		||||
          </Hidden>
 | 
			
		||||
          <Hidden smDown implementation="css">
 | 
			
		||||
            <Drawer
 | 
			
		||||
              classes={{
 | 
			
		||||
                paper: classes.drawerPaper,
 | 
			
		||||
              }}
 | 
			
		||||
              variant="permanent"
 | 
			
		||||
              open
 | 
			
		||||
            >
 | 
			
		||||
              {drawer}
 | 
			
		||||
            </Drawer>
 | 
			
		||||
          </Hidden>
 | 
			
		||||
        </nav>
 | 
			
		||||
        <main className={classes.content}>
 | 
			
		||||
          <div className={classes.toolbar} />
 | 
			
		||||
          {children}
 | 
			
		||||
        </main>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withRouter(
 | 
			
		||||
  withTheme(
 | 
			
		||||
    withFeatures(
 | 
			
		||||
      withAuthenticatedContext(
 | 
			
		||||
        withStyles(styles)(MenuAppBar)
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										58
									
								
								interface/src/components/PasswordValidator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								interface/src/components/PasswordValidator.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
const styles = createStyles({
 | 
			
		||||
  input: {
 | 
			
		||||
    "&::-ms-reveal": {
 | 
			
		||||
      display: "none"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
 | 
			
		||||
 | 
			
		||||
interface PasswordValidatorState {
 | 
			
		||||
  showPassword: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    showPassword: false
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  toggleShowPassword = () => {
 | 
			
		||||
    this.setState({
 | 
			
		||||
      showPassword: !this.state.showPassword
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { classes, ...rest } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <TextValidator
 | 
			
		||||
        {...rest}
 | 
			
		||||
        type={this.state.showPassword ? 'text' : 'password'}
 | 
			
		||||
        InputProps={{
 | 
			
		||||
          classes,
 | 
			
		||||
          endAdornment:
 | 
			
		||||
            <InputAdornment position="end">
 | 
			
		||||
              <IconButton
 | 
			
		||||
                aria-label="Toggle password visibility"
 | 
			
		||||
                onClick={this.toggleShowPassword}
 | 
			
		||||
              >
 | 
			
		||||
                {this.state.showPassword ? <Visibility /> : <VisibilityOff />}
 | 
			
		||||
              </IconButton>
 | 
			
		||||
            </InputAdornment>
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withStyles(styles)(PasswordValidator);
 | 
			
		||||
							
								
								
									
										113
									
								
								interface/src/components/RestController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								interface/src/components/RestController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
 | 
			
		||||
 | 
			
		||||
import { redirectingAuthorizedFetch } from '../authentication';
 | 
			
		||||
 | 
			
		||||
export interface RestControllerProps<D> extends WithSnackbarProps {
 | 
			
		||||
  handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
 | 
			
		||||
 | 
			
		||||
  setData: (data: D, callback?: () => void) => void;
 | 
			
		||||
  saveData: () => void;
 | 
			
		||||
  loadData: () => void;
 | 
			
		||||
 | 
			
		||||
  data?: D;
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  errorMessage?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
  switch (event.target.type) {
 | 
			
		||||
    case "number":
 | 
			
		||||
      return event.target.valueAsNumber;
 | 
			
		||||
    case "checkbox":
 | 
			
		||||
      return event.target.checked;
 | 
			
		||||
    default:
 | 
			
		||||
      return event.target.value
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface RestControllerState<D> {
 | 
			
		||||
  data?: D;
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  errorMessage?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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>> {
 | 
			
		||||
 | 
			
		||||
      state: RestControllerState<D> = {
 | 
			
		||||
        data: undefined,
 | 
			
		||||
        loading: false,
 | 
			
		||||
        errorMessage: undefined
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      setData = (data: D, callback?: () => void) => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
          data,
 | 
			
		||||
          loading: false,
 | 
			
		||||
          errorMessage: undefined
 | 
			
		||||
        }, callback);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      loadData = () => {
 | 
			
		||||
        this.setState({
 | 
			
		||||
          data: undefined,
 | 
			
		||||
          loading: true,
 | 
			
		||||
          errorMessage: undefined
 | 
			
		||||
        });
 | 
			
		||||
        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' });
 | 
			
		||||
          this.setState({ data: undefined, loading: false, errorMessage });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      saveData = () => {
 | 
			
		||||
        this.setState({ loading: true });
 | 
			
		||||
        redirectingAuthorizedFetch(endpointUrl, {
 | 
			
		||||
          method: 'POST',
 | 
			
		||||
          body: JSON.stringify(this.state.data),
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        }).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' });
 | 
			
		||||
          this.setState({ data: json, loading: false });
 | 
			
		||||
        }).catch(error => {
 | 
			
		||||
          const errorMessage = error.message || "Unknown error";
 | 
			
		||||
          this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
 | 
			
		||||
          this.setState({ data: undefined, loading: false, errorMessage });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
        const data = { ...this.state.data!, [name]: extractEventValue(event) };
 | 
			
		||||
        this.setState({ data });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      render() {
 | 
			
		||||
        return <RestController
 | 
			
		||||
          {...this.state}
 | 
			
		||||
          {...this.props as P}
 | 
			
		||||
          handleValueChange={this.handleValueChange}
 | 
			
		||||
          setData={this.setData}
 | 
			
		||||
          saveData={this.saveData}
 | 
			
		||||
          loadData={this.loadData}
 | 
			
		||||
        />;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								interface/src/components/RestFormLoader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								interface/src/components/RestFormLoader.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
 | 
			
		||||
import { Button, LinearProgress, Typography } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
import { RestControllerProps } from '.';
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme: Theme) =>
 | 
			
		||||
  createStyles({
 | 
			
		||||
    loadingSettings: {
 | 
			
		||||
      margin: theme.spacing(0.5),
 | 
			
		||||
    },
 | 
			
		||||
    loadingSettingsDetails: {
 | 
			
		||||
      margin: theme.spacing(4),
 | 
			
		||||
      textAlign: "center"
 | 
			
		||||
    },
 | 
			
		||||
    button: {
 | 
			
		||||
      marginRight: theme.spacing(2),
 | 
			
		||||
      marginTop: theme.spacing(2),
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
 | 
			
		||||
 | 
			
		||||
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
 | 
			
		||||
  render: (props: RestFormProps<D>) => JSX.Element;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
 | 
			
		||||
  const { loading, errorMessage, loadData, render, data, ...rest } = props;
 | 
			
		||||
  const classes = useStyles();
 | 
			
		||||
  if (loading || !data) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classes.loadingSettings}>
 | 
			
		||||
        <LinearProgress className={classes.loadingSettingsDetails} />
 | 
			
		||||
        <Typography variant="h6" className={classes.loadingSettingsDetails}>
 | 
			
		||||
          Loading…
 | 
			
		||||
        </Typography>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (errorMessage) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classes.loadingSettings}>
 | 
			
		||||
        <Typography variant="h6" className={classes.loadingSettingsDetails}>
 | 
			
		||||
          {errorMessage}
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
 | 
			
		||||
          Retry
 | 
			
		||||
        </Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return render({ ...rest, loadData, data });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								interface/src/components/SectionContent.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								interface/src/components/SectionContent.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { Typography, Paper } from '@material-ui/core';
 | 
			
		||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme: Theme) =>
 | 
			
		||||
  createStyles({
 | 
			
		||||
    content: {
 | 
			
		||||
      padding: theme.spacing(2),
 | 
			
		||||
      margin: theme.spacing(3),
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
interface SectionContentProps {
 | 
			
		||||
  title: string;
 | 
			
		||||
  titleGutter?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SectionContent: React.FC<SectionContentProps> = (props) => {
 | 
			
		||||
  const { children, title, titleGutter } = props;
 | 
			
		||||
  const classes = useStyles();
 | 
			
		||||
  return (
 | 
			
		||||
    <Paper className={classes.content}>
 | 
			
		||||
      <Typography variant="h6" gutterBottom={titleGutter}>
 | 
			
		||||
        {title}
 | 
			
		||||
      </Typography>
 | 
			
		||||
      {children}
 | 
			
		||||
    </Paper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SectionContent;
 | 
			
		||||
							
								
								
									
										96
									
								
								interface/src/components/SingleUpload.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								interface/src/components/SingleUpload.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
import React, { FC, Fragment } from 'react';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
interface SingleUploadStyleProps extends DropzoneState {
 | 
			
		||||
  uploading: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
 | 
			
		||||
 | 
			
		||||
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
 | 
			
		||||
  if (props.isDragAccept) {
 | 
			
		||||
    return theme.palette.success.main;
 | 
			
		||||
  }
 | 
			
		||||
  if (props.isDragReject) {
 | 
			
		||||
    return theme.palette.error.main;
 | 
			
		||||
  }
 | 
			
		||||
  if (props.isDragActive) {
 | 
			
		||||
    return theme.palette.info.main;
 | 
			
		||||
  }
 | 
			
		||||
  return theme.palette.grey[700];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme: Theme) => createStyles({
 | 
			
		||||
  dropzone: {
 | 
			
		||||
    padding: theme.spacing(8, 2),
 | 
			
		||||
    borderWidth: 2,
 | 
			
		||||
    borderRadius: 2,
 | 
			
		||||
    borderStyle: 'dashed',
 | 
			
		||||
    color: theme.palette.grey[700],
 | 
			
		||||
    transition: 'border .24s ease-in-out',
 | 
			
		||||
    cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export interface SingleUploadProps {
 | 
			
		||||
  onDrop: (acceptedFiles: File[]) => void;
 | 
			
		||||
  onCancel: () => void;
 | 
			
		||||
  accept?: string | string[];
 | 
			
		||||
  uploading: boolean;
 | 
			
		||||
  progress?: ProgressEvent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const renderProgress = (progress?: ProgressEvent) => (
 | 
			
		||||
    <LinearProgress
 | 
			
		||||
      variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
 | 
			
		||||
      value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div {...getRootProps({ className: classes.dropzone })}>
 | 
			
		||||
      <input {...getInputProps()} />
 | 
			
		||||
      <Box flexDirection="column" display="flex" alignItems="center">
 | 
			
		||||
        <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}>
 | 
			
		||||
              Cancel
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Fragment>
 | 
			
		||||
        )}
 | 
			
		||||
      </Box>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SingleUpload;
 | 
			
		||||
							
								
								
									
										133
									
								
								interface/src/components/WebSocketController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								interface/src/components/WebSocketController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Sockette from 'sockette';
 | 
			
		||||
import throttle from 'lodash/throttle';
 | 
			
		||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
 | 
			
		||||
 | 
			
		||||
import { addAccessTokenParameter } from '../authentication';
 | 
			
		||||
import { extractEventValue } from '.';
 | 
			
		||||
 | 
			
		||||
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
 | 
			
		||||
  handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
 | 
			
		||||
 | 
			
		||||
  setData: (data: D, callback?: () => void) => void;
 | 
			
		||||
  saveData: () => void;
 | 
			
		||||
  saveDataAndClear(): () => void;
 | 
			
		||||
 | 
			
		||||
  connected: boolean;
 | 
			
		||||
  data?: D;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface WebSocketControllerState<D> {
 | 
			
		||||
  ws: Sockette;
 | 
			
		||||
  connected: boolean;
 | 
			
		||||
  clientId?: string;
 | 
			
		||||
  data?: D;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum WebSocketMessageType {
 | 
			
		||||
  ID = "id",
 | 
			
		||||
  PAYLOAD = "payload"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface WebSocketIdMessage {
 | 
			
		||||
  type: typeof WebSocketMessageType.ID;
 | 
			
		||||
  id: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface WebSocketPayloadMessage<D> {
 | 
			
		||||
  type: typeof WebSocketMessageType.PAYLOAD;
 | 
			
		||||
  origin_id: string;
 | 
			
		||||
  payload: 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>>) {
 | 
			
		||||
  return withSnackbar(
 | 
			
		||||
    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,
 | 
			
		||||
          }),
 | 
			
		||||
          connected: false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      componentWillUnmount() {
 | 
			
		||||
        this.state.ws.close();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      onMessage = (event: MessageEvent) => {
 | 
			
		||||
        const rawData = event.data;
 | 
			
		||||
        if (typeof rawData === 'string' || rawData instanceof String) {
 | 
			
		||||
          this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      handleMessage = (message: WebSocketMessage<D>) => {
 | 
			
		||||
        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 }
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      onOpen = () => {
 | 
			
		||||
        this.setState({ connected: true });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      onClose = () => {
 | 
			
		||||
        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;
 | 
			
		||||
        if (connected) {
 | 
			
		||||
          ws.json(data);
 | 
			
		||||
        }
 | 
			
		||||
      }, wsThrottle);
 | 
			
		||||
 | 
			
		||||
      saveDataAndClear = throttle(() => {
 | 
			
		||||
        const { ws, connected, data } = this.state;
 | 
			
		||||
        if (connected) {
 | 
			
		||||
          this.setState({
 | 
			
		||||
            data: undefined
 | 
			
		||||
          }, () => ws.json(data));
 | 
			
		||||
        }
 | 
			
		||||
      }, wsThrottle);
 | 
			
		||||
 | 
			
		||||
      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}
 | 
			
		||||
          handleValueChange={this.handleValueChange}
 | 
			
		||||
          setData={this.setData}
 | 
			
		||||
          saveData={this.saveData}
 | 
			
		||||
          saveDataAndClear={this.saveDataAndClear}
 | 
			
		||||
          connected={this.state.connected}
 | 
			
		||||
          data={this.state.data}
 | 
			
		||||
        />;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								interface/src/components/WebSocketFormLoader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								interface/src/components/WebSocketFormLoader.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
 | 
			
		||||
import { LinearProgress, Typography } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
import { WebSocketControllerProps } from '.';
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme: Theme) =>
 | 
			
		||||
  createStyles({
 | 
			
		||||
    loadingSettings: {
 | 
			
		||||
      margin: theme.spacing(0.5),
 | 
			
		||||
    },
 | 
			
		||||
    loadingSettingsDetails: {
 | 
			
		||||
      margin: theme.spacing(4),
 | 
			
		||||
      textAlign: "center"
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
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>) {
 | 
			
		||||
  const { connected, render, data, ...rest } = props;
 | 
			
		||||
  const classes = useStyles();
 | 
			
		||||
  if (!connected || !data) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classes.loadingSettings}>
 | 
			
		||||
        <LinearProgress className={classes.loadingSettingsDetails} />
 | 
			
		||||
        <Typography variant="h6" className={classes.loadingSettingsDetails}>
 | 
			
		||||
          Connecting to WebSocket...
 | 
			
		||||
        </Typography>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return render({ ...rest, data });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								interface/src/components/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								interface/src/components/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
export { default as BlockFormControlLabel } from './BlockFormControlLabel';
 | 
			
		||||
export { default as FormActions } from './FormActions';
 | 
			
		||||
export { default as FormButton } from './FormButton';
 | 
			
		||||
export { default as HighlightAvatar } from './HighlightAvatar';
 | 
			
		||||
export { default as MenuAppBar } from './MenuAppBar';
 | 
			
		||||
export { default as PasswordValidator } from './PasswordValidator';
 | 
			
		||||
export { default as RestFormLoader } from './RestFormLoader';
 | 
			
		||||
export { default as SectionContent } from './SectionContent';
 | 
			
		||||
export { default as WebSocketFormLoader } from './WebSocketFormLoader';
 | 
			
		||||
export { default as ErrorButton } from './ErrorButton';
 | 
			
		||||
export { default as SingleUpload } from './SingleUpload';
 | 
			
		||||
 | 
			
		||||
export * from './RestFormLoader';
 | 
			
		||||
export * from './RestController';
 | 
			
		||||
 | 
			
		||||
export * from './WebSocketFormLoader';
 | 
			
		||||
export * from './WebSocketController';
 | 
			
		||||
							
								
								
									
										23
									
								
								interface/src/features/ApplicationContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								interface/src/features/ApplicationContext.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Features } from './types';
 | 
			
		||||
 | 
			
		||||
export interface ApplicationContext {
 | 
			
		||||
  features: Features;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ApplicationContextDefaultValue = {} as ApplicationContext
 | 
			
		||||
export const ApplicationContext = React.createContext(
 | 
			
		||||
  ApplicationContextDefaultValue
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export function withAuthenticatedContexApplicationContext<T extends ApplicationContext>(Component: React.ComponentType<T>) {
 | 
			
		||||
  return class extends React.Component<Omit<T, keyof ApplicationContext>> {
 | 
			
		||||
    render() {
 | 
			
		||||
      return (
 | 
			
		||||
        <ApplicationContext.Consumer>
 | 
			
		||||
          {authenticatedContext => <Component {...this.props as T} features={authenticatedContext} />}
 | 
			
		||||
        </ApplicationContext.Consumer>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								interface/src/features/FeaturesContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								interface/src/features/FeaturesContext.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Features } from './types';
 | 
			
		||||
 | 
			
		||||
export interface FeaturesContext {
 | 
			
		||||
  features: Features;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const FeaturesContextDefaultValue = {} as FeaturesContext
 | 
			
		||||
export const FeaturesContext = React.createContext(
 | 
			
		||||
  FeaturesContextDefaultValue
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export interface WithFeaturesProps {
 | 
			
		||||
  features: Features;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.Consumer>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								interface/src/features/FeaturesWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								interface/src/features/FeaturesWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import { Features } from './types';
 | 
			
		||||
import { FeaturesContext } from './FeaturesContext';
 | 
			
		||||
import FullScreenLoading from '../components/FullScreenLoading';
 | 
			
		||||
import ApplicationError from '../components/ApplicationError';
 | 
			
		||||
import { FEATURES_ENDPOINT } from '../api';
 | 
			
		||||
 | 
			
		||||
interface FeaturesWrapperState {
 | 
			
		||||
  features?: Features;
 | 
			
		||||
  error?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
 | 
			
		||||
 | 
			
		||||
  state: FeaturesWrapperState = {};
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.fetchFeaturesDetails();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetchFeaturesDetails = () => {
 | 
			
		||||
    fetch(FEATURES_ENDPOINT)
 | 
			
		||||
      .then(response => {
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
          return response.json();
 | 
			
		||||
        } else {
 | 
			
		||||
          throw Error("Unexpected status code: " + response.status);
 | 
			
		||||
        }
 | 
			
		||||
      }).then(features => {
 | 
			
		||||
        this.setState({ features });
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => {
 | 
			
		||||
        this.setState({ error: error.message });
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { features, error } = this.state;
 | 
			
		||||
    if (features) {
 | 
			
		||||
      return (
 | 
			
		||||
        <FeaturesContext.Provider value={{
 | 
			
		||||
          features
 | 
			
		||||
        }}>
 | 
			
		||||
          {this.props.children}
 | 
			
		||||
        </FeaturesContext.Provider>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (error) {
 | 
			
		||||
      return (
 | 
			
		||||
        <ApplicationError error={error} />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <FullScreenLoading />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default FeaturesWrapper;
 | 
			
		||||
							
								
								
									
										8
									
								
								interface/src/features/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								interface/src/features/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
export interface Features {
 | 
			
		||||
  project: boolean;
 | 
			
		||||
  security: boolean;
 | 
			
		||||
  mqtt: boolean;
 | 
			
		||||
  ntp: boolean;
 | 
			
		||||
  ota: boolean;
 | 
			
		||||
  upload_firmware: boolean;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								interface/src/history.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								interface/src/history.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import { createBrowserHistory } from 'history';
 | 
			
		||||
 | 
			
		||||
export default createBrowserHistory({
 | 
			
		||||
  /* pass a configuration object here if needed */
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										13
									
								
								interface/src/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								interface/src/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { render } from 'react-dom';
 | 
			
		||||
 | 
			
		||||
import history from './history';
 | 
			
		||||
import { Router } from 'react-router';
 | 
			
		||||
 | 
			
		||||
import App from './App';
 | 
			
		||||
 | 
			
		||||
render((
 | 
			
		||||
  <Router history={history}>
 | 
			
		||||
    <App/>
 | 
			
		||||
  </Router>
 | 
			
		||||
), document.getElementById("root"))
 | 
			
		||||
							
								
								
									
										37
									
								
								interface/src/mqtt/Mqtt.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								interface/src/mqtt/Mqtt.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import React, { 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 { MenuAppBar } from '../components';
 | 
			
		||||
import MqttStatusController from './MqttStatusController';
 | 
			
		||||
import MqttSettingsController from './MqttSettingsController';
 | 
			
		||||
 | 
			
		||||
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
 | 
			
		||||
 | 
			
		||||
class Mqtt extends Component<MqttProps> {
 | 
			
		||||
 | 
			
		||||
  handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
 | 
			
		||||
    this.props.history.push(path);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { authenticatedContext } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <MenuAppBar sectionTitle="MQTT">
 | 
			
		||||
        <Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
 | 
			
		||||
          <Tab value="/mqtt/status" label="MQTT Status" />
 | 
			
		||||
          <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} />
 | 
			
		||||
          <Redirect to="/mqtt/status" />
 | 
			
		||||
        </Switch>
 | 
			
		||||
      </MenuAppBar>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withAuthenticatedContext(Mqtt);
 | 
			
		||||
							
								
								
									
										30
									
								
								interface/src/mqtt/MqttSettingsController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								interface/src/mqtt/MqttSettingsController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
 | 
			
		||||
import { MQTT_SETTINGS_ENDPOINT } from '../api';
 | 
			
		||||
 | 
			
		||||
import MqttSettingsForm from './MqttSettingsForm';
 | 
			
		||||
import { MqttSettings } from './types';
 | 
			
		||||
 | 
			
		||||
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
 | 
			
		||||
 | 
			
		||||
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <SectionContent title="MQTT Settings" titleGutter>
 | 
			
		||||
        <RestFormLoader
 | 
			
		||||
          {...this.props}
 | 
			
		||||
          render={formProps => <MqttSettingsForm {...formProps} />}
 | 
			
		||||
        />
 | 
			
		||||
      </SectionContent>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
 | 
			
		||||
							
								
								
									
										128
									
								
								interface/src/mqtt/MqttSettingsForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								interface/src/mqtt/MqttSettingsForm.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
 | 
			
		||||
 | 
			
		||||
import { Checkbox, TextField } from '@material-ui/core';
 | 
			
		||||
import SaveIcon from '@material-ui/icons/Save';
 | 
			
		||||
 | 
			
		||||
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel, PasswordValidator } from '../components';
 | 
			
		||||
import { isIP, isHostname, or } from '../validators';
 | 
			
		||||
 | 
			
		||||
import { MqttSettings } from './types';
 | 
			
		||||
 | 
			
		||||
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
 | 
			
		||||
 | 
			
		||||
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { data, handleValueChange, saveData } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <ValidatorForm onSubmit={saveData}>
 | 
			
		||||
        <BlockFormControlLabel
 | 
			
		||||
          control={
 | 
			
		||||
            <Checkbox
 | 
			
		||||
              checked={data.enabled}
 | 
			
		||||
              onChange={handleValueChange('enabled')}
 | 
			
		||||
              value="enabled"
 | 
			
		||||
            />
 | 
			
		||||
          }
 | 
			
		||||
          label="Enable MQTT?"
 | 
			
		||||
        />
 | 
			
		||||
        <TextValidator
 | 
			
		||||
          validators={['required', 'isIPOrHostname']}
 | 
			
		||||
          errorMessages={['Host is required', "Not a valid IP address or hostname"]}
 | 
			
		||||
          name="host"
 | 
			
		||||
          label="Host"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.host}
 | 
			
		||||
          onChange={handleValueChange('host')}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        />
 | 
			
		||||
        <TextValidator
 | 
			
		||||
          validators={['required', 'isNumber', 'minNumber:0', 'maxNumber:65535']}
 | 
			
		||||
          errorMessages={['Port is required', "Must be a number", "Must be greater than 0 ", "Max value is 65535"]}
 | 
			
		||||
          name="port"
 | 
			
		||||
          label="Port"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.port}
 | 
			
		||||
          type="number"
 | 
			
		||||
          onChange={handleValueChange('port')}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          name="username"
 | 
			
		||||
          label="Username"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.username}
 | 
			
		||||
          onChange={handleValueChange('username')}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        />
 | 
			
		||||
        <PasswordValidator
 | 
			
		||||
          name="password"
 | 
			
		||||
          label="Password"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.password}
 | 
			
		||||
          onChange={handleValueChange('password')}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          name="client_id"
 | 
			
		||||
          label="Client ID (optional)"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.client_id}
 | 
			
		||||
          onChange={handleValueChange('client_id')}
 | 
			
		||||
          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"]}
 | 
			
		||||
          name="keep_alive"
 | 
			
		||||
          label="Keep Alive (seconds)"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.keep_alive}
 | 
			
		||||
          type="number"
 | 
			
		||||
          onChange={handleValueChange('keep_alive')}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        />
 | 
			
		||||
        <BlockFormControlLabel
 | 
			
		||||
          control={
 | 
			
		||||
            <Checkbox
 | 
			
		||||
              checked={data.clean_session}
 | 
			
		||||
              onChange={handleValueChange('clean_session')}
 | 
			
		||||
              value="clean_session"
 | 
			
		||||
            />
 | 
			
		||||
          }
 | 
			
		||||
          label="Clean Session?"
 | 
			
		||||
        />
 | 
			
		||||
        <TextValidator
 | 
			
		||||
          validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
 | 
			
		||||
          errorMessages={['Max topic length is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
 | 
			
		||||
          name="max_topic_length"
 | 
			
		||||
          label="Max Topic Length"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.max_topic_length}
 | 
			
		||||
          type="number"
 | 
			
		||||
          onChange={handleValueChange('max_topic_length')}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        />
 | 
			
		||||
        <FormActions>
 | 
			
		||||
          <FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
 | 
			
		||||
            Save
 | 
			
		||||
          </FormButton>
 | 
			
		||||
        </FormActions>
 | 
			
		||||
      </ValidatorForm>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MqttSettingsForm;
 | 
			
		||||
							
								
								
									
										45
									
								
								interface/src/mqtt/MqttStatus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								interface/src/mqtt/MqttStatus.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import { Theme } from "@material-ui/core";
 | 
			
		||||
import { MqttStatus, MqttDisconnectReason } from "./types";
 | 
			
		||||
 | 
			
		||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
 | 
			
		||||
  if (!enabled) {
 | 
			
		||||
    return theme.palette.info.main;
 | 
			
		||||
  }
 | 
			
		||||
  if (connected) {
 | 
			
		||||
    return theme.palette.success.main;
 | 
			
		||||
  }
 | 
			
		||||
  return theme.palette.error.main;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
 | 
			
		||||
  if (!enabled) {
 | 
			
		||||
    return "Not enabled";
 | 
			
		||||
  }
 | 
			
		||||
  if (connected) {
 | 
			
		||||
    return "Connected";
 | 
			
		||||
  }
 | 
			
		||||
  return "Disconnected";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
 | 
			
		||||
  switch (disconnect_reason) {
 | 
			
		||||
    case MqttDisconnectReason.TCP_DISCONNECTED:
 | 
			
		||||
      return "TCP disconnected";
 | 
			
		||||
    case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
 | 
			
		||||
      return "Unacceptable protocol version";
 | 
			
		||||
    case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
 | 
			
		||||
      return "Client ID rejected";
 | 
			
		||||
    case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
 | 
			
		||||
      return "Server unavailable";
 | 
			
		||||
    case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
 | 
			
		||||
      return "Malformed credentials";
 | 
			
		||||
    case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
 | 
			
		||||
      return "Not authorized";
 | 
			
		||||
    case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
 | 
			
		||||
      return "Device out of memory";
 | 
			
		||||
    case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
 | 
			
		||||
      return "Server fingerprint invalid";
 | 
			
		||||
    default:
 | 
			
		||||
      return "Unknown"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								interface/src/mqtt/MqttStatusController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								interface/src/mqtt/MqttStatusController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
 | 
			
		||||
import { MQTT_STATUS_ENDPOINT } from '../api';
 | 
			
		||||
 | 
			
		||||
import MqttStatusForm from './MqttStatusForm';
 | 
			
		||||
import { MqttStatus } from './types';
 | 
			
		||||
 | 
			
		||||
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
 | 
			
		||||
 | 
			
		||||
class MqttStatusController extends Component<MqttStatusControllerProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <SectionContent title="MQTT Status">
 | 
			
		||||
        <RestFormLoader
 | 
			
		||||
          {...this.props}
 | 
			
		||||
          render={formProps => <MqttStatusForm {...formProps} />}
 | 
			
		||||
        />
 | 
			
		||||
      </SectionContent>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default restController(MQTT_STATUS_ENDPOINT, MqttStatusController);
 | 
			
		||||
							
								
								
									
										83
									
								
								interface/src/mqtt/MqttStatusForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								interface/src/mqtt/MqttStatusForm.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
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 DeviceHubIcon from '@material-ui/icons/DeviceHub';
 | 
			
		||||
import RefreshIcon from '@material-ui/icons/Refresh';
 | 
			
		||||
import ReportIcon from '@material-ui/icons/Report';
 | 
			
		||||
 | 
			
		||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
 | 
			
		||||
import { mqttStatusHighlight, mqttStatus, disconnectReason } from './MqttStatus';
 | 
			
		||||
import { MqttStatus } from './types';
 | 
			
		||||
 | 
			
		||||
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
 | 
			
		||||
 | 
			
		||||
class MqttStatusForm extends Component<MqttStatusFormProps> {
 | 
			
		||||
 | 
			
		||||
  renderConnectionStatus() {
 | 
			
		||||
    const { data } = this.props
 | 
			
		||||
    if (data.connected) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Fragment>
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <ListItemAvatar>
 | 
			
		||||
              <Avatar>#</Avatar>
 | 
			
		||||
            </ListItemAvatar>
 | 
			
		||||
            <ListItemText primary="Client ID" secondary={data.client_id} />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          <Divider variant="inset" component="li" />
 | 
			
		||||
        </Fragment>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <Fragment>
 | 
			
		||||
        <ListItem>
 | 
			
		||||
          <ListItemAvatar>
 | 
			
		||||
            <Avatar>
 | 
			
		||||
              <ReportIcon />
 | 
			
		||||
            </Avatar>
 | 
			
		||||
          </ListItemAvatar>
 | 
			
		||||
          <ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <Divider variant="inset" component="li" />
 | 
			
		||||
      </Fragment>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createListItems() {
 | 
			
		||||
    const { data, theme } = this.props
 | 
			
		||||
    return (
 | 
			
		||||
      <Fragment>
 | 
			
		||||
        <ListItem>
 | 
			
		||||
          <ListItemAvatar>
 | 
			
		||||
            <HighlightAvatar color={mqttStatusHighlight(data, theme)}>
 | 
			
		||||
              <DeviceHubIcon />
 | 
			
		||||
            </HighlightAvatar>
 | 
			
		||||
          </ListItemAvatar>
 | 
			
		||||
          <ListItemText primary="Status" secondary={mqttStatus(data)} />
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <Divider variant="inset" component="li" />
 | 
			
		||||
        {data.enabled && this.renderConnectionStatus()}
 | 
			
		||||
      </Fragment>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <Fragment>
 | 
			
		||||
        <List>
 | 
			
		||||
          {this.createListItems()}
 | 
			
		||||
        </List>
 | 
			
		||||
        <FormActions>
 | 
			
		||||
          <FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
 | 
			
		||||
            Refresh
 | 
			
		||||
          </FormButton>
 | 
			
		||||
        </FormActions>
 | 
			
		||||
      </Fragment>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTheme(MqttStatusForm);
 | 
			
		||||
							
								
								
									
										29
									
								
								interface/src/mqtt/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								interface/src/mqtt/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
export enum MqttDisconnectReason {
 | 
			
		||||
  TCP_DISCONNECTED = 0,
 | 
			
		||||
  MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1,
 | 
			
		||||
  MQTT_IDENTIFIER_REJECTED = 2,
 | 
			
		||||
  MQTT_SERVER_UNAVAILABLE = 3,
 | 
			
		||||
  MQTT_MALFORMED_CREDENTIALS = 4,
 | 
			
		||||
  MQTT_NOT_AUTHORIZED = 5,
 | 
			
		||||
  ESP8266_NOT_ENOUGH_SPACE = 6,
 | 
			
		||||
  TLS_BAD_FINGERPRINT = 7
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MqttStatus {
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
  connected: boolean;
 | 
			
		||||
  client_id: string;
 | 
			
		||||
  disconnect_reason: MqttDisconnectReason;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MqttSettings {
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
  host: string;
 | 
			
		||||
  port: number;
 | 
			
		||||
  username: string;
 | 
			
		||||
  password: string;
 | 
			
		||||
  client_id: string;
 | 
			
		||||
  keep_alive: number;
 | 
			
		||||
  clean_session: boolean;
 | 
			
		||||
  max_topic_length: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								interface/src/ntp/NTPSettingsController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								interface/src/ntp/NTPSettingsController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
 | 
			
		||||
import { NTP_SETTINGS_ENDPOINT } from '../api';
 | 
			
		||||
 | 
			
		||||
import NTPSettingsForm from './NTPSettingsForm';
 | 
			
		||||
import { NTPSettings } from './types';
 | 
			
		||||
 | 
			
		||||
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
 | 
			
		||||
 | 
			
		||||
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <SectionContent title="NTP Settings" titleGutter>
 | 
			
		||||
        <RestFormLoader
 | 
			
		||||
          {...this.props}
 | 
			
		||||
          render={formProps => <NTPSettingsForm {...formProps} />}
 | 
			
		||||
        />
 | 
			
		||||
      </SectionContent>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
 | 
			
		||||
							
								
								
									
										80
									
								
								interface/src/ntp/NTPSettingsForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								interface/src/ntp/NTPSettingsForm.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
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 { isIP, isHostname, or } from '../validators';
 | 
			
		||||
 | 
			
		||||
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
 | 
			
		||||
import { NTPSettings } from './types';
 | 
			
		||||
 | 
			
		||||
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
 | 
			
		||||
 | 
			
		||||
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeTimeZone = (event: React.ChangeEvent<HTMLSelectElement>) => {
 | 
			
		||||
    const { data, setData } = this.props;
 | 
			
		||||
    setData({
 | 
			
		||||
      ...data,
 | 
			
		||||
      tz_label: event.target.value,
 | 
			
		||||
      tz_format: TIME_ZONES[event.target.value]
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { data, handleValueChange, saveData } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <ValidatorForm onSubmit={saveData}>
 | 
			
		||||
        <BlockFormControlLabel
 | 
			
		||||
          control={
 | 
			
		||||
            <Checkbox
 | 
			
		||||
              checked={data.enabled}
 | 
			
		||||
              onChange={handleValueChange('enabled')}
 | 
			
		||||
              value="enabled"
 | 
			
		||||
            />
 | 
			
		||||
          }
 | 
			
		||||
          label="Enable NTP?"
 | 
			
		||||
        />
 | 
			
		||||
        <TextValidator
 | 
			
		||||
          validators={['required', 'isIPOrHostname']}
 | 
			
		||||
          errorMessages={['Server is required', "Not a valid IP address or hostname"]}
 | 
			
		||||
          name="server"
 | 
			
		||||
          label="Server"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          value={data.server}
 | 
			
		||||
          onChange={handleValueChange('server')}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        />
 | 
			
		||||
        <SelectValidator
 | 
			
		||||
          validators={['required']}
 | 
			
		||||
          errorMessages={['Time zone is required']}
 | 
			
		||||
          name="tz_label"
 | 
			
		||||
          label="Time zone"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          native="true"
 | 
			
		||||
          value={selectedTimeZone(data.tz_label, data.tz_format)}
 | 
			
		||||
          onChange={this.changeTimeZone}
 | 
			
		||||
          margin="normal"
 | 
			
		||||
        >
 | 
			
		||||
          <MenuItem disabled>Time zone...</MenuItem>
 | 
			
		||||
          {timeZoneSelectItems()}
 | 
			
		||||
        </SelectValidator>
 | 
			
		||||
        <FormActions>
 | 
			
		||||
          <FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
 | 
			
		||||
            Save
 | 
			
		||||
          </FormButton>
 | 
			
		||||
        </FormActions>
 | 
			
		||||
      </ValidatorForm>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default NTPSettingsForm;
 | 
			
		||||
							
								
								
									
										26
									
								
								interface/src/ntp/NTPStatus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								interface/src/ntp/NTPStatus.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
import { Theme } from "@material-ui/core";
 | 
			
		||||
import { NTPStatus, NTPSyncStatus } from "./types";
 | 
			
		||||
 | 
			
		||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
 | 
			
		||||
 | 
			
		||||
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case NTPSyncStatus.NTP_INACTIVE:
 | 
			
		||||
      return theme.palette.info.main;
 | 
			
		||||
    case NTPSyncStatus.NTP_ACTIVE:
 | 
			
		||||
      return theme.palette.success.main;
 | 
			
		||||
    default:
 | 
			
		||||
      return theme.palette.error.main;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ntpStatus = ({ status }: NTPStatus) => {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case NTPSyncStatus.NTP_INACTIVE:
 | 
			
		||||
      return "Inactive";
 | 
			
		||||
    case NTPSyncStatus.NTP_ACTIVE:
 | 
			
		||||
      return "Active";
 | 
			
		||||
    default:
 | 
			
		||||
      return "Unknown";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								interface/src/ntp/NTPStatusController.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								interface/src/ntp/NTPStatusController.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
 | 
			
		||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
 | 
			
		||||
import { NTP_STATUS_ENDPOINT } from '../api';
 | 
			
		||||
 | 
			
		||||
import NTPStatusForm from './NTPStatusForm';
 | 
			
		||||
import { NTPStatus } from './types';
 | 
			
		||||
 | 
			
		||||
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
 | 
			
		||||
 | 
			
		||||
class NTPStatusController extends Component<NTPStatusControllerProps> {
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <SectionContent title="NTP Status">
 | 
			
		||||
        <RestFormLoader
 | 
			
		||||
          {...this.props}
 | 
			
		||||
          render={formProps => <NTPStatusForm {...formProps} />}
 | 
			
		||||
        />
 | 
			
		||||
      </SectionContent>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
 | 
			
		||||
							
								
								
									
										198
									
								
								interface/src/ntp/NTPStatusForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								interface/src/ntp/NTPStatusForm.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
import React, { Component, Fragment } from 'react';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
 | 
			
		||||
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 SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
 | 
			
		||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
 | 
			
		||||
import DNSIcon from '@material-ui/icons/Dns';
 | 
			
		||||
import UpdateIcon from '@material-ui/icons/Update';
 | 
			
		||||
import AvTimerIcon from '@material-ui/icons/AvTimer';
 | 
			
		||||
import RefreshIcon from '@material-ui/icons/Refresh';
 | 
			
		||||
 | 
			
		||||
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
 | 
			
		||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
 | 
			
		||||
import { formatIsoDateTime, formatLocalDateTime } from './TimeFormat';
 | 
			
		||||
import { NTPStatus, Time } from './types';
 | 
			
		||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
 | 
			
		||||
import { TIME_ENDPOINT } from '../api';
 | 
			
		||||
 | 
			
		||||
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
 | 
			
		||||
 | 
			
		||||
interface NTPStatusFormState {
 | 
			
		||||
  settingTime: boolean;
 | 
			
		||||
  localTime: string;
 | 
			
		||||
  processing: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
 | 
			
		||||
 | 
			
		||||
  constructor(props: NTPStatusFormProps) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.state = {
 | 
			
		||||
      settingTime: false,
 | 
			
		||||
      localTime: '',
 | 
			
		||||
      processing: false
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
    this.setState({ localTime: event.target.value });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openSetTime = () => {
 | 
			
		||||
    this.setState({ localTime: formatLocalDateTime(moment()), settingTime: true, });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  closeSetTime = () => {
 | 
			
		||||
    this.setState({ settingTime: false });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createAdjustedTime = (): Time => {
 | 
			
		||||
    const currentLocalTime = moment(this.props.data.time_local);
 | 
			
		||||
    const newLocalTime = moment(this.state.localTime);
 | 
			
		||||
    newLocalTime.subtract(currentLocalTime.utcOffset())
 | 
			
		||||
    newLocalTime.milliseconds(0);
 | 
			
		||||
    newLocalTime.utc();
 | 
			
		||||
    return {
 | 
			
		||||
      time_utc: newLocalTime.format()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  configureTime = () => {
 | 
			
		||||
    this.setState({ processing: true });
 | 
			
		||||
    redirectingAuthorizedFetch(TIME_ENDPOINT,
 | 
			
		||||
      {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        body: JSON.stringify(this.createAdjustedTime()),
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .then(response => {
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
          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);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .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}
 | 
			
		||||
      >
 | 
			
		||||
        <DialogTitle>Set Time</DialogTitle>
 | 
			
		||||
        <DialogContent dividers>
 | 
			
		||||
          <Box mb={2}>Enter local date and time below to set the device's time.</Box>
 | 
			
		||||
          <TextField
 | 
			
		||||
            label="Local Time"
 | 
			
		||||
            type="datetime-local"
 | 
			
		||||
            value={this.state.localTime}
 | 
			
		||||
            onChange={this.updateLocalTime}
 | 
			
		||||
            disabled={this.state.processing}
 | 
			
		||||
            variant="outlined"
 | 
			
		||||
            fullWidth
 | 
			
		||||
            InputLabelProps={{
 | 
			
		||||
              shrink: true,
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
        <DialogActions>
 | 
			
		||||
          <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>
 | 
			
		||||
            Set Time
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DialogActions>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { data, theme } = this.props
 | 
			
		||||
    const me = this.props.authenticatedContext.me;
 | 
			
		||||
    return (
 | 
			
		||||
      <Fragment>
 | 
			
		||||
        <List>
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <ListItemAvatar>
 | 
			
		||||
              <HighlightAvatar color={ntpStatusHighlight(data, theme)}>
 | 
			
		||||
                <UpdateIcon />
 | 
			
		||||
              </HighlightAvatar>
 | 
			
		||||
            </ListItemAvatar>
 | 
			
		||||
            <ListItemText primary="Status" secondary={ntpStatus(data)} />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          <Divider variant="inset" component="li" />
 | 
			
		||||
          {isNtpActive(data) && (
 | 
			
		||||
            <Fragment>
 | 
			
		||||
              <ListItem>
 | 
			
		||||
                <ListItemAvatar>
 | 
			
		||||
                  <Avatar>
 | 
			
		||||
                    <DNSIcon />
 | 
			
		||||
                  </Avatar>
 | 
			
		||||
                </ListItemAvatar>
 | 
			
		||||
                <ListItemText primary="NTP Server" secondary={data.server} />
 | 
			
		||||
              </ListItem>
 | 
			
		||||
              <Divider variant="inset" component="li" />
 | 
			
		||||
            </Fragment>
 | 
			
		||||
          )}
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <ListItemAvatar>
 | 
			
		||||
              <Avatar>
 | 
			
		||||
                <AccessTimeIcon />
 | 
			
		||||
              </Avatar>
 | 
			
		||||
            </ListItemAvatar>
 | 
			
		||||
            <ListItemText primary="Local Time" secondary={formatIsoDateTime(data.time_local)} />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          <Divider variant="inset" component="li" />
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <ListItemAvatar>
 | 
			
		||||
              <Avatar>
 | 
			
		||||
                <SwapVerticalCircleIcon />
 | 
			
		||||
              </Avatar>
 | 
			
		||||
            </ListItemAvatar>
 | 
			
		||||
            <ListItemText primary="UTC Time" secondary={formatIsoDateTime(data.time_utc)} />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
          <Divider variant="inset" component="li" />
 | 
			
		||||
          <ListItem>
 | 
			
		||||
            <ListItemAvatar>
 | 
			
		||||
              <Avatar>
 | 
			
		||||
                <AvTimerIcon />
 | 
			
		||||
              </Avatar>
 | 
			
		||||
            </ListItemAvatar>
 | 
			
		||||
            <ListItemText primary="Uptime" secondary={moment.duration(data.uptime, 'seconds').humanize()} />
 | 
			
		||||
          </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}>
 | 
			
		||||
              Refresh
 | 
			
		||||
            </FormButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
          {me.admin && !isNtpActive(data) && (
 | 
			
		||||
            <Box flexWrap="none" padding={1} whiteSpace="nowrap">
 | 
			
		||||
              <Button onClick={this.openSetTime} variant="contained" color="primary" startIcon={<AccessTimeIcon />}>
 | 
			
		||||
                Set Time
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Box>
 | 
			
		||||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
        {this.renderSetTimeDialog()}
 | 
			
		||||
      </Fragment>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withAuthenticatedContext(withTheme(NTPStatusForm));
 | 
			
		||||
							
								
								
									
										39
									
								
								interface/src/ntp/NetworkTime.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								interface/src/ntp/NetworkTime.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import React, { 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 { MenuAppBar } from '../components';
 | 
			
		||||
 | 
			
		||||
import NTPStatusController from './NTPStatusController';
 | 
			
		||||
import NTPSettingsController from './NTPSettingsController';
 | 
			
		||||
 | 
			
		||||
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
 | 
			
		||||
 | 
			
		||||
class NetworkTime extends Component<NetworkTimeProps> {
 | 
			
		||||
 | 
			
		||||
  handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
 | 
			
		||||
    this.props.history.push(path);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { authenticatedContext } = this.props;
 | 
			
		||||
    return (
 | 
			
		||||
      <MenuAppBar sectionTitle="Network Time">
 | 
			
		||||
        <Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
 | 
			
		||||
          <Tab value="/ntp/status" label="NTP Status" />
 | 
			
		||||
          <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} />
 | 
			
		||||
          <Redirect to="/ntp/status" />
 | 
			
		||||
        </Switch>
 | 
			
		||||
      </MenuAppBar>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withAuthenticatedContext(NetworkTime)
 | 
			
		||||
							
								
								
									
										479
									
								
								interface/src/ntp/TZ.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										479
									
								
								interface/src/ntp/TZ.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,479 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import MenuItem from '@material-ui/core/MenuItem';
 | 
			
		||||
 | 
			
		||||
type TimeZones = {
 | 
			
		||||
  [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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
  ));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								interface/src/ntp/TimeFormat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								interface/src/ntp/TimeFormat.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import moment, { Moment } from 'moment';
 | 
			
		||||
 | 
			
		||||
export const formatIsoDateTime = (isoDateString: string) => moment.parseZone(isoDateString).format('ll @ HH:mm:ss');
 | 
			
		||||
 | 
			
		||||
export const formatLocalDateTime = (moment: Moment) => moment.format('YYYY-MM-DDTHH:mm');
 | 
			
		||||
							
								
								
									
										23
									
								
								interface/src/ntp/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								interface/src/ntp/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
export enum NTPSyncStatus {
 | 
			
		||||
  NTP_INACTIVE = 0,
 | 
			
		||||
  NTP_ACTIVE = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NTPStatus {
 | 
			
		||||
  status: NTPSyncStatus;
 | 
			
		||||
  time_utc: string;
 | 
			
		||||
  time_local: string;
 | 
			
		||||
  server: string;
 | 
			
		||||
  uptime: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NTPSettings {
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
  server: string;
 | 
			
		||||
  tz_label: string;
 | 
			
		||||
  tz_format: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Time {
 | 
			
		||||
  time_utc: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								interface/src/project/GeneralInformation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								interface/src/project/GeneralInformation.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
			
		||||
import React, {Component} from 'react';
 | 
			
		||||
import {Box, List, ListItem, ListItemText} from '@material-ui/core';
 | 
			
		||||
import {
 | 
			
		||||
    FormButton,
 | 
			
		||||
    restController,
 | 
			
		||||
    RestControllerProps,
 | 
			
		||||
    RestFormLoader,
 | 
			
		||||
    SectionContent
 | 
			
		||||
} from '../components';
 | 
			
		||||
import {ENDPOINT_ROOT} from "../api";
 | 
			
		||||
import {GeneralInformaitonState} from "./types";
 | 
			
		||||
import RefreshIcon from "@material-ui/icons/Refresh";
 | 
			
		||||
 | 
			
		||||
// define api endpoint
 | 
			
		||||
export const GENERALINFORMATION_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "generalinfo";
 | 
			
		||||
 | 
			
		||||
type GeneralInformationRestControllerProps = RestControllerProps<GeneralInformaitonState>;
 | 
			
		||||
 | 
			
		||||
class GeneralInformation extends Component<GeneralInformationRestControllerProps> {
 | 
			
		||||
    intervalhandler: number | undefined;
 | 
			
		||||
 | 
			
		||||
    componentDidMount() {
 | 
			
		||||
        this.props.loadData();
 | 
			
		||||
 | 
			
		||||
        // this.intervalhandler = window.setInterval(() => {
 | 
			
		||||
        //     this.props.loadData();
 | 
			
		||||
        //     console.log("refreshing data");
 | 
			
		||||
        //     console.log(this.props.data)
 | 
			
		||||
        // }, 10000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount() {
 | 
			
		||||
        clearInterval(this.intervalhandler);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <SectionContent title='Information' titleGutter>
 | 
			
		||||
                <RestFormLoader
 | 
			
		||||
                    {...this.props}
 | 
			
		||||
                    render={props => (
 | 
			
		||||
                            <>
 | 
			
		||||
                                <List>
 | 
			
		||||
                                    <ListItem>
 | 
			
		||||
                                        <ListItemText
 | 
			
		||||
                                            primary="Chip läuft seit:"
 | 
			
		||||
                                            secondary={this.stringifyTime(props.data.runtime)}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </ListItem>
 | 
			
		||||
                                    <ListItem>
 | 
			
		||||
                                        <ListItemText
 | 
			
		||||
                                            primary="Zuletzt zu wenig Wasser:"
 | 
			
		||||
                                            secondary={props.data.lastWaterOutage !== 0 ? "vor " + this.stringifyTime(props.data.lastWaterOutage) : "noch nie!"}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </ListItem>
 | 
			
		||||
                                    <ListItem>
 | 
			
		||||
                                        <ListItemText
 | 
			
		||||
                                            primary="Letzer Pumpenzyklus"
 | 
			
		||||
                                            secondary={props.data.lastpumptime !== 0 ? "vor " + this.stringifyTime(props.data.lastpumptime) : "noch nie!"}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </ListItem>
 | 
			
		||||
                                    <ListItem>
 | 
			
		||||
                                        <ListItemText
 | 
			
		||||
                                            primary="Letze Pumpdauer:"
 | 
			
		||||
                                            secondary={props.data.lastPumpDuration !== 0 ? this.stringifyTime(props.data.lastPumpDuration) : "-"}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </ListItem>
 | 
			
		||||
                                    <ListItem>
 | 
			
		||||
                                        <ListItemText
 | 
			
		||||
                                            primary="Temperatur/Luftfeuchtigkeit:"
 | 
			
		||||
                                            secondary={(props.data.temp !== -1 ? props.data.temp + "C" : "Auslesefehler!") + " / " + (props.data.hum !== -1 ? props.data.hum + "%" : "Auslesefehler!")}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </ListItem>
 | 
			
		||||
                                    <ListItem>
 | 
			
		||||
                                        <ListItemText
 | 
			
		||||
                                            primary="WasserSensor / DruckSensor"
 | 
			
		||||
                                            secondary={(props.data.watersensor ? "EIN" : "AUS!") + " / " + (props.data.pressuresensor ? "EIN" : "AUS")}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </ListItem>
 | 
			
		||||
                                </List>
 | 
			
		||||
                                <Box display="flex" flexWrap="wrap">
 | 
			
		||||
                                    <Box flexGrow={1} padding={1}>
 | 
			
		||||
                                        <FormButton startIcon={<RefreshIcon/>} variant="contained" color="secondary"
 | 
			
		||||
                                                    onClick={this.props.loadData}>
 | 
			
		||||
                                            Refresh
 | 
			
		||||
                                        </FormButton>
 | 
			
		||||
                                    </Box>
 | 
			
		||||
                                    <Box flexWrap="none" padding={1} whiteSpace="nowrap">
 | 
			
		||||
                                        Version: {props.data.version}
 | 
			
		||||
                                    </Box>
 | 
			
		||||
                                </Box>
 | 
			
		||||
                            </>
 | 
			
		||||
                        )}
 | 
			
		||||
                />
 | 
			
		||||
            </SectionContent>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * stringify seconds to a pretty format
 | 
			
		||||
     * @param sec number of seconds
 | 
			
		||||
     */
 | 
			
		||||
    stringifyTime(sec: number): string {
 | 
			
		||||
        if (sec >= 86400) {
 | 
			
		||||
            // display days
 | 
			
		||||
            return (Math.trunc(sec / 86400) + "d " + Math.trunc((sec % 86400) / 3600) + "h " + Math.trunc((sec % 3600) / 60) + "min " + sec % 60 + "sec");
 | 
			
		||||
        } else if (sec >= 3600) {
 | 
			
		||||
            // display hours
 | 
			
		||||
            return (Math.trunc(sec / 3600) + "h " + Math.trunc((sec % 3600) / 60) + "min " + sec % 60 + "sec");
 | 
			
		||||
        } else if (sec >= 60) {
 | 
			
		||||
            // only seconds and minutes
 | 
			
		||||
            return (Math.trunc(sec / 60) + "min " + sec % 60 + "sec");
 | 
			
		||||
        } else {
 | 
			
		||||
            // only seconds
 | 
			
		||||
            return (sec + "sec");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default restController(GENERALINFORMATION_SETTINGS_ENDPOINT, GeneralInformation);
 | 
			
		||||
							
								
								
									
										27
									
								
								interface/src/project/ProjectMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								interface/src/project/ProjectMenu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import React, { Component } from 'react';
 | 
			
		||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
 | 
			
		||||
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
 | 
			
		||||
 | 
			
		||||
import { PROJECT_PATH } from '../api';
 | 
			
		||||
 | 
			
		||||
class ProjectMenu extends Component<RouteComponentProps> {
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const path = this.props.match.url;
 | 
			
		||||
    return (
 | 
			
		||||
      <List>
 | 
			
		||||
        <ListItem to={`/${PROJECT_PATH}/pumpe/`} selected={path.startsWith(`/${PROJECT_PATH}/pumpe/`)} button component={Link}>
 | 
			
		||||
          <ListItemIcon>
 | 
			
		||||
            <SettingsRemoteIcon />
 | 
			
		||||
          </ListItemIcon>
 | 
			
		||||
          <ListItemText primary="Pumpensteuerung" />
 | 
			
		||||
        </ListItem>
 | 
			
		||||
      </List>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withRouter(ProjectMenu);
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user