mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Merge remote-tracking branch 'origin/tech-upgrade' into dev
This commit is contained in:
25
.github/workflows/pre_release.yml
vendored
25
.github/workflows/pre_release.yml
vendored
@@ -1,24 +1,22 @@
|
|||||||
name: "pre-release"
|
name: 'pre-release'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "dev"
|
- 'dev'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-release:
|
pre-release:
|
||||||
|
name: 'Automatic pre-release build'
|
||||||
name: "Automatic pre-release build"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Get EMS-ESP source code and version
|
- name: Get EMS-ESP source code and version
|
||||||
id: build_info
|
id: build_info
|
||||||
@@ -34,24 +32,23 @@ jobs:
|
|||||||
- name: Build WebUI
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm ci
|
yarn install
|
||||||
npx typesafe-i18n --no-watch
|
yarn run typesafe-i18n --no-watch
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run -e ci
|
||||||
|
|
||||||
- name: Create a GH Release
|
- name: Create a GH Release
|
||||||
id: "automatic_releases"
|
id: 'automatic_releases'
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
title: Development Build v${{steps.build_info.outputs.VERSION}}
|
||||||
automatic_release_tag: "latest"
|
automatic_release_tag: 'latest'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: |
|
files: |
|
||||||
CHANGELOG_LATEST.md
|
CHANGELOG_LATEST.md
|
||||||
./build/firmware/*.*
|
./build/firmware/*.*
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/tagged_release.yml
vendored
26
.github/workflows/tagged_release.yml
vendored
@@ -1,23 +1,21 @@
|
|||||||
name: "tagged-release"
|
name: 'tagged-release'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tagged-release:
|
tagged-release:
|
||||||
|
name: 'Tagged Release'
|
||||||
name: "Tagged Release"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/setup-python@v4
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-node@v3
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '18'
|
||||||
|
|
||||||
- name: Install PlatformIO
|
- name: Install PlatformIO
|
||||||
run: |
|
run: |
|
||||||
@@ -29,19 +27,19 @@ jobs:
|
|||||||
- name: Build WebUI
|
- name: Build WebUI
|
||||||
run: |
|
run: |
|
||||||
cd interface
|
cd interface
|
||||||
npm ci
|
yarn install
|
||||||
npx typesafe-i18n --no-watch
|
yarn run typesafe-i18n --no-watch
|
||||||
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
sed -i "s/= 'pl'/= 'en'/" ./src/i18n/i18n-util.ts
|
||||||
npm run build
|
yarn run build
|
||||||
|
|
||||||
- name: Build firmware
|
- name: Build firmware
|
||||||
run: |
|
run: |
|
||||||
platformio run -e ci
|
platformio run -e ci
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: 'marvinpinto/action-automatic-releases@latest'
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: |
|
files: |
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
|||||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,12 +1,13 @@
|
|||||||
# vscode
|
# vscode
|
||||||
.vscode
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/extensions.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/settings.json
|
||||||
|
|
||||||
# build
|
# c++ compiling
|
||||||
build/
|
|
||||||
.clang_complete
|
.clang_complete
|
||||||
.gcc-flags.json
|
.gcc-flags.json
|
||||||
cppcheck.out.xml
|
cppcheck.out.xml
|
||||||
debug.log
|
|
||||||
|
|
||||||
# platformio
|
# platformio
|
||||||
.pio
|
.pio
|
||||||
@@ -15,18 +16,25 @@ pio_local.ini
|
|||||||
# OS specific
|
# OS specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*Thumbs.db
|
*Thumbs.db
|
||||||
|
|
||||||
# project specfic
|
|
||||||
/scripts/stackdmp.txt
|
|
||||||
emsesp
|
emsesp
|
||||||
|
|
||||||
|
# web specfic
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
/data/www
|
/data/www
|
||||||
/lib/framework/WWWData.h
|
/lib/framework/WWWData.h
|
||||||
/interface/build
|
/interface/build
|
||||||
node_modules
|
node_modules
|
||||||
/interface/.eslintcache
|
/interface/.eslintcache
|
||||||
|
stats.html
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# scripts
|
||||||
test.sh
|
test.sh
|
||||||
|
scripts/run.sh
|
||||||
scripts/__pycache__
|
scripts/__pycache__
|
||||||
.temp
|
/scripts/stackdmp.txt
|
||||||
|
|
||||||
# i18n generated files
|
# i18n generated files
|
||||||
interface/src/i18n/i18n-react.tsx
|
interface/src/i18n/i18n-react.tsx
|
||||||
@@ -40,6 +48,6 @@ interface/src/i18n/i18n-util.async.ts
|
|||||||
sonar/
|
sonar/
|
||||||
build_wrapper_output_directory/
|
build_wrapper_output_directory/
|
||||||
|
|
||||||
# other build files
|
# entity dump results
|
||||||
dump_entities.csv
|
dump_entities.csv
|
||||||
dump_entities.xls*
|
dump_entities.xls*
|
||||||
|
|||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
.prettierrc
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 120
|
"tabWidth": 2,
|
||||||
}
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
23
.vscode/tasks.json
vendored
Normal file
23
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "PlatformIO: Execute EMS-ESP (standalone)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./.pio/build/standalone/program",
|
||||||
|
"linux": {
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
// Workaroung for sdl2 `-m32` crash
|
||||||
|
// https://bugs.launchpad.net/ubuntu/+source/libsdl2/+bug/1775067/comments/7
|
||||||
|
"DBUS_FATAL_WARNINGS": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependsOn": ["PlatformIO: Build EMS-ESP (standalone)"],
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## **IMPORTANT! BREAKING CHANGES**
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
|
- When upgrading to v3.5 for the first time from v3.4 on a BBQKees Gateway board you will need to use the [EMS-EPS Flasher](https://github.com/emsesp/EMS-ESP-Flasher/releases) to correctly re-partition the flash. Make sure you backup the settings and customizations from the WebUI (System->Upload/Download) and restore after the upgrade.
|
||||||
- Support for multiple EMS-ESPs [#759] has been added as an optional setting for MQTT. When enabled, which is now the default, all MQTT Discovery Entity IDs will include the MQTT base name and the shortname of the EMS-ESP device entity. For example what was previously `sensor.boiler_actual_boiler_temperature` will now become `sensor.ems_esp_boiler_boiltemp`. If you still want to use the old format and retain the history and script compatibility in Home Assistant then set this back to the old format.
|
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,23 @@
|
|||||||
|
|
||||||
## **IMPORTANT! BREAKING CHANGES**
|
## **IMPORTANT! BREAKING CHANGES**
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
-
|
- Workaround for better Domoticz MQTT intergration? [#904](https://github.com/emsesp/EMS-ESP32/issues/904)
|
||||||
|
- Warn user in WebUI of unsaved changes [#911](https://github.com/emsesp/EMS-ESP32/issues/911)
|
||||||
|
- Detect old Tado thermostat, device-id 0x19, no entities
|
||||||
|
- Some more HM200 entities [#500](https://github.com/emsesp/EMS-ESP32/issues/500)
|
||||||
|
- Custom Scheduler [#701](https://github.com/emsesp/EMS-ESP32/issues/701)
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
-
|
- HA-discovery for analog sensor commands [#1035](https://github.com/emsesp/EMS-ESP32/issues/1035)
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
-
|
- Optional upgrade to platform-espressif32 6.0.0 (after 5.3.0) [#862](https://github.com/emsesp/EMS-ESP32/issues/862)
|
||||||
|
- Use byte 0 for detection RC30 active heatingcircuit [#786](https://github.com/emsesp/EMS-ESP32/issues/786)
|
||||||
|
- Write repeated selflowtemp if tx-queue is empty without verify [#954](https://github.com/emsesp/EMS-ESP32/issues/954)
|
||||||
|
- HA discovery recreate after disconnect by device [#1067](https://github.com/emsesp/EMS-ESP32/issues/1067)
|
||||||
|
- File upload: check flash size (overflow) instead of filesize
|
||||||
|
- Improved HA Discovery so previous configs no longer need to be removed when starting [#1077](https://github.com/emsesp/EMS-ESP32/pull/1077) (thanks @pswid!)
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -37,10 +37,10 @@ CXX_STANDARD := -std=c++11
|
|||||||
# Defined Symbols
|
# Defined Symbols
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
DEFINES += -DARDUINOJSON_ENABLE_STD_STRING=1 -DARDUINOJSON_ENABLE_PROGMEM=1 -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||||
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_USE_SERIAL
|
DEFINES += -DEMSESP_DEBUG -DEMSESP_STANDALONE -DEMSESP_TEST
|
||||||
DEFINES += $(ARGS)
|
DEFINES += $(ARGS)
|
||||||
|
|
||||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.5.0b11\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.5.0-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Sources & Files
|
# Sources & Files
|
||||||
@@ -79,9 +79,7 @@ CPPFLAGS += -g3
|
|||||||
CPPFLAGS += -Os
|
CPPFLAGS += -Os
|
||||||
|
|
||||||
CFLAGS += $(CPPFLAGS)
|
CFLAGS += $(CPPFLAGS)
|
||||||
CFLAGS += -Wall
|
CFLAGS += -Wall -Wextra -Werror -Wswitch-enum -Wno-unused-parameter -Wno-inconsistent-missing-override -Wno-unused-lambda-capture
|
||||||
CFLAGS += -Wextra
|
|
||||||
CFLAGS += -Wno-unused-parameter
|
|
||||||
|
|
||||||
CXXFLAGS += $(CFLAGS) -MMD
|
CXXFLAGS += $(CFLAGS) -MMD
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# 
|
|
||||||
|
|
||||||
# Firmware Installation
|
|
||||||
|
|
||||||
Follow the instructions in the [documentation](https://emsesp.github.io/docs) on how to install the firmware binaries in the Assets below.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# 
|
|
||||||
|
|
||||||
This is a snapshot of the current "beta" development code and firmware binaries for the ESP32. It has all the latest features and fixes but please be aware that this is still experimental firmware used for testing and thus may contain the odd bug. Use at your own risk and remember to report an issue if you find something unusual.
|
|
||||||
|
|
||||||
# Firmware Installation
|
|
||||||
|
|
||||||
Follow the instructions in the [documentation](https://emsesp.github.io/docs) on how to install the firmware binaries in the Assets below.
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# This enables lint extensions
|
|
||||||
EXTEND_ESLINT=true
|
|
||||||
|
|
||||||
# This is the name of your project. It appears on the sign-in page and in the menu bar.
|
|
||||||
REACT_APP_PROJECT_NAME=EMS-ESP
|
|
||||||
|
|
||||||
# This is the url path your project will be exposed under.
|
|
||||||
REACT_APP_PROJECT_PATH=ems-esp
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
GENERATE_SOURCEMAP=false
|
|
||||||
|
|
||||||
REACT_APP_HOSTED=true
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
GENERATE_SOURCEMAP=false
|
|
||||||
5
interface/.eslintignore
Normal file
5
interface/.eslintignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
.prettierrc
|
||||||
|
.eslintrc.js
|
||||||
|
env.d.ts
|
||||||
50
interface/.eslintrc.js
Normal file
50
interface/.eslintrc.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: ['@typescript-eslint', 'deprecation'],
|
||||||
|
extends: [
|
||||||
|
// By extending from a plugin config, we can get recommended rules without having to add them manually.
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'plugin:jsx-a11y/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
// This disables the formatting rules in ESLint that Prettier is going to be responsible for handling.
|
||||||
|
// Make sure it's always the last config, so it gets the chance to override other configs.
|
||||||
|
'eslint-config-prettier'
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
// Tells eslint-plugin-react to automatically detect the version of React to use.
|
||||||
|
version: 'detect'
|
||||||
|
},
|
||||||
|
// Tells eslint how to resolve imports
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
paths: ['src'],
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx']
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true // always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
project: ['tsconfig.json'],
|
||||||
|
createDefaultProgram: true
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Add your own rules here to override ones from the extended configs.
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'jsx-a11y/no-autofocus': 'off',
|
||||||
|
'react-refresh/only-export-components': 'off',
|
||||||
|
'no-console': 'warn',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'warn',
|
||||||
|
'@typescript-eslint/consistent-type-definitions': ['off', 'type'],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'deprecation/deprecation': 'warn'
|
||||||
|
}
|
||||||
|
};
|
||||||
7
interface/.gitignore
vendored
Normal file
7
interface/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
9
interface/.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
9
interface/.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
873
interface/.yarn/releases/yarn-3.4.1.cjs
vendored
Executable file
873
interface/.yarn/releases/yarn-3.4.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
20
interface/.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
20
interface/.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real eslint/bin/eslint.js your application uses
|
||||||
|
module.exports = absRequire(`eslint/bin/eslint.js`);
|
||||||
20
interface/.yarn/sdks/eslint/lib/api.js
vendored
Normal file
20
interface/.yarn/sdks/eslint/lib/api.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real eslint your application uses
|
||||||
|
module.exports = absRequire(`eslint`);
|
||||||
6
interface/.yarn/sdks/eslint/package.json
vendored
Normal file
6
interface/.yarn/sdks/eslint/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "eslint",
|
||||||
|
"version": "8.34.0-sdk",
|
||||||
|
"main": "./lib/api.js",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
5
interface/.yarn/sdks/integrations.yml
vendored
Normal file
5
interface/.yarn/sdks/integrations.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# This file is automatically generated by @yarnpkg/sdks.
|
||||||
|
# Manual changes might be lost!
|
||||||
|
|
||||||
|
integrations:
|
||||||
|
- vscode
|
||||||
20
interface/.yarn/sdks/prettier/index.js
vendored
Executable file
20
interface/.yarn/sdks/prettier/index.js
vendored
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require prettier/index.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real prettier/index.js your application uses
|
||||||
|
module.exports = absRequire(`prettier/index.js`);
|
||||||
6
interface/.yarn/sdks/prettier/package.json
vendored
Normal file
6
interface/.yarn/sdks/prettier/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "prettier",
|
||||||
|
"version": "2.8.4-sdk",
|
||||||
|
"main": "./index.js",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
20
interface/.yarn/sdks/typescript/bin/tsc
vendored
Executable file
20
interface/.yarn/sdks/typescript/bin/tsc
vendored
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/bin/tsc
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/bin/tsc your application uses
|
||||||
|
module.exports = absRequire(`typescript/bin/tsc`);
|
||||||
20
interface/.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
20
interface/.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/bin/tsserver
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/bin/tsserver your application uses
|
||||||
|
module.exports = absRequire(`typescript/bin/tsserver`);
|
||||||
20
interface/.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
20
interface/.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsc.js your application uses
|
||||||
|
module.exports = absRequire(`typescript/lib/tsc.js`);
|
||||||
223
interface/.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
223
interface/.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const moduleWrapper = tsserver => {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
return tsserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {isAbsolute} = require(`path`);
|
||||||
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
|
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
|
return `${locator.name}@${locator.reference}`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||||
|
// doesn't understand. This layer makes sure to remove the protocol
|
||||||
|
// before forwarding it to TS, and to add it back on all returned paths.
|
||||||
|
|
||||||
|
function toEditorPath(str) {
|
||||||
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
|
// this makes it much easier to work with workspaces that list peer
|
||||||
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
|
// file instances instead of the real ones.
|
||||||
|
//
|
||||||
|
// We only do this to modules owned by the the dependency tree roots.
|
||||||
|
// This avoids breaking the resolution when jumping inside a vendor
|
||||||
|
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||||
|
// errors on react).
|
||||||
|
//
|
||||||
|
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||||
|
if (resolved) {
|
||||||
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
|
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||||
|
str = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str = normalize(str);
|
||||||
|
|
||||||
|
if (str.match(/\.zip\//)) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||||
|
// VSCode only adds it automatically for supported schemes,
|
||||||
|
// so we have to do it manually for the `zip` scheme.
|
||||||
|
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||||
|
//
|
||||||
|
// 2021-10-08: VSCode changed the format in 1.61.
|
||||||
|
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-04-06: VSCode changed the format in 1.66.
|
||||||
|
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-05-06: VSCode changed the format in 1.68
|
||||||
|
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
case `vscode <1.61`: {
|
||||||
|
str = `^zip:${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.66`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.68`: {
|
||||||
|
str = `^/zip${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// To make "go to definition" work,
|
||||||
|
// We have to resolve the actual file system path from virtual path
|
||||||
|
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = resolve(`zipfile:${str}`);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||||
|
// We have to resolve the actual file system path from virtual path,
|
||||||
|
// everything else is up to neovim
|
||||||
|
case `neovim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = `zipfile://${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
str = `zip:${str}`;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromEditorPath(str) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
|
// before `zipfile:`
|
||||||
|
return process.platform === `win32`
|
||||||
|
? str.replace(/^.*zipfile:\//, ``)
|
||||||
|
: str.replace(/^.*zipfile:/, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `neovim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||||
|
return str.replace(/^zipfile:\/\//, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`:
|
||||||
|
default: {
|
||||||
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force enable 'allowLocalPluginLoads'
|
||||||
|
// TypeScript tries to resolve plugins using a path relative to itself
|
||||||
|
// which doesn't work when using the global cache
|
||||||
|
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||||
|
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||||
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
|
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||||
|
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||||
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// And here is the point where we hijack the VSCode <-> TS communications
|
||||||
|
// by adding ourselves in the middle. We locate everything that looks
|
||||||
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
|
const Session = tsserver.server.Session;
|
||||||
|
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||||
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
|
Object.assign(Session.prototype, {
|
||||||
|
onMessage(/** @type {string | object} */ message) {
|
||||||
|
const isStringMessage = typeof message === 'string';
|
||||||
|
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedMessage != null &&
|
||||||
|
typeof parsedMessage === `object` &&
|
||||||
|
parsedMessage.arguments &&
|
||||||
|
typeof parsedMessage.arguments.hostInfo === `string`
|
||||||
|
) {
|
||||||
|
hostInfo = parsedMessage.arguments.hostInfo;
|
||||||
|
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||||
|
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||||
|
// The RegExp from https://semver.org/ but without the caret at the start
|
||||||
|
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
|
) ?? []).map(Number)
|
||||||
|
|
||||||
|
if (major === 1) {
|
||||||
|
if (minor < 61) {
|
||||||
|
hostInfo += ` <1.61`;
|
||||||
|
} else if (minor < 66) {
|
||||||
|
hostInfo += ` <1.66`;
|
||||||
|
} else if (minor < 68) {
|
||||||
|
hostInfo += ` <1.68`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
|
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalOnMessage.call(
|
||||||
|
this,
|
||||||
|
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
send(/** @type {any} */ msg) {
|
||||||
|
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||||
|
return typeof value === `string` ? toEditorPath(value) : value;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tsserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||||
|
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
||||||
223
interface/.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
223
interface/.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const moduleWrapper = tsserver => {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
return tsserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {isAbsolute} = require(`path`);
|
||||||
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
|
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
|
return `${locator.name}@${locator.reference}`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||||
|
// doesn't understand. This layer makes sure to remove the protocol
|
||||||
|
// before forwarding it to TS, and to add it back on all returned paths.
|
||||||
|
|
||||||
|
function toEditorPath(str) {
|
||||||
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
|
// this makes it much easier to work with workspaces that list peer
|
||||||
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
|
// file instances instead of the real ones.
|
||||||
|
//
|
||||||
|
// We only do this to modules owned by the the dependency tree roots.
|
||||||
|
// This avoids breaking the resolution when jumping inside a vendor
|
||||||
|
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||||
|
// errors on react).
|
||||||
|
//
|
||||||
|
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||||
|
if (resolved) {
|
||||||
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
|
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||||
|
str = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str = normalize(str);
|
||||||
|
|
||||||
|
if (str.match(/\.zip\//)) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||||
|
// VSCode only adds it automatically for supported schemes,
|
||||||
|
// so we have to do it manually for the `zip` scheme.
|
||||||
|
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||||
|
//
|
||||||
|
// 2021-10-08: VSCode changed the format in 1.61.
|
||||||
|
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-04-06: VSCode changed the format in 1.66.
|
||||||
|
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-05-06: VSCode changed the format in 1.68
|
||||||
|
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
case `vscode <1.61`: {
|
||||||
|
str = `^zip:${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.66`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.68`: {
|
||||||
|
str = `^/zip${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// To make "go to definition" work,
|
||||||
|
// We have to resolve the actual file system path from virtual path
|
||||||
|
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = resolve(`zipfile:${str}`);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||||
|
// We have to resolve the actual file system path from virtual path,
|
||||||
|
// everything else is up to neovim
|
||||||
|
case `neovim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = `zipfile://${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
str = `zip:${str}`;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromEditorPath(str) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
|
// before `zipfile:`
|
||||||
|
return process.platform === `win32`
|
||||||
|
? str.replace(/^.*zipfile:\//, ``)
|
||||||
|
: str.replace(/^.*zipfile:/, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `neovim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||||
|
return str.replace(/^zipfile:\/\//, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`:
|
||||||
|
default: {
|
||||||
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force enable 'allowLocalPluginLoads'
|
||||||
|
// TypeScript tries to resolve plugins using a path relative to itself
|
||||||
|
// which doesn't work when using the global cache
|
||||||
|
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||||
|
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||||
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
|
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||||
|
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||||
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// And here is the point where we hijack the VSCode <-> TS communications
|
||||||
|
// by adding ourselves in the middle. We locate everything that looks
|
||||||
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
|
const Session = tsserver.server.Session;
|
||||||
|
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||||
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
|
Object.assign(Session.prototype, {
|
||||||
|
onMessage(/** @type {string | object} */ message) {
|
||||||
|
const isStringMessage = typeof message === 'string';
|
||||||
|
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedMessage != null &&
|
||||||
|
typeof parsedMessage === `object` &&
|
||||||
|
parsedMessage.arguments &&
|
||||||
|
typeof parsedMessage.arguments.hostInfo === `string`
|
||||||
|
) {
|
||||||
|
hostInfo = parsedMessage.arguments.hostInfo;
|
||||||
|
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||||
|
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||||
|
// The RegExp from https://semver.org/ but without the caret at the start
|
||||||
|
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
|
) ?? []).map(Number)
|
||||||
|
|
||||||
|
if (major === 1) {
|
||||||
|
if (minor < 61) {
|
||||||
|
hostInfo += ` <1.61`;
|
||||||
|
} else if (minor < 66) {
|
||||||
|
hostInfo += ` <1.66`;
|
||||||
|
} else if (minor < 68) {
|
||||||
|
hostInfo += ` <1.68`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
|
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalOnMessage.call(
|
||||||
|
this,
|
||||||
|
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
send(/** @type {any} */ msg) {
|
||||||
|
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||||
|
return typeof value === `string` ? toEditorPath(value) : value;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tsserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||||
|
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
||||||
20
interface/.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
20
interface/.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/typescript.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/typescript.js your application uses
|
||||||
|
module.exports = absRequire(`typescript/lib/typescript.js`);
|
||||||
6
interface/.yarn/sdks/typescript/package.json
vendored
Normal file
6
interface/.yarn/sdks/typescript/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "typescript",
|
||||||
|
"version": "4.9.5-sdk",
|
||||||
|
"main": "./lib/typescript.js",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
9
interface/.yarnrc.yml
Normal file
9
interface/.yarnrc.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
plugins:
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||||
|
spec: '@yarnpkg/plugin-typescript'
|
||||||
|
|
||||||
|
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
||||||
|
# use these if not using PnP and have node_modules
|
||||||
|
# compressionLevel: 0
|
||||||
|
# nmMode: hardlinks-local
|
||||||
|
# enableGlobalCache: true
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
const ProgmemGenerator = require('./progmem-generator.js');
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = function override(config, env) {
|
|
||||||
const hosted = process.env.REACT_APP_HOSTED;
|
|
||||||
|
|
||||||
if (env === 'production' && !hosted) {
|
|
||||||
// rename the ouput file, we need it's path to be short, for embedded FS
|
|
||||||
config.output.filename = 'js/[id].[chunkhash:4].js';
|
|
||||||
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
|
||||||
|
|
||||||
// take out the manifest plugin
|
|
||||||
config.plugins = config.plugins.filter((plugin) => !(plugin instanceof WebpackManifestPlugin));
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
// don't emit license file
|
|
||||||
const terserPlugin = config.optimization.minimizer.find((plugin) => plugin instanceof TerserPlugin);
|
|
||||||
terserPlugin.options.extractComments = false;
|
|
||||||
|
|
||||||
// build progmem data files
|
|
||||||
config.plugins.push(new ProgmemGenerator({ outputPath: '../lib/framework/WWWData.h', bytesPerLine: 20 }));
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
14
interface/index.html
Normal file
14
interface/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1, minimum-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/css/roboto.css" />
|
||||||
|
<link rel="manifest" href="/app/manifest.json" />
|
||||||
|
<title>EMS-ESP</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17971
interface/package-lock.json
generated
17971
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,104 +1,71 @@
|
|||||||
{
|
{
|
||||||
"name": "EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"version": "3.5.0",
|
"version": "3.6.0",
|
||||||
|
"description": "build EMS-ESP TypeScript WebUI",
|
||||||
|
"homepage": "https://emsesp.github.io/docs",
|
||||||
|
"author": "proddy",
|
||||||
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:3080",
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"build-hosted": "tsc && vite build --mode hosted",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"preview-standalone": "npm-run-all -p preview typesafe-i18n mock-api",
|
||||||
|
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
||||||
|
"standalone": "npm-run-all -p dev typesafe-i18n mock-api",
|
||||||
|
"typesafe-i18n": "typesafe-i18n",
|
||||||
|
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@msgpack/msgpack": "^2.8.0",
|
"@msgpack/msgpack": "^2.8.0",
|
||||||
"@mui/icons-material": "^5.11.9",
|
"@mui/icons-material": "^5.11.9",
|
||||||
"@mui/material": "^5.11.9",
|
"@mui/material": "^5.11.10",
|
||||||
"@table-library/react-table-library": "4.0.25",
|
"@remix-run/router": "^1.3.2",
|
||||||
"@types/lodash": "^4.14.191",
|
"@table-library/react-table-library": "4.0.28",
|
||||||
"@types/node": "^18.14.0",
|
"@types/lodash-es": "^4.17.6",
|
||||||
|
"@types/node": "^18.14.2",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
"axios": "^1.3.3",
|
"axios": "^1.3.4",
|
||||||
|
"history": "^5.3.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
"notistack": "^2.0.8",
|
"notistack": "^2.0.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-rewired": "^2.2.1",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.7.1",
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"typesafe-i18n": "^5.24.1",
|
"typesafe-i18n": "^5.24.1",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"start": "react-app-rewired start",
|
|
||||||
"build": "react-app-rewired build",
|
|
||||||
"test": "react-app-rewired test",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"format": "prettier --write '**/*.{ts,tsx,js,css,json,md}'",
|
|
||||||
"build-hosted": "env-cmd -f .env.hosted npm run build",
|
|
||||||
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
|
|
||||||
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
|
|
||||||
"standalone": "npm-run-all -p start typesafe-i18n mock-api",
|
|
||||||
"lint": "eslint . --ext .ts,.tsx",
|
|
||||||
"typesafe-i18n": "typesafe-i18n"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"eol-last": 1,
|
|
||||||
"react/jsx-closing-bracket-location": 1,
|
|
||||||
"react/jsx-closing-tag-location": 1,
|
|
||||||
"react/jsx-wrap-multilines": 1,
|
|
||||||
"react/jsx-curly-newline": 1,
|
|
||||||
"no-multiple-empty-lines": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"max": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-trailing-spaces": 1,
|
|
||||||
"semi": 1,
|
|
||||||
"no-extra-semi": 1,
|
|
||||||
"react/jsx-max-props-per-line": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"when": "multiline"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"react/jsx-first-prop-new-line": [
|
|
||||||
1,
|
|
||||||
"multiline"
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-shadow": 1,
|
|
||||||
"max-len": [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
"code": 220
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"arrow-parens": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/mime-types": "^2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||||
|
"@typescript-eslint/parser": "^5.53.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||||
|
"eslint": "^8.35.0",
|
||||||
|
"eslint-config-prettier": "^8.6.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.5.3",
|
||||||
|
"eslint-plugin-deprecation": "^1.3.3",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.20",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"http-proxy-middleware": "^2.0.6"
|
"prettier": "^2.8.4",
|
||||||
}
|
"rollup-plugin-visualizer": "^5.9.0",
|
||||||
|
"vite": "^4.1.4",
|
||||||
|
"vite-plugin-svgr": "^2.4.0",
|
||||||
|
"vite-tsconfig-paths": "^4.0.5"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@3.4.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ var zlib = require('zlib');
|
|||||||
var mime = require('mime-types');
|
var mime = require('mime-types');
|
||||||
|
|
||||||
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
|
||||||
|
const INDENT = ' ';
|
||||||
|
|
||||||
function getFilesSync(dir, files = []) {
|
function getFilesSync(dir, files = []) {
|
||||||
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||||
@@ -28,17 +29,16 @@ function cleanAndOpen(path) {
|
|||||||
return createWriteStream(path, { flags: 'w+' });
|
return createWriteStream(path, { flags: 'w+' });
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProgmemGenerator {
|
export default function ProgmemGenerator({ outputPath = './WWWData.h', bytesPerLine = 20 }) {
|
||||||
constructor(options = {}) {
|
return {
|
||||||
const { outputPath, bytesPerLine = 20, indent = ' ', includes = ARDUINO_INCLUDES } = options;
|
name: 'ProgmemGenerator',
|
||||||
this.options = { outputPath, bytesPerLine, indent, includes };
|
writeBundle: () => {
|
||||||
}
|
console.log('Generating ' + outputPath);
|
||||||
|
const includes = ARDUINO_INCLUDES;
|
||||||
apply(compiler) {
|
const indent = INDENT;
|
||||||
compiler.hooks.emit.tapAsync({ name: 'ProgmemGenerator' }, (compilation, callback) => {
|
|
||||||
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
|
||||||
const fileInfo = [];
|
const fileInfo = [];
|
||||||
const writeStream = cleanAndOpen(resolve(compilation.options.context, outputPath));
|
const writeStream = cleanAndOpen(resolve(outputPath));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const writeIncludes = () => {
|
const writeIncludes = () => {
|
||||||
writeStream.write(includes);
|
writeStream.write(includes);
|
||||||
@@ -48,7 +48,8 @@ class ProgmemGenerator {
|
|||||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||||
const mimeType = mime.lookup(relativeFilePath);
|
const mimeType = mime.lookup(relativeFilePath);
|
||||||
var size = 0;
|
var size = 0;
|
||||||
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
writeStream.write('const uint8_t ' + variable + '[] = {');
|
||||||
|
// const zipBuffer = zlib.brotliCompressSync(buffer, { quality: 1 });
|
||||||
const zipBuffer = zlib.gzipSync(buffer);
|
const zipBuffer = zlib.gzipSync(buffer);
|
||||||
zipBuffer.forEach((b) => {
|
zipBuffer.forEach((b) => {
|
||||||
if (!(size % bytesPerLine)) {
|
if (!(size % bytesPerLine)) {
|
||||||
@@ -72,17 +73,18 @@ class ProgmemGenerator {
|
|||||||
|
|
||||||
const writeFiles = () => {
|
const writeFiles = () => {
|
||||||
// process static files
|
// process static files
|
||||||
const buildPath = compilation.options.output.path;
|
const buildPath = resolve('build');
|
||||||
for (const filePath of getFilesSync(buildPath)) {
|
for (const filePath of getFilesSync(buildPath)) {
|
||||||
const readStream = readFileSync(filePath);
|
const readStream = readFileSync(filePath);
|
||||||
const relativeFilePath = relative(buildPath, filePath);
|
const relativeFilePath = relative(buildPath, filePath);
|
||||||
writeFile(relativeFilePath, readStream);
|
writeFile(relativeFilePath, readStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
// process assets
|
// process assets
|
||||||
const { assets } = compilation;
|
// const { assets } = compilation;
|
||||||
Object.keys(assets).forEach((relativeFilePath) => {
|
// Object.keys(assets).forEach((relativeFilePath) => {
|
||||||
writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
// writeFile(relativeFilePath, coherseToBuffer(assets[relativeFilePath].source()));
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateWWWClass = () => {
|
const generateWWWClass = () => {
|
||||||
@@ -109,13 +111,11 @@ ${indent.repeat(2)}}
|
|||||||
writeWWWClass();
|
writeWWWClass();
|
||||||
|
|
||||||
writeStream.on('finish', () => {
|
writeStream.on('finish', () => {
|
||||||
callback();
|
// callback();
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
writeStream.end();
|
writeStream.end();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ProgmemGenerator;
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -1,17 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1, minimum-scale=1"
|
|
||||||
/>
|
|
||||||
<link rel="stylesheet" href="%PUBLIC_URL%/css/roboto.css" />
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/app/manifest.json" />
|
|
||||||
<title>EMS-ESP</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
import { FC, createRef, createContext, useContext, useEffect, useState, RefObject } from 'react';
|
import { FC, createRef, useEffect, useState, RefObject } from 'react';
|
||||||
import { SnackbarProvider } from 'notistack';
|
import { SnackbarProvider } from 'notistack';
|
||||||
|
|
||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
import { FeaturesLoader } from './contexts/features';
|
import CustomTheme from 'CustomTheme';
|
||||||
|
import AppRouting from 'AppRouting';
|
||||||
import CustomTheme from './CustomTheme';
|
|
||||||
import AppRouting from './AppRouting';
|
|
||||||
|
|
||||||
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
import { localStorageDetector } from 'typesafe-i18n/detectors';
|
||||||
import TypesafeI18n from './i18n/i18n-react';
|
import TypesafeI18n from 'i18n/i18n-react';
|
||||||
import { detectLocale } from './i18n/i18n-util';
|
import { detectLocale } from 'i18n/i18n-util';
|
||||||
import { loadLocaleAsync } from './i18n/i18n-util.async';
|
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||||
|
|
||||||
const detectedLocale = detectLocale(localStorageDetector);
|
const detectedLocale = detectLocale(localStorageDetector);
|
||||||
|
|
||||||
@@ -23,10 +21,6 @@ const App: FC = () => {
|
|||||||
notistackRef.current.closeSnackbar(key);
|
notistackRef.current.closeSnackbar(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColorModeContext = createContext({ toggleColorMode: () => {} });
|
|
||||||
|
|
||||||
const colorMode = useContext(ColorModeContext);
|
|
||||||
|
|
||||||
const [wasLoaded, setWasLoaded] = useState(false);
|
const [wasLoaded, setWasLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,26 +30,24 @@ const App: FC = () => {
|
|||||||
if (!wasLoaded) return null;
|
if (!wasLoaded) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColorModeContext.Provider value={colorMode}>
|
<TypesafeI18n locale={detectedLocale}>
|
||||||
<TypesafeI18n locale={detectedLocale}>
|
<CustomTheme>
|
||||||
<CustomTheme>
|
<SnackbarProvider
|
||||||
<SnackbarProvider
|
maxSnack={3}
|
||||||
maxSnack={3}
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
ref={notistackRef}
|
||||||
ref={notistackRef}
|
action={(key) => (
|
||||||
action={(key) => (
|
<IconButton onClick={onClickDismiss(key)} size="small">
|
||||||
<IconButton onClick={onClickDismiss(key)} size="small">
|
<CloseIcon />
|
||||||
<CloseIcon />
|
</IconButton>
|
||||||
</IconButton>
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{/* <FeaturesLoader> */}
|
||||||
<FeaturesLoader>
|
<AppRouting />
|
||||||
<AppRouting />
|
{/* </FeaturesLoader> */}
|
||||||
</FeaturesLoader>
|
</SnackbarProvider>
|
||||||
</SnackbarProvider>
|
</CustomTheme>
|
||||||
</CustomTheme>
|
</TypesafeI18n>
|
||||||
</TypesafeI18n>
|
|
||||||
</ColorModeContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { FC, useContext, useEffect } from 'react';
|
import { FC, useContext, useEffect } from 'react';
|
||||||
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
|
|
||||||
|
import { Route, Routes, Navigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { useSnackbar, VariantType } from 'notistack';
|
import { useSnackbar, VariantType } from 'notistack';
|
||||||
|
|
||||||
import { useI18nContext } from './i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
import { Authentication, AuthenticationContext } from './contexts/authentication';
|
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
||||||
import { FeaturesContext } from './contexts/features';
|
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
||||||
import { RequireAuthenticated, RequireUnauthenticated } from './components';
|
|
||||||
|
|
||||||
import SignIn from './SignIn';
|
import SignIn from 'SignIn';
|
||||||
import AuthenticatedRouting from './AuthenticatedRouting';
|
import AuthenticatedRouting from 'AuthenticatedRouting';
|
||||||
|
|
||||||
interface SecurityRedirectProps {
|
interface SecurityRedirectProps {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -42,7 +43,6 @@ export const RemoveTrailingSlashes = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppRouting: FC = () => {
|
const AppRouting: FC = () => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,16 +51,14 @@ const AppRouting: FC = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
||||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
|
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
|
||||||
{features.security && (
|
<Route
|
||||||
<Route
|
path="/"
|
||||||
path="/"
|
element={
|
||||||
element={
|
<RequireUnauthenticated>
|
||||||
<RequireUnauthenticated>
|
<SignIn />
|
||||||
<SignIn />
|
</RequireUnauthenticated>
|
||||||
</RequireUnauthenticated>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Route
|
<Route
|
||||||
path="/*"
|
path="/*"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import { FC, useCallback, useContext, useEffect } from 'react';
|
import { FC, useCallback, useEffect } from 'react';
|
||||||
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
import { FeaturesContext } from './contexts/features';
|
import * as AuthenticationApi from 'api/authentication';
|
||||||
import * as AuthenticationApi from './api/authentication';
|
import { PROJECT_PATH } from 'api/env';
|
||||||
import { PROJECT_PATH } from './api/env';
|
import { AXIOS } from 'api/endpoints';
|
||||||
import { AXIOS } from './api/endpoints';
|
import { Layout, RequireAdmin } from 'components';
|
||||||
import { Layout, RequireAdmin } from './components';
|
|
||||||
|
|
||||||
import ProjectRouting from './project/ProjectRouting';
|
import ProjectRouting from 'project/ProjectRouting';
|
||||||
|
|
||||||
import NetworkConnection from './framework/network/NetworkConnection';
|
import NetworkConnection from 'framework/network/NetworkConnection';
|
||||||
import AccessPoint from './framework/ap/AccessPoint';
|
import AccessPoint from 'framework/ap/AccessPoint';
|
||||||
import NetworkTime from './framework/ntp/NetworkTime';
|
import NetworkTime from 'framework/ntp/NetworkTime';
|
||||||
import Mqtt from './framework/mqtt/Mqtt';
|
import Mqtt from 'framework/mqtt/Mqtt';
|
||||||
import System from './framework/system/System';
|
import System from 'framework/system/System';
|
||||||
import Security from './framework/security/Security';
|
import Security from 'framework/security/Security';
|
||||||
|
|
||||||
const AuthenticatedRouting: FC = () => {
|
const AuthenticatedRouting: FC = () => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -41,23 +39,21 @@ const AuthenticatedRouting: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
{features.project && <Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />}
|
<Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />
|
||||||
<Route path="/network/*" element={<NetworkConnection />} />
|
<Route path="/network/*" element={<NetworkConnection />} />
|
||||||
<Route path="/ap/*" element={<AccessPoint />} />
|
<Route path="/ap/*" element={<AccessPoint />} />
|
||||||
{features.ntp && <Route path="/ntp/*" element={<NetworkTime />} />}
|
<Route path="/ntp/*" element={<NetworkTime />} />
|
||||||
{features.mqtt && <Route path="/mqtt/*" element={<Mqtt />} />}
|
<Route path="/mqtt/*" element={<Mqtt />} />
|
||||||
{features.security && (
|
<Route
|
||||||
<Route
|
path="/security/*"
|
||||||
path="/security/*"
|
element={
|
||||||
element={
|
<RequireAdmin>
|
||||||
<RequireAdmin>
|
<Security />
|
||||||
<Security />
|
</RequireAdmin>
|
||||||
</RequireAdmin>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Route path="/system/*" element={<System />} />
|
<Route path="/system/*" element={<System />} />
|
||||||
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute(features)} />} />
|
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CssBaseline } from '@mui/material';
|
|||||||
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
import { createTheme, responsiveFontSizes, ThemeProvider } from '@mui/material/styles';
|
||||||
import { blueGrey, blue } from '@mui/material/colors';
|
import { blueGrey, blue } from '@mui/material/colors';
|
||||||
|
|
||||||
import { RequiredChildrenProps } from './utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
const theme = responsiveFontSizes(
|
const theme = responsiveFontSizes(
|
||||||
createTheme({
|
createTheme({
|
||||||
|
|||||||
@@ -5,26 +5,26 @@ import { useSnackbar } from 'notistack';
|
|||||||
import { Box, Fab, Paper, Typography, Button } from '@mui/material';
|
import { Box, Fab, Paper, Typography, Button } from '@mui/material';
|
||||||
import ForwardIcon from '@mui/icons-material/Forward';
|
import ForwardIcon from '@mui/icons-material/Forward';
|
||||||
|
|
||||||
import * as AuthenticationApi from './api/authentication';
|
import * as AuthenticationApi from 'api/authentication';
|
||||||
import { PROJECT_NAME } from './api/env';
|
import { PROJECT_NAME } from 'api/env';
|
||||||
import { AuthenticationContext } from './contexts/authentication';
|
import { AuthenticationContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import { extractErrorMessage, onEnterCallback, updateValue } from './utils';
|
import { extractErrorMessage, onEnterCallback, updateValue } from 'utils';
|
||||||
import { SignInRequest } from './types';
|
import { SignInRequest } from 'types';
|
||||||
import { ValidatedTextField } from './components';
|
import { ValidatedTextField } from 'components';
|
||||||
import { SIGN_IN_REQUEST_VALIDATOR, validate } from './validators';
|
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
|
||||||
|
|
||||||
import { I18nContext } from './i18n/i18n-react';
|
import { I18nContext } from 'i18n/i18n-react';
|
||||||
import type { Locales } from './i18n/i18n-types';
|
import type { Locales } from 'i18n/i18n-types';
|
||||||
import { loadLocaleAsync } from './i18n/i18n-util.async';
|
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||||
|
|
||||||
import { ReactComponent as NLflag } from './i18n/NL.svg';
|
import { ReactComponent as NLflag } from 'i18n/NL.svg';
|
||||||
import { ReactComponent as DEflag } from './i18n/DE.svg';
|
import { ReactComponent as DEflag } from 'i18n/DE.svg';
|
||||||
import { ReactComponent as GBflag } from './i18n/GB.svg';
|
import { ReactComponent as GBflag } from 'i18n/GB.svg';
|
||||||
import { ReactComponent as SVflag } from './i18n/SV.svg';
|
import { ReactComponent as SVflag } from 'i18n/SV.svg';
|
||||||
import { ReactComponent as PLflag } from './i18n/PL.svg';
|
import { ReactComponent as PLflag } from 'i18n/PL.svg';
|
||||||
import { ReactComponent as NOflag } from './i18n/NO.svg';
|
import { ReactComponent as NOflag } from 'i18n/NO.svg';
|
||||||
import { ReactComponent as FRflag } from './i18n/FR.svg';
|
import { ReactComponent as FRflag } from 'i18n/FR.svg';
|
||||||
|
|
||||||
const SignIn: FC = () => {
|
const SignIn: FC = () => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
@@ -93,11 +93,10 @@ const SignIn: FC = () => {
|
|||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
paddingTop: '200px',
|
paddingTop: '172px',
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: '50% ' + theme.spacing(2),
|
backgroundPosition: '50% ' + theme.spacing(2),
|
||||||
backgroundSize: 'auto 150px',
|
|
||||||
width: '100%'
|
width: '100%'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import { APSettings, APStatus } from '../types';
|
import { APSettings, APStatus } from 'types';
|
||||||
import { AXIOS } from './endpoints';
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
export function readAPStatus(): AxiosPromise<APStatus> {
|
export function readAPStatus(): AxiosPromise<APStatus> {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as H from 'history';
|
|||||||
import jwtDecode from 'jwt-decode';
|
import jwtDecode from 'jwt-decode';
|
||||||
import { Path } from 'react-router-dom';
|
import { Path } from 'react-router-dom';
|
||||||
|
|
||||||
import { Features, Me, SignInRequest, SignInResponse } from '../types';
|
import { Me, SignInRequest, SignInResponse } from 'types';
|
||||||
|
|
||||||
import { ACCESS_TOKEN, AXIOS } from './endpoints';
|
import { ACCESS_TOKEN, AXIOS } from './endpoints';
|
||||||
import { PROJECT_PATH } from './env';
|
import { PROJECT_PATH } from './env';
|
||||||
@@ -11,7 +11,7 @@ import { PROJECT_PATH } from './env';
|
|||||||
export const SIGN_IN_PATHNAME = 'loginPathname';
|
export const SIGN_IN_PATHNAME = 'loginPathname';
|
||||||
export const SIGN_IN_SEARCH = 'loginSearch';
|
export const SIGN_IN_SEARCH = 'loginSearch';
|
||||||
|
|
||||||
export const getDefaultRoute = (features: Features) => (features.project ? `/${PROJECT_PATH}` : '/wifi');
|
export const getDefaultRoute = `/${PROJECT_PATH}`;
|
||||||
|
|
||||||
export function verifyAuthorization(): AxiosPromise<void> {
|
export function verifyAuthorization(): AxiosPromise<void> {
|
||||||
return AXIOS.get('/verifyAuthorization');
|
return AXIOS.get('/verifyAuthorization');
|
||||||
@@ -40,12 +40,12 @@ export function clearLoginRedirect() {
|
|||||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect(features: Features): Partial<Path> {
|
export function fetchLoginRedirect(): Partial<Path> {
|
||||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
return {
|
return {
|
||||||
pathname: signInPathname || getDefaultRoute(features),
|
pathname: signInPathname || `/${PROJECT_PATH}`,
|
||||||
search: (signInPathname && signInSearch) || undefined
|
search: (signInPathname && signInSearch) || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME || 'EMS-ESP';
|
export const PROJECT_NAME = 'EMS-ESP';
|
||||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH || 'project';
|
export const PROJECT_PATH = 'project';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import { Features } from '../types';
|
import { Features } from 'types';
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import { AxiosPromise } from 'axios';
|
||||||
import { MqttSettings, MqttStatus } from '../types';
|
import { MqttSettings, MqttStatus } from 'types';
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import { WiFiNetworkList, NetworkSettings, NetworkStatus } from '../types';
|
import { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types';
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import { AxiosPromise } from 'axios';
|
||||||
import { NTPSettings, NTPStatus, Time } from '../types';
|
import { NTPSettings, NTPStatus, Time } from 'types';
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import { SecuritySettings, Token } from '../types';
|
import { SecuritySettings, Token } from 'types';
|
||||||
|
|
||||||
import { AXIOS } from './endpoints';
|
import { AXIOS } from './endpoints';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AxiosPromise } from 'axios';
|
import { AxiosPromise } from 'axios';
|
||||||
|
|
||||||
import { OTASettings, SystemStatus, LogSettings, LogEntries } from '../types';
|
import { OTASettings, SystemStatus, LogSettings, LogEntries } from 'types';
|
||||||
|
|
||||||
import { AXIOS, AXIOS_BIN, FileUploadConfig, startUploadFile } from './endpoints';
|
import { AXIOS, AXIOS_BIN, FileUploadConfig, startUploadFile } from './endpoints';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FC } from 'react';
|
|||||||
|
|
||||||
import { Paper, Divider } from '@mui/material';
|
import { Paper, Divider } from '@mui/material';
|
||||||
|
|
||||||
import { RequiredChildrenProps } from '../utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
interface SectionContentProps extends RequiredChildrenProps {
|
interface SectionContentProps extends RequiredChildrenProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from './upload';
|
|||||||
export { default as SectionContent } from './SectionContent';
|
export { default as SectionContent } from './SectionContent';
|
||||||
export { default as ButtonRow } from './ButtonRow';
|
export { default as ButtonRow } from './ButtonRow';
|
||||||
export { default as MessageBox } from './MessageBox';
|
export { default as MessageBox } from './MessageBox';
|
||||||
|
export { default as BlockNavigation } from './routing/BlockNavigation';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useLocation } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Box, Toolbar } from '@mui/material';
|
import { Box, Toolbar } from '@mui/material';
|
||||||
|
|
||||||
import { PROJECT_NAME } from '../../api/env';
|
import { PROJECT_NAME } from 'api/env';
|
||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
import LayoutDrawer from './LayoutDrawer';
|
import LayoutDrawer from './LayoutDrawer';
|
||||||
import LayoutAppBar from './LayoutAppBar';
|
import LayoutAppBar from './LayoutAppBar';
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { FC, useContext } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
|
import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
|
||||||
import LayoutAuthMenu from './LayoutAuthMenu';
|
import LayoutAuthMenu from './LayoutAuthMenu';
|
||||||
|
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 240;
|
export const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
interface LayoutAppBarProps {
|
interface LayoutAppBarProps {
|
||||||
@@ -15,8 +13,6 @@ interface LayoutAppBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
@@ -41,7 +37,7 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
|||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
{features.security && <LayoutAuthMenu />}
|
<LayoutAuthMenu />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ import {
|
|||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||||
|
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import { I18nContext } from '../../i18n/i18n-react';
|
import { I18nContext } from 'i18n/i18n-react';
|
||||||
import type { Locales } from '../../i18n/i18n-types';
|
import type { Locales } from 'i18n/i18n-types';
|
||||||
import { loadLocaleAsync } from '../../i18n/i18n-util.async';
|
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||||
|
|
||||||
import { ReactComponent as NLflag } from '../../i18n/NL.svg';
|
import { ReactComponent as NLflag } from 'i18n/NL.svg';
|
||||||
import { ReactComponent as DEflag } from '../../i18n/DE.svg';
|
import { ReactComponent as DEflag } from 'i18n/DE.svg';
|
||||||
import { ReactComponent as GBflag } from '../../i18n/GB.svg';
|
import { ReactComponent as GBflag } from 'i18n/GB.svg';
|
||||||
import { ReactComponent as SVflag } from '../../i18n/SV.svg';
|
import { ReactComponent as SVflag } from 'i18n/SV.svg';
|
||||||
import { ReactComponent as PLflag } from '../../i18n/PL.svg';
|
import { ReactComponent as PLflag } from 'i18n/PL.svg';
|
||||||
import { ReactComponent as NOflag } from '../../i18n/NO.svg';
|
import { ReactComponent as NOflag } from 'i18n/NO.svg';
|
||||||
import { ReactComponent as FRflag } from '../../i18n/FR.svg';
|
import { ReactComponent as FRflag } from 'i18n/FR.svg';
|
||||||
|
|
||||||
const ItemTypography = styled(Typography)<TypographyProps>({
|
const ItemTypography = styled(Typography)<TypographyProps>({
|
||||||
maxWidth: '250px',
|
maxWidth: '250px',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FC } from 'react';
|
|||||||
|
|
||||||
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
||||||
|
|
||||||
import { PROJECT_NAME } from '../../api/env';
|
import { PROJECT_NAME } from 'api/env';
|
||||||
|
|
||||||
import LayoutMenu from './LayoutMenu';
|
import LayoutMenu from './LayoutMenu';
|
||||||
import { DRAWER_WIDTH } from './Layout';
|
import { DRAWER_WIDTH } from './Layout';
|
||||||
|
|||||||
@@ -9,32 +9,28 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
|||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||||
|
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
import ProjectMenu from 'project/ProjectMenu';
|
||||||
import ProjectMenu from '../../project/ProjectMenu';
|
|
||||||
|
|
||||||
import LayoutMenuItem from './LayoutMenuItem';
|
import LayoutMenuItem from './LayoutMenuItem';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const LayoutMenu: FC = () => {
|
const LayoutMenu: FC = () => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{features.project && (
|
<List disablePadding component="nav">
|
||||||
<List disablePadding component="nav">
|
<ProjectMenu />
|
||||||
<ProjectMenu />
|
<Divider />
|
||||||
<Divider />
|
</List>
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
<List disablePadding component="nav">
|
<List disablePadding component="nav">
|
||||||
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
||||||
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
||||||
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />}
|
<LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />
|
||||||
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
|
<LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />
|
||||||
<LayoutMenuItem
|
<LayoutMenuItem
|
||||||
icon={LockIcon}
|
icon={LockIcon}
|
||||||
label={LL.SECURITY(0)}
|
label={LL.SECURITY(0)}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Link, useLocation } from 'react-router-dom';
|
|||||||
|
|
||||||
import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material';
|
import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material';
|
||||||
|
|
||||||
import { grey } from '@mui/material/colors';
|
import { routeMatches } from 'utils';
|
||||||
|
|
||||||
import { routeMatches } from '../../utils';
|
import { grey } from '@mui/material/colors';
|
||||||
|
|
||||||
interface LayoutMenuItemProps {
|
interface LayoutMenuItemProps {
|
||||||
icon: React.ComponentType<SvgIconProps>;
|
icon: React.ComponentType<SvgIconProps>;
|
||||||
@@ -17,13 +17,15 @@ interface LayoutMenuItemProps {
|
|||||||
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
|
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const selected = routeMatches(to, pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem disablePadding selected={routeMatches(to, pathname)}>
|
<ListItem disablePadding>
|
||||||
<ListItemButton component={Link} to={to} disabled={disabled}>
|
<ListItemButton component={Link} to={to} disabled={disabled} selected={selected}>
|
||||||
<ListItemIcon sx={{ color: grey[500] }}>
|
<ListItemIcon sx={{ color: selected ? '#90caf9' : grey[500] }}>
|
||||||
<Icon />
|
<Icon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{label}</ListItemText>
|
<ListItemText sx={{ color: selected ? '#90caf9' : grey[100] }}>{label}</ListItemText>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { FC } from 'react';
|
|||||||
import { Box, Button, CircularProgress, Typography } from '@mui/material';
|
import { Box, Button, CircularProgress, Typography } from '@mui/material';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
|
|
||||||
import { MessageBox } from '..';
|
import { MessageBox } from 'components';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
interface FormLoaderProps {
|
interface FormLoaderProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FC } from 'react';
|
|||||||
|
|
||||||
import { CircularProgress, Box, Typography, Theme } from '@mui/material';
|
import { CircularProgress, Box, Typography, Theme } from '@mui/material';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
interface LoadingSpinnerProps {
|
interface LoadingSpinnerProps {
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
|||||||
30
interface/src/components/routing/BlockNavigation.tsx
Normal file
30
interface/src/components/routing/BlockNavigation.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import type { Blocker } from '@remix-run/router';
|
||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
||||||
|
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
|
interface BlockNavigationProps {
|
||||||
|
blocker: Blocker;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={blocker.state === 'blocked'}>
|
||||||
|
<DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle>
|
||||||
|
<DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" onClick={() => blocker.reset?.()} color="secondary">
|
||||||
|
{LL.STAY()}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={() => blocker.proceed?.()} color="primary">
|
||||||
|
{LL.LEAVE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockNavigation;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FC, useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
AuthenticatedContext,
|
AuthenticatedContext,
|
||||||
AuthenticatedContextValue,
|
AuthenticatedContextValue,
|
||||||
AuthenticationContext
|
AuthenticationContext
|
||||||
} from '../../contexts/authentication/context';
|
} from 'contexts/authentication/context';
|
||||||
import { storeLoginRedirect } from '../../api/authentication';
|
import { storeLoginRedirect } from 'api/authentication';
|
||||||
|
|
||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { FC, useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import * as AuthenticationApi from '../../api/authentication';
|
import * as AuthenticationApi from 'api/authentication';
|
||||||
import { AuthenticationContext } from '../../contexts/authentication';
|
import { AuthenticationContext } from 'contexts/authentication';
|
||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
|
||||||
|
|
||||||
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
|
||||||
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect(features)} /> : <>{children}</>;
|
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect()} /> : <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireUnauthenticated;
|
export default RequireUnauthenticated;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Tabs, useMediaQuery, useTheme } from '@mui/material';
|
import { Tabs, useMediaQuery, useTheme } from '@mui/material';
|
||||||
|
|
||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
|
|
||||||
interface RouterTabsProps extends RequiredChildrenProps {
|
interface RouterTabsProps extends RequiredChildrenProps {
|
||||||
value: string | false;
|
value: string | false;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Box, Button, LinearProgress, Theme, Typography, useTheme } from '@mui/m
|
|||||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const getBorderColor = (theme: Theme, props: DropzoneState) => {
|
const getBorderColor = (theme: Theme, props: DropzoneState) => {
|
||||||
if (props.isDragAccept) {
|
if (props.isDragAccept) {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import axios, { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios';
|
import axios, { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
import { extractErrorMessage } from '../../utils';
|
import { extractErrorMessage } from 'utils';
|
||||||
import { FileUploadConfig } from '../../api/endpoints';
|
import { FileUploadConfig } from 'api/endpoints';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
interface MediaUploadOptions {
|
interface MediaUploadOptions {
|
||||||
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { FC, useCallback, useContext, useEffect, useState } from 'react';
|
import { FC, useCallback, useEffect, useState } from 'react';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
import * as AuthenticationApi from '../../api/authentication';
|
import * as AuthenticationApi from 'api/authentication';
|
||||||
import { ACCESS_TOKEN } from '../../api/endpoints';
|
import { ACCESS_TOKEN } from 'api/endpoints';
|
||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from 'utils';
|
||||||
import { LoadingSpinner } from '../../components';
|
import { LoadingSpinner } from 'components';
|
||||||
import { Me } from '../../types';
|
import { Me } from 'types';
|
||||||
import { FeaturesContext } from '../features';
|
|
||||||
import { AuthenticationContext } from './context';
|
import { AuthenticationContext } from './context';
|
||||||
|
|
||||||
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -43,11 +41,6 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!features.security) {
|
|
||||||
setMe({ admin: true, username: 'admin' });
|
|
||||||
setInitialized(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
try {
|
try {
|
||||||
@@ -62,7 +55,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
}
|
}
|
||||||
}, [features]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { Me } from '../../types';
|
import { Me } from 'types';
|
||||||
|
|
||||||
export interface AuthenticationContextValue {
|
export interface AuthenticationContextValue {
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FC, useCallback, useEffect, useState } from 'react';
|
import { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import * as FeaturesApi from '../../api/features';
|
import * as FeaturesApi from 'api/features';
|
||||||
|
|
||||||
import { extractErrorMessage, RequiredChildrenProps } from '../../utils';
|
import { extractErrorMessage, RequiredChildrenProps } from 'utils';
|
||||||
import { Features } from '../../types';
|
import { Features } from 'types';
|
||||||
import { ApplicationError, LoadingSpinner } from '../../components';
|
import { ApplicationError, LoadingSpinner } from 'components';
|
||||||
|
|
||||||
import { FeaturesContext } from '.';
|
import { FeaturesContext } from '.';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
import { Features } from '../../types';
|
import { Features } from 'types';
|
||||||
|
|
||||||
export interface FeaturesContextValue {
|
export interface FeaturesContextValue {
|
||||||
features: Features;
|
features: Features;
|
||||||
|
|||||||
@@ -1,41 +1,44 @@
|
|||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { ValidateFieldsError } from 'async-validator';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash-es';
|
||||||
|
|
||||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import { createAPSettingsValidator, validate } from '../../validators';
|
import { createAPSettingsValidator, validate } from 'validators';
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField
|
ValidatedTextField,
|
||||||
} from '../../components';
|
BlockNavigation
|
||||||
|
} from 'components';
|
||||||
|
|
||||||
import { APProvisionMode, APSettings } from '../../types';
|
import { APProvisionMode, APSettings } from 'types';
|
||||||
import { numberValue, updateValue, useRest } from '../../utils';
|
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||||
import * as APApi from '../../api/ap';
|
import * as APApi from 'api/ap';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||||
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
|
||||||
};
|
};
|
||||||
|
|
||||||
const APSettingsForm: FC = () => {
|
const APSettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<APSettings>({
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
read: APApi.readAPSettings,
|
useRest<APSettings>({
|
||||||
update: APApi.updateAPSettings
|
read: APApi.readAPSettings,
|
||||||
});
|
update: APApi.updateAPSettings
|
||||||
|
});
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = updateValue(setData);
|
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -163,24 +166,37 @@ const APSettingsForm: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ButtonRow>
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
<Button
|
<ButtonRow>
|
||||||
startIcon={<SaveIcon />}
|
<Button
|
||||||
disabled={saving}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
disabled={saving}
|
||||||
color="primary"
|
variant="outlined"
|
||||||
type="submit"
|
color="primary"
|
||||||
onClick={validateAndSubmit}
|
type="submit"
|
||||||
>
|
onClick={() => loadData()}
|
||||||
{LL.SAVE()}
|
>
|
||||||
</Button>
|
{LL.CANCEL()}
|
||||||
</ButtonRow>
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
disabled={saving}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
|
onClick={validateAndSubmit}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
|||||||
import ComputerIcon from '@mui/icons-material/Computer';
|
import ComputerIcon from '@mui/icons-material/Computer';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
|
|
||||||
import * as APApi from '../../api/ap';
|
import * as APApi from 'api/ap';
|
||||||
import { APNetworkStatus, APStatus } from '../../types';
|
import { APNetworkStatus, APStatus } from 'types';
|
||||||
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||||
import { useRest } from '../../utils';
|
import { useRest } from 'utils';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { Navigate, Routes, Route } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
|
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import APStatusForm from './APStatusForm';
|
import APStatusForm from './APStatusForm';
|
||||||
import APSettingsForm from './APSettingsForm';
|
import APSettingsForm from './APSettingsForm';
|
||||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const AccessPoint: FC = () => {
|
const AccessPoint: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
@@ -27,6 +27,7 @@ const AccessPoint: FC = () => {
|
|||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<APStatusForm />} />
|
<Route path="status" element={<APStatusForm />} />
|
||||||
|
<Route index element={<Navigate to="status" />} />
|
||||||
<Route
|
<Route
|
||||||
path="settings"
|
path="settings"
|
||||||
element={
|
element={
|
||||||
@@ -35,6 +36,7 @@ const AccessPoint: FC = () => {
|
|||||||
</RequireAdmin>
|
</RequireAdmin>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* <Route path="/*" element={<Navigate to="status" />} /> */}
|
||||||
<Route path="/*" element={<Navigate replace to="status" />} />
|
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
|
|
||||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import MqttStatusForm from './MqttStatusForm';
|
import MqttStatusForm from './MqttStatusForm';
|
||||||
import MqttSettingsForm from './MqttSettingsForm';
|
import MqttSettingsForm from './MqttSettingsForm';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const Mqtt: FC = () => {
|
const Mqtt: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|||||||
@@ -2,34 +2,38 @@ import { FC, useState } from 'react';
|
|||||||
import { ValidateFieldsError } from 'async-validator';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
|
|
||||||
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
|
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
|
||||||
|
|
||||||
import { createMqttSettingsValidator, validate } from '../../validators';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
|
import { createMqttSettingsValidator, validate } from 'validators';
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField
|
ValidatedTextField,
|
||||||
} from '../../components';
|
BlockNavigation
|
||||||
import { MqttSettings } from '../../types';
|
} from 'components';
|
||||||
import { numberValue, updateValue, useRest } from '../../utils';
|
import { MqttSettings } from 'types';
|
||||||
import * as MqttApi from '../../api/mqtt';
|
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||||
|
import * as MqttApi from 'api/mqtt';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const MqttSettingsForm: FC = () => {
|
const MqttSettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<MqttSettings>({
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
read: MqttApi.readMqttSettings,
|
useRest<MqttSettings>({
|
||||||
update: MqttApi.updateMqttSettings
|
read: MqttApi.readMqttSettings,
|
||||||
});
|
update: MqttApi.updateMqttSettings
|
||||||
|
});
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
const updateFormValue = updateValue(setData);
|
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -223,6 +227,21 @@ const MqttSettingsForm: FC = () => {
|
|||||||
{data.ha_enabled && (
|
{data.ha_enabled && (
|
||||||
<>
|
<>
|
||||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||||
|
<Grid item>
|
||||||
|
<ValidatedTextField
|
||||||
|
name="entity_format"
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_5()}
|
||||||
|
value={data.discovery_type}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>Home Assistant</MenuItem>
|
||||||
|
<MenuItem value={1}>Domoticz</MenuItem>
|
||||||
|
</ValidatedTextField>
|
||||||
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
name="discovery_prefix"
|
name="discovery_prefix"
|
||||||
@@ -372,24 +391,38 @@ const MqttSettingsForm: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ButtonRow>
|
|
||||||
<Button
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
startIcon={<SaveIcon />}
|
<ButtonRow>
|
||||||
disabled={saving}
|
<Button
|
||||||
variant="outlined"
|
startIcon={<CancelIcon />}
|
||||||
color="primary"
|
disabled={saving}
|
||||||
type="submit"
|
variant="outlined"
|
||||||
onClick={validateAndSubmit}
|
color="primary"
|
||||||
>
|
type="submit"
|
||||||
{LL.SAVE()}
|
onClick={() => loadData()}
|
||||||
</Button>
|
>
|
||||||
</ButtonRow>
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
disabled={saving}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
|
onClick={validateAndSubmit}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import ReportIcon from '@mui/icons-material/Report';
|
|||||||
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
|
||||||
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||||
|
|
||||||
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||||
import { MqttStatus, MqttDisconnectReason } from '../../types';
|
import { MqttStatus, MqttDisconnectReason } from 'types';
|
||||||
import * as MqttApi from '../../api/mqtt';
|
import * as MqttApi from 'api/mqtt';
|
||||||
import { useRest } from '../../utils';
|
import { useRest } from 'utils';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, { FC, useCallback, useContext, useState } from 'react';
|
import { FC, useCallback, useContext, useState } from 'react';
|
||||||
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
|
import { Navigate, Routes, Route, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
|
|
||||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||||
import { WiFiNetwork } from '../../types';
|
import { WiFiNetwork } from 'types';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||||
import NetworkStatusForm from './NetworkStatusForm';
|
import NetworkStatusForm from './NetworkStatusForm';
|
||||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||||
import NetworkSettingsForm from './NetworkSettingsForm';
|
import NetworkSettingsForm from './NetworkSettingsForm';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const NetworkConnection: FC = () => {
|
const NetworkConnection: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
|
|
||||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
@@ -28,20 +29,21 @@ import {
|
|||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField,
|
ValidatedTextField,
|
||||||
MessageBox
|
MessageBox,
|
||||||
} from '../../components';
|
BlockNavigation
|
||||||
import { NetworkSettings } from '../../types';
|
} from 'components';
|
||||||
import * as NetworkApi from '../../api/network';
|
import { NetworkSettings } from 'types';
|
||||||
import { numberValue, updateValue, useRest } from '../../utils';
|
import * as NetworkApi from 'api/network';
|
||||||
import * as EMSESP from '../../project/api';
|
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||||
|
import * as EMSESP from 'project/api';
|
||||||
|
|
||||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||||
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
|
||||||
import { ValidateFieldsError } from 'async-validator';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
import { validate } from '../../validators';
|
import { validate } from 'validators';
|
||||||
import { createNetworkSettingsValidator } from '../../validators/network';
|
import { createNetworkSettingsValidator } from 'validators/network';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import RestartMonitor from '../system/RestartMonitor';
|
import RestartMonitor from '../system/RestartMonitor';
|
||||||
|
|
||||||
const WiFiSettingsForm: FC = () => {
|
const WiFiSettingsForm: FC = () => {
|
||||||
@@ -52,7 +54,19 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
|
|
||||||
const [initialized, setInitialized] = useState(false);
|
const [initialized, setInitialized] = useState(false);
|
||||||
const [restarting, setRestarting] = useState(false);
|
const [restarting, setRestarting] = useState(false);
|
||||||
const { loadData, saving, data, setData, saveData, errorMessage, restartNeeded } = useRest<NetworkSettings>({
|
const {
|
||||||
|
loadData,
|
||||||
|
saving,
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
origData,
|
||||||
|
dirtyFlags,
|
||||||
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
|
saveData,
|
||||||
|
errorMessage,
|
||||||
|
restartNeeded
|
||||||
|
} = useRest<NetworkSettings>({
|
||||||
read: NetworkApi.readNetworkSettings,
|
read: NetworkApi.readNetworkSettings,
|
||||||
update: NetworkApi.updateNetworkSettings
|
update: NetworkApi.updateNetworkSettings
|
||||||
});
|
});
|
||||||
@@ -78,7 +92,7 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [initialized, setInitialized, data, setData, selectedNetwork]);
|
}, [initialized, setInitialized, data, setData, selectedNetwork]);
|
||||||
|
|
||||||
const updateFormValue = updateValue(setData);
|
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -287,17 +301,28 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
)}
|
)}
|
||||||
{!restartNeeded && (
|
|
||||||
|
{!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<SaveIcon />}
|
startIcon={<CancelIcon />}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
onClick={() => loadData()}
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
disabled={saving}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
onClick={validateAndSubmit}
|
onClick={validateAndSubmit}
|
||||||
>
|
>
|
||||||
{LL.SAVE()}
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
)}
|
||||||
@@ -307,6 +332,7 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <RestartMonitor /> : content()}
|
{restarting ? <RestartMonitor /> : content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import DnsIcon from '@mui/icons-material/Dns';
|
|||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import RouterIcon from '@mui/icons-material/Router';
|
import RouterIcon from '@mui/icons-material/Router';
|
||||||
|
|
||||||
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||||
import { NetworkConnectionStatus, NetworkStatus } from '../../types';
|
import { NetworkConnectionStatus, NetworkStatus } from 'types';
|
||||||
import * as NetworkApi from '../../api/network';
|
import * as NetworkApi from 'api/network';
|
||||||
import { useRest } from '../../utils';
|
import { useRest } from 'utils';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const isConnected = ({ status }: NetworkStatus) =>
|
const isConnected = ({ status }: NetworkStatus) =>
|
||||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { WiFiNetwork } from '../../types';
|
import { WiFiNetwork } from 'types';
|
||||||
|
|
||||||
export interface WiFiConnectionContextValue {
|
export interface WiFiConnectionContextValue {
|
||||||
selectedNetwork?: WiFiNetwork;
|
selectedNetwork?: WiFiNetwork;
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { useSnackbar } from 'notistack';
|
|||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||||
|
|
||||||
import * as NetworkApi from '../../api/network';
|
import * as NetworkApi from 'api/network';
|
||||||
import { WiFiNetwork, WiFiNetworkList } from '../../types';
|
import { WiFiNetwork, WiFiNetworkList } from 'types';
|
||||||
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||||
|
|
||||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const NUM_POLLS = 10;
|
const NUM_POLLS = 10;
|
||||||
const POLLING_FREQUENCY = 500;
|
const POLLING_FREQUENCY = 500;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import LockOpenIcon from '@mui/icons-material/LockOpen';
|
|||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import WifiIcon from '@mui/icons-material/Wifi';
|
import WifiIcon from '@mui/icons-material/Wifi';
|
||||||
|
|
||||||
import { MessageBox } from '../../components';
|
import { MessageBox } from 'components';
|
||||||
|
|
||||||
import { WiFiEncryptionType, WiFiNetwork, WiFiNetworkList } from '../../types';
|
import { WiFiEncryptionType, WiFiNetwork, WiFiNetworkList } from 'types';
|
||||||
|
|
||||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
interface WiFiNetworkSelectorProps {
|
interface WiFiNetworkSelectorProps {
|
||||||
networkList: WiFiNetworkList;
|
networkList: WiFiNetworkList;
|
||||||
|
|||||||
@@ -2,27 +2,36 @@ import { FC, useState } from 'react';
|
|||||||
import { ValidateFieldsError } from 'async-validator';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
|
|
||||||
import { Button, Checkbox, MenuItem } from '@mui/material';
|
import { Button, Checkbox, MenuItem } from '@mui/material';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import { validate } from '../../validators';
|
import { validate } from 'validators';
|
||||||
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
|
import {
|
||||||
import { NTPSettings } from '../../types';
|
BlockFormControlLabel,
|
||||||
import { updateValue, useRest } from '../../utils';
|
ButtonRow,
|
||||||
import * as NTPApi from '../../api/ntp';
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
ValidatedTextField,
|
||||||
|
BlockNavigation
|
||||||
|
} from 'components';
|
||||||
|
import { NTPSettings } from 'types';
|
||||||
|
import { updateValueDirty, useRest } from 'utils';
|
||||||
|
import * as NTPApi from 'api/ntp';
|
||||||
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
|
||||||
import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp';
|
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const NTPSettingsForm: FC = () => {
|
const NTPSettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NTPSettings>({
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
read: NTPApi.readNTPSettings,
|
useRest<NTPSettings>({
|
||||||
update: NTPApi.updateNTPSettings
|
read: NTPApi.readNTPSettings,
|
||||||
});
|
update: NTPApi.updateNTPSettings
|
||||||
|
});
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const updateFormValue = updateValue(setData);
|
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -79,24 +88,37 @@ const NTPSettingsForm: FC = () => {
|
|||||||
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
|
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
|
||||||
{timeZoneSelectItems()}
|
{timeZoneSelectItems()}
|
||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
<ButtonRow>
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
<Button
|
<ButtonRow>
|
||||||
startIcon={<SaveIcon />}
|
<Button
|
||||||
disabled={saving}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
disabled={saving}
|
||||||
color="primary"
|
variant="outlined"
|
||||||
type="submit"
|
color="primary"
|
||||||
onClick={validateAndSubmit}
|
type="submit"
|
||||||
>
|
onClick={() => loadData()}
|
||||||
{LL.SAVE()}
|
>
|
||||||
</Button>
|
{LL.CANCEL()}
|
||||||
</ButtonRow>
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
disabled={saving}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
|
onClick={validateAndSubmit}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ import UpdateIcon from '@mui/icons-material/Update';
|
|||||||
import DnsIcon from '@mui/icons-material/Dns';
|
import DnsIcon from '@mui/icons-material/Dns';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import * as NTPApi from '../../api/ntp';
|
import * as NTPApi from 'api/ntp';
|
||||||
import { NTPStatus, NTPSyncStatus } from '../../types';
|
import { NTPStatus, NTPSyncStatus } from 'types';
|
||||||
import { ButtonRow, FormLoader, SectionContent } from '../../components';
|
import { ButtonRow, FormLoader, SectionContent } from 'components';
|
||||||
import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from '../../utils';
|
import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from 'utils';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
|
||||||
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
|
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
|
||||||
@@ -72,7 +72,7 @@ const NTPStatusForm: FC = () => {
|
|||||||
const ntpStatus = ({ status }: NTPStatus) => {
|
const ntpStatus = ({ status }: NTPStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NTPSyncStatus.NTP_DISABLED:
|
case NTPSyncStatus.NTP_DISABLED:
|
||||||
return LL.DISABLED(0);
|
return LL.NOT_ENABLED();
|
||||||
case NTPSyncStatus.NTP_INACTIVE:
|
case NTPSyncStatus.NTP_INACTIVE:
|
||||||
return LL.INACTIVE(0);
|
return LL.INACTIVE(0);
|
||||||
case NTPSyncStatus.NTP_ACTIVE:
|
case NTPSyncStatus.NTP_ACTIVE:
|
||||||
@@ -127,9 +127,8 @@ const NTPStatusForm: FC = () => {
|
|||||||
onClick={configureTime}
|
onClick={configureTime}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
color="primary"
|
color="primary"
|
||||||
autoFocus
|
|
||||||
>
|
>
|
||||||
{LL.SAVE()}
|
{LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, { FC, useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
|
|
||||||
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
|
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from 'components';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import NTPStatusForm from './NTPStatusForm';
|
import NTPStatusForm from './NTPStatusForm';
|
||||||
import NTPSettingsForm from './NTPSettingsForm';
|
import NTPSettingsForm from './NTPSettingsForm';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const NetworkTime: FC = () => {
|
const NetworkTime: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import {
|
|||||||
|
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
import { extractErrorMessage } from '../../utils';
|
import { extractErrorMessage } from 'utils';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
import { MessageBox } from '../../components';
|
import { MessageBox } from 'components';
|
||||||
import * as SecurityApi from '../../api/security';
|
import * as SecurityApi from 'api/security';
|
||||||
import { Token } from '../../types';
|
import { Token } from 'types';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
interface GenerateTokenProps {
|
interface GenerateTokenProps {
|
||||||
username?: string;
|
username?: string;
|
||||||
|
|||||||
@@ -9,18 +9,17 @@ import CheckIcon from '@mui/icons-material/Check';
|
|||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
||||||
|
|
||||||
import { Table } from '@table-library/react-table-library/table';
|
|
||||||
import { useTheme } from '@table-library/react-table-library/theme';
|
import { useTheme } from '@table-library/react-table-library/theme';
|
||||||
import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
|
||||||
|
|
||||||
import * as SecurityApi from '../../api/security';
|
import * as SecurityApi from 'api/security';
|
||||||
import { SecuritySettings, User } from '../../types';
|
import { SecuritySettings, User } from 'types';
|
||||||
import { ButtonRow, FormLoader, MessageBox, SectionContent } from '../../components';
|
import { ButtonRow, FormLoader, MessageBox, SectionContent } from 'components';
|
||||||
import { createUserValidator } from '../../validators';
|
import { createUserValidator } from 'validators';
|
||||||
import { useRest } from '../../utils';
|
import { useRest } from 'utils';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
import GenerateToken from './GenerateToken';
|
import GenerateToken from './GenerateToken';
|
||||||
import UserForm from './UserForm';
|
import UserForm from './UserForm';
|
||||||
@@ -185,7 +184,7 @@ const ManageUsersForm: FC = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
{LL.SAVE()}
|
{LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { Navigate, Routes, Route } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
|
|
||||||
import { RouterTabs, useRouterTab, useLayoutTitle } from '../../components';
|
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
|
||||||
|
|
||||||
import SecuritySettingsForm from './SecuritySettingsForm';
|
import SecuritySettingsForm from './SecuritySettingsForm';
|
||||||
import ManageUsersForm from './ManageUsersForm';
|
import ManageUsersForm from './ManageUsersForm';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const Security: FC = () => {
|
const Security: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|||||||
@@ -2,28 +2,31 @@ import { FC, useContext, useState } from 'react';
|
|||||||
import { ValidateFieldsError } from 'async-validator';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
|
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import * as SecurityApi from '../../api/security';
|
import * as SecurityApi from 'api/security';
|
||||||
import { SecuritySettings } from '../../types';
|
import { SecuritySettings } from 'types';
|
||||||
import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField } from '../../components';
|
import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField, BlockNavigation } from 'components';
|
||||||
import { SECURITY_SETTINGS_VALIDATOR, validate } from '../../validators';
|
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
|
||||||
import { updateValue, useRest } from '../../utils';
|
import { updateValueDirty, useRest } from 'utils';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const SecuritySettingsForm: FC = () => {
|
const SecuritySettingsForm: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<SecuritySettings>({
|
const { loadData, saving, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, saveData, errorMessage } =
|
||||||
read: SecurityApi.readSecuritySettings,
|
useRest<SecuritySettings>({
|
||||||
update: SecurityApi.updateSecuritySettings
|
read: SecurityApi.readSecuritySettings,
|
||||||
});
|
update: SecurityApi.updateSecuritySettings
|
||||||
|
});
|
||||||
|
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
const updateFormValue = updateValue(setData);
|
|
||||||
|
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -54,24 +57,37 @@ const SecuritySettingsForm: FC = () => {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
|
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
|
||||||
<ButtonRow>
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
<Button
|
<ButtonRow>
|
||||||
startIcon={<SaveIcon />}
|
<Button
|
||||||
disabled={saving}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
disabled={saving}
|
||||||
color="primary"
|
variant="outlined"
|
||||||
type="submit"
|
color="primary"
|
||||||
onClick={validateAndSubmit}
|
type="submit"
|
||||||
>
|
onClick={() => loadData()}
|
||||||
{LL.SAVE()}
|
>
|
||||||
</Button>
|
{LL.CANCEL()}
|
||||||
</ButtonRow>
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
disabled={saving}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
|
onClick={validateAndSubmit}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import SaveIcon from '@mui/icons-material/Save';
|
|||||||
|
|
||||||
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
||||||
|
|
||||||
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from '../../components';
|
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components';
|
||||||
import { User } from '../../types';
|
import { User } from 'types';
|
||||||
import { updateValue } from '../../utils';
|
import { updateValue } from 'utils';
|
||||||
import { validate } from '../../validators';
|
import { validate } from 'validators';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
interface UserFormProps {
|
interface UserFormProps {
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
@@ -93,9 +93,8 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={validateAndDone}
|
onClick={validateAndDone}
|
||||||
color="primary"
|
color="primary"
|
||||||
autoFocus
|
|
||||||
>
|
>
|
||||||
{creating ? LL.ADD(0) : LL.SAVE()}
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ import { AxiosPromise } from 'axios';
|
|||||||
|
|
||||||
import { Typography, Button, Box } from '@mui/material';
|
import { Typography, Button, Box } from '@mui/material';
|
||||||
|
|
||||||
import { FileUploadConfig } from '../../api/endpoints';
|
import { FileUploadConfig } from 'api/endpoints';
|
||||||
|
|
||||||
import { SingleUpload, useFileUpload } from '../../components';
|
import { SingleUpload, useFileUpload } from 'components';
|
||||||
|
|
||||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
|
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
import { extractErrorMessage } from '../../utils';
|
import { extractErrorMessage } from 'utils';
|
||||||
|
|
||||||
import * as EMSESP from '../../project/api';
|
import * as EMSESP from 'project/api';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
interface UploadFileProps {
|
interface UploadFileProps {
|
||||||
uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
|
||||||
@@ -69,6 +69,19 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadSchedule = async () => {
|
||||||
|
try {
|
||||||
|
const response = await EMSESP.readSchedule();
|
||||||
|
if (response.status !== 200) {
|
||||||
|
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
|
||||||
|
} else {
|
||||||
|
saveFile(response.data, 'schedule');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!uploading && (
|
{!uploading && (
|
||||||
@@ -112,7 +125,15 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => downloadCustomizations()}
|
onClick={() => downloadCustomizations()}
|
||||||
>
|
>
|
||||||
{LL.CUSTOMIZATION()}
|
{LL.CUSTOMIZATIONS()}
|
||||||
|
</Button>
|
||||||
|
<Box color="warning.main">
|
||||||
|
<Typography mt={2} mb={1} variant="body2">
|
||||||
|
{LL.DOWNLOAD_SCHEDULE_TEXT()}{' '}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => downloadSchedule()}>
|
||||||
|
{LL.SCHEDULE()}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,36 +1,39 @@
|
|||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
|
|
||||||
import { Button, Checkbox } from '@mui/material';
|
import { Button, Checkbox } from '@mui/material';
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import * as SystemApi from '../../api/system';
|
import * as SystemApi from 'api/system';
|
||||||
import {
|
import {
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField
|
ValidatedTextField,
|
||||||
} from '../../components';
|
BlockNavigation
|
||||||
|
} from 'components';
|
||||||
|
|
||||||
import { OTASettings } from '../../types';
|
import { OTASettings } from 'types';
|
||||||
import { numberValue, updateValue, useRest } from '../../utils';
|
import { numberValue, updateValueDirty, useRest } from 'utils';
|
||||||
|
|
||||||
import { ValidateFieldsError } from 'async-validator';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
import { validate } from '../../validators';
|
import { validate } from 'validators';
|
||||||
import { OTA_SETTINGS_VALIDATOR } from '../../validators/system';
|
import { OTA_SETTINGS_VALIDATOR } from 'validators/system';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const OTASettingsForm: FC = () => {
|
const OTASettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<OTASettings>({
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
read: SystemApi.readOTASettings,
|
useRest<OTASettings>({
|
||||||
update: SystemApi.updateOTASettings
|
read: SystemApi.readOTASettings,
|
||||||
});
|
update: SystemApi.updateOTASettings
|
||||||
|
});
|
||||||
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const updateFormValue = updateValue(setData);
|
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
|
|
||||||
@@ -76,24 +79,37 @@ const OTASettingsForm: FC = () => {
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<ButtonRow>
|
{dirtyFlags && dirtyFlags.length !== 0 && (
|
||||||
<Button
|
<ButtonRow>
|
||||||
startIcon={<SaveIcon />}
|
<Button
|
||||||
disabled={saving}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
disabled={saving}
|
||||||
color="primary"
|
variant="outlined"
|
||||||
type="submit"
|
color="primary"
|
||||||
onClick={validateAndSubmit}
|
type="submit"
|
||||||
>
|
onClick={() => loadData()}
|
||||||
{LL.SAVE()}
|
>
|
||||||
</Button>
|
{LL.CANCEL()}
|
||||||
</ButtonRow>
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<WarningIcon color="warning" />}
|
||||||
|
disabled={saving}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
type="submit"
|
||||||
|
onClick={validateAndSubmit}
|
||||||
|
>
|
||||||
|
{LL.APPLY_CHANGES(dirtyFlags.length)}
|
||||||
|
</Button>
|
||||||
|
</ButtonRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { FC, useRef, useState, useEffect } from 'react';
|
||||||
import { FC, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import * as SystemApi from '../../api/system';
|
import * as SystemApi from 'api/system';
|
||||||
import { FormLoader } from '../../components';
|
import { FormLoader } from 'components';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const RESTART_TIMEOUT = 2 * 60 * 1000;
|
const RESTART_TIMEOUT = 2 * 60 * 1000;
|
||||||
const POLL_TIMEOUT = 2000;
|
const POLL_TIMEOUT = 2000;
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import React, { FC, useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tab } from '@mui/material';
|
import { Tab } from '@mui/material';
|
||||||
|
|
||||||
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from '../../components';
|
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from 'components';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
|
||||||
import UploadFileForm from './UploadFileForm';
|
import UploadFileForm from './UploadFileForm';
|
||||||
import SystemStatusForm from './SystemStatusForm';
|
import SystemStatusForm from './SystemStatusForm';
|
||||||
import OTASettingsForm from './OTASettingsForm';
|
import OTASettingsForm from './OTASettingsForm';
|
||||||
|
|
||||||
import SystemLog from './SystemLog';
|
import SystemLog from './SystemLog';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const System: FC = () => {
|
const System: FC = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
@@ -20,7 +19,6 @@ const System: FC = () => {
|
|||||||
useLayoutTitle(LL.SYSTEM(0));
|
useLayoutTitle(LL.SYSTEM(0));
|
||||||
|
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const { me } = useContext(AuthenticatedContext);
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const { routerTab } = useRouterTab();
|
const { routerTab } = useRouterTab();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,33 +26,28 @@ const System: FC = () => {
|
|||||||
<RouterTabs value={routerTab}>
|
<RouterTabs value={routerTab}>
|
||||||
<Tab value="status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
|
<Tab value="status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
|
||||||
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
|
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
|
||||||
|
<Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />
|
||||||
{features.ota && <Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />}
|
<Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />
|
||||||
{features.upload_firmware && <Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />}
|
|
||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<SystemStatusForm />} />
|
<Route path="status" element={<SystemStatusForm />} />
|
||||||
<Route path="log" element={<SystemLog />} />
|
<Route path="log" element={<SystemLog />} />
|
||||||
{features.ota && (
|
<Route
|
||||||
<Route
|
path="ota"
|
||||||
path="ota"
|
element={
|
||||||
element={
|
<RequireAdmin>
|
||||||
<RequireAdmin>
|
<OTASettingsForm />
|
||||||
<OTASettingsForm />
|
</RequireAdmin>
|
||||||
</RequireAdmin>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
)}
|
path="upload"
|
||||||
{features.upload_firmware && (
|
element={
|
||||||
<Route
|
<RequireAdmin>
|
||||||
path="upload"
|
<UploadFileForm />
|
||||||
element={
|
</RequireAdmin>
|
||||||
<RequireAdmin>
|
}
|
||||||
<UploadFileForm />
|
/>
|
||||||
</RequireAdmin>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Route path="/*" element={<Navigate replace to="status" />} />
|
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import { FC, useState, useEffect, useCallback, useLayoutEffect } from 'react';
|
|||||||
|
|
||||||
import { Box, styled, Button, Checkbox, MenuItem, Grid, Slider, FormLabel } from '@mui/material';
|
import { Box, styled, Button, Checkbox, MenuItem, Grid, Slider, FormLabel } from '@mui/material';
|
||||||
|
|
||||||
import * as SystemApi from '../../api/system';
|
import * as SystemApi from 'api/system';
|
||||||
import { addAccessTokenParameter } from '../../api/authentication';
|
import { addAccessTokenParameter } from 'api/authentication';
|
||||||
|
|
||||||
import { SectionContent, FormLoader, BlockFormControlLabel, ValidatedTextField } from '../../components';
|
import { SectionContent, FormLoader, BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||||
|
|
||||||
import { LogSettings, LogEntry, LogEntries, LogLevel } from '../../types';
|
import { LogSettings, LogEntry, LogEntries, LogLevel } from 'types';
|
||||||
import { updateValue, useRest, extractErrorMessage } from '../../utils';
|
import { updateValue, useRest, extractErrorMessage } from 'utils';
|
||||||
|
|
||||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
|
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
import { EVENT_SOURCE_ROOT } from '../../api/endpoints';
|
import { EVENT_SOURCE_ROOT } from 'api/endpoints';
|
||||||
|
|
||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
|
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
|
||||||
|
|
||||||
@@ -73,7 +73,6 @@ const SystemLog: FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const [reconnectTimeout, setReconnectTimeout] = useState<NodeJS.Timeout>();
|
|
||||||
const [logEntries, setLogEntries] = useState<LogEntries>({ events: [] });
|
const [logEntries, setLogEntries] = useState<LogEntries>({ events: [] });
|
||||||
const [lastIndex, setLastIndex] = useState<number>(0);
|
const [lastIndex, setLastIndex] = useState<number>(0);
|
||||||
|
|
||||||
@@ -138,7 +137,7 @@ const SystemLog: FC = () => {
|
|||||||
|
|
||||||
const onDownload = () => {
|
const onDownload = () => {
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let i of logEntries.events) {
|
for (const i of logEntries.events) {
|
||||||
result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
|
result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
|
||||||
}
|
}
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -162,7 +161,7 @@ const SystemLog: FC = () => {
|
|||||||
|
|
||||||
const fetchLog = useCallback(async () => {
|
const fetchLog = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLogEntries((await SystemApi.readLogEntries()).data);
|
await SystemApi.readLogEntries();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
|
||||||
}
|
}
|
||||||
@@ -176,20 +175,15 @@ const SystemLog: FC = () => {
|
|||||||
const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL));
|
const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL));
|
||||||
es.onmessage = onMessage;
|
es.onmessage = onMessage;
|
||||||
es.onerror = () => {
|
es.onerror = () => {
|
||||||
if (reconnectTimeout) {
|
es.close();
|
||||||
es.close();
|
reloadPage();
|
||||||
setReconnectTimeout(setTimeout(reloadPage, 1000));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
es.close();
|
es.close();
|
||||||
if (reconnectTimeout) {
|
|
||||||
clearTimeout(reconnectTimeout);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [reconnectTimeout]);
|
}, []);
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user