mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Merge remote-tracking branch 'origin/dev'
This commit is contained in:
@@ -2,7 +2,7 @@ Language: Cpp
|
||||
BasedOnStyle: LLVM
|
||||
UseTab: Never
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 220
|
||||
ColumnLimit: 160
|
||||
TabWidth: 4
|
||||
#BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
|
||||
21
.github/workflows/pre_release.yml
vendored
21
.github/workflows/pre_release.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: "pre-release"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "dev"
|
||||
@@ -13,36 +14,36 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
|
||||
- name: Get build variables
|
||||
- name: Get EMS-ESP source code and version
|
||||
id: build_info
|
||||
run: |
|
||||
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/version.h | awk -F'"' '{print $2}'`
|
||||
echo "::set-output name=version::$version"
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Install pio
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
platformio upgrade
|
||||
platformio update
|
||||
|
||||
- name: Build web
|
||||
- name: Build WebUI
|
||||
run: |
|
||||
cd interface
|
||||
npm install
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Build firmware
|
||||
run: |
|
||||
platformio run -e ci
|
||||
|
||||
- name: Release
|
||||
- name: Create a GH Release
|
||||
id: "automatic_releases"
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
|
||||
@@ -2,8 +2,22 @@
|
||||
|
||||
## Added
|
||||
|
||||
- new command called `commands` which lists all available commands. `ems-esp/api/{device}/commands`
|
||||
- More Home Assistant icons to match the UOMs
|
||||
- new API. Using secure access tokens and OpenAPI standard. See `doc/EMS-ESP32 API.md` and [#50](https://github.com/emsesp/EMS-ESP32/issues/50)
|
||||
- show log messages in Web UI [#71](https://github.com/emsesp/EMS-ESP32/issues/71)
|
||||
|
||||
## Fixed
|
||||
|
||||
- HA thermostat mode was not in sync with actual mode [#66](https://github.com/emsesp/EMS-ESP32/issues/66)
|
||||
- Don't publish rssi if Wifi is disabled and ethernet is being used
|
||||
- Booleans are shown as true/false in API GETs
|
||||
|
||||
## Changed
|
||||
|
||||
- `info` command always shows full names in API. For short names query the device or name directly, e.g. `http://ems-esp/api/boiler`
|
||||
- free memory is shown in kilobytes
|
||||
- boiler's warm water entities have ww added to the Home Assistant entity name [#67](https://github.com/emsesp/EMS-ESP32/issues/67)
|
||||
- improved layout and rendering of device values in the WebUI, also the edit value screen
|
||||
|
||||
## Removed
|
||||
|
||||
43
README.md
43
README.md
@@ -2,16 +2,15 @@
|
||||
|
||||
**EMS-ESP** is an open-source firmware for the Espressif ESP8266 and ESP32 microcontroller that communicates with **EMS** (Energy Management System) based equipment from manufacturers like Bosch, Buderus, Nefit, Junkers, Worcester and Sieger.
|
||||
|
||||
This is the firmware for the ESP32. Compared to version 2 for the ESP8266, this release has
|
||||
This project is the specifically for the ESP32. Compared with the previous ESP8266 (version 2) release it has the following enhancements:
|
||||
|
||||
- Ethernet Support
|
||||
- Pre-configured board layouts
|
||||
- Writing values directly from the Web UI
|
||||
- Mock API server for faster offline development
|
||||
- Expose to more commands, via MQTT
|
||||
- Improvements to Dallas sensors, Shower service
|
||||
|
||||
This version is the latest development track, where as v2 is in maintenance mode.
|
||||
- Pre-configured circuit board layouts
|
||||
- Supports writing EMS values directly from within Web UI
|
||||
- Mock API server for faster offline development and testing
|
||||
- Improved API and MQTT commands
|
||||
- Improvements to Dallas temperature sensors
|
||||
- Embedded log tracing in the Web UI
|
||||
|
||||
[](https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md)
|
||||
[](https://github.com/emsesp/EMS-ESP32/commits/main)
|
||||
@@ -26,7 +25,7 @@ If you like **EMS-ESP**, please give it a star, or fork it and contribute!
|
||||
[](https://github.com/emsesp/EMS-ES32P/network)
|
||||
[](https://www.paypal.com/paypalme/prderbyshire/2)
|
||||
|
||||
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at <https://bbqkees-electronics.nl>.
|
||||
Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus data to be read by the microcontroller. These can be ordered at <https://bbqkees-electronics.nl> or contact the contributors that can provide the schematic and designs.
|
||||
|
||||
<img src="media/gateway-integration.jpg" width=40%>
|
||||
|
||||
@@ -34,17 +33,16 @@ Note, EMS-ESP requires a small hardware circuit that can convert the EMS bus dat
|
||||
|
||||
# **Features**
|
||||
|
||||
- Compatible with both ESP8266 and ESP32
|
||||
- A multi-user secure web interface to change settings and monitor the data
|
||||
- A console, accessible via Serial and Telnet for more monitoring
|
||||
- Native support for Home Assistant via [MQTT Discovery](https://www.home-assistant.io/docs/mqtt/discovery/)
|
||||
- Can run standalone as an independent WiFi Access Point or join an existing WiFi network
|
||||
- Easy first-time configuration via a web Captive Portal
|
||||
- Support for more than [70 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
||||
- Support for more than [80 EMS devices](https://emsesp.github.io/docs/#/Supported-EMS-Devices) (boilers, thermostats, solar modules, mixer modules, heat pumps, gateways)
|
||||
|
||||
## **Demo**
|
||||
|
||||
See a live demo on [here](https://ems-esp.derbyshire.nl). The data you see is static and any username/password is accepted.
|
||||
See a live demo [here](https://ems-esp.derbyshire.nl) using fake data. Log in with any username/password.
|
||||
|
||||
# **Screenshots**
|
||||
|
||||
@@ -54,6 +52,7 @@ See a live demo on [here](https://ems-esp.derbyshire.nl). The data you see is st
|
||||
| ---------------------------------- | -------------------------------- |
|
||||
| <img src="media/web_settings.png"> | <img src="media/web_status.png"> |
|
||||
| <img src="media/web_devices.png"> | <img src="media/web_mqtt.png"> |
|
||||
| <img src="media/web_edit.png"> | <img src="media/web_log.png"> |
|
||||
|
||||
## Telnet Console
|
||||
|
||||
@@ -94,20 +93,12 @@ If you're looking for support on **EMS-ESP** there are some options available:
|
||||
|
||||
# **Contributors ✨**
|
||||
|
||||
EMS-ESP is a project originally created by [proddy](https://github.com/proddy) with the main contributors and owners:
|
||||
EMS-ESP is a project originally created and owned by [proddy](https://github.com/proddy). Key contributors are:
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://derbyshire.nl"><img src="https://avatars.githubusercontent.com/u/1230712?v=3?s=100" width="100px;" alt=""/>
|
||||
<br/><sub><b>proddy</b></sub></a>
|
||||
<br/>
|
||||
</a> <a href="https://github.com/emsesp/EMS-ESP/commits?author=proddy" title="v2 Commits">v2</a>
|
||||
<a href="https://github.com/emsesp/EMS-ESP32/commits?author=proddy" title="v3 Commits">v3</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MichaelDvP"><img src="https://avatars.githubusercontent.com/u/59284019?v=3?s=100" width="100px;" alt=""/><br /><sub><b>MichaelDvP</b></sub></a><br /></a> <a href="https://github.com/emsesp/EMS-ESP/commits?author=MichaelDvP" title="v2 Commits">v2</a>
|
||||
<a href="https://github.com/emsesp/EMS-ESP32/commits?author=MichaelDvP" title="v3 Commits">v3</a>
|
||||
@@ -126,11 +117,11 @@ You can also contribute to EMS-ESP by
|
||||
|
||||
# **Libraries used**
|
||||
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the Web UI
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging is based on these libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
|
||||
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for MQTT, with modifications from @bertmelis
|
||||
- ESPAsyncWebServer and AsyncTCP for the Web and TCP backends, with custom modifications for performance
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the framework that provides the core of the Web UI
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these open source libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for JSON
|
||||
- [AsyncMqttClient](https://github.com/marvinroger/async-mqtt-client) for the MQTT client, with custom modifications from @bertmelis and @proddy
|
||||
- ESPAsyncWebServer and AsyncTCP for the Web server and TCP backends, with custom modifications for performance
|
||||
|
||||
# **License**
|
||||
|
||||
|
||||
3
interface/.env.hosted
Normal file
3
interface/.env.hosted
Normal file
@@ -0,0 +1,3 @@
|
||||
GENERATE_SOURCEMAP=false
|
||||
|
||||
REACT_APP_HOSTED=true
|
||||
2
interface/.eslintignore
Normal file
2
interface/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
# don't ever lint node_modules
|
||||
node_modules
|
||||
27
interface/.eslintrc
Normal file
27
interface/.eslintrc
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
// 0 = ignore, 1 = warning, 2 = error
|
||||
"no-console": 0,
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
"explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/ban-types": 0,
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0
|
||||
}
|
||||
}
|
||||
|
||||
6
interface/.prettierrc
Normal file
6
interface/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80
|
||||
}
|
||||
@@ -4,34 +4,49 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const ProgmemGenerator = require('./progmem-generator.js');
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = function override(config, env) {
|
||||
if (env === "production") {
|
||||
const hosted = process.env.REACT_APP_HOSTED;
|
||||
|
||||
if (env === 'production' && !hosted) {
|
||||
console.log('Custom webpack...');
|
||||
|
||||
// rename the output file, we need it's path to be short for LittleFS
|
||||
config.output.filename = 'js/[id].[chunkhash:4].js';
|
||||
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
||||
|
||||
// take out the manifest and service worker plugins
|
||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin));
|
||||
config.plugins = config.plugins.filter(plugin => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW));
|
||||
config.plugins = config.plugins.filter(
|
||||
(plugin) => !(plugin instanceof ManifestPlugin)
|
||||
);
|
||||
config.plugins = config.plugins.filter(
|
||||
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW)
|
||||
);
|
||||
|
||||
// shorten css filenames
|
||||
const miniCssExtractPlugin = config.plugins.find((plugin) => plugin instanceof MiniCssExtractPlugin);
|
||||
miniCssExtractPlugin.options.filename = "css/[id].[contenthash:4].css";
|
||||
miniCssExtractPlugin.options.chunkFilename = "css/[id].[contenthash:4].c.css";
|
||||
const miniCssExtractPlugin = config.plugins.find(
|
||||
(plugin) => plugin instanceof MiniCssExtractPlugin
|
||||
);
|
||||
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
|
||||
miniCssExtractPlugin.options.chunkFilename =
|
||||
'css/[id].[contenthash:4].c.css';
|
||||
|
||||
// build progmem data files
|
||||
config.plugins.push(new ProgmemGenerator({ outputPath: "../lib/framework/WWWData.h", bytesPerLine: 20 }));
|
||||
config.plugins.push(
|
||||
new ProgmemGenerator({
|
||||
outputPath: '../lib/framework/WWWData.h',
|
||||
bytesPerLine: 20
|
||||
})
|
||||
);
|
||||
|
||||
// add compression plugin, compress javascript
|
||||
config.plugins.push(new CompressionPlugin({
|
||||
filename: "[path].gz[query]",
|
||||
algorithm: "gzip",
|
||||
config.plugins.push(
|
||||
new CompressionPlugin({
|
||||
filename: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: /\.(js)$/,
|
||||
deleteOriginalAssets: true
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
992
interface/package-lock.json
generated
992
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "esp8266-react",
|
||||
"name": "emsesp-react",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@msgpack/msgpack": "^2.7.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^15.0.1",
|
||||
"@types/react": "^17.0.4",
|
||||
@@ -13,6 +14,7 @@
|
||||
"@types/react-router": "^5.1.13",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"compression-webpack-plugin": "^5.0.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"express": "^4.17.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -34,9 +36,12 @@
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"eject": "react-scripts eject",
|
||||
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
||||
"build-hosted": "env-cmd -f .env.hosted npm run build",
|
||||
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
|
||||
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
||||
"dev": "run-p start mock-api"
|
||||
"standalone": "npm-run-all -p start mock-api",
|
||||
"lint": "eslint . --ext .ts,.tsx"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
@@ -55,9 +60,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^6.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"http-proxy-middleware": "^1.1.1",
|
||||
"nodemon": "^2.0.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.0.5",
|
||||
"react-app-rewired": "^2.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
const { resolve, relative, sep } = require('path');
|
||||
const { readdirSync, existsSync, unlinkSync, readFileSync, createWriteStream } = require('fs');
|
||||
const {
|
||||
readdirSync,
|
||||
existsSync,
|
||||
unlinkSync,
|
||||
readFileSync,
|
||||
createWriteStream
|
||||
} = require('fs');
|
||||
var zlib = require('zlib');
|
||||
var mime = require('mime-types');
|
||||
|
||||
const ARDUINO_INCLUDES = "#include <Arduino.h>\n\n";
|
||||
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||
|
||||
function getFilesSync(dir, files = []) {
|
||||
readdirSync(dir, { withFileTypes: true }).forEach(entry => {
|
||||
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||
const entryPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
getFilesSync(entryPath, files);
|
||||
} else {
|
||||
files.push(entryPath);
|
||||
}
|
||||
})
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -25,13 +31,17 @@ function cleanAndOpen(path) {
|
||||
if (existsSync(path)) {
|
||||
unlinkSync(path);
|
||||
}
|
||||
return createWriteStream(path, { flags: "w+" });
|
||||
return createWriteStream(path, { flags: 'w+' });
|
||||
}
|
||||
|
||||
class ProgmemGenerator {
|
||||
|
||||
constructor(options = {}) {
|
||||
const { outputPath, bytesPerLine = 20, indent = " ", includes = ARDUINO_INCLUDES } = options;
|
||||
const {
|
||||
outputPath,
|
||||
bytesPerLine = 20,
|
||||
indent = ' ',
|
||||
includes = ARDUINO_INCLUDES
|
||||
} = options;
|
||||
this.options = { outputPath, bytesPerLine, indent, includes };
|
||||
}
|
||||
|
||||
@@ -41,30 +51,34 @@ class ProgmemGenerator {
|
||||
(compilation, callback) => {
|
||||
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
||||
const fileInfo = [];
|
||||
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
||||
const writeStream = cleanAndOpen(
|
||||
resolve(compilation.options.context, outputPath)
|
||||
);
|
||||
try {
|
||||
const writeIncludes = () => {
|
||||
writeStream.write(includes);
|
||||
}
|
||||
};
|
||||
|
||||
const writeFile = (relativeFilePath, buffer) => {
|
||||
const variable = "ESP_REACT_DATA_" + fileInfo.length;
|
||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||
const mimeType = mime.lookup(relativeFilePath);
|
||||
var size = 0;
|
||||
writeStream.write("const uint8_t " + variable + "[] PROGMEM = {");
|
||||
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
||||
const zipBuffer = zlib.gzipSync(buffer);
|
||||
zipBuffer.forEach((b) => {
|
||||
if (!(size % bytesPerLine)) {
|
||||
writeStream.write("\n");
|
||||
writeStream.write('\n');
|
||||
writeStream.write(indent);
|
||||
}
|
||||
writeStream.write("0x" + ("00" + b.toString(16).toUpperCase()).substr(-2) + ",");
|
||||
writeStream.write(
|
||||
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ','
|
||||
);
|
||||
size++;
|
||||
});
|
||||
if (size % bytesPerLine) {
|
||||
writeStream.write("\n");
|
||||
writeStream.write('\n');
|
||||
}
|
||||
writeStream.write("};\n\n");
|
||||
writeStream.write('};\n\n');
|
||||
fileInfo.push({
|
||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||
mimeType,
|
||||
@@ -84,25 +98,37 @@ class ProgmemGenerator {
|
||||
// process assets
|
||||
const { assets } = compilation;
|
||||
Object.keys(assets).forEach((relativeFilePath) => {
|
||||
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
||||
writeFile(
|
||||
relativeFilePath,
|
||||
coherseToBuffer(assets[relativeFilePath].source())
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const generateWWWClass = () => {
|
||||
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
||||
|
||||
class WWWData {
|
||||
${indent}public:
|
||||
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||
${fileInfo.map(file => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size});`).join('\n')}
|
||||
${indent.repeat(
|
||||
2
|
||||
)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||
${fileInfo
|
||||
.map(
|
||||
(file) =>
|
||||
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${
|
||||
file.variable
|
||||
}, ${file.size});`
|
||||
)
|
||||
.join('\n')}
|
||||
${indent.repeat(2)}}
|
||||
};
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const writeWWWClass = () => {
|
||||
writeStream.write(generateWWWClass());
|
||||
}
|
||||
};
|
||||
|
||||
writeIncludes();
|
||||
writeFiles();
|
||||
|
||||
@@ -3,20 +3,26 @@
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/li.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
src: local('Roboto Light'), local('Roboto-Light'),
|
||||
url(../fonts/li.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Roboto'), local('Roboto-Regular'), url(../fonts/re.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
src: local('Roboto'), local('Roboto-Regular'),
|
||||
url(../fonts/re.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/me.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
src: local('Roboto Medium'), local('Roboto-Medium'),
|
||||
url(../fonts/me.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import FeaturesWrapper from './features/FeaturesWrapper';
|
||||
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||
|
||||
class App extends Component {
|
||||
|
||||
notistackRef: RefObject<any> = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
@@ -23,21 +22,29 @@ class App extends Component {
|
||||
|
||||
onClickDismiss = (key: string | number | undefined) => () => {
|
||||
this.notistackRef.current.closeSnackbar(key);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CustomMuiTheme>
|
||||
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
<SnackbarProvider
|
||||
autoHideDuration={3000}
|
||||
maxSnack={3}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
ref={this.notistackRef}
|
||||
action={(key) => (
|
||||
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}>
|
||||
)}
|
||||
>
|
||||
<FeaturesWrapper>
|
||||
<Switch>
|
||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
||||
<Route
|
||||
exact
|
||||
path="/unauthorized"
|
||||
component={unauthorizedRedirect}
|
||||
/>
|
||||
<Route component={AppRouting} />
|
||||
</Switch>
|
||||
</FeaturesWrapper>
|
||||
@@ -47,4 +54,4 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
@@ -19,9 +19,9 @@ import Mqtt from './mqtt/Mqtt';
|
||||
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
||||
import { Features } from './features/types';
|
||||
|
||||
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/network/";
|
||||
export const getDefaultRoute = (features: Features) =>
|
||||
features.project ? `/${PROJECT_PATH}/` : '/network/';
|
||||
class AppRouting extends Component<WithFeaturesProps> {
|
||||
|
||||
componentDidMount() {
|
||||
Authentication.clearLoginRedirect();
|
||||
}
|
||||
@@ -35,9 +35,17 @@ class AppRouting extends Component<WithFeaturesProps> {
|
||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||
)}
|
||||
{features.project && (
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/*`}
|
||||
component={ProjectRouting}
|
||||
/>
|
||||
)}
|
||||
<AuthenticatedRoute exact path="/network/*" component={NetworkConnection} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/network/*"
|
||||
component={NetworkConnection}
|
||||
/>
|
||||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||
{features.ntp && (
|
||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||
@@ -52,7 +60,7 @@ class AppRouting extends Component<WithFeaturesProps> {
|
||||
<Redirect to={getDefaultRoute(features)} />
|
||||
</Switch>
|
||||
</AuthenticationWrapper>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { CssBaseline } from '@material-ui/core';
|
||||
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
|
||||
import {
|
||||
MuiThemeProvider,
|
||||
createMuiTheme,
|
||||
StylesProvider
|
||||
} from '@material-ui/core/styles';
|
||||
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
type: "dark",
|
||||
type: 'dark',
|
||||
primary: {
|
||||
main: '#33bfff',
|
||||
main: '#33bfff'
|
||||
},
|
||||
secondary: {
|
||||
main: '#3d5afe',
|
||||
main: '#3d5afe'
|
||||
},
|
||||
info: {
|
||||
main: blueGrey[500]
|
||||
@@ -29,7 +33,6 @@ const theme = createMuiTheme({
|
||||
});
|
||||
|
||||
export default class CustomMuiTheme extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StylesProvider>
|
||||
@@ -40,5 +43,4 @@ export default class CustomMuiTheme extends Component {
|
||||
</StylesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,53 +2,63 @@ import React, { Component } from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
withStyles,
|
||||
createStyles,
|
||||
Theme,
|
||||
WithStyles
|
||||
} from '@material-ui/core/styles';
|
||||
import { Paper, Typography, Fab } from '@material-ui/core';
|
||||
import ForwardIcon from '@material-ui/icons/Forward';
|
||||
|
||||
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
||||
import {
|
||||
withAuthenticationContext,
|
||||
AuthenticationContextProps
|
||||
} from './authentication/AuthenticationContext';
|
||||
import { PasswordValidator } from './components';
|
||||
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
signInPage: {
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
margin: "auto",
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
margin: 'auto',
|
||||
padding: theme.spacing(2),
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
maxWidth: theme.breakpoints.values.sm
|
||||
},
|
||||
signInPanel: {
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
padding: theme.spacing(2),
|
||||
paddingTop: "200px",
|
||||
paddingTop: '200px',
|
||||
backgroundImage: 'url("/app/icon.png")',
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
||||
backgroundSize: "auto 150px",
|
||||
width: "100%"
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50% ' + theme.spacing(2) + 'px',
|
||||
backgroundSize: 'auto 150px',
|
||||
width: '100%'
|
||||
},
|
||||
extendedIcon: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
marginRight: theme.spacing(0.5)
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
marginTop: theme.spacing(2)
|
||||
}
|
||||
});
|
||||
|
||||
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
||||
type SignInProps = WithSnackbarProps &
|
||||
WithStyles<typeof styles> &
|
||||
AuthenticationContextProps;
|
||||
|
||||
interface SignInState {
|
||||
username: string,
|
||||
password: string,
|
||||
processing: boolean
|
||||
username: string;
|
||||
password: string;
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
class SignIn extends Component<SignInProps, SignInState> {
|
||||
|
||||
constructor(props: SignInProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -60,10 +70,10 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
|
||||
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const { name, value } = event.currentTarget;
|
||||
this.setState(prevState => ({
|
||||
this.setState((prevState) => ({
|
||||
...prevState,
|
||||
[name]: value,
|
||||
}))
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
@@ -77,20 +87,21 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else if (response.status === 401) {
|
||||
throw Error("Invalid credentials.");
|
||||
throw Error('Invalid credentials.');
|
||||
} else {
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
}
|
||||
}).then(json => {
|
||||
})
|
||||
.then((json) => {
|
||||
authenticationContext.signIn(json.access_token);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(error.message, {
|
||||
variant: 'warning',
|
||||
variant: 'warning'
|
||||
});
|
||||
this.setState({ processing: false });
|
||||
});
|
||||
@@ -116,8 +127,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
inputProps={{
|
||||
autoCapitalize: "none",
|
||||
autoCorrect: "off",
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off'
|
||||
}}
|
||||
/>
|
||||
<PasswordValidator
|
||||
@@ -132,7 +143,13 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
/>
|
||||
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
||||
<Fab
|
||||
variant="extended"
|
||||
color="primary"
|
||||
className={classes.button}
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
>
|
||||
<ForwardIcon className={classes.extendedIcon} />
|
||||
Sign In
|
||||
</Fab>
|
||||
@@ -141,7 +158,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
|
||||
export default withAuthenticationContext(
|
||||
withSnackbar(withStyles(styles)(SignIn))
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { APSettings, APProvisionMode } from "./types";
|
||||
import { APSettings, APProvisionMode } from './types';
|
||||
|
||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||
}
|
||||
return (
|
||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { AP_SETTINGS_ENDPOINT } from '../api';
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
|
||||
import APSettingsForm from './APSettingsForm';
|
||||
import { APSettings } from './types';
|
||||
@@ -9,7 +14,6 @@ import { APSettings } from './types';
|
||||
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
||||
|
||||
class APSettingsController extends Component<APSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class APSettingsController extends Component<APSettingsControllerProps> {
|
||||
<SectionContent title="Access Point Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <APSettingsForm {...formProps} />}
|
||||
render={(formProps) => <APSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
||||
import {
|
||||
TextValidator,
|
||||
ValidatorForm,
|
||||
SelectValidator
|
||||
} from 'react-material-ui-form-validator';
|
||||
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { PasswordValidator, RestFormProps, FormActions, FormButton } from '../components';
|
||||
import {
|
||||
PasswordValidator,
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton
|
||||
} from '../components';
|
||||
|
||||
import { isAPEnabled } from './APModes';
|
||||
import { APSettings, APProvisionMode } from './types';
|
||||
@@ -13,7 +22,6 @@ import { isIP } from '../validators';
|
||||
type APSettingsFormProps = RestFormProps<APSettings>;
|
||||
|
||||
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIP', isIP);
|
||||
}
|
||||
@@ -22,23 +30,29 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
const { data, handleValueChange, saveData } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
||||
<SelectValidator name="provision_mode"
|
||||
<SelectValidator
|
||||
name="provision_mode"
|
||||
label="Provide Access Point…"
|
||||
value={data.provision_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('provision_mode')}
|
||||
margin="normal">
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When Network Disconnected</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
|
||||
When Network Disconnected
|
||||
</MenuItem>
|
||||
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||
</SelectValidator>
|
||||
{
|
||||
isAPEnabled(data) &&
|
||||
{isAPEnabled(data) && (
|
||||
<Fragment>
|
||||
<TextValidator
|
||||
validators={['required', 'matchRegexp:^.{1,32}$']}
|
||||
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
|
||||
errorMessages={[
|
||||
'Access Point SSID is required',
|
||||
'Access Point SSID must be 32 characters or less'
|
||||
]}
|
||||
name="ssid"
|
||||
label="Access Point SSID"
|
||||
fullWidth
|
||||
@@ -49,7 +63,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
/>
|
||||
<PasswordValidator
|
||||
validators={['required', 'matchRegexp:^.{8,64}$']}
|
||||
errorMessages={['Access Point Password is required', 'Access Point Password must be 8-64 characters']}
|
||||
errorMessages={[
|
||||
'Access Point Password is required',
|
||||
'Access Point Password must be 8-64 characters'
|
||||
]}
|
||||
name="password"
|
||||
label="Access Point Password"
|
||||
fullWidth
|
||||
@@ -71,7 +88,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
errorMessages={['Gateway IP is required', 'Must be an IP address']}
|
||||
errorMessages={[
|
||||
'Gateway IP is required',
|
||||
'Must be an IP address'
|
||||
]}
|
||||
name="gateway_ip"
|
||||
label="Gateway"
|
||||
fullWidth
|
||||
@@ -82,7 +102,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
||||
errorMessages={[
|
||||
'Subnet mask is required',
|
||||
'Must be an IP address'
|
||||
]}
|
||||
name="subnet_mask"
|
||||
label="Subnet"
|
||||
fullWidth
|
||||
@@ -92,9 +115,14 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { APStatus, APNetworkStatus } from "./types";
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { APStatus, APNetworkStatus } from './types';
|
||||
|
||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
@@ -12,17 +12,17 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const apStatus = ({ status }: APStatus) => {
|
||||
switch (status) {
|
||||
case APNetworkStatus.ACTIVE:
|
||||
return "Active";
|
||||
return 'Active';
|
||||
case APNetworkStatus.INACTIVE:
|
||||
return "Inactive";
|
||||
return 'Inactive';
|
||||
case APNetworkStatus.LINGERING:
|
||||
return "Lingering until idle";
|
||||
return 'Lingering until idle';
|
||||
default:
|
||||
return "Unknown";
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { AP_STATUS_ENDPOINT } from '../api';
|
||||
|
||||
import APStatusForm from './APStatusForm';
|
||||
@@ -9,7 +14,6 @@ import { APStatus } from './types';
|
||||
type APStatusControllerProps = RestControllerProps<APStatus>;
|
||||
|
||||
class APStatusController extends Component<APStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,10 +23,10 @@ class APStatusController extends Component<APStatusControllerProps> {
|
||||
<SectionContent title="Access Point Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <APStatusForm {...formProps} />}
|
||||
render={(formProps) => <APStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText
|
||||
} from '@material-ui/core';
|
||||
|
||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
import ComputerIcon from '@material-ui/icons/Computer';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
import { apStatusHighlight, apStatus } from './APStatus';
|
||||
import { APStatus } from './types';
|
||||
|
||||
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
||||
|
||||
class APStatusForm extends Component<APStatusFormProps> {
|
||||
|
||||
createListItems() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
@@ -61,18 +72,20 @@ class APStatusForm extends Component<APStatusFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withTheme(APStatusForm);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
AuthenticatedContextProps,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import APSettingsController from './APSettingsController';
|
||||
@@ -12,8 +16,7 @@ import APStatusController from './APStatusController';
|
||||
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class AccessPoint extends Component<AccessPointProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -21,17 +24,33 @@ class AccessPoint extends Component<AccessPointProps> {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Access Point">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="/ap/status" label="Access Point Status" />
|
||||
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/ap/settings"
|
||||
label="Access Point Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
|
||||
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ap/status"
|
||||
component={APStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ap/settings"
|
||||
component={APSettingsController}
|
||||
/>
|
||||
<Redirect to="/ap/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { ENDPOINT_ROOT } from './Env';
|
||||
|
||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
|
||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
|
||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
|
||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
|
||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "networkSettings";
|
||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + "networkStatus";
|
||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
|
||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "mqttSettings";
|
||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + "mqttStatus";
|
||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
|
||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
|
||||
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
|
||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features';
|
||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus';
|
||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings';
|
||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time';
|
||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings';
|
||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus';
|
||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks';
|
||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks';
|
||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings';
|
||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus';
|
||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings';
|
||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware';
|
||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings';
|
||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus';
|
||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus';
|
||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn';
|
||||
export const VERIFY_AUTHORIZATION_ENDPOINT =
|
||||
ENDPOINT_ROOT + 'verifyAuthorization';
|
||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings';
|
||||
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken';
|
||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart';
|
||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||
|
||||
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/");
|
||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/");
|
||||
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
|
||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
|
||||
export const EVENT_SOURCE_ROOT = calculateEndpointRoot('/es/');
|
||||
|
||||
function calculateEndpointRoot(endpointPath: string) {
|
||||
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
||||
@@ -10,7 +11,7 @@ function calculateEndpointRoot(endpointPath: string) {
|
||||
return httpRoot + endpointPath;
|
||||
}
|
||||
const location = window.location;
|
||||
return location.protocol + "//" + location.host + endpointPath;
|
||||
return location.protocol + '//' + location.host + endpointPath;
|
||||
}
|
||||
|
||||
function calculateWebSocketRoot(webSocketPath: string) {
|
||||
@@ -19,6 +20,6 @@ function calculateWebSocketRoot(webSocketPath: string) {
|
||||
return webSocketRoot + webSocketPath;
|
||||
}
|
||||
const location = window.location;
|
||||
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return webProtocol + "//" + location.host + webSocketPath;
|
||||
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return webProtocol + '//' + location.host + webSocketPath;
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './Env'
|
||||
export * from './Endpoints'
|
||||
export * from './Env';
|
||||
export * from './Endpoints';
|
||||
|
||||
@@ -1,40 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||
import {
|
||||
Redirect,
|
||||
Route,
|
||||
RouteProps,
|
||||
RouteComponentProps
|
||||
} from 'react-router-dom';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
|
||||
import * as Authentication from './Authentication';
|
||||
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
|
||||
import {
|
||||
withAuthenticationContext,
|
||||
AuthenticationContextProps,
|
||||
AuthenticatedContext,
|
||||
AuthenticatedContextValue
|
||||
} from './AuthenticationContext';
|
||||
|
||||
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
|
||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||
interface AuthenticatedRouteProps
|
||||
extends RouteProps,
|
||||
WithSnackbarProps,
|
||||
AuthenticationContextProps {
|
||||
component:
|
||||
| React.ComponentType<RouteComponentProps<any>>
|
||||
| React.ComponentType<any>;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||
|
||||
render() {
|
||||
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
||||
const {
|
||||
enqueueSnackbar,
|
||||
authenticationContext,
|
||||
component: Component,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { location } = this.props;
|
||||
const renderComponent: RenderComponent = (props) => {
|
||||
if (authenticationContext.me) {
|
||||
return (
|
||||
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
|
||||
<AuthenticatedContext.Provider
|
||||
value={authenticationContext as AuthenticatedContextValue}
|
||||
>
|
||||
<Component {...props} />
|
||||
</AuthenticatedContext.Provider>
|
||||
);
|
||||
}
|
||||
Authentication.storeLoginRedirect(location);
|
||||
enqueueSnackbar("Please sign in to continue", { variant: 'info' });
|
||||
return (
|
||||
<Redirect to='/' />
|
||||
);
|
||||
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
|
||||
return <Redirect to="/" />;
|
||||
};
|
||||
return <Route {...rest} render={renderComponent} />;
|
||||
}
|
||||
return (
|
||||
<Route {...rest} render={renderComponent} />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
||||
|
||||
@@ -27,7 +27,9 @@ export function clearLoginRedirect() {
|
||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||
}
|
||||
|
||||
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
|
||||
export function fetchLoginRedirect(
|
||||
features: Features
|
||||
): H.LocationDescriptorObject {
|
||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
@@ -38,16 +40,19 @@ export function fetchLoginRedirect(features: Features): H.LocationDescriptorObje
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the normal fetch routene with one with provides the access token if present.
|
||||
* Wraps the normal fetch routine with one with provides the access token if present.
|
||||
*/
|
||||
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||
export function authorizedFetch(
|
||||
url: RequestInfo,
|
||||
params?: RequestInit
|
||||
): Promise<Response> {
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
params = params || {};
|
||||
params.credentials = 'include';
|
||||
params.headers = {
|
||||
...params.headers,
|
||||
"Authorization": 'Bearer ' + accessToken
|
||||
Authorization: 'Bearer ' + accessToken
|
||||
};
|
||||
}
|
||||
return fetch(url, params);
|
||||
@@ -55,26 +60,31 @@ export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise
|
||||
|
||||
/**
|
||||
* fetch() does not yet support upload progress, this wrapper allows us to configure the xhr request
|
||||
* for a single file upload and takes care of adding the Authroization header and redirecting on
|
||||
* authroization errors as we do for normal fetch operations.
|
||||
* for a single file upload and takes care of adding the Authorization header and redirecting on
|
||||
* authorization errors as we do for normal fetch operations.
|
||||
*/
|
||||
export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, file: File, onProgress: (event: ProgressEvent<EventTarget>) => void): Promise<void> {
|
||||
export function redirectingAuthorizedUpload(
|
||||
xhr: XMLHttpRequest,
|
||||
url: string,
|
||||
file: File,
|
||||
onProgress: (event: ProgressEvent<EventTarget>) => void
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.open("POST", url, true);
|
||||
xhr.open('POST', url, true);
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("Authorization", 'Bearer ' + accessToken);
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
|
||||
}
|
||||
xhr.upload.onprogress = onProgress;
|
||||
xhr.onload = function () {
|
||||
if (xhr.status === 401 || xhr.status === 403) {
|
||||
history.push("/unauthorized");
|
||||
history.push('/unauthorized');
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
|
||||
xhr.onerror = function () {
|
||||
reject(new DOMException('Error', 'UploadError'));
|
||||
};
|
||||
xhr.onabort = function () {
|
||||
@@ -87,17 +97,22 @@ export function redirectingAuthorizedUpload(xhr: XMLHttpRequest, url: string, fi
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the normal fetch routene which redirects on 401 response.
|
||||
* Wraps the normal fetch routine which redirects on 401 response.
|
||||
*/
|
||||
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||
export function redirectingAuthorizedFetch(
|
||||
url: RequestInfo,
|
||||
params?: RequestInit
|
||||
): Promise<Response> {
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
authorizedFetch(url, params).then(response => {
|
||||
authorizedFetch(url, params)
|
||||
.then((response) => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
history.push("/unauthorized");
|
||||
history.push('/unauthorized');
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
}).catch(error => {
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as React from "react";
|
||||
import * as React from 'react';
|
||||
|
||||
export interface Me {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
version: string; // proddy added
|
||||
}
|
||||
|
||||
export interface AuthenticationContextValue {
|
||||
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
|
||||
me?: Me;
|
||||
}
|
||||
|
||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
|
||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
||||
export const AuthenticationContext = React.createContext(
|
||||
AuthenticationContextDefaultValue
|
||||
);
|
||||
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
|
||||
authenticationContext: AuthenticationContextValue;
|
||||
}
|
||||
|
||||
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
|
||||
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
|
||||
export function withAuthenticationContext<T extends AuthenticationContextProps>(
|
||||
Component: React.ComponentType<T>
|
||||
) {
|
||||
return class extends React.Component<
|
||||
Omit<T, keyof AuthenticationContextProps>
|
||||
> {
|
||||
render() {
|
||||
return (
|
||||
<AuthenticationContext.Consumer>
|
||||
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
|
||||
{(authenticationContext) => (
|
||||
<Component
|
||||
{...(this.props as T)}
|
||||
authenticationContext={authenticationContext}
|
||||
/>
|
||||
)}
|
||||
</AuthenticationContext.Consumer>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
||||
me: Me;
|
||||
}
|
||||
|
||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
|
||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
|
||||
export const AuthenticatedContext = React.createContext(
|
||||
AuthenticatedContextDefaultValue
|
||||
);
|
||||
@@ -47,12 +55,21 @@ export interface AuthenticatedContextProps {
|
||||
authenticatedContext: AuthenticatedContextValue;
|
||||
}
|
||||
|
||||
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
|
||||
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
|
||||
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
|
||||
Component: React.ComponentType<T>
|
||||
) {
|
||||
return class extends React.Component<
|
||||
Omit<T, keyof AuthenticatedContextProps>
|
||||
> {
|
||||
render() {
|
||||
return (
|
||||
<AuthenticatedContext.Consumer>
|
||||
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
|
||||
{(authenticatedContext) => (
|
||||
<Component
|
||||
{...(this.props as T)}
|
||||
authenticatedContext={authenticatedContext}
|
||||
/>
|
||||
)}
|
||||
</AuthenticatedContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@ import * as React from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
import history from '../history'
|
||||
import history from '../history';
|
||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||
import { AuthenticationContext, AuthenticationContextValue, Me } from './AuthenticationContext';
|
||||
import {
|
||||
AuthenticationContext,
|
||||
AuthenticationContextValue,
|
||||
Me
|
||||
} from './AuthenticationContext';
|
||||
import FullScreenLoading from '../components/FullScreenLoading';
|
||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||
|
||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
|
||||
export const decodeMeJWT = (accessToken: string): Me =>
|
||||
jwtDecode(accessToken) as Me;
|
||||
|
||||
interface AuthenticationWrapperState {
|
||||
context: AuthenticationContextValue;
|
||||
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
|
||||
|
||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||
|
||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
||||
|
||||
class AuthenticationWrapper extends React.Component<
|
||||
AuthenticationWrapperProps,
|
||||
AuthenticationWrapperState
|
||||
> {
|
||||
constructor(props: AuthenticationWrapperProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
context: {
|
||||
refresh: this.refresh,
|
||||
signIn: this.signIn,
|
||||
signOut: this.signOut,
|
||||
signOut: this.signOut
|
||||
},
|
||||
initialized: false
|
||||
};
|
||||
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
|
||||
{this.state.initialized
|
||||
? this.renderContent()
|
||||
: this.renderContentLoading()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
}
|
||||
|
||||
renderContentLoading() {
|
||||
return (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||
// return;
|
||||
// }
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||
.then(response => {
|
||||
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
||||
}).catch(error => {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
||||
variant: 'error',
|
||||
.then((response) => {
|
||||
const me =
|
||||
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me }
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me: undefined }
|
||||
});
|
||||
this.props.enqueueSnackbar(
|
||||
'Error verifying authorization: ' + error.message,
|
||||
{
|
||||
variant: 'error'
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||
}
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me: undefined }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
signIn = (accessToken: string) => {
|
||||
try {
|
||||
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||
const me: Me = decodeMeJWT(accessToken);
|
||||
this.setState({ context: { ...this.state.context, me } });
|
||||
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
||||
this.props.enqueueSnackbar(`Logged in as ${me.username}`, {
|
||||
variant: 'success'
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||
throw new Error("Failed to parse JWT " + err.message);
|
||||
}
|
||||
this.setState({
|
||||
initialized: true,
|
||||
context: { ...this.state.context, me: undefined }
|
||||
});
|
||||
throw new Error('Failed to parse JWT ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
signOut = () => {
|
||||
getStorage().removeItem(ACCESS_TOKEN);
|
||||
@@ -101,10 +127,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
me: undefined
|
||||
}
|
||||
});
|
||||
this.props.enqueueSnackbar("You have signed out", { variant: 'success', });
|
||||
this.props.enqueueSnackbar('You have signed out', { variant: 'success' });
|
||||
history.push('/');
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
||||
export default withFeatures(withSnackbar(AuthenticationWrapper));
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
import * as React from 'react';
|
||||
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||
import {
|
||||
Redirect,
|
||||
Route,
|
||||
RouteProps,
|
||||
RouteComponentProps
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
|
||||
import {
|
||||
withAuthenticationContext,
|
||||
AuthenticationContextProps
|
||||
} from './AuthenticationContext';
|
||||
import * as Authentication from './Authentication';
|
||||
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||
|
||||
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
|
||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||
interface UnauthenticatedRouteProps
|
||||
extends RouteProps,
|
||||
AuthenticationContextProps,
|
||||
WithFeaturesProps {
|
||||
component:
|
||||
| React.ComponentType<RouteComponentProps<any>>
|
||||
| React.ComponentType<any>;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||
|
||||
public render() {
|
||||
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
||||
const {
|
||||
authenticationContext,
|
||||
component: Component,
|
||||
features,
|
||||
...rest
|
||||
} = this.props;
|
||||
const renderComponent: RenderComponent = (props) => {
|
||||
if (authenticationContext.me) {
|
||||
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
|
||||
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
|
||||
}
|
||||
if (Component) {
|
||||
return (<Component {...props} />);
|
||||
return <Component {...props} />;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Route {...rest} render={renderComponent} />
|
||||
);
|
||||
};
|
||||
return <Route {...rest} render={renderComponent} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import React, { FC } from 'react';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
|
||||
import WarningIcon from "@material-ui/icons/Warning"
|
||||
import { Paper, Typography, Box, CssBaseline } from '@material-ui/core';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
|
||||
const styles = makeStyles(
|
||||
{
|
||||
const styles = makeStyles({
|
||||
siteErrorPage: {
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column"
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
siteErrorPagePanel: {
|
||||
textAlign: "center",
|
||||
padding: "280px 0 40px 0",
|
||||
textAlign: 'center',
|
||||
padding: '280px 0 40px 0',
|
||||
backgroundImage: 'url("/app/icon.png")',
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "50% 40px",
|
||||
backgroundSize: "200px auto",
|
||||
width: "100%",
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50% 40px',
|
||||
backgroundSize: '200px auto',
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
interface ApplicationErrorProps {
|
||||
error?: string;
|
||||
@@ -33,27 +31,29 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
|
||||
<div className={classes.siteErrorPage}>
|
||||
<CssBaseline />
|
||||
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
||||
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
mb={2}
|
||||
>
|
||||
<WarningIcon fontSize="large" color="error" />
|
||||
<Box ml={2}>
|
||||
<Typography variant="h4">
|
||||
Application error
|
||||
</Typography>
|
||||
<Typography variant="h4">Application error</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Failed to configure the application, please refresh to try again.
|
||||
</Typography>
|
||||
{error &&
|
||||
(
|
||||
{error && (
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Error: {error}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ApplicationError;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { FC } from "react";
|
||||
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
|
||||
import { FC } from 'react';
|
||||
import { FormControlLabel, FormControlLabelProps } from '@material-ui/core';
|
||||
|
||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||
<div>
|
||||
<FormControlLabel {...props} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default BlockFormControlLabel;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button, styled } from "@material-ui/core";
|
||||
import { Button, styled } from '@material-ui/core';
|
||||
|
||||
const ErrorButton = styled(Button)(({ theme }) => ({
|
||||
color: theme.palette.getContrastText(theme.palette.error.main),
|
||||
backgroundColor: theme.palette.error.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
backgroundColor: theme.palette.error.dark
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styled, Box } from "@material-ui/core";
|
||||
import { styled, Box } from '@material-ui/core';
|
||||
|
||||
const FormActions = styled(Box)(({ theme }) => ({
|
||||
marginTop: theme.spacing(1)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Button, styled } from "@material-ui/core";
|
||||
import { Button, styled } from '@material-ui/core';
|
||||
|
||||
const FormButton = styled(Button)(({ theme }) => ({
|
||||
margin: theme.spacing(0, 1),
|
||||
'&:last-child': {
|
||||
marginRight: 0,
|
||||
marginRight: 0
|
||||
},
|
||||
'&:first-child': {
|
||||
marginLeft: 0,
|
||||
marginLeft: 0
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -3,30 +3,30 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import { Typography, Theme } from '@material-ui/core';
|
||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
fullScreenLoading: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
flexDirection: "column"
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
progress: {
|
||||
margin: theme.spacing(4),
|
||||
margin: theme.spacing(4)
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
const FullScreenLoading = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.fullScreenLoading}>
|
||||
<CircularProgress className={classes.progress} size={100} />
|
||||
<Typography variant="h4">
|
||||
Loading…
|
||||
</Typography>
|
||||
<Typography variant="h4">Loading…</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FullScreenLoading;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Avatar, makeStyles } from "@material-ui/core";
|
||||
import React, { FC } from "react";
|
||||
import { Avatar, makeStyles } from '@material-ui/core';
|
||||
import { FC } from 'react';
|
||||
|
||||
interface HighlightAvatarProps {
|
||||
color: string;
|
||||
@@ -13,11 +13,7 @@ const useStyles = makeStyles({
|
||||
|
||||
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
||||
const classes = useStyles(props);
|
||||
return (
|
||||
<Avatar className={classes.root}>
|
||||
{props.children}
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
return <Avatar className={classes.root}>{props.children}</Avatar>;
|
||||
};
|
||||
|
||||
export default HighlightAvatar;
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import React, { RefObject, Fragment } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
|
||||
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
|
||||
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
||||
import {
|
||||
Drawer,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Avatar,
|
||||
Divider,
|
||||
Button,
|
||||
Box,
|
||||
IconButton
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
ClickAwayListener,
|
||||
Popper,
|
||||
Hidden,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListItemAvatar
|
||||
} from '@material-ui/core';
|
||||
import { Card, CardContent, CardActions } from '@material-ui/core';
|
||||
|
||||
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
|
||||
import {
|
||||
withStyles,
|
||||
createStyles,
|
||||
Theme,
|
||||
WithTheme,
|
||||
WithStyles,
|
||||
withTheme
|
||||
} from '@material-ui/core/styles';
|
||||
|
||||
import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
@@ -19,20 +46,24 @@ import MenuIcon from '@material-ui/icons/Menu';
|
||||
|
||||
import ProjectMenu from '../project/ProjectMenu';
|
||||
import { PROJECT_NAME } from '../api';
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||
|
||||
const drawerWidth = 290;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
display: 'flex'
|
||||
},
|
||||
drawer: {
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
},
|
||||
flexShrink: 0
|
||||
}
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1
|
||||
@@ -40,8 +71,8 @@ const styles = (theme: Theme) => createStyles({
|
||||
appBar: {
|
||||
marginLeft: drawerWidth,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
},
|
||||
width: `calc(100% - ${drawerWidth}px)`
|
||||
}
|
||||
},
|
||||
toolbarImage: {
|
||||
[theme.breakpoints.up('xs')]: {
|
||||
@@ -51,31 +82,31 @@ const styles = (theme: Theme) => createStyles({
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
height: 36,
|
||||
marginRight: theme.spacing(3)
|
||||
},
|
||||
}
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: theme.spacing(2),
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
toolbar: theme.mixins.toolbar,
|
||||
drawerPaper: {
|
||||
width: drawerWidth,
|
||||
width: drawerWidth
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1
|
||||
},
|
||||
authMenu: {
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
maxWidth: 400,
|
||||
maxWidth: 400
|
||||
},
|
||||
authMenuActions: {
|
||||
padding: theme.spacing(2),
|
||||
"& > * + *": {
|
||||
marginLeft: theme.spacing(2),
|
||||
'& > * + *': {
|
||||
marginLeft: theme.spacing(2)
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
interface MenuAppBarState {
|
||||
@@ -83,12 +114,16 @@ interface MenuAppBarState {
|
||||
authMenuOpen: boolean;
|
||||
}
|
||||
|
||||
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
|
||||
interface MenuAppBarProps
|
||||
extends WithFeaturesProps,
|
||||
AuthenticatedContextProps,
|
||||
WithTheme,
|
||||
WithStyles<typeof styles>,
|
||||
RouteComponentProps {
|
||||
sectionTitle: string;
|
||||
}
|
||||
|
||||
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
|
||||
constructor(props: MenuAppBarProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -101,38 +136,48 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
|
||||
handleToggle = () => {
|
||||
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = (event: React.MouseEvent<Document>) => {
|
||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
|
||||
if (
|
||||
this.anchorRef.current &&
|
||||
this.anchorRef.current.contains(event.currentTarget)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.setState({ authMenuOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerToggle = () => {
|
||||
this.setState({ mobileOpen: !this.state.mobileOpen });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
|
||||
const {
|
||||
classes,
|
||||
theme,
|
||||
children,
|
||||
sectionTitle,
|
||||
authenticatedContext,
|
||||
features
|
||||
} = this.props;
|
||||
const { mobileOpen, authMenuOpen } = this.state;
|
||||
const path = this.props.match.url;
|
||||
const drawer = (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<Box display="flex">
|
||||
<img src="/app/icon.png" className={classes.toolbarImage} alt={PROJECT_NAME} />
|
||||
<img
|
||||
src="/app/icon.png"
|
||||
className={classes.toolbarImage}
|
||||
alt={PROJECT_NAME}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" color="textPrimary">
|
||||
{PROJECT_NAME}
|
||||
</Typography>
|
||||
|
||||
<Typography align="right" variant="caption" color="textPrimary">
|
||||
v{authenticatedContext.me.version}
|
||||
</Typography>
|
||||
|
||||
<Divider absolute />
|
||||
</Toolbar>
|
||||
|
||||
@@ -144,20 +189,35 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
)}
|
||||
|
||||
<List>
|
||||
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/network/"
|
||||
selected={path.startsWith('/network/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsEthernetIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Network Connection" />
|
||||
</ListItem>
|
||||
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/ap/"
|
||||
selected={path.startsWith('/ap/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsInputAntennaIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Access Point" />
|
||||
</ListItem>
|
||||
{features.ntp && (
|
||||
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/ntp/"
|
||||
selected={path.startsWith('/ntp/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AccessTimeIcon />
|
||||
</ListItemIcon>
|
||||
@@ -165,7 +225,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
</ListItem>
|
||||
)}
|
||||
{features.mqtt && (
|
||||
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
|
||||
<ListItem
|
||||
to="/mqtt/"
|
||||
selected={path.startsWith('/mqtt/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DeviceHubIcon />
|
||||
</ListItemIcon>
|
||||
@@ -173,14 +238,25 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
</ListItem>
|
||||
)}
|
||||
{features.security && (
|
||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||
<ListItem
|
||||
to="/security/"
|
||||
selected={path.startsWith('/security/')}
|
||||
button
|
||||
component={Link}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<LockIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Security" />
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
|
||||
<ListItem
|
||||
to="/system/"
|
||||
selected={path.startsWith('/system/')}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
@@ -201,7 +277,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
>
|
||||
<AccountCircleIcon />
|
||||
</IconButton>
|
||||
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
|
||||
<Popper
|
||||
open={authMenuOpen}
|
||||
anchorEl={this.anchorRef.current}
|
||||
transition
|
||||
className={classes.authMenu}
|
||||
>
|
||||
<ClickAwayListener onClickAway={this.handleClose}>
|
||||
<Card id="menu-list-grow">
|
||||
<CardContent>
|
||||
@@ -212,13 +293,27 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
<AccountCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
|
||||
<ListItemText
|
||||
primary={
|
||||
'Signed in as: ' + authenticatedContext.me.username
|
||||
}
|
||||
secondary={
|
||||
authenticatedContext.me.admin ? 'Admin User' : undefined
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions className={classes.authMenuActions}>
|
||||
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
color="primary"
|
||||
onClick={authenticatedContext.signOut}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</ClickAwayListener>
|
||||
@@ -239,7 +334,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
className={classes.title}
|
||||
>
|
||||
{sectionTitle}
|
||||
</Typography>
|
||||
{features.security && userMenu}
|
||||
@@ -253,10 +353,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
open={mobileOpen}
|
||||
onClose={this.handleDrawerToggle}
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
paper: classes.drawerPaper
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
keepMounted: true
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
@@ -265,7 +365,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
<Hidden smDown implementation="css">
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
paper: classes.drawerPaper
|
||||
}}
|
||||
variant="permanent"
|
||||
open
|
||||
@@ -285,10 +385,6 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
|
||||
export default withRouter(
|
||||
withTheme(
|
||||
withFeatures(
|
||||
withAuthenticatedContext(
|
||||
withStyles(styles)(MenuAppBar)
|
||||
)
|
||||
)
|
||||
withFeatures(withAuthenticatedContext(withStyles(styles)(MenuAppBar)))
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
|
||||
import {
|
||||
TextValidator,
|
||||
ValidatorComponentProps
|
||||
} from 'react-material-ui-form-validator';
|
||||
|
||||
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import { InputAdornment, IconButton } from '@material-ui/core';
|
||||
@@ -7,20 +10,23 @@ import {Visibility,VisibilityOff } from '@material-ui/icons';
|
||||
|
||||
const styles = createStyles({
|
||||
input: {
|
||||
"&::-ms-reveal": {
|
||||
display: "none"
|
||||
'&::-ms-reveal': {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
||||
type PasswordValidatorProps = WithStyles<typeof styles> &
|
||||
Exclude<ValidatorComponentProps, 'type' | 'InputProps'>;
|
||||
|
||||
interface PasswordValidatorState {
|
||||
showPassword: boolean;
|
||||
}
|
||||
|
||||
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
||||
|
||||
class PasswordValidator extends React.Component<
|
||||
PasswordValidatorProps,
|
||||
PasswordValidatorState
|
||||
> {
|
||||
state = {
|
||||
showPassword: false
|
||||
};
|
||||
@@ -29,7 +35,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
||||
this.setState({
|
||||
showPassword: !this.state.showPassword
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, ...rest } = this.props;
|
||||
@@ -39,7 +45,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
InputProps={{
|
||||
classes,
|
||||
endAdornment:
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="Toggle password visibility"
|
||||
@@ -48,11 +54,11 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withStyles(styles)(PasswordValidator);
|
||||
|
||||
@@ -4,7 +4,9 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
|
||||
export interface RestControllerProps<D> extends WithSnackbarProps {
|
||||
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleValueChange: (
|
||||
name: keyof D
|
||||
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
|
||||
setData: (data: D, callback?: () => void) => void;
|
||||
saveData: () => void;
|
||||
@@ -15,16 +17,18 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
export const extractEventValue = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
switch (event.target.type) {
|
||||
case "number":
|
||||
case 'number':
|
||||
return event.target.valueAsNumber;
|
||||
case "checkbox":
|
||||
case 'checkbox':
|
||||
return event.target.checked;
|
||||
default:
|
||||
return event.target.value
|
||||
}
|
||||
return event.target.value;
|
||||
}
|
||||
};
|
||||
|
||||
interface RestControllerState<D> {
|
||||
data?: D;
|
||||
@@ -32,10 +36,15 @@ interface RestControllerState<D> {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
|
||||
export function restController<D, P extends RestControllerProps<D>>(
|
||||
endpointUrl: string,
|
||||
RestController: React.ComponentType<P & RestControllerProps<D>>
|
||||
) {
|
||||
return withSnackbar(
|
||||
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
|
||||
|
||||
class extends React.Component<
|
||||
Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps,
|
||||
RestControllerState<D>
|
||||
> {
|
||||
state: RestControllerState<D> = {
|
||||
data: undefined,
|
||||
loading: false,
|
||||
@@ -43,12 +52,15 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
||||
};
|
||||
|
||||
setData = (data: D, callback?: () => void) => {
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
data,
|
||||
loading: false,
|
||||
errorMessage: undefined
|
||||
}, callback);
|
||||
}
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
loadData = () => {
|
||||
this.setState({
|
||||
@@ -56,19 +68,24 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
||||
loading: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
redirectingAuthorizedFetch(endpointUrl).then(response => {
|
||||
redirectingAuthorizedFetch(endpointUrl)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}).then(json => {
|
||||
this.setState({ data: json, loading: false })
|
||||
}).catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
this.setState({ data: json, loading: false });
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage = error.message || 'Unknown error';
|
||||
this.props.enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ data: undefined, loading: false, errorMessage });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
saveData = () => {
|
||||
this.setState({ loading: true });
|
||||
@@ -78,36 +95,47 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => {
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}).then(json => {
|
||||
this.props.enqueueSnackbar("Update successful.", { variant: 'success' });
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
this.props.enqueueSnackbar('Update successful.', {
|
||||
variant: 'success'
|
||||
});
|
||||
this.setState({ data: json, loading: false });
|
||||
}).catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage = error.message || 'Unknown error';
|
||||
this.props.enqueueSnackbar('Problem updating: ' + errorMessage, {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ data: undefined, loading: false, errorMessage });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleValueChange = (name: keyof D) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||
this.setState({ data });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <RestController
|
||||
return (
|
||||
<RestController
|
||||
{...this.state}
|
||||
{...this.props as P}
|
||||
{...(this.props as P)}
|
||||
handleValueChange={this.handleValueChange}
|
||||
setData={this.setData}
|
||||
saveData={this.saveData}
|
||||
loadData={this.loadData}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,20 +8,23 @@ import { RestControllerProps } from '.';
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
loadingSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
margin: theme.spacing(0.5)
|
||||
},
|
||||
loadingSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
textAlign: 'center'
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
marginTop: theme.spacing(2)
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
|
||||
export type RestFormProps<D> = Omit<
|
||||
RestControllerProps<D>,
|
||||
'loading' | 'errorMessage'
|
||||
> & { data: D };
|
||||
|
||||
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
|
||||
render: (props: RestFormProps<D>) => JSX.Element;
|
||||
@@ -46,7 +49,12 @@ export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
|
||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={loadData}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
content: {
|
||||
padding: theme.spacing(2),
|
||||
margin: theme.spacing(3),
|
||||
margin: theme.spacing(3)
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -15,13 +15,14 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
interface SectionContentProps {
|
||||
title: string;
|
||||
titleGutter?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const SectionContent: React.FC<SectionContentProps> = (props) => {
|
||||
const { children, title, titleGutter } = props;
|
||||
const { children, title, titleGutter, id } = props;
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.content}>
|
||||
<Paper id={id} className={classes.content}>
|
||||
<Typography variant="h6" gutterBottom={titleGutter}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
@@ -4,13 +4,20 @@ import { useDropzone, DropzoneState } from 'react-dropzone';
|
||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
||||
import CancelIcon from '@material-ui/icons/Cancel';
|
||||
import { Theme, Box, Typography, LinearProgress, Button } from '@material-ui/core';
|
||||
import {
|
||||
Theme,
|
||||
Box,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
Button
|
||||
} from '@material-ui/core';
|
||||
|
||||
interface SingleUploadStyleProps extends DropzoneState {
|
||||
uploading: boolean;
|
||||
}
|
||||
|
||||
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
|
||||
const progressPercentage = (progress: ProgressEvent) =>
|
||||
Math.round((progress.loaded * 100) / progress.total);
|
||||
|
||||
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
||||
if (props.isDragAccept) {
|
||||
@@ -23,9 +30,10 @@ const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
||||
return theme.palette.info.main;
|
||||
}
|
||||
return theme.palette.grey[700];
|
||||
}
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
dropzone: {
|
||||
padding: theme.spacing(8, 2),
|
||||
borderWidth: 2,
|
||||
@@ -33,11 +41,14 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
borderStyle: 'dashed',
|
||||
color: theme.palette.grey[700],
|
||||
transition: 'border .24s ease-in-out',
|
||||
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
|
||||
cursor: (props: SingleUploadStyleProps) =>
|
||||
props.uploading ? 'default' : 'pointer',
|
||||
width: '100%',
|
||||
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
|
||||
borderColor: (props: SingleUploadStyleProps) =>
|
||||
getBorderColor(theme, props)
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
export interface SingleUploadProps {
|
||||
onDrop: (acceptedFiles: File[]) => void;
|
||||
@@ -47,26 +58,44 @@ export interface SingleUploadProps {
|
||||
progress?: ProgressEvent;
|
||||
}
|
||||
|
||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
|
||||
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
|
||||
const SingleUpload: FC<SingleUploadProps> = ({
|
||||
onDrop,
|
||||
onCancel,
|
||||
accept,
|
||||
uploading,
|
||||
progress
|
||||
}) => {
|
||||
const dropzoneState = useDropzone({
|
||||
onDrop,
|
||||
accept,
|
||||
disabled: uploading,
|
||||
multiple: false
|
||||
});
|
||||
const { getRootProps, getInputProps } = dropzoneState;
|
||||
const classes = useStyles({ ...dropzoneState, uploading });
|
||||
|
||||
|
||||
const renderProgressText = () => {
|
||||
if (uploading) {
|
||||
if (progress?.lengthComputable) {
|
||||
return `Uploading: ${progressPercentage(progress)}%`;
|
||||
}
|
||||
return "Uploading\u2026";
|
||||
}
|
||||
return "Drop file or click here";
|
||||
return 'Uploading\u2026';
|
||||
}
|
||||
return 'Drop file or click here';
|
||||
};
|
||||
|
||||
const renderProgress = (progress?: ProgressEvent) => (
|
||||
<LinearProgress
|
||||
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
|
||||
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
|
||||
variant={
|
||||
!progress || progress.lengthComputable ? 'determinate' : 'indeterminate'
|
||||
}
|
||||
value={
|
||||
!progress
|
||||
? 0
|
||||
: progress.lengthComputable
|
||||
? progressPercentage(progress)
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -74,16 +103,19 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
||||
<div {...getRootProps({ className: classes.dropzone })}>
|
||||
<input {...getInputProps()} />
|
||||
<Box flexDirection="column" display="flex" alignItems="center">
|
||||
<CloudUploadIcon fontSize='large' />
|
||||
<Typography variant="h6">
|
||||
{renderProgressText()}
|
||||
</Typography>
|
||||
<CloudUploadIcon fontSize="large" />
|
||||
<Typography variant="h6">{renderProgressText()}</Typography>
|
||||
{uploading && (
|
||||
<Fragment>
|
||||
<Box width="100%" p={2}>
|
||||
{renderProgress(progress)}
|
||||
</Box>
|
||||
<Button startIcon={<CancelIcon />} variant="contained" color="secondary" onClick={onCancel}>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Fragment>
|
||||
@@ -91,6 +123,6 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SingleUpload;
|
||||
|
||||
@@ -7,7 +7,9 @@ import { addAccessTokenParameter } from '../authentication';
|
||||
import { extractEventValue } from '.';
|
||||
|
||||
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
|
||||
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleValueChange: (
|
||||
name: keyof D
|
||||
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
|
||||
setData: (data: D, callback?: () => void) => void;
|
||||
saveData: () => void;
|
||||
@@ -25,8 +27,8 @@ interface WebSocketControllerState<D> {
|
||||
}
|
||||
|
||||
enum WebSocketMessageType {
|
||||
ID = "id",
|
||||
PAYLOAD = "payload"
|
||||
ID = 'id',
|
||||
PAYLOAD = 'payload'
|
||||
}
|
||||
|
||||
interface WebSocketIdMessage {
|
||||
@@ -40,21 +42,32 @@ interface WebSocketPayloadMessage<D> {
|
||||
payload: D;
|
||||
}
|
||||
|
||||
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>;
|
||||
export type WebSocketMessage<D> =
|
||||
| WebSocketIdMessage
|
||||
| WebSocketPayloadMessage<D>;
|
||||
|
||||
export function webSocketController<D, P extends WebSocketControllerProps<D>>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>) {
|
||||
export function webSocketController<D, P extends WebSocketControllerProps<D>>(
|
||||
wsUrl: string,
|
||||
wsThrottle: number,
|
||||
WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>
|
||||
) {
|
||||
return withSnackbar(
|
||||
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
|
||||
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
|
||||
class extends React.Component<
|
||||
Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps,
|
||||
WebSocketControllerState<D>
|
||||
> {
|
||||
constructor(
|
||||
props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps
|
||||
) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
||||
onmessage: this.onMessage,
|
||||
onopen: this.onOpen,
|
||||
onclose: this.onClose,
|
||||
onclose: this.onClose
|
||||
}),
|
||||
connected: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -64,37 +77,42 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
||||
onMessage = (event: MessageEvent) => {
|
||||
const rawData = event.data;
|
||||
if (typeof rawData === 'string' || rawData instanceof String) {
|
||||
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
|
||||
}
|
||||
this.handleMessage(
|
||||
JSON.parse(rawData as string) as WebSocketMessage<D>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleMessage = (message: WebSocketMessage<D>) => {
|
||||
const { clientId, data } = this.state;
|
||||
|
||||
switch (message.type) {
|
||||
case WebSocketMessageType.ID:
|
||||
this.setState({ clientId: message.id });
|
||||
break;
|
||||
case WebSocketMessageType.PAYLOAD:
|
||||
const { clientId, data } = this.state;
|
||||
if (clientId && (!data || clientId !== message.origin_id)) {
|
||||
this.setState(
|
||||
{ data: message.payload }
|
||||
);
|
||||
this.setState({ data: message.payload });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onOpen = () => {
|
||||
this.setState({ connected: true });
|
||||
}
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({ connected: false, clientId: undefined, data: undefined });
|
||||
}
|
||||
this.setState({
|
||||
connected: false,
|
||||
clientId: undefined,
|
||||
data: undefined
|
||||
});
|
||||
};
|
||||
|
||||
setData = (data: D, callback?: () => void) => {
|
||||
this.setState({ data }, callback);
|
||||
}
|
||||
};
|
||||
|
||||
saveData = throttle(() => {
|
||||
const { ws, connected, data } = this.state;
|
||||
@@ -106,28 +124,35 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
||||
saveDataAndClear = throttle(() => {
|
||||
const { ws, connected, data } = this.state;
|
||||
if (connected) {
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
data: undefined
|
||||
}, () => ws.json(data));
|
||||
},
|
||||
() => ws.json(data)
|
||||
);
|
||||
}
|
||||
}, wsThrottle);
|
||||
|
||||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleValueChange = (name: keyof D) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||
this.setState({ data });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <WebSocketController
|
||||
{...this.props as P}
|
||||
return (
|
||||
<WebSocketController
|
||||
{...(this.props as P)}
|
||||
handleValueChange={this.handleValueChange}
|
||||
setData={this.setData}
|
||||
saveData={this.saveData}
|
||||
saveDataAndClear={this.saveDataAndClear}
|
||||
connected={this.state.connected}
|
||||
data={this.state.data}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
import { LinearProgress, Typography } from '@material-ui/core';
|
||||
|
||||
@@ -8,22 +6,27 @@ import { WebSocketControllerProps } from '.';
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
loadingSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
margin: theme.spacing(0.5)
|
||||
},
|
||||
loadingSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
textAlign: 'center'
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export type WebSocketFormProps<D> = Omit<WebSocketControllerProps<D>, "connected"> & { data: D };
|
||||
export type WebSocketFormProps<D> = Omit<
|
||||
WebSocketControllerProps<D>,
|
||||
'connected'
|
||||
> & { data: D };
|
||||
|
||||
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
|
||||
render: (props: WebSocketFormProps<D>) => JSX.Element;
|
||||
}
|
||||
|
||||
export default function WebSocketFormLoader<D>(props: WebSocketFormLoaderProps<D>) {
|
||||
export default function WebSocketFormLoader<D>(
|
||||
props: WebSocketFormLoaderProps<D>
|
||||
) {
|
||||
const { connected, render, data, ...rest } = props;
|
||||
const classes = useStyles();
|
||||
if (!connected || !data) {
|
||||
|
||||
14
interface/src/components/WindowSize.tsx
Normal file
14
interface/src/components/WindowSize.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
export function useWindowSize() {
|
||||
const [size, setSize] = useState([0, 0]);
|
||||
useLayoutEffect(() => {
|
||||
function updateSize() {
|
||||
setSize([window.innerWidth, window.innerHeight]);
|
||||
}
|
||||
window.addEventListener('resize', updateSize);
|
||||
updateSize();
|
||||
return () => window.removeEventListener('resize', updateSize);
|
||||
}, []);
|
||||
return size;
|
||||
}
|
||||
@@ -15,3 +15,5 @@ export * from './RestController';
|
||||
|
||||
export * from './WebSocketFormLoader';
|
||||
export * from './WebSocketController';
|
||||
|
||||
export * from './WindowSize';
|
||||
|
||||
@@ -5,21 +5,26 @@ export interface FeaturesContextValue {
|
||||
features: Features;
|
||||
}
|
||||
|
||||
const FeaturesContextDefaultValue = {} as FeaturesContextValue
|
||||
export const FeaturesContext = React.createContext(
|
||||
FeaturesContextDefaultValue
|
||||
);
|
||||
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
|
||||
export const FeaturesContext = React.createContext(FeaturesContextDefaultValue);
|
||||
|
||||
export interface WithFeaturesProps {
|
||||
features: Features;
|
||||
}
|
||||
|
||||
export function withFeatures<T extends WithFeaturesProps>(Component: React.ComponentType<T>) {
|
||||
export function withFeatures<T extends WithFeaturesProps>(
|
||||
Component: React.ComponentType<T>
|
||||
) {
|
||||
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
|
||||
render() {
|
||||
return (
|
||||
<FeaturesContext.Consumer>
|
||||
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
|
||||
{(featuresContext) => (
|
||||
<Component
|
||||
{...(this.props as T)}
|
||||
features={featuresContext.features}
|
||||
/>
|
||||
)}
|
||||
</FeaturesContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { Features } from './types';
|
||||
import { FeaturesContext } from './FeaturesContext';
|
||||
@@ -9,10 +9,9 @@ import { FEATURES_ENDPOINT } from '../api';
|
||||
interface FeaturesWrapperState {
|
||||
features?: Features;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||
|
||||
state: FeaturesWrapperState = {};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -21,41 +20,39 @@ class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||
|
||||
fetchFeaturesDetails = () => {
|
||||
fetch(FEATURES_ENDPOINT)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw Error("Unexpected status code: " + response.status);
|
||||
throw Error('Unexpected status code: ' + response.status);
|
||||
}
|
||||
}).then(features => {
|
||||
})
|
||||
.then((features) => {
|
||||
this.setState({ features });
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.setState({ error: error.message });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { features, error } = this.state;
|
||||
if (features) {
|
||||
return (
|
||||
<FeaturesContext.Provider value={{
|
||||
<FeaturesContext.Provider
|
||||
value={{
|
||||
features
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</FeaturesContext.Provider>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<ApplicationError error={error} />
|
||||
);
|
||||
return <ApplicationError error={error} />;
|
||||
}
|
||||
return (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FeaturesWrapper;
|
||||
|
||||
@@ -2,4 +2,4 @@ import { createBrowserHistory } from 'history';
|
||||
|
||||
export default createBrowserHistory({
|
||||
/* pass a configuration object here if needed */
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6,8 +6,9 @@ import { Router } from 'react-router';
|
||||
|
||||
import App from './App';
|
||||
|
||||
render((
|
||||
render(
|
||||
<Router history={history}>
|
||||
<App />
|
||||
</Router>
|
||||
), document.getElementById("root"))
|
||||
</Router>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
AuthenticatedContextProps,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
import MqttStatusController from './MqttStatusController';
|
||||
import MqttSettingsController from './MqttSettingsController';
|
||||
@@ -11,8 +15,7 @@ import MqttSettingsController from './MqttSettingsController';
|
||||
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class Mqtt extends Component<MqttProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -20,17 +23,33 @@ class Mqtt extends Component<MqttProps> {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<MenuAppBar sectionTitle="MQTT">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="/mqtt/status" label="MQTT Status" />
|
||||
<Tab value="/mqtt/settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/mqtt/settings"
|
||||
label="MQTT Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
|
||||
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/mqtt/status"
|
||||
component={MqttStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/mqtt/settings"
|
||||
component={MqttSettingsController}
|
||||
/>
|
||||
<Redirect to="/mqtt/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { MQTT_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import MqttSettingsForm from './MqttSettingsForm';
|
||||
@@ -9,7 +14,6 @@ import { MqttSettings } from './types';
|
||||
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
||||
|
||||
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
||||
<SectionContent title="MQTT Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <MqttSettingsForm {...formProps} />}
|
||||
render={(formProps) => <MqttSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
import {
|
||||
TextValidator,
|
||||
ValidatorForm,
|
||||
SelectValidator,
|
||||
} from "react-material-ui-form-validator";
|
||||
SelectValidator
|
||||
} from 'react-material-ui-form-validator';
|
||||
|
||||
import { Checkbox, TextField, Typography } from "@material-ui/core";
|
||||
import SaveIcon from "@material-ui/icons/Save";
|
||||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
import { Checkbox, TextField, Typography } from '@material-ui/core';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
BlockFormControlLabel,
|
||||
PasswordValidator,
|
||||
} from "../components";
|
||||
import { isIP, isHostname, or, isPath } from "../validators";
|
||||
PasswordValidator
|
||||
} from '../components';
|
||||
import { isIP, isHostname, or, isPath } from '../validators';
|
||||
|
||||
import { MqttSettings } from "./types";
|
||||
import { MqttSettings } from './types';
|
||||
|
||||
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
||||
|
||||
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule("isIPOrHostname", or(isIP, isHostname));
|
||||
ValidatorForm.addValidationRule("isPath", isPath);
|
||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||
ValidatorForm.addValidationRule('isPath', isPath);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -36,38 +36,38 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.enabled}
|
||||
onChange={handleValueChange("enabled")}
|
||||
onChange={handleValueChange('enabled')}
|
||||
value="enabled"
|
||||
/>
|
||||
}
|
||||
label="Enable MQTT"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={["required", "isIPOrHostname"]}
|
||||
validators={['required', 'isIPOrHostname']}
|
||||
errorMessages={[
|
||||
"Host is required",
|
||||
"Not a valid IP address or hostname",
|
||||
'Host is required',
|
||||
'Not a valid IP address or hostname'
|
||||
]}
|
||||
name="host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.host}
|
||||
onChange={handleValueChange("host")}
|
||||
onChange={handleValueChange('host')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Port is required",
|
||||
"Must be a number",
|
||||
"Must be greater than 0 ",
|
||||
"Max value is 65535",
|
||||
'Port is required',
|
||||
'Must be a number',
|
||||
'Must be greater than 0 ',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="port"
|
||||
label="Port"
|
||||
@@ -75,18 +75,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.port}
|
||||
type="number"
|
||||
onChange={handleValueChange("port")}
|
||||
onChange={handleValueChange('port')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={["required", "isPath"]}
|
||||
errorMessages={["Base is required", "Not a valid Path"]}
|
||||
validators={['required', 'isPath']}
|
||||
errorMessages={['Base is required', 'Not a valid Path']}
|
||||
name="base"
|
||||
label="Base"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.base}
|
||||
onChange={handleValueChange("base")}
|
||||
onChange={handleValueChange('base')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
@@ -95,7 +95,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.username}
|
||||
onChange={handleValueChange("username")}
|
||||
onChange={handleValueChange('username')}
|
||||
margin="normal"
|
||||
/>
|
||||
<PasswordValidator
|
||||
@@ -104,7 +104,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.password}
|
||||
onChange={handleValueChange("password")}
|
||||
onChange={handleValueChange('password')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
@@ -113,21 +113,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.client_id}
|
||||
onChange={handleValueChange("client_id")}
|
||||
onChange={handleValueChange('client_id')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:1",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:1',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Keep alive is required",
|
||||
"Must be a number",
|
||||
"Must be greater than 0",
|
||||
"Max value is 65535",
|
||||
'Keep alive is required',
|
||||
'Must be a number',
|
||||
'Must be greater than 0',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="keep_alive"
|
||||
label="Keep Alive (seconds)"
|
||||
@@ -135,7 +135,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.keep_alive}
|
||||
type="number"
|
||||
onChange={handleValueChange("keep_alive")}
|
||||
onChange={handleValueChange('keep_alive')}
|
||||
margin="normal"
|
||||
/>
|
||||
<SelectValidator
|
||||
@@ -144,7 +144,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
value={data.mqtt_qos}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("mqtt_qos")}
|
||||
onChange={handleValueChange('mqtt_qos')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>0 (default)</MenuItem>
|
||||
@@ -155,7 +155,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.clean_session}
|
||||
onChange={handleValueChange("clean_session")}
|
||||
onChange={handleValueChange('clean_session')}
|
||||
value="clean_session"
|
||||
/>
|
||||
}
|
||||
@@ -165,7 +165,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.mqtt_retain}
|
||||
onChange={handleValueChange("mqtt_retain")}
|
||||
onChange={handleValueChange('mqtt_retain')}
|
||||
value="mqtt_retain"
|
||||
/>
|
||||
}
|
||||
@@ -181,7 +181,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
value={data.nested_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("nested_format")}
|
||||
onChange={handleValueChange('nested_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>nested on a single topic</MenuItem>
|
||||
@@ -193,7 +193,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
value={data.dallas_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("dallas_format")}
|
||||
onChange={handleValueChange('dallas_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>by Sensor ID</MenuItem>
|
||||
@@ -205,7 +205,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
value={data.bool_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("bool_format")}
|
||||
onChange={handleValueChange('bool_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
||||
@@ -219,7 +219,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
value={data.subscribe_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("subscribe_format")}
|
||||
onChange={handleValueChange('subscribe_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>general device topic</MenuItem>
|
||||
@@ -230,7 +230,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.ha_enabled}
|
||||
onChange={handleValueChange("ha_enabled")}
|
||||
onChange={handleValueChange('ha_enabled')}
|
||||
value="ha_enabled"
|
||||
/>
|
||||
}
|
||||
@@ -243,7 +243,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
value={data.ha_climate_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("ha_climate_format")}
|
||||
onChange={handleValueChange('ha_climate_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>use Current temperature (default)</MenuItem>
|
||||
@@ -257,16 +257,16 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
</Typography>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Publish time is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or greater",
|
||||
"Max value is 65535",
|
||||
'Publish time is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or greater',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="publish_time_boiler"
|
||||
label="Boiler Publish Interval (seconds, 0=on change)"
|
||||
@@ -274,21 +274,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.publish_time_boiler}
|
||||
type="number"
|
||||
onChange={handleValueChange("publish_time_boiler")}
|
||||
onChange={handleValueChange('publish_time_boiler')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Publish time is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or greater",
|
||||
"Max value is 65535",
|
||||
'Publish time is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or greater',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="publish_time_thermostat"
|
||||
label="Thermostat Publish Interval (seconds, 0=on change)"
|
||||
@@ -296,21 +296,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.publish_time_thermostat}
|
||||
type="number"
|
||||
onChange={handleValueChange("publish_time_thermostat")}
|
||||
onChange={handleValueChange('publish_time_thermostat')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Publish time is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or greater",
|
||||
"Max value is 65535",
|
||||
'Publish time is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or greater',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="publish_time_solar"
|
||||
label="Solar Publish Interval (seconds, 0=on change)"
|
||||
@@ -318,21 +318,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.publish_time_solar}
|
||||
type="number"
|
||||
onChange={handleValueChange("publish_time_solar")}
|
||||
onChange={handleValueChange('publish_time_solar')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Publish time is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or greater",
|
||||
"Max value is 65535",
|
||||
'Publish time is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or greater',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="publish_time_mixer"
|
||||
label="Mixer Publish Interval (seconds, 0=on change)"
|
||||
@@ -340,21 +340,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.publish_time_mixer}
|
||||
type="number"
|
||||
onChange={handleValueChange("publish_time_mixer")}
|
||||
onChange={handleValueChange('publish_time_mixer')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Publish time is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or greater",
|
||||
"Max value is 65535",
|
||||
'Publish time is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or greater',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="publish_time_sensor"
|
||||
label="Sensors Publish Interval (seconds, 0=on change)"
|
||||
@@ -362,21 +362,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.publish_time_sensor}
|
||||
type="number"
|
||||
onChange={handleValueChange("publish_time_sensor")}
|
||||
onChange={handleValueChange('publish_time_sensor')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Publish time is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or greater",
|
||||
"Max value is 65535",
|
||||
'Publish time is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or greater',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="publish_time_other"
|
||||
label="All other Modules Publish Interval (seconds, 0=on change)"
|
||||
@@ -384,7 +384,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.publish_time_other}
|
||||
type="number"
|
||||
onChange={handleValueChange("publish_time_other")}
|
||||
onChange={handleValueChange('publish_time_other')}
|
||||
margin="normal"
|
||||
/>
|
||||
<FormActions>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { MqttStatus, MqttDisconnectReason } from "./types";
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { MqttStatus, MqttDisconnectReason } from './types';
|
||||
|
||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||
export const mqttStatusHighlight = (
|
||||
{ enabled, connected }: MqttStatus,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (!enabled) {
|
||||
return theme.palette.info.main;
|
||||
}
|
||||
@@ -9,48 +12,48 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
|
||||
return theme.palette.success.main;
|
||||
}
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
||||
if (!enabled) {
|
||||
return "Not enabled";
|
||||
return 'Not enabled';
|
||||
}
|
||||
if (connected) {
|
||||
return "Connected";
|
||||
}
|
||||
return "Disconnected";
|
||||
return 'Connected';
|
||||
}
|
||||
return 'Disconnected';
|
||||
};
|
||||
|
||||
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||
switch (disconnect_reason) {
|
||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||
return "TCP disconnected";
|
||||
return 'TCP disconnected';
|
||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
return "Unacceptable protocol version";
|
||||
return 'Unacceptable protocol version';
|
||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||
return "Client ID rejected";
|
||||
return 'Client ID rejected';
|
||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||
return "Server unavailable";
|
||||
return 'Server unavailable';
|
||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||
return "Malformed credentials";
|
||||
return 'Malformed credentials';
|
||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||
return "Not authorized";
|
||||
return 'Not authorized';
|
||||
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||
return "Device out of memory";
|
||||
return 'Device out of memory';
|
||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||
return "Server fingerprint invalid";
|
||||
return 'Server fingerprint invalid';
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
|
||||
export const mqttPublishHighlight = (
|
||||
{ mqtt_fails }: MqttStatus,
|
||||
theme: Theme
|
||||
) => {
|
||||
if (mqtt_fails === 0) return theme.palette.success.main;
|
||||
|
||||
if (mqtt_fails === 0)
|
||||
return theme.palette.success.main;
|
||||
|
||||
if (mqtt_fails < 10)
|
||||
return theme.palette.warning.main;
|
||||
if (mqtt_fails < 10) return theme.palette.warning.main;
|
||||
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { MQTT_STATUS_ENDPOINT } from '../api';
|
||||
|
||||
import MqttStatusForm from './MqttStatusForm';
|
||||
@@ -9,7 +14,6 @@ import { MqttStatus } from './types';
|
||||
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
||||
|
||||
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,10 +23,10 @@ class MqttStatusController extends Component<MqttStatusControllerProps> {
|
||||
<SectionContent title="MQTT Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <MqttStatusForm {...formProps} />}
|
||||
render={(formProps) => <MqttStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText
|
||||
} from '@material-ui/core';
|
||||
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import ReportIcon from '@material-ui/icons/Report';
|
||||
import SpeakerNotesOffIcon from "@material-ui/icons/SpeakerNotesOff";
|
||||
import SpeakerNotesOffIcon from '@material-ui/icons/SpeakerNotesOff';
|
||||
|
||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
||||
import { mqttStatusHighlight, mqttStatus, mqttPublishHighlight, disconnectReason } from './MqttStatus';
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
import {
|
||||
mqttStatusHighlight,
|
||||
mqttStatus,
|
||||
mqttPublishHighlight,
|
||||
disconnectReason
|
||||
} from './MqttStatus';
|
||||
import { MqttStatus } from './types';
|
||||
|
||||
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
||||
|
||||
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
|
||||
renderConnectionStatus() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
if (data.connected) {
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -50,7 +66,10 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
<ReportIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
|
||||
<ListItemText
|
||||
primary="Disconnect Reason"
|
||||
secondary={disconnectReason(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</Fragment>
|
||||
@@ -58,7 +77,7 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
}
|
||||
|
||||
createListItems() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
@@ -78,18 +97,20 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withTheme(MqttStatusForm);
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import NetworkStatusController from './NetworkStatusController';
|
||||
import NetworkSettingsController from './NetworkSettingsController';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
||||
import {
|
||||
NetworkConnectionContext,
|
||||
NetworkConnectionContextValue
|
||||
} from './NetworkConnectionContext';
|
||||
|
||||
import { WiFiNetwork } from './types';
|
||||
|
||||
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnectionContextValue> {
|
||||
|
||||
class NetworkConnection extends Component<
|
||||
NetworkConnectionProps,
|
||||
NetworkConnectionContextValue
|
||||
> {
|
||||
constructor(props: NetworkConnectionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -28,13 +37,13 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
||||
selectNetwork = (network: WiFiNetwork) => {
|
||||
this.setState({ selectedNetwork: network });
|
||||
this.props.history.push('/network/settings');
|
||||
}
|
||||
};
|
||||
|
||||
deselectNetwork = () => {
|
||||
this.setState({ selectedNetwork: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -43,20 +52,44 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
||||
return (
|
||||
<NetworkConnectionContext.Provider value={this.state}>
|
||||
<MenuAppBar sectionTitle="Network Connection">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="/network/status" label="Network Status" />
|
||||
<Tab value="/network/scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab value="/network/settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/network/scan"
|
||||
label="Scan WiFi Networks"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
<Tab
|
||||
value="/network/settings"
|
||||
label="Network Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/network/status" component={NetworkStatusController} />
|
||||
<AuthenticatedRoute exact path="/network/scan" component={WiFiNetworkScanner} />
|
||||
<AuthenticatedRoute exact path="/network/settings" component={NetworkSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/network/status"
|
||||
component={NetworkStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/network/scan"
|
||||
component={WiFiNetworkScanner}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/network/settings"
|
||||
component={NetworkSettingsController}
|
||||
/>
|
||||
<Redirect to="/network/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
</NetworkConnectionContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface NetworkConnectionContextValue {
|
||||
deselectNetwork: () => void;
|
||||
}
|
||||
|
||||
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue
|
||||
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue;
|
||||
export const NetworkConnectionContext = React.createContext(
|
||||
NetworkConnectionContextDefaultValue
|
||||
);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import NetworkSettingsForm from './NetworkSettingsForm';
|
||||
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
||||
import { NetworkSettings } from './types';
|
||||
@@ -8,7 +13,6 @@ import { NetworkSettings } from './types';
|
||||
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
||||
|
||||
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -18,12 +22,14 @@ class NetworkSettingsController extends Component<NetworkSettingsControllerProps
|
||||
<SectionContent title="Network Settings">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NetworkSettingsForm {...formProps} />}
|
||||
render={(formProps) => <NetworkSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(NETWORK_SETTINGS_ENDPOINT, NetworkSettingsController);
|
||||
export default restController(
|
||||
NETWORK_SETTINGS_ENDPOINT,
|
||||
NetworkSettingsController
|
||||
);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import { Checkbox, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction } from '@material-ui/core';
|
||||
import {
|
||||
Checkbox,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction
|
||||
} from '@material-ui/core';
|
||||
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
@@ -10,31 +17,42 @@ import LockOpenIcon from '@material-ui/icons/LockOpen';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
|
||||
import {
|
||||
RestFormProps,
|
||||
PasswordValidator,
|
||||
BlockFormControlLabel,
|
||||
FormActions,
|
||||
FormButton
|
||||
} from '../components';
|
||||
import { isIP, isHostname, optional } from '../validators';
|
||||
|
||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
||||
import {
|
||||
NetworkConnectionContext,
|
||||
NetworkConnectionContextValue
|
||||
} from './NetworkConnectionContext';
|
||||
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
||||
import { NetworkSettings } from './types';
|
||||
|
||||
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
||||
|
||||
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
|
||||
static contextType = NetworkConnectionContext;
|
||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||
|
||||
constructor(props: NetworkStatusFormProps, context: NetworkConnectionContextValue) {
|
||||
constructor(
|
||||
props: NetworkStatusFormProps,
|
||||
context: NetworkConnectionContextValue
|
||||
) {
|
||||
super(props);
|
||||
|
||||
const { selectedNetwork } = context;
|
||||
if (selectedNetwork) {
|
||||
const networkSettings: NetworkSettings = {
|
||||
ssid: selectedNetwork.ssid,
|
||||
password: "",
|
||||
password: '',
|
||||
hostname: props.data.hostname,
|
||||
static_ip_config: false,
|
||||
}
|
||||
static_ip_config: false
|
||||
};
|
||||
props.setData(networkSettings);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +66,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
deselectNetworkAndLoadData = () => {
|
||||
this.context.deselectNetwork();
|
||||
this.props.loadData();
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.context.deselectNetwork();
|
||||
@@ -59,27 +77,38 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
const { data, handleValueChange, saveData } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
||||
{
|
||||
selectedNetwork ?
|
||||
{selectedNetwork ? (
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
||||
{isNetworkOpen(selectedNetwork) ? (
|
||||
<LockOpenIcon />
|
||||
) : (
|
||||
<LockIcon />
|
||||
)}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={selectedNetwork.ssid}
|
||||
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
|
||||
secondary={
|
||||
'Security: ' +
|
||||
networkSecurityMode(selectedNetwork) +
|
||||
', Ch: ' +
|
||||
selectedNetwork.channel
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
||||
<IconButton
|
||||
aria-label="Manual Config"
|
||||
onClick={deselectNetwork}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</List>
|
||||
:
|
||||
) : (
|
||||
<TextValidator
|
||||
validators={['matchRegexp:^.{0,32}$']}
|
||||
errorMessages={['SSID must be 32 characters or less']}
|
||||
@@ -91,9 +120,8 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
onChange={handleValueChange('ssid')}
|
||||
margin="normal"
|
||||
/>
|
||||
}
|
||||
{
|
||||
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
|
||||
)}
|
||||
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||
<PasswordValidator
|
||||
validators={['matchRegexp:^.{0,64}$']}
|
||||
errorMessages={['Password must be 64 characters or less']}
|
||||
@@ -105,10 +133,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
onChange={handleValueChange('password')}
|
||||
margin="normal"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<TextValidator
|
||||
validators={['required', 'isHostname']}
|
||||
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
||||
errorMessages={['Hostname is required', 'Not a valid hostname']}
|
||||
name="hostname"
|
||||
label="Hostname"
|
||||
fullWidth
|
||||
@@ -122,13 +150,12 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
<Checkbox
|
||||
value="static_ip_config"
|
||||
checked={data.static_ip_config}
|
||||
onChange={handleValueChange("static_ip_config")}
|
||||
onChange={handleValueChange('static_ip_config')}
|
||||
/>
|
||||
}
|
||||
label="Static IP Config"
|
||||
/>
|
||||
{
|
||||
data.static_ip_config &&
|
||||
{data.static_ip_config && (
|
||||
<Fragment>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
@@ -154,7 +181,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isIP']}
|
||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
||||
errorMessages={[
|
||||
'Subnet mask is required',
|
||||
'Must be an IP address'
|
||||
]}
|
||||
name="subnet_mask"
|
||||
label="Subnet"
|
||||
fullWidth
|
||||
@@ -186,9 +216,14 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||
margin="normal"
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
)}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { NetworkStatus, NetworkConnectionStatus } from "./types";
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { NetworkStatus, NetworkConnectionStatus } from './types';
|
||||
|
||||
export const isConnected = ({ status }: NetworkStatus) => {
|
||||
return (
|
||||
@@ -36,22 +36,22 @@ export const networkStatusHighlight = (
|
||||
export const networkStatus = ({ status }: NetworkStatus) => {
|
||||
switch (status) {
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||
return "Inactive";
|
||||
return 'Inactive';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||
return "Idle";
|
||||
return 'Idle';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||
return "No SSID Available";
|
||||
return 'No SSID Available';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||
return "Connected (WiFi)";
|
||||
return 'Connected (WiFi)';
|
||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||
return "Connected (Ethernet)";
|
||||
return 'Connected (Ethernet)';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||
return "Connection Failed";
|
||||
return 'Connection Failed';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||
return "Connection Lost";
|
||||
return 'Connection Lost';
|
||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||
return "Disconnected";
|
||||
return 'Disconnected';
|
||||
default:
|
||||
return "Unknown";
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import NetworkStatusForm from './NetworkStatusForm';
|
||||
import { NETWORK_STATUS_ENDPOINT } from '../api';
|
||||
import { NetworkStatus } from './types';
|
||||
@@ -8,7 +13,6 @@ import { NetworkStatus } from './types';
|
||||
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
||||
|
||||
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -18,12 +22,11 @@ class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
||||
<SectionContent title="Network Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NetworkStatusForm {...formProps} />}
|
||||
render={(formProps) => <NetworkStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from "@material-ui/core/styles";
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from "@material-ui/core";
|
||||
ListItemText
|
||||
} from '@material-ui/core';
|
||||
|
||||
import DNSIcon from "@material-ui/icons/Dns";
|
||||
import WifiIcon from "@material-ui/icons/Wifi";
|
||||
import RouterIcon from "@material-ui/icons/Router";
|
||||
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
|
||||
import SettingsInputAntennaIcon from "@material-ui/icons/SettingsInputAntenna";
|
||||
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
|
||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import DNSIcon from '@material-ui/icons/Dns';
|
||||
import WifiIcon from '@material-ui/icons/Wifi';
|
||||
import RouterIcon from '@material-ui/icons/Router';
|
||||
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
|
||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar,
|
||||
} from "../components";
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
import {
|
||||
networkStatus,
|
||||
networkStatusHighlight,
|
||||
isConnected,
|
||||
isWiFi,
|
||||
isEthernet,
|
||||
} from "./NetworkStatus";
|
||||
import { NetworkStatus } from "./types";
|
||||
isEthernet
|
||||
} from './NetworkStatus';
|
||||
import { NetworkStatus } from './types';
|
||||
|
||||
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
||||
|
||||
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
dnsServers(status: NetworkStatus) {
|
||||
if (!status.dns_ip_1) {
|
||||
return "none";
|
||||
return 'none';
|
||||
}
|
||||
return status.dns_ip_1 + (status.dns_ip_2 ? "," + status.dns_ip_2 : "");
|
||||
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
|
||||
}
|
||||
|
||||
createListItems() {
|
||||
@@ -110,7 +110,7 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Gateway IP"
|
||||
secondary={data.gateway_ip || "none"}
|
||||
secondary={data.gateway_ip || 'none'}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
|
||||
import { createStyles, WithStyles, Theme, withStyles, Typography, LinearProgress } from '@material-ui/core';
|
||||
import {
|
||||
createStyles,
|
||||
WithStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
Typography,
|
||||
LinearProgress
|
||||
} from '@material-ui/core';
|
||||
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
|
||||
|
||||
import { FormActions, FormButton, SectionContent } from '../components';
|
||||
@@ -11,9 +18,9 @@ import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
|
||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||
import { WiFiNetworkList, WiFiNetwork } from './types';
|
||||
|
||||
const NUM_POLLS = 10
|
||||
const POLLING_FREQUENCY = 500
|
||||
const RETRY_EXCEPTION_TYPE = "retry"
|
||||
const NUM_POLLS = 10;
|
||||
const POLLING_FREQUENCY = 500;
|
||||
const RETRY_EXCEPTION_TYPE = 'retry';
|
||||
|
||||
interface WiFiNetworkScannerState {
|
||||
scanningForNetworks: boolean;
|
||||
@@ -21,28 +28,31 @@ interface WiFiNetworkScannerState {
|
||||
networkList?: WiFiNetworkList;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
scanningSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
margin: theme.spacing(0.5)
|
||||
},
|
||||
scanningSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
textAlign: 'center'
|
||||
},
|
||||
scanningProgress: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
||||
|
||||
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
|
||||
|
||||
pollCount: number = 0;
|
||||
class WiFiNetworkScanner extends Component<
|
||||
WiFiNetworkScannerProps,
|
||||
WiFiNetworkScannerState
|
||||
> {
|
||||
pollCount = 0;
|
||||
|
||||
state: WiFiNetworkScannerState = {
|
||||
scanningForNetworks: false,
|
||||
scanningForNetworks: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -54,22 +64,35 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
if (!scanningForNetworks) {
|
||||
this.scanNetworks();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scanNetworks() {
|
||||
this.pollCount = 0;
|
||||
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
|
||||
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
||||
this.setState({
|
||||
scanningForNetworks: true,
|
||||
networkList: undefined,
|
||||
errorMessage: undefined
|
||||
});
|
||||
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT)
|
||||
.then((response) => {
|
||||
if (response.status === 202) {
|
||||
this.schedulePollTimeout();
|
||||
return;
|
||||
}
|
||||
throw Error("Scanning for networks returned unexpected response code: " + response.status);
|
||||
}).catch(error => {
|
||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
||||
variant: 'error',
|
||||
throw Error(
|
||||
'Scanning for networks returned unexpected response code: ' +
|
||||
response.status
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({
|
||||
scanningForNetworks: false,
|
||||
networkList: undefined,
|
||||
errorMessage: error.message
|
||||
});
|
||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,21 +103,20 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
retryError() {
|
||||
return {
|
||||
name: RETRY_EXCEPTION_TYPE,
|
||||
message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
|
||||
message:
|
||||
'Network list not ready, will retry in ' + POLLING_FREQUENCY + 'ms.'
|
||||
};
|
||||
}
|
||||
|
||||
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
|
||||
if (network1.rssi < network2.rssi)
|
||||
return 1;
|
||||
if (network1.rssi > network2.rssi)
|
||||
return -1;
|
||||
if (network1.rssi < network2.rssi) return 1;
|
||||
if (network1.rssi > network2.rssi) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
pollNetworkList = () => {
|
||||
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
@@ -103,24 +125,34 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
this.schedulePollTimeout();
|
||||
throw this.retryError();
|
||||
} else {
|
||||
throw Error("Device did not return network list in timely manner.");
|
||||
throw Error('Device did not return network list in timely manner.');
|
||||
}
|
||||
}
|
||||
throw Error("Device returned unexpected response code: " + response.status);
|
||||
throw Error(
|
||||
'Device returned unexpected response code: ' + response.status
|
||||
);
|
||||
})
|
||||
.then(json => {
|
||||
json.networks.sort(this.compareNetworks)
|
||||
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
|
||||
.then((json) => {
|
||||
json.networks.sort(this.compareNetworks);
|
||||
this.setState({
|
||||
scanningForNetworks: false,
|
||||
networkList: json,
|
||||
errorMessage: undefined
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
||||
variant: 'error',
|
||||
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
||||
}
|
||||
this.setState({
|
||||
scanningForNetworks: false,
|
||||
networkList: undefined,
|
||||
errorMessage: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
renderNetworkScanner() {
|
||||
const { classes } = this.props;
|
||||
@@ -144,9 +176,7 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<WiFiNetworkSelector networkList={networkList} />
|
||||
);
|
||||
return <WiFiNetworkSelector networkList={networkList} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -155,14 +185,19 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
||||
<SectionContent title="Network Scanner">
|
||||
{this.renderNetworkScanner()}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<PermScanWifiIcon />} variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}>
|
||||
<FormButton
|
||||
startIcon={<PermScanWifiIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.requestNetworkScan}
|
||||
disabled={scanningForNetworks}
|
||||
>
|
||||
Scan again…
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Avatar, Badge } from '@material-ui/core';
|
||||
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListItemAvatar
|
||||
} from '@material-ui/core';
|
||||
|
||||
import WifiIcon from '@material-ui/icons/Wifi';
|
||||
import LockIcon from '@material-ui/icons/Lock';
|
||||
@@ -16,13 +22,16 @@ interface WiFiNetworkSelectorProps {
|
||||
}
|
||||
|
||||
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
||||
|
||||
static contextType = NetworkConnectionContext;
|
||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||
|
||||
renderNetwork = (network: WiFiNetwork) => {
|
||||
return (
|
||||
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
|
||||
<ListItem
|
||||
key={network.bssid}
|
||||
button
|
||||
onClick={() => this.context.selectNetwork(network)}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
||||
@@ -30,25 +39,27 @@ class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={network.ssid}
|
||||
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
|
||||
secondary={
|
||||
'Security: ' +
|
||||
networkSecurityMode(network) +
|
||||
', Ch: ' +
|
||||
network.channel
|
||||
}
|
||||
/>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent={network.rssi + "db"}>
|
||||
<Badge badgeContent={network.rssi + 'db'}>
|
||||
<WifiIcon />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<List>
|
||||
{this.props.networkList.networks.map(this.renderNetwork)}
|
||||
</List>
|
||||
<List>{this.props.networkList.networks.map(this.renderNetwork)}</List>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default WiFiNetworkSelector;
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { WiFiNetwork, WiFiEncryptionType } from "./types";
|
||||
import { WiFiNetwork, WiFiEncryptionType } from './types';
|
||||
|
||||
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) => encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
||||
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
|
||||
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
||||
|
||||
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
||||
switch (encryption_type) {
|
||||
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
||||
return "WEP";
|
||||
return 'WEP';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
||||
return "WPA";
|
||||
return 'WPA';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
||||
return "WPA2";
|
||||
return 'WPA2';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
||||
return "WPA/WPA2";
|
||||
return 'WPA/WPA2';
|
||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
||||
return "WPA2 Enterprise";
|
||||
return 'WPA2 Enterprise';
|
||||
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
||||
return "None";
|
||||
return 'None';
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { NTP_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import NTPSettingsForm from './NTPSettingsForm';
|
||||
@@ -9,7 +14,6 @@ import { NTPSettings } from './types';
|
||||
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
||||
|
||||
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||
<SectionContent title="NTP Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NTPSettingsForm {...formProps} />}
|
||||
render={(formProps) => <NTPSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import React from 'react';
|
||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
||||
import {
|
||||
TextValidator,
|
||||
ValidatorForm,
|
||||
SelectValidator
|
||||
} from 'react-material-ui-form-validator';
|
||||
|
||||
import { Checkbox, MenuItem } from '@material-ui/core';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
BlockFormControlLabel
|
||||
} from '../components';
|
||||
import { isIP, isHostname, or } from '../validators';
|
||||
|
||||
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
||||
@@ -13,7 +22,6 @@ import { NTPSettings } from './types';
|
||||
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
||||
|
||||
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||
}
|
||||
@@ -25,7 +33,7 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
tz_label: event.target.value,
|
||||
tz_format: TIME_ZONES[event.target.value]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, handleValueChange, saveData } = this.props;
|
||||
@@ -43,7 +51,10 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isIPOrHostname']}
|
||||
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
|
||||
errorMessages={[
|
||||
'Server is required',
|
||||
'Not a valid IP address or hostname'
|
||||
]}
|
||||
name="server"
|
||||
label="Server"
|
||||
fullWidth
|
||||
@@ -68,7 +79,12 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||
{timeZoneSelectItems()}
|
||||
</SelectValidator>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Theme } from "@material-ui/core";
|
||||
import { NTPStatus, NTPSyncStatus } from "./types";
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { NTPStatus, NTPSyncStatus } from './types';
|
||||
|
||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
||||
export const isNtpActive = ({ status }: NTPStatus) =>
|
||||
status === NTPSyncStatus.NTP_ACTIVE;
|
||||
|
||||
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
@@ -12,15 +13,15 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||
default:
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ntpStatus = ({ status }: NTPStatus) => {
|
||||
switch (status) {
|
||||
case NTPSyncStatus.NTP_INACTIVE:
|
||||
return "Inactive";
|
||||
return 'Inactive';
|
||||
case NTPSyncStatus.NTP_ACTIVE:
|
||||
return "Active";
|
||||
return 'Active';
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { NTP_STATUS_ENDPOINT } from '../api';
|
||||
|
||||
import NTPStatusForm from './NTPStatusForm';
|
||||
@@ -9,7 +14,6 @@ import { NTPStatus } from './types';
|
||||
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
||||
|
||||
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,11 @@ class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||
<SectionContent title="NTP Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <NTPStatusForm {...formProps} />}
|
||||
render={(formProps) => <NTPStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Button
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
TextField
|
||||
} from '@material-ui/core';
|
||||
|
||||
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||
@@ -13,12 +28,22 @@ import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
||||
import { formatDuration, formatDateTime, formatLocalDateTime } from './TimeFormat';
|
||||
import {
|
||||
formatDuration,
|
||||
formatDateTime,
|
||||
formatLocalDateTime
|
||||
} from './TimeFormat';
|
||||
import { NTPStatus, Time } from './types';
|
||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
import { TIME_ENDPOINT } from '../api';
|
||||
|
||||
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
|
||||
type NTPStatusFormProps = RestFormProps<NTPStatus> &
|
||||
WithTheme &
|
||||
AuthenticatedContextProps;
|
||||
|
||||
interface NTPStatusFormState {
|
||||
settingTime: boolean;
|
||||
@@ -27,7 +52,6 @@ interface NTPStatusFormState {
|
||||
}
|
||||
|
||||
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
|
||||
constructor(props: NTPStatusFormProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -41,20 +65,20 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
this.setState({
|
||||
localTime: event.target.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
openSetTime = () => {
|
||||
this.setState({
|
||||
localTime: formatLocalDateTime(new Date()),
|
||||
settingTime: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
closeSetTime = () => {
|
||||
this.setState({
|
||||
settingTime: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
createTime = (): Time => ({
|
||||
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
||||
@@ -62,27 +86,34 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
|
||||
configureTime = () => {
|
||||
this.setState({ processing: true });
|
||||
redirectingAuthorizedFetch(TIME_ENDPOINT,
|
||||
{
|
||||
redirectingAuthorizedFetch(TIME_ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.createTime()),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
|
||||
this.setState({ processing: false, settingTime: false }, this.props.loadData);
|
||||
this.props.enqueueSnackbar('Time set successfully', {
|
||||
variant: 'success'
|
||||
});
|
||||
this.setState(
|
||||
{ processing: false, settingTime: false },
|
||||
this.props.loadData
|
||||
);
|
||||
} else {
|
||||
throw Error("Error setting time, status code: " + response.status);
|
||||
throw Error('Error setting time, status code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || 'Problem setting the time',
|
||||
{ variant: 'error' }
|
||||
);
|
||||
this.setState({ processing: false, settingTime: false });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderSetTimeDialog() {
|
||||
return (
|
||||
@@ -94,7 +125,9 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
>
|
||||
<DialogTitle>Set Time</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
|
||||
<Box mb={2}>
|
||||
Enter local date and time below to set the device's time.
|
||||
</Box>
|
||||
<TextField
|
||||
label="Local Time"
|
||||
type="datetime-local"
|
||||
@@ -104,24 +137,35 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={this.closeSetTime}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button startIcon={<AccessTimeIcon />} variant="contained" onClick={this.configureTime} disabled={this.state.processing} color="primary" autoFocus>
|
||||
<Button
|
||||
startIcon={<AccessTimeIcon />}
|
||||
variant="contained"
|
||||
onClick={this.configureTime}
|
||||
disabled={this.state.processing}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Set Time
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, theme } = this.props
|
||||
const { data, theme } = this.props;
|
||||
const me = this.props.authenticatedContext.me;
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -154,7 +198,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
<AccessTimeIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
|
||||
<ListItemText
|
||||
primary="Local Time"
|
||||
secondary={formatDateTime(data.local_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -163,7 +210,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
<SwapVerticalCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
|
||||
<ListItemText
|
||||
primary="UTC Time"
|
||||
secondary={formatDateTime(data.utc_time)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem>
|
||||
@@ -172,19 +222,32 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||
<AvTimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Uptime" secondary={formatDuration(data.uptime)} />
|
||||
<ListItemText
|
||||
primary="Uptime"
|
||||
secondary={formatDuration(data.uptime)}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
</List>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} padding={1}>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</Box>
|
||||
{me.admin && !isNtpActive(data) && (
|
||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||
<Button onClick={this.openSetTime} variant="contained" color="primary" startIcon={<AccessTimeIcon />}>
|
||||
<Button
|
||||
onClick={this.openSetTime}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AccessTimeIcon />}
|
||||
>
|
||||
Set Time
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import NTPStatusController from './NTPStatusController';
|
||||
@@ -12,8 +16,7 @@ import NTPSettingsController from './NTPSettingsController';
|
||||
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class NetworkTime extends Component<NetworkTimeProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
@@ -21,19 +24,34 @@ class NetworkTime extends Component<NetworkTimeProps> {
|
||||
const { authenticatedContext } = this.props;
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Network Time">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="/ntp/status" label="NTP Status" />
|
||||
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
|
||||
<Tab
|
||||
value="/ntp/settings"
|
||||
label="NTP Settings"
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/ntp/status" component={NTPStatusController} />
|
||||
<AuthenticatedRoute exact path="/ntp/settings" component={NTPSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ntp/status"
|
||||
component={NTPStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ntp/settings"
|
||||
component={NTPSettingsController}
|
||||
/>
|
||||
<Redirect to="/ntp/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(NetworkTime)
|
||||
export default withAuthenticatedContext(NetworkTime);
|
||||
|
||||
@@ -1,479 +1,480 @@
|
||||
import React from 'react';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
type TimeZones = {
|
||||
[name: string]: string
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
export const TIME_ZONES: TimeZones = {
|
||||
"Africa/Abidjan": "GMT0",
|
||||
"Africa/Accra": "GMT0",
|
||||
"Africa/Addis_Ababa": "EAT-3",
|
||||
"Africa/Algiers": "CET-1",
|
||||
"Africa/Asmara": "EAT-3",
|
||||
"Africa/Bamako": "GMT0",
|
||||
"Africa/Bangui": "WAT-1",
|
||||
"Africa/Banjul": "GMT0",
|
||||
"Africa/Bissau": "GMT0",
|
||||
"Africa/Blantyre": "CAT-2",
|
||||
"Africa/Brazzaville": "WAT-1",
|
||||
"Africa/Bujumbura": "CAT-2",
|
||||
"Africa/Cairo": "EET-2",
|
||||
"Africa/Casablanca": "UNK-1",
|
||||
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Africa/Conakry": "GMT0",
|
||||
"Africa/Dakar": "GMT0",
|
||||
"Africa/Dar_es_Salaam": "EAT-3",
|
||||
"Africa/Djibouti": "EAT-3",
|
||||
"Africa/Douala": "WAT-1",
|
||||
"Africa/El_Aaiun": "UNK-1",
|
||||
"Africa/Freetown": "GMT0",
|
||||
"Africa/Gaborone": "CAT-2",
|
||||
"Africa/Harare": "CAT-2",
|
||||
"Africa/Johannesburg": "SAST-2",
|
||||
"Africa/Juba": "EAT-3",
|
||||
"Africa/Kampala": "EAT-3",
|
||||
"Africa/Khartoum": "CAT-2",
|
||||
"Africa/Kigali": "CAT-2",
|
||||
"Africa/Kinshasa": "WAT-1",
|
||||
"Africa/Lagos": "WAT-1",
|
||||
"Africa/Libreville": "WAT-1",
|
||||
"Africa/Lome": "GMT0",
|
||||
"Africa/Luanda": "WAT-1",
|
||||
"Africa/Lubumbashi": "CAT-2",
|
||||
"Africa/Lusaka": "CAT-2",
|
||||
"Africa/Malabo": "WAT-1",
|
||||
"Africa/Maputo": "CAT-2",
|
||||
"Africa/Maseru": "SAST-2",
|
||||
"Africa/Mbabane": "SAST-2",
|
||||
"Africa/Mogadishu": "EAT-3",
|
||||
"Africa/Monrovia": "GMT0",
|
||||
"Africa/Nairobi": "EAT-3",
|
||||
"Africa/Ndjamena": "WAT-1",
|
||||
"Africa/Niamey": "WAT-1",
|
||||
"Africa/Nouakchott": "GMT0",
|
||||
"Africa/Ouagadougou": "GMT0",
|
||||
"Africa/Porto-Novo": "WAT-1",
|
||||
"Africa/Sao_Tome": "GMT0",
|
||||
"Africa/Tripoli": "EET-2",
|
||||
"Africa/Tunis": "CET-1",
|
||||
"Africa/Windhoek": "CAT-2",
|
||||
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
|
||||
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Anguilla": "AST4",
|
||||
"America/Antigua": "AST4",
|
||||
"America/Araguaina": "UNK3",
|
||||
"America/Argentina/Buenos_Aires": "UNK3",
|
||||
"America/Argentina/Catamarca": "UNK3",
|
||||
"America/Argentina/Cordoba": "UNK3",
|
||||
"America/Argentina/Jujuy": "UNK3",
|
||||
"America/Argentina/La_Rioja": "UNK3",
|
||||
"America/Argentina/Mendoza": "UNK3",
|
||||
"America/Argentina/Rio_Gallegos": "UNK3",
|
||||
"America/Argentina/Salta": "UNK3",
|
||||
"America/Argentina/San_Juan": "UNK3",
|
||||
"America/Argentina/San_Luis": "UNK3",
|
||||
"America/Argentina/Tucuman": "UNK3",
|
||||
"America/Argentina/Ushuaia": "UNK3",
|
||||
"America/Aruba": "AST4",
|
||||
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
|
||||
"America/Atikokan": "EST5",
|
||||
"America/Bahia": "UNK3",
|
||||
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Barbados": "AST4",
|
||||
"America/Belem": "UNK3",
|
||||
"America/Belize": "CST6",
|
||||
"America/Blanc-Sablon": "AST4",
|
||||
"America/Boa_Vista": "UNK4",
|
||||
"America/Bogota": "UNK5",
|
||||
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Campo_Grande": "UNK4",
|
||||
"America/Cancun": "EST5",
|
||||
"America/Caracas": "UNK4",
|
||||
"America/Cayenne": "UNK3",
|
||||
"America/Cayman": "EST5",
|
||||
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Costa_Rica": "CST6",
|
||||
"America/Creston": "MST7",
|
||||
"America/Cuiaba": "UNK4",
|
||||
"America/Curacao": "AST4",
|
||||
"America/Danmarkshavn": "GMT0",
|
||||
"America/Dawson": "MST7",
|
||||
"America/Dawson_Creek": "MST7",
|
||||
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Dominica": "AST4",
|
||||
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Eirunepe": "UNK5",
|
||||
"America/El_Salvador": "CST6",
|
||||
"America/Fort_Nelson": "MST7",
|
||||
"America/Fortaleza": "UNK3",
|
||||
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
|
||||
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Grenada": "AST4",
|
||||
"America/Guadeloupe": "AST4",
|
||||
"America/Guatemala": "CST6",
|
||||
"America/Guayaquil": "UNK5",
|
||||
"America/Guyana": "UNK4",
|
||||
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
|
||||
"America/Hermosillo": "MST7",
|
||||
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Jamaica": "EST5",
|
||||
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kralendijk": "AST4",
|
||||
"America/La_Paz": "UNK4",
|
||||
"America/Lima": "UNK5",
|
||||
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Lower_Princes": "AST4",
|
||||
"America/Maceio": "UNK3",
|
||||
"America/Managua": "CST6",
|
||||
"America/Manaus": "UNK4",
|
||||
"America/Marigot": "AST4",
|
||||
"America/Martinique": "AST4",
|
||||
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
|
||||
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Montevideo": "UNK3",
|
||||
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Montserrat": "AST4",
|
||||
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Noronha": "UNK2",
|
||||
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Panama": "EST5",
|
||||
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Paramaribo": "UNK3",
|
||||
"America/Phoenix": "MST7",
|
||||
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Port_of_Spain": "AST4",
|
||||
"America/Porto_Velho": "UNK4",
|
||||
"America/Puerto_Rico": "AST4",
|
||||
"America/Punta_Arenas": "UNK3",
|
||||
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Recife": "UNK3",
|
||||
"America/Regina": "CST6",
|
||||
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rio_Branco": "UNK5",
|
||||
"America/Santarem": "UNK3",
|
||||
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
|
||||
"America/Santo_Domingo": "AST4",
|
||||
"America/Sao_Paulo": "UNK3",
|
||||
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/St_Barthelemy": "AST4",
|
||||
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
|
||||
"America/St_Kitts": "AST4",
|
||||
"America/St_Lucia": "AST4",
|
||||
"America/St_Thomas": "AST4",
|
||||
"America/St_Vincent": "AST4",
|
||||
"America/Swift_Current": "CST6",
|
||||
"America/Tegucigalpa": "CST6",
|
||||
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tortola": "AST4",
|
||||
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Whitehorse": "MST7",
|
||||
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"Antarctica/Casey": "UNK-8",
|
||||
"Antarctica/Davis": "UNK-7",
|
||||
"Antarctica/DumontDUrville": "UNK-10",
|
||||
"Antarctica/Macquarie": "UNK-11",
|
||||
"Antarctica/Mawson": "UNK-5",
|
||||
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Antarctica/Palmer": "UNK3",
|
||||
"Antarctica/Rothera": "UNK3",
|
||||
"Antarctica/Syowa": "UNK-3",
|
||||
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
|
||||
"Antarctica/Vostok": "UNK-6",
|
||||
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Asia/Aden": "UNK-3",
|
||||
"Asia/Almaty": "UNK-6",
|
||||
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
|
||||
"Asia/Anadyr": "UNK-12",
|
||||
"Asia/Aqtau": "UNK-5",
|
||||
"Asia/Aqtobe": "UNK-5",
|
||||
"Asia/Ashgabat": "UNK-5",
|
||||
"Asia/Atyrau": "UNK-5",
|
||||
"Asia/Baghdad": "UNK-3",
|
||||
"Asia/Bahrain": "UNK-3",
|
||||
"Asia/Baku": "UNK-4",
|
||||
"Asia/Bangkok": "UNK-7",
|
||||
"Asia/Barnaul": "UNK-7",
|
||||
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
|
||||
"Asia/Bishkek": "UNK-6",
|
||||
"Asia/Brunei": "UNK-8",
|
||||
"Asia/Chita": "UNK-9",
|
||||
"Asia/Choibalsan": "UNK-8",
|
||||
"Asia/Colombo": "UNK-5:30",
|
||||
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
|
||||
"Asia/Dhaka": "UNK-6",
|
||||
"Asia/Dili": "UNK-9",
|
||||
"Asia/Dubai": "UNK-4",
|
||||
"Asia/Dushanbe": "UNK-5",
|
||||
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Ho_Chi_Minh": "UNK-7",
|
||||
"Asia/Hong_Kong": "HKT-8",
|
||||
"Asia/Hovd": "UNK-7",
|
||||
"Asia/Irkutsk": "UNK-8",
|
||||
"Asia/Jakarta": "WIB-7",
|
||||
"Asia/Jayapura": "WIT-9",
|
||||
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
|
||||
"Asia/Kabul": "UNK-4:30",
|
||||
"Asia/Kamchatka": "UNK-12",
|
||||
"Asia/Karachi": "PKT-5",
|
||||
"Asia/Kathmandu": "UNK-5:45",
|
||||
"Asia/Khandyga": "UNK-9",
|
||||
"Asia/Kolkata": "IST-5:30",
|
||||
"Asia/Krasnoyarsk": "UNK-7",
|
||||
"Asia/Kuala_Lumpur": "UNK-8",
|
||||
"Asia/Kuching": "UNK-8",
|
||||
"Asia/Kuwait": "UNK-3",
|
||||
"Asia/Macau": "CST-8",
|
||||
"Asia/Magadan": "UNK-11",
|
||||
"Asia/Makassar": "WITA-8",
|
||||
"Asia/Manila": "PST-8",
|
||||
"Asia/Muscat": "UNK-4",
|
||||
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Novokuznetsk": "UNK-7",
|
||||
"Asia/Novosibirsk": "UNK-7",
|
||||
"Asia/Omsk": "UNK-6",
|
||||
"Asia/Oral": "UNK-5",
|
||||
"Asia/Phnom_Penh": "UNK-7",
|
||||
"Asia/Pontianak": "WIB-7",
|
||||
"Asia/Pyongyang": "KST-9",
|
||||
"Asia/Qatar": "UNK-3",
|
||||
"Asia/Qyzylorda": "UNK-5",
|
||||
"Asia/Riyadh": "UNK-3",
|
||||
"Asia/Sakhalin": "UNK-11",
|
||||
"Asia/Samarkand": "UNK-5",
|
||||
"Asia/Seoul": "KST-9",
|
||||
"Asia/Shanghai": "CST-8",
|
||||
"Asia/Singapore": "UNK-8",
|
||||
"Asia/Srednekolymsk": "UNK-11",
|
||||
"Asia/Taipei": "CST-8",
|
||||
"Asia/Tashkent": "UNK-5",
|
||||
"Asia/Tbilisi": "UNK-4",
|
||||
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
|
||||
"Asia/Thimphu": "UNK-6",
|
||||
"Asia/Tokyo": "JST-9",
|
||||
"Asia/Tomsk": "UNK-7",
|
||||
"Asia/Ulaanbaatar": "UNK-8",
|
||||
"Asia/Urumqi": "UNK-6",
|
||||
"Asia/Ust-Nera": "UNK-10",
|
||||
"Asia/Vientiane": "UNK-7",
|
||||
"Asia/Vladivostok": "UNK-10",
|
||||
"Asia/Yakutsk": "UNK-9",
|
||||
"Asia/Yangon": "UNK-6:30",
|
||||
"Asia/Yekaterinburg": "UNK-5",
|
||||
"Asia/Yerevan": "UNK-4",
|
||||
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Cape_Verde": "UNK1",
|
||||
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Reykjavik": "GMT0",
|
||||
"Atlantic/South_Georgia": "UNK2",
|
||||
"Atlantic/St_Helena": "GMT0",
|
||||
"Atlantic/Stanley": "UNK3",
|
||||
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Brisbane": "AEST-10",
|
||||
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Darwin": "ACST-9:30",
|
||||
"Australia/Eucla": "UNK-8:45",
|
||||
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Lindeman": "AEST-10",
|
||||
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
|
||||
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Perth": "AWST-8",
|
||||
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Etc/GMT": "GMT0",
|
||||
"Etc/GMT+0": "GMT0",
|
||||
"Etc/GMT+1": "UNK1",
|
||||
"Etc/GMT+10": "UNK10",
|
||||
"Etc/GMT+11": "UNK11",
|
||||
"Etc/GMT+12": "UNK12",
|
||||
"Etc/GMT+2": "UNK2",
|
||||
"Etc/GMT+3": "UNK3",
|
||||
"Etc/GMT+4": "UNK4",
|
||||
"Etc/GMT+5": "UNK5",
|
||||
"Etc/GMT+6": "UNK6",
|
||||
"Etc/GMT+7": "UNK7",
|
||||
"Etc/GMT+8": "UNK8",
|
||||
"Etc/GMT+9": "UNK9",
|
||||
"Etc/GMT-0": "GMT0",
|
||||
"Etc/GMT-1": "UNK-1",
|
||||
"Etc/GMT-10": "UNK-10",
|
||||
"Etc/GMT-11": "UNK-11",
|
||||
"Etc/GMT-12": "UNK-12",
|
||||
"Etc/GMT-13": "UNK-13",
|
||||
"Etc/GMT-14": "UNK-14",
|
||||
"Etc/GMT-2": "UNK-2",
|
||||
"Etc/GMT-3": "UNK-3",
|
||||
"Etc/GMT-4": "UNK-4",
|
||||
"Etc/GMT-5": "UNK-5",
|
||||
"Etc/GMT-6": "UNK-6",
|
||||
"Etc/GMT-7": "UNK-7",
|
||||
"Etc/GMT-8": "UNK-8",
|
||||
"Etc/GMT-9": "UNK-9",
|
||||
"Etc/GMT0": "GMT0",
|
||||
"Etc/Greenwich": "GMT0",
|
||||
"Etc/UCT": "UTC0",
|
||||
"Etc/UTC": "UTC0",
|
||||
"Etc/Universal": "UTC0",
|
||||
"Etc/Zulu": "UTC0",
|
||||
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Astrakhan": "UNK-4",
|
||||
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
|
||||
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Istanbul": "UNK-3",
|
||||
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Kaliningrad": "EET-2",
|
||||
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Kirov": "UNK-3",
|
||||
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Minsk": "UNK-3",
|
||||
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Moscow": "MSK-3",
|
||||
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Samara": "UNK-4",
|
||||
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Saratov": "UNK-4",
|
||||
"Europe/Simferopol": "MSK-3",
|
||||
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Ulyanovsk": "UNK-4",
|
||||
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Volgograd": "UNK-4",
|
||||
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Indian/Antananarivo": "EAT-3",
|
||||
"Indian/Chagos": "UNK-6",
|
||||
"Indian/Christmas": "UNK-7",
|
||||
"Indian/Cocos": "UNK-6:30",
|
||||
"Indian/Comoro": "EAT-3",
|
||||
"Indian/Kerguelen": "UNK-5",
|
||||
"Indian/Mahe": "UNK-4",
|
||||
"Indian/Maldives": "UNK-5",
|
||||
"Indian/Mauritius": "UNK-4",
|
||||
"Indian/Mayotte": "EAT-3",
|
||||
"Indian/Reunion": "UNK-4",
|
||||
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
|
||||
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Pacific/Bougainville": "UNK-11",
|
||||
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
|
||||
"Pacific/Chuuk": "UNK-10",
|
||||
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
|
||||
"Pacific/Efate": "UNK-11",
|
||||
"Pacific/Enderbury": "UNK-13",
|
||||
"Pacific/Fakaofo": "UNK-13",
|
||||
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
|
||||
"Pacific/Funafuti": "UNK-12",
|
||||
"Pacific/Galapagos": "UNK6",
|
||||
"Pacific/Gambier": "UNK9",
|
||||
"Pacific/Guadalcanal": "UNK-11",
|
||||
"Pacific/Guam": "ChST-10",
|
||||
"Pacific/Honolulu": "HST10",
|
||||
"Pacific/Kiritimati": "UNK-14",
|
||||
"Pacific/Kosrae": "UNK-11",
|
||||
"Pacific/Kwajalein": "UNK-12",
|
||||
"Pacific/Majuro": "UNK-12",
|
||||
"Pacific/Marquesas": "UNK9:30",
|
||||
"Pacific/Midway": "SST11",
|
||||
"Pacific/Nauru": "UNK-12",
|
||||
"Pacific/Niue": "UNK11",
|
||||
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
|
||||
"Pacific/Noumea": "UNK-11",
|
||||
"Pacific/Pago_Pago": "SST11",
|
||||
"Pacific/Palau": "UNK-9",
|
||||
"Pacific/Pitcairn": "UNK8",
|
||||
"Pacific/Pohnpei": "UNK-11",
|
||||
"Pacific/Port_Moresby": "UNK-10",
|
||||
"Pacific/Rarotonga": "UNK10",
|
||||
"Pacific/Saipan": "ChST-10",
|
||||
"Pacific/Tahiti": "UNK10",
|
||||
"Pacific/Tarawa": "UNK-12",
|
||||
"Pacific/Tongatapu": "UNK-13",
|
||||
"Pacific/Wake": "UNK-12",
|
||||
"Pacific/Wallis": "UNK-12"
|
||||
}
|
||||
'Africa/Abidjan': 'GMT0',
|
||||
'Africa/Accra': 'GMT0',
|
||||
'Africa/Addis_Ababa': 'EAT-3',
|
||||
'Africa/Algiers': 'CET-1',
|
||||
'Africa/Asmara': 'EAT-3',
|
||||
'Africa/Bamako': 'GMT0',
|
||||
'Africa/Bangui': 'WAT-1',
|
||||
'Africa/Banjul': 'GMT0',
|
||||
'Africa/Bissau': 'GMT0',
|
||||
'Africa/Blantyre': 'CAT-2',
|
||||
'Africa/Brazzaville': 'WAT-1',
|
||||
'Africa/Bujumbura': 'CAT-2',
|
||||
'Africa/Cairo': 'EET-2',
|
||||
'Africa/Casablanca': 'UNK-1',
|
||||
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Africa/Conakry': 'GMT0',
|
||||
'Africa/Dakar': 'GMT0',
|
||||
'Africa/Dar_es_Salaam': 'EAT-3',
|
||||
'Africa/Djibouti': 'EAT-3',
|
||||
'Africa/Douala': 'WAT-1',
|
||||
'Africa/El_Aaiun': 'UNK-1',
|
||||
'Africa/Freetown': 'GMT0',
|
||||
'Africa/Gaborone': 'CAT-2',
|
||||
'Africa/Harare': 'CAT-2',
|
||||
'Africa/Johannesburg': 'SAST-2',
|
||||
'Africa/Juba': 'EAT-3',
|
||||
'Africa/Kampala': 'EAT-3',
|
||||
'Africa/Khartoum': 'CAT-2',
|
||||
'Africa/Kigali': 'CAT-2',
|
||||
'Africa/Kinshasa': 'WAT-1',
|
||||
'Africa/Lagos': 'WAT-1',
|
||||
'Africa/Libreville': 'WAT-1',
|
||||
'Africa/Lome': 'GMT0',
|
||||
'Africa/Luanda': 'WAT-1',
|
||||
'Africa/Lubumbashi': 'CAT-2',
|
||||
'Africa/Lusaka': 'CAT-2',
|
||||
'Africa/Malabo': 'WAT-1',
|
||||
'Africa/Maputo': 'CAT-2',
|
||||
'Africa/Maseru': 'SAST-2',
|
||||
'Africa/Mbabane': 'SAST-2',
|
||||
'Africa/Mogadishu': 'EAT-3',
|
||||
'Africa/Monrovia': 'GMT0',
|
||||
'Africa/Nairobi': 'EAT-3',
|
||||
'Africa/Ndjamena': 'WAT-1',
|
||||
'Africa/Niamey': 'WAT-1',
|
||||
'Africa/Nouakchott': 'GMT0',
|
||||
'Africa/Ouagadougou': 'GMT0',
|
||||
'Africa/Porto-Novo': 'WAT-1',
|
||||
'Africa/Sao_Tome': 'GMT0',
|
||||
'Africa/Tripoli': 'EET-2',
|
||||
'Africa/Tunis': 'CET-1',
|
||||
'Africa/Windhoek': 'CAT-2',
|
||||
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
|
||||
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Anguilla': 'AST4',
|
||||
'America/Antigua': 'AST4',
|
||||
'America/Araguaina': 'UNK3',
|
||||
'America/Argentina/Buenos_Aires': 'UNK3',
|
||||
'America/Argentina/Catamarca': 'UNK3',
|
||||
'America/Argentina/Cordoba': 'UNK3',
|
||||
'America/Argentina/Jujuy': 'UNK3',
|
||||
'America/Argentina/La_Rioja': 'UNK3',
|
||||
'America/Argentina/Mendoza': 'UNK3',
|
||||
'America/Argentina/Rio_Gallegos': 'UNK3',
|
||||
'America/Argentina/Salta': 'UNK3',
|
||||
'America/Argentina/San_Juan': 'UNK3',
|
||||
'America/Argentina/San_Luis': 'UNK3',
|
||||
'America/Argentina/Tucuman': 'UNK3',
|
||||
'America/Argentina/Ushuaia': 'UNK3',
|
||||
'America/Aruba': 'AST4',
|
||||
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
|
||||
'America/Atikokan': 'EST5',
|
||||
'America/Bahia': 'UNK3',
|
||||
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Barbados': 'AST4',
|
||||
'America/Belem': 'UNK3',
|
||||
'America/Belize': 'CST6',
|
||||
'America/Blanc-Sablon': 'AST4',
|
||||
'America/Boa_Vista': 'UNK4',
|
||||
'America/Bogota': 'UNK5',
|
||||
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Campo_Grande': 'UNK4',
|
||||
'America/Cancun': 'EST5',
|
||||
'America/Caracas': 'UNK4',
|
||||
'America/Cayenne': 'UNK3',
|
||||
'America/Cayman': 'EST5',
|
||||
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
|
||||
'America/Costa_Rica': 'CST6',
|
||||
'America/Creston': 'MST7',
|
||||
'America/Cuiaba': 'UNK4',
|
||||
'America/Curacao': 'AST4',
|
||||
'America/Danmarkshavn': 'GMT0',
|
||||
'America/Dawson': 'MST7',
|
||||
'America/Dawson_Creek': 'MST7',
|
||||
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Dominica': 'AST4',
|
||||
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Eirunepe': 'UNK5',
|
||||
'America/El_Salvador': 'CST6',
|
||||
'America/Fort_Nelson': 'MST7',
|
||||
'America/Fortaleza': 'UNK3',
|
||||
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
|
||||
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Grenada': 'AST4',
|
||||
'America/Guadeloupe': 'AST4',
|
||||
'America/Guatemala': 'CST6',
|
||||
'America/Guayaquil': 'UNK5',
|
||||
'America/Guyana': 'UNK4',
|
||||
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
|
||||
'America/Hermosillo': 'MST7',
|
||||
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Knox': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Petersburg': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Vevay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Indiana/Winamac': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Jamaica': 'EST5',
|
||||
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Kentucky/Louisville': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Kralendijk': 'AST4',
|
||||
'America/La_Paz': 'UNK4',
|
||||
'America/Lima': 'UNK5',
|
||||
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Lower_Princes': 'AST4',
|
||||
'America/Maceio': 'UNK3',
|
||||
'America/Managua': 'CST6',
|
||||
'America/Manaus': 'UNK4',
|
||||
'America/Marigot': 'AST4',
|
||||
'America/Martinique': 'AST4',
|
||||
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
|
||||
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
|
||||
'America/Moncton': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
|
||||
'America/Montevideo': 'UNK3',
|
||||
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Montserrat': 'AST4',
|
||||
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/New_York': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Noronha': 'UNK2',
|
||||
'America/North_Dakota/Beulah': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/North_Dakota/Center': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/North_Dakota/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'America/Panama': 'EST5',
|
||||
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Paramaribo': 'UNK3',
|
||||
'America/Phoenix': 'MST7',
|
||||
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Port_of_Spain': 'AST4',
|
||||
'America/Porto_Velho': 'UNK4',
|
||||
'America/Puerto_Rico': 'AST4',
|
||||
'America/Punta_Arenas': 'UNK3',
|
||||
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Recife': 'UNK3',
|
||||
'America/Regina': 'CST6',
|
||||
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Rio_Branco': 'UNK5',
|
||||
'America/Santarem': 'UNK3',
|
||||
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
|
||||
'America/Santo_Domingo': 'AST4',
|
||||
'America/Sao_Paulo': 'UNK3',
|
||||
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/St_Barthelemy': 'AST4',
|
||||
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
|
||||
'America/St_Kitts': 'AST4',
|
||||
'America/St_Lucia': 'AST4',
|
||||
'America/St_Thomas': 'AST4',
|
||||
'America/St_Vincent': 'AST4',
|
||||
'America/Swift_Current': 'CST6',
|
||||
'America/Tegucigalpa': 'CST6',
|
||||
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'America/Thunder_Bay': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
|
||||
'America/Tortola': 'AST4',
|
||||
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
|
||||
'America/Whitehorse': 'MST7',
|
||||
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
|
||||
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
|
||||
'Antarctica/Casey': 'UNK-8',
|
||||
'Antarctica/Davis': 'UNK-7',
|
||||
'Antarctica/DumontDUrville': 'UNK-10',
|
||||
'Antarctica/Macquarie': 'UNK-11',
|
||||
'Antarctica/Mawson': 'UNK-5',
|
||||
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||
'Antarctica/Palmer': 'UNK3',
|
||||
'Antarctica/Rothera': 'UNK3',
|
||||
'Antarctica/Syowa': 'UNK-3',
|
||||
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
|
||||
'Antarctica/Vostok': 'UNK-6',
|
||||
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Asia/Aden': 'UNK-3',
|
||||
'Asia/Almaty': 'UNK-6',
|
||||
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
|
||||
'Asia/Anadyr': 'UNK-12',
|
||||
'Asia/Aqtau': 'UNK-5',
|
||||
'Asia/Aqtobe': 'UNK-5',
|
||||
'Asia/Ashgabat': 'UNK-5',
|
||||
'Asia/Atyrau': 'UNK-5',
|
||||
'Asia/Baghdad': 'UNK-3',
|
||||
'Asia/Bahrain': 'UNK-3',
|
||||
'Asia/Baku': 'UNK-4',
|
||||
'Asia/Bangkok': 'UNK-7',
|
||||
'Asia/Barnaul': 'UNK-7',
|
||||
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
|
||||
'Asia/Bishkek': 'UNK-6',
|
||||
'Asia/Brunei': 'UNK-8',
|
||||
'Asia/Chita': 'UNK-9',
|
||||
'Asia/Choibalsan': 'UNK-8',
|
||||
'Asia/Colombo': 'UNK-5:30',
|
||||
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
|
||||
'Asia/Dhaka': 'UNK-6',
|
||||
'Asia/Dili': 'UNK-9',
|
||||
'Asia/Dubai': 'UNK-4',
|
||||
'Asia/Dushanbe': 'UNK-5',
|
||||
'Asia/Famagusta': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Asia/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||
'Asia/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||
'Asia/Ho_Chi_Minh': 'UNK-7',
|
||||
'Asia/Hong_Kong': 'HKT-8',
|
||||
'Asia/Hovd': 'UNK-7',
|
||||
'Asia/Irkutsk': 'UNK-8',
|
||||
'Asia/Jakarta': 'WIB-7',
|
||||
'Asia/Jayapura': 'WIT-9',
|
||||
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
|
||||
'Asia/Kabul': 'UNK-4:30',
|
||||
'Asia/Kamchatka': 'UNK-12',
|
||||
'Asia/Karachi': 'PKT-5',
|
||||
'Asia/Kathmandu': 'UNK-5:45',
|
||||
'Asia/Khandyga': 'UNK-9',
|
||||
'Asia/Kolkata': 'IST-5:30',
|
||||
'Asia/Krasnoyarsk': 'UNK-7',
|
||||
'Asia/Kuala_Lumpur': 'UNK-8',
|
||||
'Asia/Kuching': 'UNK-8',
|
||||
'Asia/Kuwait': 'UNK-3',
|
||||
'Asia/Macau': 'CST-8',
|
||||
'Asia/Magadan': 'UNK-11',
|
||||
'Asia/Makassar': 'WITA-8',
|
||||
'Asia/Manila': 'PST-8',
|
||||
'Asia/Muscat': 'UNK-4',
|
||||
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Asia/Novokuznetsk': 'UNK-7',
|
||||
'Asia/Novosibirsk': 'UNK-7',
|
||||
'Asia/Omsk': 'UNK-6',
|
||||
'Asia/Oral': 'UNK-5',
|
||||
'Asia/Phnom_Penh': 'UNK-7',
|
||||
'Asia/Pontianak': 'WIB-7',
|
||||
'Asia/Pyongyang': 'KST-9',
|
||||
'Asia/Qatar': 'UNK-3',
|
||||
'Asia/Qyzylorda': 'UNK-5',
|
||||
'Asia/Riyadh': 'UNK-3',
|
||||
'Asia/Sakhalin': 'UNK-11',
|
||||
'Asia/Samarkand': 'UNK-5',
|
||||
'Asia/Seoul': 'KST-9',
|
||||
'Asia/Shanghai': 'CST-8',
|
||||
'Asia/Singapore': 'UNK-8',
|
||||
'Asia/Srednekolymsk': 'UNK-11',
|
||||
'Asia/Taipei': 'CST-8',
|
||||
'Asia/Tashkent': 'UNK-5',
|
||||
'Asia/Tbilisi': 'UNK-4',
|
||||
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
|
||||
'Asia/Thimphu': 'UNK-6',
|
||||
'Asia/Tokyo': 'JST-9',
|
||||
'Asia/Tomsk': 'UNK-7',
|
||||
'Asia/Ulaanbaatar': 'UNK-8',
|
||||
'Asia/Urumqi': 'UNK-6',
|
||||
'Asia/Ust-Nera': 'UNK-10',
|
||||
'Asia/Vientiane': 'UNK-7',
|
||||
'Asia/Vladivostok': 'UNK-10',
|
||||
'Asia/Yakutsk': 'UNK-9',
|
||||
'Asia/Yangon': 'UNK-6:30',
|
||||
'Asia/Yekaterinburg': 'UNK-5',
|
||||
'Asia/Yerevan': 'UNK-4',
|
||||
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
|
||||
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Cape_Verde': 'UNK1',
|
||||
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Atlantic/Reykjavik': 'GMT0',
|
||||
'Atlantic/South_Georgia': 'UNK2',
|
||||
'Atlantic/St_Helena': 'GMT0',
|
||||
'Atlantic/Stanley': 'UNK3',
|
||||
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Brisbane': 'AEST-10',
|
||||
'Australia/Broken_Hill': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Darwin': 'ACST-9:30',
|
||||
'Australia/Eucla': 'UNK-8:45',
|
||||
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Lindeman': 'AEST-10',
|
||||
'Australia/Lord_Howe': 'UNK-10:30UNK-11,M10.1.0,M4.1.0',
|
||||
'Australia/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Australia/Perth': 'AWST-8',
|
||||
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||
'Etc/GMT': 'GMT0',
|
||||
'Etc/GMT+0': 'GMT0',
|
||||
'Etc/GMT+1': 'UNK1',
|
||||
'Etc/GMT+10': 'UNK10',
|
||||
'Etc/GMT+11': 'UNK11',
|
||||
'Etc/GMT+12': 'UNK12',
|
||||
'Etc/GMT+2': 'UNK2',
|
||||
'Etc/GMT+3': 'UNK3',
|
||||
'Etc/GMT+4': 'UNK4',
|
||||
'Etc/GMT+5': 'UNK5',
|
||||
'Etc/GMT+6': 'UNK6',
|
||||
'Etc/GMT+7': 'UNK7',
|
||||
'Etc/GMT+8': 'UNK8',
|
||||
'Etc/GMT+9': 'UNK9',
|
||||
'Etc/GMT-0': 'GMT0',
|
||||
'Etc/GMT-1': 'UNK-1',
|
||||
'Etc/GMT-10': 'UNK-10',
|
||||
'Etc/GMT-11': 'UNK-11',
|
||||
'Etc/GMT-12': 'UNK-12',
|
||||
'Etc/GMT-13': 'UNK-13',
|
||||
'Etc/GMT-14': 'UNK-14',
|
||||
'Etc/GMT-2': 'UNK-2',
|
||||
'Etc/GMT-3': 'UNK-3',
|
||||
'Etc/GMT-4': 'UNK-4',
|
||||
'Etc/GMT-5': 'UNK-5',
|
||||
'Etc/GMT-6': 'UNK-6',
|
||||
'Etc/GMT-7': 'UNK-7',
|
||||
'Etc/GMT-8': 'UNK-8',
|
||||
'Etc/GMT-9': 'UNK-9',
|
||||
'Etc/GMT0': 'GMT0',
|
||||
'Etc/Greenwich': 'GMT0',
|
||||
'Etc/UCT': 'UTC0',
|
||||
'Etc/UTC': 'UTC0',
|
||||
'Etc/Universal': 'UTC0',
|
||||
'Etc/Zulu': 'UTC0',
|
||||
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Andorra': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Astrakhan': 'UNK-4',
|
||||
'Europe/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Berlin': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Busingen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
|
||||
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Helsinki': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Istanbul': 'UNK-3',
|
||||
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Kaliningrad': 'EET-2',
|
||||
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Kirov': 'UNK-3',
|
||||
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Madrid': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Minsk': 'UNK-3',
|
||||
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Moscow': 'MSK-3',
|
||||
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Paris': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Samara': 'UNK-4',
|
||||
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Sarajevo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Saratov': 'UNK-4',
|
||||
'Europe/Simferopol': 'MSK-3',
|
||||
'Europe/Skopje': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Ulyanovsk': 'UNK-4',
|
||||
'Europe/Uzhgorod': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Vatican': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Volgograd': 'UNK-4',
|
||||
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Zagreb': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Europe/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||
'Indian/Antananarivo': 'EAT-3',
|
||||
'Indian/Chagos': 'UNK-6',
|
||||
'Indian/Christmas': 'UNK-7',
|
||||
'Indian/Cocos': 'UNK-6:30',
|
||||
'Indian/Comoro': 'EAT-3',
|
||||
'Indian/Kerguelen': 'UNK-5',
|
||||
'Indian/Mahe': 'UNK-4',
|
||||
'Indian/Maldives': 'UNK-5',
|
||||
'Indian/Mauritius': 'UNK-4',
|
||||
'Indian/Mayotte': 'EAT-3',
|
||||
'Indian/Reunion': 'UNK-4',
|
||||
'Pacific/Apia': 'UNK-13UNK,M9.5.0/3,M4.1.0/4',
|
||||
'Pacific/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||
'Pacific/Bougainville': 'UNK-11',
|
||||
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
|
||||
'Pacific/Chuuk': 'UNK-10',
|
||||
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
|
||||
'Pacific/Efate': 'UNK-11',
|
||||
'Pacific/Enderbury': 'UNK-13',
|
||||
'Pacific/Fakaofo': 'UNK-13',
|
||||
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
|
||||
'Pacific/Funafuti': 'UNK-12',
|
||||
'Pacific/Galapagos': 'UNK6',
|
||||
'Pacific/Gambier': 'UNK9',
|
||||
'Pacific/Guadalcanal': 'UNK-11',
|
||||
'Pacific/Guam': 'ChST-10',
|
||||
'Pacific/Honolulu': 'HST10',
|
||||
'Pacific/Kiritimati': 'UNK-14',
|
||||
'Pacific/Kosrae': 'UNK-11',
|
||||
'Pacific/Kwajalein': 'UNK-12',
|
||||
'Pacific/Majuro': 'UNK-12',
|
||||
'Pacific/Marquesas': 'UNK9:30',
|
||||
'Pacific/Midway': 'SST11',
|
||||
'Pacific/Nauru': 'UNK-12',
|
||||
'Pacific/Niue': 'UNK11',
|
||||
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
|
||||
'Pacific/Noumea': 'UNK-11',
|
||||
'Pacific/Pago_Pago': 'SST11',
|
||||
'Pacific/Palau': 'UNK-9',
|
||||
'Pacific/Pitcairn': 'UNK8',
|
||||
'Pacific/Pohnpei': 'UNK-11',
|
||||
'Pacific/Port_Moresby': 'UNK-10',
|
||||
'Pacific/Rarotonga': 'UNK10',
|
||||
'Pacific/Saipan': 'ChST-10',
|
||||
'Pacific/Tahiti': 'UNK10',
|
||||
'Pacific/Tarawa': 'UNK-12',
|
||||
'Pacific/Tongatapu': 'UNK-13',
|
||||
'Pacific/Wake': 'UNK-12',
|
||||
'Pacific/Wallis': 'UNK-12'
|
||||
};
|
||||
|
||||
export function selectedTimeZone(label: string, format: string) {
|
||||
return TIME_ZONES[label] === format ? label : undefined;
|
||||
}
|
||||
|
||||
export function timeZoneSelectItems() {
|
||||
return Object.keys(TIME_ZONES).map(label => (
|
||||
<MenuItem key={label} value={label}>{label}</MenuItem>
|
||||
return Object.keys(TIME_ZONES).map((label) => (
|
||||
<MenuItem key={label} value={label}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import parseMilliseconds from 'parse-ms';
|
||||
|
||||
const LOCALE_FORMAT = new Intl.DateTimeFormat(
|
||||
[...window.navigator.languages],
|
||||
{
|
||||
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
@@ -10,23 +8,22 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat(
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: false
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const formatDateTime = (dateTime: string) => {
|
||||
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
|
||||
}
|
||||
};
|
||||
|
||||
export const formatLocalDateTime = (date: Date) => {
|
||||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||
.toISOString()
|
||||
.slice(0, -1)
|
||||
.substr(0, 19);
|
||||
}
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: number) => {
|
||||
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
||||
var formatted = '';
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += pluralize(days, 'day');
|
||||
}
|
||||
@@ -40,6 +37,7 @@ export const formatDuration = (duration: number) => {
|
||||
formatted += pluralize(seconds, 'second');
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
};
|
||||
|
||||
const pluralize = (count: number, noun: string, suffix: string = 's') => ` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
type BoardProfiles = {
|
||||
[name: string]: string
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
export const BOARD_PROFILES: BoardProfiles = {
|
||||
"S32": "BBQKees Gateway S32",
|
||||
"E32": "BBQKees Gateway E32",
|
||||
"NODEMCU": "NodeMCU 32S",
|
||||
"MH-ET": "MH-ET Live D1 Mini",
|
||||
"LOLIN": "Lolin D32",
|
||||
"OLIMEX": "Olimex ESP32-EVB",
|
||||
"TLK110": "Generic Ethernet (TLK110)",
|
||||
"LAN8720": "Generic Ethernet (LAN8720)"
|
||||
}
|
||||
S32: 'BBQKees Gateway S32',
|
||||
E32: 'BBQKees Gateway E32',
|
||||
NODEMCU: 'NodeMCU 32S',
|
||||
'MH-ET': 'MH-ET Live D1 Mini',
|
||||
LOLIN: 'Lolin D32',
|
||||
OLIMEX: 'Olimex ESP32-EVB',
|
||||
TLK110: 'Generic Ethernet (TLK110)',
|
||||
LAN8720: 'Generic Ethernet (LAN8720)'
|
||||
};
|
||||
|
||||
export function boardProfileSelectItems() {
|
||||
return Object.keys(BOARD_PROFILES).map(code => (
|
||||
<MenuItem key={code} value={code}>{BOARD_PROFILES[code]}</MenuItem>
|
||||
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||
<MenuItem key={code} value={code}>
|
||||
{BOARD_PROFILES[code]}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
@@ -12,30 +12,46 @@ import EMSESPDevicesController from './EMSESPDevicesController';
|
||||
import EMSESPHelp from './EMSESPHelp';
|
||||
|
||||
class EMSESP extends Component<RouteComponentProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Dashboard">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value={`/${PROJECT_PATH}/devices`} label="Devices & Sensors" />
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab
|
||||
value={`/${PROJECT_PATH}/devices`}
|
||||
label="Devices & Sensors"
|
||||
/>
|
||||
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
||||
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/devices`} component={EMSESPDevicesController} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/help`} component={EMSESPHelp} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/devices`}
|
||||
component={EMSESPDevicesController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/status`}
|
||||
component={EMSESPStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/help`}
|
||||
component={EMSESPHelp}
|
||||
/>
|
||||
<Redirect to={`/${PROJECT_PATH}/devices`} />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EMSESP;
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPDevicesForm from './EMSESPDevicesForm';
|
||||
import { EMSESPDevices } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices";
|
||||
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + 'allDevices';
|
||||
|
||||
type EMSESPDevicesControllerProps = RestControllerProps<EMSESPDevices>;
|
||||
|
||||
class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="Devices & Sensors">
|
||||
<SectionContent title="Devices & Sensors">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <EMSESPDevicesForm {...formProps} />}
|
||||
render={(formProps) => <EMSESPDevicesForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,70 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { withStyles, Theme, createStyles } from "@material-ui/core/styles";
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
|
||||
import parseMilliseconds from 'parse-ms';
|
||||
|
||||
import { Decoder } from '@msgpack/msgpack';
|
||||
const decoder = new Decoder();
|
||||
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableRow, TableContainer, withWidth, WithWidthProps, isWidthDown,
|
||||
Button, Tooltip, DialogTitle, DialogContent, DialogActions, Box, Dialog, Typography
|
||||
} from "@material-ui/core";
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableContainer,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
isWidthDown,
|
||||
Button,
|
||||
Tooltip,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
Dialog,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
|
||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import ListIcon from "@material-ui/icons/List";
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import ListIcon from '@material-ui/icons/List';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
|
||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication";
|
||||
import { RestFormProps, FormButton, extractEventValue } from "../components";
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
import { EMSESPDevices, EMSESPDeviceData, Device, DeviceValue } from "./EMSESPtypes";
|
||||
import { RestFormProps, FormButton, extractEventValue } from '../components';
|
||||
|
||||
import {
|
||||
EMSESPDevices,
|
||||
EMSESPDeviceData,
|
||||
Device,
|
||||
DeviceValue,
|
||||
DeviceValueUOM,
|
||||
DeviceValueUOM_s
|
||||
} from './EMSESPtypes';
|
||||
|
||||
import ValueForm from './ValueForm';
|
||||
|
||||
import { ENDPOINT_ROOT } from "../api";
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
|
||||
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
|
||||
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
|
||||
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + "writeValue";
|
||||
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices';
|
||||
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData';
|
||||
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue';
|
||||
|
||||
const StyledTableCell = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
head: {
|
||||
backgroundColor: theme.palette.common.black,
|
||||
color: theme.palette.common.white,
|
||||
color: theme.palette.common.white
|
||||
},
|
||||
body: {
|
||||
fontSize: 14,
|
||||
},
|
||||
fontSize: 14
|
||||
}
|
||||
})
|
||||
)(TableCell);
|
||||
|
||||
@@ -42,8 +74,8 @@ const CustomTooltip = withStyles((theme: Theme) => ({
|
||||
color: 'white',
|
||||
boxShadow: theme.shadows[1],
|
||||
fontSize: 11,
|
||||
border: '1px solid #dadde9',
|
||||
},
|
||||
border: '1px solid #dadde9'
|
||||
}
|
||||
}))(Tooltip);
|
||||
|
||||
function compareDevices(a: Device, b: Device) {
|
||||
@@ -61,87 +93,118 @@ interface EMSESPDevicesFormState {
|
||||
processing: boolean;
|
||||
deviceData?: EMSESPDeviceData;
|
||||
selectedDevice?: number;
|
||||
devicevalue?: DeviceValue;
|
||||
edit_devicevalue?: DeviceValue;
|
||||
}
|
||||
|
||||
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> & AuthenticatedContextProps & WithWidthProps;
|
||||
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> &
|
||||
AuthenticatedContextProps &
|
||||
WithWidthProps;
|
||||
|
||||
function formatTemp(t: string) {
|
||||
if (t == null) {
|
||||
return "n/a";
|
||||
export const formatDuration = (duration_min: number) => {
|
||||
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += pluralize(days, 'day');
|
||||
}
|
||||
if (hours) {
|
||||
formatted += pluralize(hours, 'hour');
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += pluralize(minutes, 'minute');
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||
|
||||
function formatValue(value: any, uom: number) {
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDuration(value * 60) : '0 hours';
|
||||
case DeviceValueUOM.MINUTES:
|
||||
return value ? formatDuration(value) : '0 minutes';
|
||||
case DeviceValueUOM.NONE:
|
||||
return value;
|
||||
case DeviceValueUOM.NUM:
|
||||
return new Intl.NumberFormat().format(value);
|
||||
case DeviceValueUOM.BOOLEAN:
|
||||
return value ? 'on' : 'off';
|
||||
default:
|
||||
return (
|
||||
new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]
|
||||
);
|
||||
}
|
||||
return t + " °C";
|
||||
}
|
||||
|
||||
function formatUnit(u: string) {
|
||||
if (u == null) {
|
||||
return u;
|
||||
}
|
||||
return " " + u;
|
||||
}
|
||||
|
||||
class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesFormState> {
|
||||
class EMSESPDevicesForm extends Component<
|
||||
EMSESPDevicesFormProps,
|
||||
EMSESPDevicesFormState
|
||||
> {
|
||||
state: EMSESPDevicesFormState = {
|
||||
confirmScanDevices: false,
|
||||
processing: false
|
||||
};
|
||||
|
||||
handleValueChange = (name: keyof DeviceValue) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ devicevalue: { ...this.state.devicevalue!, [name]: extractEventValue(event) } });
|
||||
handleValueChange = (name: keyof DeviceValue) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({
|
||||
edit_devicevalue: {
|
||||
...this.state.edit_devicevalue!,
|
||||
[name]: extractEventValue(event)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cancelEditingValue = () => {
|
||||
this.setState({
|
||||
devicevalue: undefined
|
||||
});
|
||||
}
|
||||
this.setState({ edit_devicevalue: undefined });
|
||||
};
|
||||
|
||||
doneEditingValue = () => {
|
||||
const { devicevalue } = this.state;
|
||||
const { edit_devicevalue, selectedDevice } = this.state;
|
||||
|
||||
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ devicevalue: devicevalue }),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: selectedDevice,
|
||||
devicevalue: edit_devicevalue
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar("Write command sent to device", { variant: "success" });
|
||||
this.props.enqueueSnackbar('Write command sent to device', {
|
||||
variant: 'success'
|
||||
});
|
||||
} else if (response.status === 204) {
|
||||
this.props.enqueueSnackbar("Write command failed", { variant: "error" });
|
||||
this.props.enqueueSnackbar('Write command failed', {
|
||||
variant: 'error'
|
||||
});
|
||||
} else if (response.status === 403) {
|
||||
this.props.enqueueSnackbar("Write access denied", { variant: "error" });
|
||||
this.props.enqueueSnackbar('Write access denied', {
|
||||
variant: 'error'
|
||||
});
|
||||
} else {
|
||||
throw Error("Unexpected response code: " + response.status);
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || "Problem writing value", { variant: "error" }
|
||||
);
|
||||
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||
variant: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
if (devicevalue) {
|
||||
this.setState({
|
||||
devicevalue: undefined
|
||||
});
|
||||
if (edit_devicevalue) {
|
||||
this.setState({ edit_devicevalue: undefined });
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
sendCommand = (i: any) => {
|
||||
this.setState({
|
||||
devicevalue: {
|
||||
id: this.state.selectedDevice!,
|
||||
data: this.state.deviceData?.data[i]!,
|
||||
uom: this.state.deviceData?.data[i + 1]!,
|
||||
name: this.state.deviceData?.data[i + 2]!,
|
||||
cmd: this.state.deviceData?.data[i + 3]!,
|
||||
}
|
||||
});
|
||||
}
|
||||
sendCommand = (dv: DeviceValue) => {
|
||||
this.setState({ edit_devicevalue: dv });
|
||||
};
|
||||
|
||||
noDevices = () => {
|
||||
return this.props.data.devices.length === 0;
|
||||
@@ -166,22 +229,41 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
{!this.noDevices() && (
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||
>
|
||||
<TableBody>
|
||||
{data.devices.sort(compareDevices).map((device) => (
|
||||
<TableRow hover key={device.id} onClick={() => this.handleRowClick(device)}>
|
||||
<TableRow
|
||||
hover
|
||||
key={device.id}
|
||||
onClick={() => this.handleRowClick(device)}
|
||||
>
|
||||
<TableCell>
|
||||
<CustomTooltip
|
||||
title={"DeviceID:0x" + ("00" + device.deviceid.toString(16).toUpperCase()).slice(-2) + " ProductID:" + device.productid + " Version:" + device.version}
|
||||
title={
|
||||
'DeviceID:0x' +
|
||||
(
|
||||
'00' + device.deviceid.toString(16).toUpperCase()
|
||||
).slice(-2) +
|
||||
' ProductID:' +
|
||||
device.productid +
|
||||
' Version:' +
|
||||
device.version
|
||||
}
|
||||
placement="right-end"
|
||||
>
|
||||
<Button startIcon={<ListIcon />} size="small" variant="outlined">
|
||||
<Button
|
||||
startIcon={<ListIcon />}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
{device.type}
|
||||
</Button>
|
||||
</CustomTooltip>
|
||||
</TableCell>
|
||||
<TableCell align="right">{device.brand + " " + device.name} </TableCell>
|
||||
<TableCell align="right">
|
||||
{device.brand + ' ' + device.name}{' '}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -191,10 +273,13 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
<Box
|
||||
bgcolor="error.main"
|
||||
color="error.contrastText"
|
||||
p={2} mt={2} mb={2}
|
||||
p={2}
|
||||
mt={2}
|
||||
mb={2}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
No EMS devices found. Check the connections and for possible Tx errors.
|
||||
No EMS devices found. Check the connections and for possible Tx
|
||||
errors.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -227,7 +312,7 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
</TableCell>
|
||||
<TableCell align="center">{sensorData.id}</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatTemp(sensorData.temp)}
|
||||
{formatValue(sensorData.temp, DeviceValueUOM.DEGREES)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -255,14 +340,25 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
>
|
||||
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
Are you sure you want to initiate a scan on the EMS bus for all new devices?
|
||||
Are you sure you want to start a scan on the EMS bus for all new
|
||||
devices?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={this.onScanDevicesRejected} color="secondary">
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={this.onScanDevicesRejected}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />} variant="contained" onClick={this.onScanDevicesConfirmed} disabled={this.state.processing} color="primary" autoFocus>
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
onClick={this.onScanDevicesConfirmed}
|
||||
disabled={this.state.processing}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Start Scan
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -283,17 +379,17 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar("Device scan is starting...", {
|
||||
variant: "info",
|
||||
this.props.enqueueSnackbar('Device scan is starting...', {
|
||||
variant: 'info'
|
||||
});
|
||||
this.setState({ processing: false, confirmScanDevices: false });
|
||||
} else {
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(error.message || "Problem with scan", {
|
||||
variant: "error",
|
||||
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ processing: false, confirmScanDevices: false });
|
||||
});
|
||||
@@ -302,25 +398,26 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
handleRowClick = (device: any) => {
|
||||
this.setState({ selectedDevice: device.id, deviceData: undefined });
|
||||
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: device.id }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
throw Error("Unexpected response code: " + response.status);
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
.then((arrayBuffer) => {
|
||||
const json: any = decoder.decode(arrayBuffer);
|
||||
this.setState({ deviceData: json });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || "Problem getting device data",
|
||||
{ variant: "error" }
|
||||
error.message || 'Problem getting device data',
|
||||
{ variant: 'error' }
|
||||
);
|
||||
this.setState({ deviceData: undefined });
|
||||
});
|
||||
@@ -351,34 +448,37 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
<TableContainer>
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||
>
|
||||
<TableHead>
|
||||
</TableHead>
|
||||
<TableHead></TableHead>
|
||||
<TableBody>
|
||||
{deviceData.data.map((item, i) => {
|
||||
if (i % 4) {
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
{deviceData.data.map((item, i) => (
|
||||
<TableRow hover key={i}>
|
||||
<TableCell padding="checkbox" style={{ width: 18 }}>
|
||||
{deviceData.data[i + 3] && me.admin && (
|
||||
<CustomTooltip title="change value" placement="left-end"
|
||||
{item.c && me.admin && (
|
||||
<CustomTooltip
|
||||
title="change value"
|
||||
placement="left-end"
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
size="small"
|
||||
aria-label="Edit"
|
||||
onClick={() => this.sendCommand(item)}
|
||||
>
|
||||
<IconButton edge="start" size="small" aria-label="Edit"
|
||||
onClick={() => this.sendCommand(i)}>
|
||||
<EditIcon color="primary" fontSize="small" />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell padding="none" component="th" scope="row">{deviceData.data[i + 2]}</TableCell>
|
||||
<TableCell padding="none" align="right">{deviceData.data[i]}{formatUnit(deviceData.data[i + 1])}</TableCell>
|
||||
<TableCell padding="none" component="th" scope="row">
|
||||
{item.n}
|
||||
</TableCell>
|
||||
<TableCell padding="none" align="right">
|
||||
{formatValue(item.v, item.u)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
})}
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -395,7 +495,7 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
}
|
||||
|
||||
render() {
|
||||
const { devicevalue } = this.state;
|
||||
const { edit_devicevalue } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<br></br>
|
||||
@@ -405,26 +505,34 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
||||
<br></br>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} padding={1}>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData} >
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</Box>
|
||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" onClick={this.onScanDevices} >
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
onClick={this.onScanDevices}
|
||||
>
|
||||
Scan Devices
|
||||
</FormButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{this.renderScanDevicesDialog()}
|
||||
{
|
||||
devicevalue &&
|
||||
{edit_devicevalue && (
|
||||
<ValueForm
|
||||
devicevalue={devicevalue}
|
||||
devicevalue={edit_devicevalue}
|
||||
onDoneEditing={this.doneEditingValue}
|
||||
onCancelEditing={this.cancelEditingValue}
|
||||
handleValueChange={this.handleValueChange}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,40 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Typography, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@material-ui/core';
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Link,
|
||||
ListItemAvatar
|
||||
} from '@material-ui/core';
|
||||
import { SectionContent } from '../components';
|
||||
|
||||
import CommentIcon from "@material-ui/icons/CommentTwoTone";
|
||||
import MenuBookIcon from "@material-ui/icons/MenuBookTwoTone";
|
||||
import GitHubIcon from "@material-ui/icons/GitHub";
|
||||
import StarIcon from "@material-ui/icons/Star";
|
||||
import ImportExportIcon from "@material-ui/icons/ImportExport";
|
||||
import BugReportIcon from "@material-ui/icons/BugReportTwoTone";
|
||||
import CommentIcon from '@material-ui/icons/CommentTwoTone';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBookTwoTone';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import ImportExportIcon from '@material-ui/icons/ImportExport';
|
||||
import BugReportIcon from '@material-ui/icons/BugReportTwoTone';
|
||||
|
||||
export const WebAPISystemSettings = window.location.origin + "/api?device=system&cmd=settings";
|
||||
export const WebAPISystemInfo = window.location.origin + "/api?device=system&cmd=info";
|
||||
export const WebAPISystemSettings =
|
||||
window.location.origin + '/api/system/settings';
|
||||
export const WebAPISystemInfo = window.location.origin + '/api/system/info';
|
||||
|
||||
class EMSESPHelp extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title='EMS-ESP Help' titleGutter>
|
||||
|
||||
<SectionContent title="EMS-ESP Help" titleGutter>
|
||||
<List>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<MenuBookIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For the latest news and updates go to the <Link href="https://emsesp.github.io/docs" color="primary">{'official documentation'} website</Link>
|
||||
For the latest news and updates go to the{' '}
|
||||
<Link href="https://emsesp.github.io/docs" color="primary">
|
||||
{'official documentation'} website
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
@@ -34,7 +43,10 @@ class EMSESPHelp extends Component {
|
||||
<CommentIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For live community chat join our <Link href="https://discord.gg/3J3GgnzpyT" color="primary">{'Discord'} server</Link>
|
||||
For live community chat join our{' '}
|
||||
<Link href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||
{'Discord'} server
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
@@ -43,7 +55,13 @@ class EMSESPHelp extends Component {
|
||||
<GitHubIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
To report an issue or feature request go to <Link href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">{'click here'}</Link>
|
||||
To report an issue or feature request go to{' '}
|
||||
<Link
|
||||
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
|
||||
color="primary"
|
||||
>
|
||||
{'click here'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
@@ -52,34 +70,41 @@ class EMSESPHelp extends Component {
|
||||
<ImportExportIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
To list your system settings <Link target="_blank" href={WebAPISystemSettings} color="primary">{'click here'}</Link>
|
||||
To export your system settings{' '}
|
||||
<Link target="_blank" href={WebAPISystemSettings} color="primary">
|
||||
{'click here'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<BugReportIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
To create a report of the current EMS-ESP status <Link target="_blank" href={WebAPISystemInfo} color="primary">{'click here'}</Link>
|
||||
To export the current status of EMS-ESP{' '}
|
||||
<Link target="_blank" href={WebAPISystemInfo} color="primary">
|
||||
{'click here'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
</List>
|
||||
|
||||
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
|
||||
<Typography variant="h6">
|
||||
EMS-ESP is free and open-source.
|
||||
<br></br>Please consider supporting this project by giving it a <StarIcon style={{ color: '#fdff3a' }} /> on our <Link href="https://github.com/emsesp/EMS-ESP32" color="primary">{'GitHub page'}</Link>.
|
||||
<br></br>Please consider supporting this project by giving it a{' '}
|
||||
<StarIcon style={{ color: '#fdff3a' }} /> on our{' '}
|
||||
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
|
||||
{'GitHub page'}
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</Box>
|
||||
<br></br>
|
||||
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EMSESPHelp;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
@@ -10,26 +10,31 @@ import { AuthenticatedRoute } from '../authentication';
|
||||
import EMSESPSettingsController from './EMSESPSettingsController';
|
||||
|
||||
class EMSESP extends Component<RouteComponentProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Settings">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/settings`} component={EMSESPSettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/settings`}
|
||||
component={EMSESPSettingsController}
|
||||
/>
|
||||
<Redirect to={`/${PROJECT_PATH}/settings`} />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EMSESP;
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
// import { Container } from '@material-ui/core';
|
||||
import { Component } from 'react';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPSettingsForm from './EMSESPSettingsForm';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
|
||||
import { EMSESPSettings } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings';
|
||||
|
||||
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
||||
|
||||
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
// <Container maxWidth="md" disableGutters>
|
||||
<SectionContent title='' titleGutter>
|
||||
<SectionContent title="" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => (
|
||||
<EMSESPSettingsForm {...formProps} />
|
||||
)}
|
||||
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
// </Container>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
|
||||
export default restController(
|
||||
EMSESP_SETTINGS_ENDPOINT,
|
||||
EMSESPSettingsController
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import { Component } from 'react';
|
||||
|
||||
import {
|
||||
ValidatorForm,
|
||||
TextValidator,
|
||||
SelectValidator,
|
||||
} from "react-material-ui-form-validator";
|
||||
SelectValidator
|
||||
} from 'react-material-ui-form-validator';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
@@ -12,33 +13,33 @@ import {
|
||||
Link,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
} from "@material-ui/core";
|
||||
import SaveIcon from "@material-ui/icons/Save";
|
||||
import MenuItem from "@material-ui/core/MenuItem";
|
||||
Grid
|
||||
} from '@material-ui/core';
|
||||
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps,
|
||||
} from "../authentication";
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
BlockFormControlLabel,
|
||||
} from "../components";
|
||||
BlockFormControlLabel
|
||||
} from '../components';
|
||||
|
||||
import { isIP, optional } from "../validators";
|
||||
import { isIP, optional } from '../validators';
|
||||
|
||||
import { EMSESPSettings } from "./EMSESPtypes";
|
||||
import { EMSESPSettings } from './EMSESPtypes';
|
||||
|
||||
import { boardProfileSelectItems } from "./EMSESPBoardProfiles";
|
||||
import { boardProfileSelectItems } from './EMSESPBoardProfiles';
|
||||
|
||||
import { ENDPOINT_ROOT } from "../api";
|
||||
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile';
|
||||
|
||||
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
|
||||
AuthenticatedContextProps &
|
||||
@@ -48,40 +49,40 @@ interface EMSESPSettingsFormState {
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
class EMSESPSettingsForm extends Component<EMSESPSettingsFormProps> {
|
||||
state: EMSESPSettingsFormState = {
|
||||
processing: false,
|
||||
processing: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule("isOptionalIP", optional(isIP));
|
||||
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
|
||||
}
|
||||
|
||||
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { data, setData } = this.props;
|
||||
setData({
|
||||
...data,
|
||||
board_profile: event.target.value,
|
||||
board_profile: event.target.value
|
||||
});
|
||||
|
||||
if (event.target.value === "CUSTOM") return;
|
||||
if (event.target.value === 'CUSTOM') return;
|
||||
|
||||
this.setState({ processing: true });
|
||||
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code: event.target.value }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Unexpected response code: " + response.status);
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
this.props.enqueueSnackbar("Profile loaded", { variant: "success" });
|
||||
this.props.enqueueSnackbar('Profile loaded', { variant: 'success' });
|
||||
setData({
|
||||
...data,
|
||||
led_gpio: json.led_gpio,
|
||||
@@ -89,14 +90,14 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
rx_gpio: json.rx_gpio,
|
||||
tx_gpio: json.tx_gpio,
|
||||
pbutton_gpio: json.pbutton_gpio,
|
||||
board_profile: event.target.value,
|
||||
board_profile: event.target.value
|
||||
});
|
||||
this.setState({ processing: false });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || "Problem fetching board profile",
|
||||
{ variant: "warning" }
|
||||
error.message || 'Problem fetching board profile',
|
||||
{ variant: 'warning' }
|
||||
);
|
||||
this.setState({ processing: false });
|
||||
});
|
||||
@@ -108,13 +109,13 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
<ValidatorForm onSubmit={saveData}>
|
||||
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
|
||||
<Typography variant="body1">
|
||||
Adjust any of the EMS-ESP settings here. For help refer to the{" "}
|
||||
Adjust any of the EMS-ESP settings here. For help refer to the{' '}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
|
||||
color="primary"
|
||||
>
|
||||
{"online documentation"}
|
||||
{'online documentation'}
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
@@ -139,7 +140,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
value={data.tx_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("tx_mode")}
|
||||
onChange={handleValueChange('tx_mode')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>Off</MenuItem>
|
||||
@@ -156,7 +157,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
value={data.ems_bus_id}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("ems_bus_id")}
|
||||
onChange={handleValueChange('ems_bus_id')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||
@@ -169,16 +170,16 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
<Grid item xs={6}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:120",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:120'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Tx delay is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or higher",
|
||||
"Max value is 120",
|
||||
'Tx delay is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 120'
|
||||
]}
|
||||
name="tx_delay"
|
||||
label="Tx start delay (seconds)"
|
||||
@@ -186,7 +187,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.tx_delay}
|
||||
type="number"
|
||||
onChange={handleValueChange("tx_delay")}
|
||||
onChange={handleValueChange('tx_delay')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -216,12 +217,12 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
margin="normal"
|
||||
>
|
||||
{boardProfileSelectItems()}
|
||||
<MenuItem key={"CUSTOM"} value={"CUSTOM"}>
|
||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||
Custom...
|
||||
</MenuItem>
|
||||
</SelectValidator>
|
||||
|
||||
{data.board_profile === "CUSTOM" && (
|
||||
{data.board_profile === 'CUSTOM' && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
@@ -232,18 +233,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:40",
|
||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
"GPIO is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or higher",
|
||||
"Max value is 40",
|
||||
"Not a valid GPIO",
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="rx_gpio"
|
||||
label="Rx GPIO"
|
||||
@@ -251,25 +252,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.rx_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange("rx_gpio")}
|
||||
onChange={handleValueChange('rx_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:40",
|
||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
"GPIO is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or higher",
|
||||
"Max value is 40",
|
||||
"Not a valid GPIO",
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="tx_gpio"
|
||||
label="Tx GPIO"
|
||||
@@ -277,25 +278,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.tx_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange("tx_gpio")}
|
||||
onChange={handleValueChange('tx_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:40",
|
||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
"GPIO is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or higher",
|
||||
"Max value is 40",
|
||||
"Not a valid GPIO",
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="pbutton_gpio"
|
||||
label="Button GPIO"
|
||||
@@ -303,25 +304,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.pbutton_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange("pbutton_gpio")}
|
||||
onChange={handleValueChange('pbutton_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:40",
|
||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
"GPIO is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or higher",
|
||||
"Max value is 40",
|
||||
"Not a valid GPIO",
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="dallas_gpio"
|
||||
label="Dallas GPIO (0=none)"
|
||||
@@ -329,25 +330,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.dallas_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange("dallas_gpio")}
|
||||
onChange={handleValueChange('dallas_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:40",
|
||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
"GPIO is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or higher",
|
||||
"Max value is 40",
|
||||
"Not a valid GPIO",
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="led_gpio"
|
||||
label="LED GPIO (0=none)"
|
||||
@@ -355,7 +356,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.led_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange("led_gpio")}
|
||||
onChange={handleValueChange('led_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -372,7 +373,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.hide_led}
|
||||
onChange={handleValueChange("hide_led")}
|
||||
onChange={handleValueChange('hide_led')}
|
||||
value="hide_led"
|
||||
/>
|
||||
}
|
||||
@@ -385,7 +386,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.dallas_parasite}
|
||||
onChange={handleValueChange("dallas_parasite")}
|
||||
onChange={handleValueChange('dallas_parasite')}
|
||||
value="dallas_parasite"
|
||||
/>
|
||||
}
|
||||
@@ -396,18 +397,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.api_enabled}
|
||||
onChange={handleValueChange("api_enabled")}
|
||||
value="api_enabled"
|
||||
checked={data.notoken_api}
|
||||
onChange={handleValueChange('notoken_api')}
|
||||
value="notoken_api"
|
||||
/>
|
||||
}
|
||||
label="Enable API write commands"
|
||||
label="Bypass Access Token authorization on API calls"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.analog_enabled}
|
||||
onChange={handleValueChange("analog_enabled")}
|
||||
onChange={handleValueChange('analog_enabled')}
|
||||
value="analog_enabled"
|
||||
/>
|
||||
}
|
||||
@@ -424,7 +425,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_timer}
|
||||
onChange={handleValueChange("shower_timer")}
|
||||
onChange={handleValueChange('shower_timer')}
|
||||
value="shower_timer"
|
||||
/>
|
||||
}
|
||||
@@ -434,7 +435,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_alert}
|
||||
onChange={handleValueChange("shower_alert")}
|
||||
onChange={handleValueChange('shower_alert')}
|
||||
value="shower_alert"
|
||||
/>
|
||||
}
|
||||
@@ -451,7 +452,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.syslog_enabled}
|
||||
onChange={handleValueChange("syslog_enabled")}
|
||||
onChange={handleValueChange('syslog_enabled')}
|
||||
value="syslog_enabled"
|
||||
/>
|
||||
}
|
||||
@@ -468,30 +469,30 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
>
|
||||
<Grid item xs={5}>
|
||||
<TextValidator
|
||||
validators={["isOptionalIP"]}
|
||||
errorMessages={["Not a valid IP address"]}
|
||||
validators={['isOptionalIP']}
|
||||
errorMessages={['Not a valid IP address']}
|
||||
name="syslog_host"
|
||||
label="IP"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_host}
|
||||
onChange={handleValueChange("syslog_host")}
|
||||
onChange={handleValueChange('syslog_host')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Port is required",
|
||||
"Must be a number",
|
||||
"Must be greater than 0 ",
|
||||
"Max value is 65535",
|
||||
'Port is required',
|
||||
'Must be a number',
|
||||
'Must be greater than 0 ',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="syslog_port"
|
||||
label="Port"
|
||||
@@ -499,7 +500,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.syslog_port}
|
||||
type="number"
|
||||
onChange={handleValueChange("syslog_port")}
|
||||
onChange={handleValueChange('syslog_port')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -510,7 +511,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
value={data.syslog_level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange("syslog_level")}
|
||||
onChange={handleValueChange('syslog_level')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={-1}>OFF</MenuItem>
|
||||
@@ -524,16 +525,16 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
<Grid item xs={6}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
"required",
|
||||
"isNumber",
|
||||
"minNumber:0",
|
||||
"maxNumber:65535",
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
"Syslog Mark is required",
|
||||
"Must be a number",
|
||||
"Must be 0 or higher",
|
||||
"Max value is 10",
|
||||
'Syslog Mark is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 10'
|
||||
]}
|
||||
name="syslog_mark_interval"
|
||||
label="Mark Interval seconds (0=off)"
|
||||
@@ -541,7 +542,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
variant="outlined"
|
||||
value={data.syslog_mark_interval}
|
||||
type="number"
|
||||
onChange={handleValueChange("syslog_mark_interval")}
|
||||
onChange={handleValueChange('syslog_mark_interval')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -549,7 +550,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.trace_raw}
|
||||
onChange={handleValueChange("trace_raw")}
|
||||
onChange={handleValueChange('trace_raw')}
|
||||
value="trace_raw"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes';
|
||||
|
||||
export const isConnected = ({ status }: EMSESPStatus) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||
export const isConnected = ({ status }: EMSESPStatus) =>
|
||||
status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||
|
||||
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
||||
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.warning.main;
|
||||
@@ -15,20 +15,20 @@ export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const busStatus = ({ status }: EMSESPStatus) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return "Connected";
|
||||
return 'Connected';
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return "Tx Errors";
|
||||
return 'Tx Errors';
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return "Disconnected";
|
||||
return 'Disconnected';
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const qualityHighlight = (value: number, theme: Theme) => {
|
||||
if (value >= 95) {
|
||||
@@ -36,5 +36,4 @@ export const qualityHighlight = ( value: number, theme: Theme) => {
|
||||
}
|
||||
|
||||
return theme.palette.error.main;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPStatusForm from './EMSESPStatusForm';
|
||||
import { EMSESPStatus } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + "emsespStatus";
|
||||
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'emsespStatus';
|
||||
|
||||
type EMSESPStatusControllerProps = RestControllerProps<EMSESPStatus>;
|
||||
|
||||
class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="EMS Status">
|
||||
<SectionContent title="EMS Status" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <EMSESPStatusForm {...formProps} />}
|
||||
render={(formProps) => <EMSESPStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from "@material-ui/core/styles";
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
@@ -13,35 +13,32 @@ import {
|
||||
ListItemText,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
isWidthDown,
|
||||
} from "@material-ui/core";
|
||||
isWidthDown
|
||||
} from '@material-ui/core';
|
||||
|
||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar,
|
||||
} from "../components";
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
|
||||
import {
|
||||
busStatus,
|
||||
busStatusHighlight,
|
||||
isConnected,
|
||||
} from "./EMSESPStatus";
|
||||
import { busStatus, busStatusHighlight, isConnected } from './EMSESPStatus';
|
||||
|
||||
import { EMSESPStatus } from "./EMSESPtypes";
|
||||
import { EMSESPStatus } from './EMSESPtypes';
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> & WithTheme & WithWidthProps;
|
||||
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> &
|
||||
WithTheme &
|
||||
WithWidthProps;
|
||||
|
||||
class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
|
||||
createListItems() {
|
||||
const { data, theme, width } = this.props;
|
||||
return (
|
||||
@@ -52,24 +49,30 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
<DeviceHubIcon />
|
||||
</HighlightAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Connection Status" secondary={busStatus(data)} />
|
||||
<ListItemText
|
||||
primary="Connection Status"
|
||||
secondary={busStatus(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
{isConnected(data) && (
|
||||
<TableContainer>
|
||||
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||
>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
# Telegrams Received
|
||||
</TableCell>
|
||||
<TableCell align="right">{formatNumber(data.rx_received)} (quality {data.rx_quality}%)
|
||||
<TableCell># Telegrams Received</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatNumber(data.rx_received)} (quality{' '}
|
||||
{data.rx_quality}%)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell >
|
||||
# Telegrams Sent
|
||||
</TableCell >
|
||||
<TableCell align="right">{formatNumber(data.tx_sent)} (quality {data.tx_quality}%)
|
||||
<TableCell># Telegrams Sent</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatNumber(data.tx_sent)} (quality {data.tx_quality}
|
||||
%)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
@@ -86,7 +89,11 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface EMSESPSettings {
|
||||
dallas_parasite: boolean;
|
||||
led_gpio: number;
|
||||
hide_led: boolean;
|
||||
api_enabled: boolean;
|
||||
notoken_api: boolean;
|
||||
analog_enabled: boolean;
|
||||
pbutton_gpio: number;
|
||||
trace_raw: boolean;
|
||||
@@ -58,15 +58,54 @@ export interface EMSESPDevices {
|
||||
sensors: Sensor[];
|
||||
}
|
||||
|
||||
export interface EMSESPDeviceData {
|
||||
name: string;
|
||||
data: string[];
|
||||
export interface DeviceValue {
|
||||
v: any;
|
||||
u: number;
|
||||
n: string;
|
||||
c: string;
|
||||
}
|
||||
|
||||
export interface DeviceValue {
|
||||
id: number;
|
||||
data: string,
|
||||
uom: string,
|
||||
name: string,
|
||||
cmd: string
|
||||
export interface EMSESPDeviceData {
|
||||
name: string;
|
||||
data: DeviceValue[];
|
||||
}
|
||||
|
||||
export enum DeviceValueUOM {
|
||||
NONE = 0,
|
||||
DEGREES,
|
||||
PERCENT,
|
||||
LMIN,
|
||||
KWH,
|
||||
WH,
|
||||
HOURS,
|
||||
MINUTES,
|
||||
UA,
|
||||
BAR,
|
||||
KW,
|
||||
W,
|
||||
KB,
|
||||
SECONDS,
|
||||
DBM,
|
||||
NUM,
|
||||
BOOLEAN
|
||||
}
|
||||
|
||||
export const DeviceValueUOM_s = [
|
||||
'',
|
||||
'°C',
|
||||
'%',
|
||||
'l/min',
|
||||
'kWh',
|
||||
'Wh',
|
||||
'hours',
|
||||
'minutes',
|
||||
'uA',
|
||||
'bar',
|
||||
'kW',
|
||||
'W',
|
||||
'KB',
|
||||
'seconds',
|
||||
'dBm',
|
||||
'number',
|
||||
'on/off'
|
||||
];
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
|
||||
import { Component } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
|
||||
|
||||
import TuneIcon from '@material-ui/icons/Tune';
|
||||
import DashboardIcon from "@material-ui/icons/Dashboard";
|
||||
import DashboardIcon from '@material-ui/icons/Dashboard';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
type ProjectProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
@@ -16,13 +19,28 @@ class ProjectMenu extends Component<ProjectProps> {
|
||||
const path = this.props.match.url;
|
||||
return (
|
||||
<List>
|
||||
<ListItem to='/ems-esp/' selected={path.startsWith('/ems-esp/status') || path.startsWith('/ems-esp/devices') || path.startsWith('/ems-esp/help')} button component={Link}>
|
||||
<ListItem
|
||||
to="/ems-esp/"
|
||||
selected={
|
||||
path.startsWith('/ems-esp/status') ||
|
||||
path.startsWith('/ems-esp/devices') ||
|
||||
path.startsWith('/ems-esp/help')
|
||||
}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DashboardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Dashboard" />
|
||||
</ListItem>
|
||||
<ListItem to='/ems-esp/settings' selected={path.startsWith('/ems-esp/settings')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||
<ListItem
|
||||
to="/ems-esp/settings"
|
||||
selected={path.startsWith('/ems-esp/settings')}
|
||||
button
|
||||
component={Link}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<TuneIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch } from 'react-router';
|
||||
|
||||
import { AuthenticatedRoute } from '../authentication';
|
||||
@@ -7,24 +7,32 @@ import EMSESPDashboard from './EMSESPDashboard';
|
||||
import EMSESPSettings from './EMSESPSettings';
|
||||
|
||||
class ProjectRouting extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/ems-esp/status/*" component={EMSESPDashboard} />
|
||||
<AuthenticatedRoute exact path="/ems-esp/settings" component={EMSESPSettings} />
|
||||
<AuthenticatedRoute exact path="/ems-esp/*" component={EMSESPDashboard} />
|
||||
{
|
||||
/*
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/status/*"
|
||||
component={EMSESPDashboard}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/settings"
|
||||
component={EMSESPSettings}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/*"
|
||||
component={EMSESPDashboard}
|
||||
/>
|
||||
{/*
|
||||
* The redirect below caters for the default project route and redirecting invalid paths.
|
||||
* The "to" property must match one of the routes above for this to work correctly.
|
||||
*/
|
||||
}
|
||||
*/}
|
||||
<Redirect to={`/ems-esp/status`} />
|
||||
</Switch>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ProjectRouting;
|
||||
|
||||
@@ -1,59 +1,116 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, Typography } from '@material-ui/core';
|
||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
Typography,
|
||||
FormHelperText,
|
||||
OutlinedInput,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
MenuItem
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
import { DeviceValue } from './EMSESPtypes';
|
||||
import { DeviceValue, DeviceValueUOM, DeviceValueUOM_s } from './EMSESPtypes';
|
||||
|
||||
interface ValueFormProps {
|
||||
devicevalue: DeviceValue;
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
handleValueChange: (data: keyof DeviceValue) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleValueChange: (
|
||||
data: keyof DeviceValue
|
||||
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
class ValueForm extends React.Component<ValueFormProps> {
|
||||
|
||||
formRef: RefObject<any> = React.createRef();
|
||||
|
||||
submit = () => {
|
||||
this.formRef.current.submit();
|
||||
}
|
||||
|
||||
buildLabel = (devicevalue: DeviceValue) => {
|
||||
if ((devicevalue.uom === "") || (!devicevalue.uom)) {
|
||||
return "New value";
|
||||
}
|
||||
return "New value (" + devicevalue.uom + ")";
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { devicevalue, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
|
||||
const {
|
||||
devicevalue,
|
||||
handleValueChange,
|
||||
onDoneEditing,
|
||||
onCancelEditing
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||
<Dialog maxWidth="xs" onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open>
|
||||
<DialogTitle id="user-form-dialog-title">Change the {devicevalue.name}</DialogTitle>
|
||||
<Dialog
|
||||
maxWidth="xs"
|
||||
onClose={onCancelEditing}
|
||||
aria-labelledby="user-form-dialog-title"
|
||||
open
|
||||
>
|
||||
<DialogTitle id="user-form-dialog-title">Change Value</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextValidator
|
||||
validators={['required']}
|
||||
errorMessages={['is required']}
|
||||
name="data"
|
||||
label={this.buildLabel(devicevalue)}
|
||||
{devicevalue.u !== DeviceValueUOM.BOOLEAN && (
|
||||
<OutlinedInput
|
||||
id="outlined-adornment-value"
|
||||
value={devicevalue.v}
|
||||
autoFocus
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={devicevalue.data}
|
||||
margin="normal"
|
||||
onChange={handleValueChange('data')}
|
||||
onChange={handleValueChange('v')}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
{DeviceValueUOM_s[devicevalue.u]}
|
||||
</InputAdornment>
|
||||
}
|
||||
aria-describedby="outlined-value-helper-text"
|
||||
inputProps={{
|
||||
'aria-label': 'value'
|
||||
}}
|
||||
/>
|
||||
<Box color="warning.main" p={1} pl={0} pr={0} mt={0} mb={0}>
|
||||
)}
|
||||
{devicevalue.u === DeviceValueUOM.BOOLEAN && (
|
||||
<TextField
|
||||
id="outlined-select-value"
|
||||
select
|
||||
value={devicevalue.v}
|
||||
autoFocus
|
||||
fullWidth
|
||||
onChange={handleValueChange('v')}
|
||||
variant="outlined"
|
||||
>
|
||||
<MenuItem value="true">on</MenuItem>
|
||||
<MenuItem value="false">off</MenuItem>
|
||||
</TextField>
|
||||
)}
|
||||
<FormHelperText id="outlined-value-helper-text">
|
||||
{devicevalue.n}
|
||||
</FormHelperText>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={4} mb={0}>
|
||||
<Typography variant="body2">
|
||||
<i>Note: it may take a few seconds before the change is visible. If nothing happens check the logs.</i>
|
||||
<i>
|
||||
Note: it may take a few seconds before the change is
|
||||
registered with the EMS device.
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>Cancel</FormButton>
|
||||
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>Done</FormButton>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onCancelEditing}
|
||||
>
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={this.submit}
|
||||
>
|
||||
Done
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ValidatorForm>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextField } from '@material-ui/core';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
TextField
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
@@ -15,25 +24,34 @@ interface GenerateTokenState {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenState> {
|
||||
|
||||
class GenerateToken extends React.Component<
|
||||
GenerateTokenProps,
|
||||
GenerateTokenState
|
||||
> {
|
||||
state: GenerateTokenState = {};
|
||||
|
||||
componentDidMount() {
|
||||
const { username } = this.props;
|
||||
redirectingAuthorizedFetch(GENERATE_TOKEN_ENDPOINT + "?" + new URLSearchParams({ username }), { method: 'GET' })
|
||||
.then(response => {
|
||||
redirectingAuthorizedFetch(
|
||||
GENERATE_TOKEN_ENDPOINT + '?' + new URLSearchParams({ username }),
|
||||
{ method: 'GET' }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw Error("Error generating token: " + response.status);
|
||||
throw Error('Error generating token: ' + response.status);
|
||||
}
|
||||
}).then(generatedToken => {
|
||||
console.log(generatedToken);
|
||||
})
|
||||
.then((generatedToken) => {
|
||||
// console.log(generatedToken);
|
||||
this.setState({ token: generatedToken.token });
|
||||
})
|
||||
.catch(error => {
|
||||
this.props.enqueueSnackbar(error.message || "Problem generating token", { variant: 'error' });
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || 'Problem generating token',
|
||||
{ variant: 'error' }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,31 +59,56 @@ class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenSta
|
||||
const { onClose, username } = this.props;
|
||||
const { token } = this.state;
|
||||
return (
|
||||
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open fullWidth maxWidth="sm">
|
||||
<DialogTitle id="generate-token-dialog-title">Token for: {username}</DialogTitle>
|
||||
<Dialog
|
||||
onClose={onClose}
|
||||
aria-labelledby="generate-token-dialog-title"
|
||||
open
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle id="generate-token-dialog-title">
|
||||
Token for: {username}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{token ?
|
||||
{token ? (
|
||||
<Fragment>
|
||||
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
|
||||
<Box
|
||||
bgcolor="primary.main"
|
||||
color="primary.contrastText"
|
||||
p={2}
|
||||
mt={2}
|
||||
mb={2}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
The token below may be used to access the secured APIs. This may be used for bearer authentication with the "Authorization" header or using the "access_token" query paramater.
|
||||
The token below may be used to access the secured APIs, either
|
||||
as a Bearer authentication in the "Authorization" header or
|
||||
using the "access_token" query parameter.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mt={2} mb={2}>
|
||||
<TextField label="Token" multiline value={token} fullWidth contentEditable={false} />
|
||||
<TextField
|
||||
label="Token"
|
||||
multiline
|
||||
value={token}
|
||||
fullWidth
|
||||
contentEditable={false}
|
||||
/>
|
||||
</Box>
|
||||
</Fragment>
|
||||
:
|
||||
) : (
|
||||
<Box m={4} textAlign="center">
|
||||
<LinearProgress />
|
||||
<Typography variant="h6">
|
||||
Generating token…
|
||||
</Typography>
|
||||
<Typography variant="h6">Generating token…</Typography>
|
||||
</Box>
|
||||
}
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import ManageUsersForm from './ManageUsersForm';
|
||||
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
|
||||
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
|
||||
|
||||
class ManageUsersController extends Component<ManageUsersControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,14 @@ class ManageUsersController extends Component<ManageUsersControllerProps> {
|
||||
<SectionContent title="Manage Users" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <ManageUsersForm {...formProps} />}
|
||||
render={(formProps) => <ManageUsersForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
|
||||
export default restController(
|
||||
SECURITY_SETTINGS_ENDPOINT,
|
||||
ManageUsersController
|
||||
);
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow, withWidth, WithWidthProps, isWidthDown } from '@material-ui/core';
|
||||
import { Box, Button, Typography, } from '@material-ui/core';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableFooter,
|
||||
TableRow,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
isWidthDown
|
||||
} from '@material-ui/core';
|
||||
import { Box, Button, Typography } from '@material-ui/core';
|
||||
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
@@ -13,8 +23,16 @@ import SaveIcon from '@material-ui/icons/Save';
|
||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||
import VpnKeyIcon from '@material-ui/icons/VpnKey';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
extractEventValue
|
||||
} from '../components';
|
||||
|
||||
import UserForm from './UserForm';
|
||||
import { SecuritySettings, User } from './types';
|
||||
@@ -30,16 +48,20 @@ function compareUsers(a: User, b: User) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
|
||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> &
|
||||
AuthenticatedContextProps &
|
||||
WithWidthProps;
|
||||
|
||||
type ManageUsersFormState = {
|
||||
creating: boolean;
|
||||
user?: User;
|
||||
generateTokenFor?: string;
|
||||
}
|
||||
|
||||
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
||||
};
|
||||
|
||||
class ManageUsersForm extends React.Component<
|
||||
ManageUsersFormProps,
|
||||
ManageUsersFormState
|
||||
> {
|
||||
state: ManageUsersFormState = {
|
||||
creating: false
|
||||
};
|
||||
@@ -48,38 +70,38 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
this.setState({
|
||||
creating: true,
|
||||
user: {
|
||||
username: "",
|
||||
password: "",
|
||||
username: '',
|
||||
password: '',
|
||||
admin: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
uniqueUsername = (username: string) => {
|
||||
return !this.props.data.users.find(u => u.username === username);
|
||||
}
|
||||
return !this.props.data.users.find((u) => u.username === username);
|
||||
};
|
||||
|
||||
noAdminConfigured = () => {
|
||||
return !this.props.data.users.find(u => u.admin);
|
||||
}
|
||||
return !this.props.data.users.find((u) => u.admin);
|
||||
};
|
||||
|
||||
removeUser = (user: User) => {
|
||||
const { data } = this.props;
|
||||
const users = data.users.filter(u => u.username !== user.username);
|
||||
const users = data.users.filter((u) => u.username !== user.username);
|
||||
this.props.setData({ ...data, users });
|
||||
}
|
||||
};
|
||||
|
||||
closeGenerateToken = () => {
|
||||
this.setState({
|
||||
generateTokenFor: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
generateToken = (user: User) => {
|
||||
this.setState({
|
||||
generateTokenFor: user.username
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
startEditingUser = (user: User) => {
|
||||
this.setState({
|
||||
@@ -92,13 +114,13 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
this.setState({
|
||||
user: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
doneEditingUser = () => {
|
||||
const { user } = this.state;
|
||||
if (user) {
|
||||
const { data } = this.props;
|
||||
const users = data.users.filter(u => u.username !== user.username);
|
||||
const users = data.users.filter((u) => u.username !== user.username);
|
||||
users.push(user);
|
||||
this.props.setData({ ...data, users });
|
||||
this.setState({
|
||||
@@ -107,14 +129,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
}
|
||||
};
|
||||
|
||||
handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ user: { ...this.state.user!, [name]: extractEventValue(event) } });
|
||||
handleUserValueChange = (name: keyof User) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({
|
||||
user: { ...this.state.user!, [name]: extractEventValue(event) }
|
||||
});
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.saveData();
|
||||
this.props.authenticatedContext.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width, data } = this.props;
|
||||
@@ -122,7 +148,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
return (
|
||||
<Fragment>
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Username</TableCell>
|
||||
@@ -131,7 +160,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.users.sort(compareUsers).map(user => (
|
||||
{data.users.sort(compareUsers).map((user) => (
|
||||
<TableRow key={user.username}>
|
||||
<TableCell component="th" scope="row">
|
||||
{user.username}
|
||||
@@ -140,13 +169,25 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
{user.admin ? <CheckIcon /> : <CloseIcon />}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconButton size="small" aria-label="Generate Token" onClick={() => this.generateToken(user)}>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Generate Token"
|
||||
onClick={() => this.generateToken(user)}
|
||||
>
|
||||
<VpnKeyIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Delete"
|
||||
onClick={() => this.removeUser(user)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" aria-label="Edit" onClick={() => this.startEditingUser(user)}>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Edit"
|
||||
onClick={() => this.startEditingUser(user)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
@@ -157,34 +198,50 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="center" padding="default">
|
||||
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
|
||||
<Button
|
||||
startIcon={<PersonAddIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.createUser}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
{
|
||||
this.noAdminConfigured() &&
|
||||
(
|
||||
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
|
||||
{this.noAdminConfigured() && (
|
||||
<Box
|
||||
bgcolor="error.main"
|
||||
color="error.contrastText"
|
||||
p={2}
|
||||
mt={2}
|
||||
mb={2}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
You must have at least one admin user configured.
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}>
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={this.noAdminConfigured()}
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
{
|
||||
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
|
||||
}
|
||||
{
|
||||
user &&
|
||||
{generateTokenFor && (
|
||||
<GenerateToken
|
||||
username={generateTokenFor}
|
||||
onClose={this.closeGenerateToken}
|
||||
/>
|
||||
)}
|
||||
{user && (
|
||||
<UserForm
|
||||
user={user}
|
||||
creating={creating}
|
||||
@@ -193,11 +250,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
handleValueChange={this.handleUserValueChange}
|
||||
uniqueUsername={this.uniqueUsername}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(withWidth()(ManageUsersForm));
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||
import {
|
||||
AuthenticatedContextProps,
|
||||
AuthenticatedRoute
|
||||
} from '../authentication';
|
||||
import { MenuAppBar } from '../components';
|
||||
|
||||
import ManageUsersController from './ManageUsersController';
|
||||
@@ -12,25 +15,36 @@ import SecuritySettingsController from './SecuritySettingsController';
|
||||
type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
|
||||
class Security extends Component<SecurityProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Security">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value="/security/users" label="Manage Users" />
|
||||
<Tab value="/security/settings" label="Security Settings" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/security/users" component={ManageUsersController} />
|
||||
<AuthenticatedRoute exact path="/security/settings" component={SecuritySettingsController} />
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/security/users"
|
||||
component={ManageUsersController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/security/settings"
|
||||
component={SecuritySettingsController}
|
||||
/>
|
||||
<Redirect to="/security/users" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||
|
||||
import SecuritySettingsForm from './SecuritySettingsForm';
|
||||
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
|
||||
type SecuritySettingsControllerProps = RestControllerProps<SecuritySettings>;
|
||||
|
||||
class SecuritySettingsController extends Component<SecuritySettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
@@ -19,12 +23,14 @@ class SecuritySettingsController extends Component<SecuritySettingsControllerPro
|
||||
<SectionContent title="Security Settings" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <SecuritySettingsForm {...formProps} />}
|
||||
render={(formProps) => <SecuritySettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(SECURITY_SETTINGS_ENDPOINT, SecuritySettingsController);
|
||||
export default restController(
|
||||
SECURITY_SETTINGS_ENDPOINT,
|
||||
SecuritySettingsController
|
||||
);
|
||||
|
||||
@@ -4,19 +4,27 @@ import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import { RestFormProps, PasswordValidator, FormActions, FormButton } from '../components';
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
import {
|
||||
RestFormProps,
|
||||
PasswordValidator,
|
||||
FormActions,
|
||||
FormButton
|
||||
} from '../components';
|
||||
|
||||
import { SecuritySettings } from './types';
|
||||
|
||||
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
||||
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> &
|
||||
AuthenticatedContextProps;
|
||||
|
||||
class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.saveData();
|
||||
this.props.authenticatedContext.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, handleValueChange } = this.props;
|
||||
@@ -24,7 +32,10 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
<PasswordValidator
|
||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||
errorMessages={['Password Required', 'Password must be 64 characters or less']}
|
||||
errorMessages={[
|
||||
'Password Required',
|
||||
'Password must be 64 characters or less'
|
||||
]}
|
||||
name="jwt_secret"
|
||||
label="Super User Password"
|
||||
fullWidth
|
||||
@@ -33,20 +44,32 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
||||
onChange={handleValueChange('jwt_secret')}
|
||||
margin="normal"
|
||||
/>
|
||||
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
|
||||
<Box
|
||||
bgcolor="primary.main"
|
||||
color="primary.contrastText"
|
||||
p={2}
|
||||
mt={2}
|
||||
mb={2}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
The Super User password is used to sign authentication tokens and is also the Console's `su` password. If you modify this all users will be signed out.
|
||||
The Super User password is used to sign authentication tokens and is
|
||||
also the Console's `su` password. If you modify this all users will
|
||||
be signed out.
|
||||
</Typography>
|
||||
</Box>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(SecuritySettingsForm);
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Checkbox } from '@material-ui/core';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Checkbox
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { PasswordValidator, BlockFormControlLabel, FormButton } from '../components';
|
||||
import {
|
||||
PasswordValidator,
|
||||
BlockFormControlLabel,
|
||||
FormButton
|
||||
} from '../components';
|
||||
|
||||
import { User } from './types';
|
||||
|
||||
@@ -11,33 +21,67 @@ interface UserFormProps {
|
||||
creating: boolean;
|
||||
user: User;
|
||||
uniqueUsername: (value: any) => boolean;
|
||||
handleValueChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleValueChange: (
|
||||
name: keyof User
|
||||
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
}
|
||||
|
||||
class UserForm extends React.Component<UserFormProps> {
|
||||
|
||||
formRef: RefObject<any> = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
|
||||
ValidatorForm.addValidationRule(
|
||||
'uniqueUsername',
|
||||
this.props.uniqueUsername
|
||||
);
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
this.formRef.current.submit();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
|
||||
const {
|
||||
user,
|
||||
creating,
|
||||
handleValueChange,
|
||||
onDoneEditing,
|
||||
onCancelEditing
|
||||
} = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open fullWidth maxWidth="sm">
|
||||
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
|
||||
<Dialog
|
||||
onClose={onCancelEditing}
|
||||
aria-labelledby="user-form-dialog-title"
|
||||
open
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle id="user-form-dialog-title">
|
||||
{creating ? 'Add' : 'Modify'} User
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextValidator
|
||||
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []}
|
||||
errorMessages={creating ? ['Username is required', "Username already exists", "Must be 1-24 characters: alpha numeric, '_' or '.'"] : []}
|
||||
validators={
|
||||
creating
|
||||
? [
|
||||
'required',
|
||||
'uniqueUsername',
|
||||
'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'
|
||||
]
|
||||
: []
|
||||
}
|
||||
errorMessages={
|
||||
creating
|
||||
? [
|
||||
'Username is required',
|
||||
'Username already exists',
|
||||
"Must be 1-24 characters: alpha numeric, '_' or '.'"
|
||||
]
|
||||
: []
|
||||
}
|
||||
name="username"
|
||||
label="Username"
|
||||
fullWidth
|
||||
@@ -49,7 +93,10 @@ class UserForm extends React.Component<UserFormProps> {
|
||||
/>
|
||||
<PasswordValidator
|
||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||
errorMessages={['Password is required', 'Password must be 64 characters or less']}
|
||||
errorMessages={[
|
||||
'Password is required',
|
||||
'Password must be 64 characters or less'
|
||||
]}
|
||||
name="password"
|
||||
label="Password"
|
||||
fullWidth
|
||||
@@ -70,10 +117,19 @@ class UserForm extends React.Component<UserFormProps> {
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onCancelEditing}
|
||||
>
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={this.submit}
|
||||
>
|
||||
Done
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
|
||||
@@ -28,10 +28,7 @@ type Config = {
|
||||
export function register(config?: Config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(
|
||||
process.env.PUBLIC_URL,
|
||||
window.location.href
|
||||
);
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
@@ -65,7 +62,7 @@ export function register(config?: Config) {
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
@@ -101,7 +98,7 @@ function registerValidSW(swUrl: string, config?: Config) {
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
@@ -111,7 +108,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' }
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
@@ -119,7 +116,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
@@ -138,7 +135,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user