all: sync with master, upd chlog
This commit is contained in:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
'name': 'build'
|
'name': 'build'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.23.6'
|
'GO_VERSION': '1.24.1'
|
||||||
'NODE_VERSION': '16'
|
'NODE_VERSION': '18'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
'push':
|
'push':
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
'name': 'lint'
|
'name': 'lint'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.23.6'
|
'GO_VERSION': '1.24.1'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
'push':
|
'push':
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,6 +19,10 @@
|
|||||||
/agh-backup/
|
/agh-backup/
|
||||||
/bin/
|
/bin/
|
||||||
/build/*
|
/build/*
|
||||||
|
/client/blob-report/
|
||||||
|
/client/playwright-report/
|
||||||
|
/client/playwright/.cache/
|
||||||
|
/client/test-results/
|
||||||
/data/
|
/data/
|
||||||
/dist/
|
/dist/
|
||||||
/filtering/tests/filtering.TestLotsOfRules*.pprof
|
/filtering/tests/filtering.TestLotsOfRules*.pprof
|
||||||
|
|||||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -9,11 +9,11 @@ The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/
|
|||||||
<!--
|
<!--
|
||||||
## [v0.108.0] – TBA
|
## [v0.108.0] – TBA
|
||||||
|
|
||||||
## [v0.107.58] - 2025-03-11 (APPROX.)
|
## [v0.107.59] - 2025-04-01 (APPROX.)
|
||||||
|
|
||||||
See also the [v0.107.58 GitHub milestone][ms-v0.107.58].
|
See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
|
||||||
|
|
||||||
[ms-v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/milestone/93?closed=1
|
[ms-v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/milestone/94?closed=1
|
||||||
|
|
||||||
NOTE: Add new changes BELOW THIS COMMENT.
|
NOTE: Add new changes BELOW THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
@@ -22,7 +22,33 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## [v0.107.57] - 2025-02-19
|
## [v0.107.58] - 2025-03-13
|
||||||
|
|
||||||
|
See also the [v0.107.58 GitHub milestone][ms-v0.107.58].
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.1][go-1.24.1].
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Invalid ICMPv6 Router Advertisement messages ([#7547]).
|
||||||
|
|
||||||
|
- Disabled button for autofilled login form.
|
||||||
|
|
||||||
|
- Formatting of elapsed times less than one millisecond.
|
||||||
|
|
||||||
|
- Changes to global upstream DNS settings not applying to custom client upstream configurations.
|
||||||
|
|
||||||
|
- The formatting of large numbers in the clients tables on the *Client settings* page ([#7583]).
|
||||||
|
|
||||||
|
[#7547]: https://github.com/AdguardTeam/AdGuardHome/issues/7547
|
||||||
|
[#7583]: https://github.com/AdguardTeam/AdGuardHome/issues/7583
|
||||||
|
|
||||||
|
[go-1.24.1]: https://groups.google.com/g/golang-announce/c/4t3lzH3I0eI
|
||||||
|
[ms-v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/milestone/93?closed=1
|
||||||
|
|
||||||
|
## [v0.107.57] - 2025-02-20
|
||||||
|
|
||||||
See also the [v0.107.57 GitHub milestone][ms-v0.107.57].
|
See also the [v0.107.57 GitHub milestone][ms-v0.107.57].
|
||||||
|
|
||||||
@@ -41,6 +67,7 @@ See also the [v0.107.57 GitHub milestone][ms-v0.107.57].
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- The hostnames of DHCP clients not being shown in the *Top clients* table on the dashboard ([#7627]).
|
- The hostnames of DHCP clients not being shown in the *Top clients* table on the dashboard ([#7627]).
|
||||||
|
|
||||||
- The formatting of large numbers in the upstream table and query log ([#7590]).
|
- The formatting of large numbers in the upstream table and query log ([#7590]).
|
||||||
|
|
||||||
[#7590]: https://github.com/AdguardTeam/AdGuardHome/issues/7590
|
[#7590]: https://github.com/AdguardTeam/AdGuardHome/issues/7590
|
||||||
@@ -3017,11 +3044,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
|||||||
[ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1
|
[ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...HEAD
|
||||||
[v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58
|
[v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...HEAD
|
||||||
|
[v0.107.58]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...v0.107.58
|
||||||
[v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...v0.107.57
|
[v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...v0.107.57
|
||||||
[v0.107.56]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.55...v0.107.56
|
[v0.107.56]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.55...v0.107.56
|
||||||
[v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.54...v0.107.55
|
[v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.54...v0.107.55
|
||||||
|
|||||||
15
Makefile
15
Makefile
@@ -27,13 +27,12 @@ DIST_DIR = dist
|
|||||||
GOAMD64 = v1
|
GOAMD64 = v1
|
||||||
GOPROXY = https://proxy.golang.org|direct
|
GOPROXY = https://proxy.golang.org|direct
|
||||||
GOTELEMETRY = off
|
GOTELEMETRY = off
|
||||||
GOTOOLCHAIN = go1.23.6
|
GOTOOLCHAIN = go1.24.1
|
||||||
GPG_KEY = devteam@adguard.com
|
GPG_KEY = devteam@adguard.com
|
||||||
GPG_KEY_PASSPHRASE = not-a-real-password
|
GPG_KEY_PASSPHRASE = not-a-real-password
|
||||||
NPM = npm
|
NPM = npm
|
||||||
NPM_FLAGS = --prefix $(CLIENT_DIR)
|
NPM_FLAGS = --prefix $(CLIENT_DIR)
|
||||||
NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress --ignore-engines\
|
NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress
|
||||||
--ignore-optional --ignore-platform --ignore-scripts
|
|
||||||
RACE = 0
|
RACE = 0
|
||||||
REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
|
REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
|
||||||
SIGN = 1
|
SIGN = 1
|
||||||
@@ -105,10 +104,12 @@ build-docker: ; $(ENV) "$(SHELL)" ./scripts/make/build-docker.sh
|
|||||||
build-release: $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT))
|
build-release: $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT))
|
||||||
$(ENV) "$(SHELL)" ./scripts/make/build-release.sh
|
$(ENV) "$(SHELL)" ./scripts/make/build-release.sh
|
||||||
|
|
||||||
js-build: ; $(NPM) $(NPM_FLAGS) run build-prod
|
js-build: ; $(NPM) $(NPM_FLAGS) run build-prod
|
||||||
js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
||||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
js-typecheck: ; $(NPM) $(NPM_FLAGS) run typecheck
|
||||||
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||||
|
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
||||||
|
js-test-e2e: ; $(NPM) $(NPM_FLAGS) run test:e2e
|
||||||
|
|
||||||
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
go-bench: ; $(ENV) "$(SHELL)" ./scripts/make/go-bench.sh
|
||||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -290,6 +290,22 @@ When you need to debug the frontend without recompiling the production version e
|
|||||||
[targ-docker]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-dockersh-build-a-multi-architecture-docker-image
|
[targ-docker]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-dockersh-build-a-multi-architecture-docker-image
|
||||||
[targ-release]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-releasesh-build-a-release-for-all-platforms
|
[targ-release]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-releasesh-build-a-release-for-all-platforms
|
||||||
|
|
||||||
|
#### <a href="#e2e-frontend-tests" id="e2e-frontend-tests" name="e2e-frontend-tests">End-to-End (E2E) Frontend Tests</a>
|
||||||
|
|
||||||
|
AdGuard Home uses [Playwright](https://playwright.dev) for E2E testing. Tests are located in `tests/e2e`.
|
||||||
|
|
||||||
|
**Running Tests:**
|
||||||
|
- `npm run test:e2e` – run all tests (headless).
|
||||||
|
- `npm run test:e2e:interactive` – run tests interactively.
|
||||||
|
- `npm run test:e2e:debug` – run tests in debug mode.
|
||||||
|
- `npm run test:e2e:codegen` – generate new test code.
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
1. Run `npm install` to install dependencies.
|
||||||
|
2. Run `npx playwright install` to set up required browsers.
|
||||||
|
|
||||||
|
> **Warning:** Playwright will download and install its own browser binaries for testing, which may differ from the browsers installed on your system.
|
||||||
|
|
||||||
## <a href="#contributing" id="contributing" name="contributing">Contributing</a>
|
## <a href="#contributing" id="contributing" name="contributing">Contributing</a>
|
||||||
|
|
||||||
You are welcome to fork this repository, make your changes and [submit a pull request][pr]. Please make sure you follow our [code guidelines][guide] though.
|
You are welcome to fork this repository, make your changes and [submit a pull request][pr]. Please make sure you follow our [code guidelines][guide] though.
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
# Make sure to sync any changes with the branch overrides below.
|
# Make sure to sync any changes with the branch overrides below.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'edge'
|
'channel': 'edge'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
- 'Build frontend':
|
- 'Build frontend':
|
||||||
@@ -277,8 +277,8 @@
|
|||||||
# need to build a few of these.
|
# need to build a few of these.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'beta'
|
'channel': 'beta'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||||
# release-vX.Y.Z branches are the branches from which the actual final
|
# release-vX.Y.Z branches are the branches from which the actual final
|
||||||
# release is built.
|
# release is built.
|
||||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||||
@@ -293,5 +293,5 @@
|
|||||||
# are the ones that actually get released.
|
# are the ones that actually get released.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'release'
|
'channel': 'release'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
'key': 'AHBRTSPECS'
|
'key': 'AHBRTSPECS'
|
||||||
'name': 'AdGuard Home - Build and run tests'
|
'name': 'AdGuard Home - Build and run tests'
|
||||||
'variables':
|
'variables':
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||||
'channel': 'development'
|
'channel': 'development'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
@@ -29,6 +29,12 @@
|
|||||||
jobs:
|
jobs:
|
||||||
- 'Artifact'
|
- 'Artifact'
|
||||||
|
|
||||||
|
- 'E2E':
|
||||||
|
manual: false
|
||||||
|
final: false
|
||||||
|
jobs:
|
||||||
|
- 'Test e2e'
|
||||||
|
|
||||||
'Test frontend':
|
'Test frontend':
|
||||||
'docker':
|
'docker':
|
||||||
'image': '${bamboo.dockerFrontend}'
|
'image': '${bamboo.dockerFrontend}'
|
||||||
@@ -48,7 +54,7 @@
|
|||||||
|
|
||||||
set -e -f -u -x
|
set -e -f -u -x
|
||||||
|
|
||||||
make VERBOSE=1 js-deps js-lint js-test
|
make VERBOSE=1 js-deps js-typecheck js-lint js-test
|
||||||
'final-tasks':
|
'final-tasks':
|
||||||
- 'clean'
|
- 'clean'
|
||||||
'requirements':
|
'requirements':
|
||||||
@@ -165,6 +171,38 @@
|
|||||||
'requirements':
|
'requirements':
|
||||||
- 'adg-docker': 'true'
|
- 'adg-docker': 'true'
|
||||||
|
|
||||||
|
'Test e2e':
|
||||||
|
'artifact-subscriptions':
|
||||||
|
- 'artifact': 'AdGuardHome_linux_amd64'
|
||||||
|
- 'artifact': 'AdGuardHome frontend'
|
||||||
|
'docker':
|
||||||
|
'image': '${bamboo.dockerFrontend}'
|
||||||
|
'volumes':
|
||||||
|
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||||
|
'key': 'E2ETEST'
|
||||||
|
'other':
|
||||||
|
'clean-working-dir': true
|
||||||
|
'tasks':
|
||||||
|
- 'checkout':
|
||||||
|
'force-clean-build': true
|
||||||
|
- 'script':
|
||||||
|
'interpreter': 'SHELL'
|
||||||
|
'scripts':
|
||||||
|
- |
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e -f -u -x
|
||||||
|
|
||||||
|
export CI=true
|
||||||
|
|
||||||
|
tar -xzf dist/AdGuardHome_linux_amd64.tar.gz -C /tmp
|
||||||
|
|
||||||
|
mv /tmp/AdGuardHome/AdGuardHome ./AdGuardHome
|
||||||
|
|
||||||
|
make VERBOSE=1 js-deps js-test-e2e
|
||||||
|
'requirements':
|
||||||
|
- 'adg-docker': 'true'
|
||||||
|
|
||||||
'branches':
|
'branches':
|
||||||
'create': 'for-pull-request'
|
'create': 'for-pull-request'
|
||||||
'delete':
|
'delete':
|
||||||
@@ -195,6 +233,6 @@
|
|||||||
# Set the default release channel on the release branch to beta, as we
|
# Set the default release channel on the release branch to beta, as we
|
||||||
# may need to build a few of these.
|
# may need to build a few of these.
|
||||||
'variables':
|
'variables':
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.1-bullseye'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
'dockerGo': 'adguard/go-builder:1.24.1--1'
|
||||||
'channel': 'candidate'
|
'channel': 'candidate'
|
||||||
|
|||||||
22
client/.eslintrc.json
vendored
22
client/.eslintrc.json
vendored
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["prettier"],
|
"plugins": [
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
"extends": [
|
"extends": [
|
||||||
"airbnb-base",
|
"airbnb-base",
|
||||||
"prettier",
|
"prettier",
|
||||||
@@ -21,12 +23,23 @@
|
|||||||
},
|
},
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
"node": {
|
"node": {
|
||||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
"extensions": [
|
||||||
|
".js",
|
||||||
|
".jsx",
|
||||||
|
".ts",
|
||||||
|
".tsx"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
"import/extensions": [
|
"import/extensions": [
|
||||||
"error",
|
"error",
|
||||||
"ignorePackages",
|
"ignorePackages",
|
||||||
@@ -43,7 +56,10 @@
|
|||||||
"no-console": [
|
"no-console": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
"allow": ["warn", "error"]
|
"allow": [
|
||||||
|
"warn",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"import/no-extraneous-dependencies": [
|
"import/no-extraneous-dependencies": [
|
||||||
|
|||||||
8728
client/package-lock.json
generated
vendored
8728
client/package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
23
client/package.json
vendored
23
client/package.json
vendored
@@ -7,11 +7,14 @@
|
|||||||
"build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js",
|
"build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js",
|
||||||
"watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
|
"watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
|
||||||
"watch:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
|
"watch:hot": "cross-env BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
|
||||||
"lint": "echo 'Lint temporarily disabled'",
|
"lint": "eslint --ext .ts,.tsx src",
|
||||||
"lint-new": "eslint './src/**/*.(ts|tsx)'",
|
"lint:fix": "eslint --ext .ts,.tsx src --fix",
|
||||||
"lint:fix": "eslint './src/**/*.(ts|tsx)' --fix",
|
"test": "vitest --run",
|
||||||
"test": "jest",
|
"test:watch": "vitest --watch",
|
||||||
"test:watch": "jest --watch",
|
"test:e2e": "npx playwright test tests/e2e",
|
||||||
|
"test:e2e:interactive": "npx playwright test --ui",
|
||||||
|
"test:e2e:debug": "npx playwright test --debug",
|
||||||
|
"test:e2e:codegen": "npx playwright codegen",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"typecheck:watch": "tsc --noEmit --watch"
|
"typecheck:watch": "tsc --noEmit --watch"
|
||||||
},
|
},
|
||||||
@@ -20,6 +23,7 @@
|
|||||||
"@nivo/line": "^0.64.0",
|
"@nivo/line": "^0.64.0",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"countries-and-timezones": "^3.6.0",
|
"countries-and-timezones": "^3.6.0",
|
||||||
"date-fns": "^1.29.0",
|
"date-fns": "^1.29.0",
|
||||||
"i18next": "^19.6.2",
|
"i18next": "^19.6.2",
|
||||||
@@ -34,6 +38,7 @@
|
|||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-click-outside": "^3.0.1",
|
"react-click-outside": "^3.0.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
|
"react-hook-form": "^7.54.0",
|
||||||
"react-i18next": "^11.7.2",
|
"react-i18next": "^11.7.2",
|
||||||
"react-modal": "^3.11.2",
|
"react-modal": "^3.11.2",
|
||||||
"react-popper-tooltip": "^2.11.1",
|
"react-popper-tooltip": "^2.11.1",
|
||||||
@@ -46,7 +51,6 @@
|
|||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-actions": "^2.6.5",
|
"redux-actions": "^2.6.5",
|
||||||
"redux-form": "^8.3.10",
|
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"ts-migrate": "^0.1.35",
|
"ts-migrate": "^0.1.35",
|
||||||
"url-polyfill": "^1.1.12"
|
"url-polyfill": "^1.1.12"
|
||||||
@@ -60,15 +64,15 @@
|
|||||||
"@babel/plugin-transform-runtime": "^7.24.3",
|
"@babel/plugin-transform-runtime": "^7.24.3",
|
||||||
"@babel/preset-env": "^7.24.5",
|
"@babel/preset-env": "^7.24.5",
|
||||||
"@babel/preset-react": "^7.24.1",
|
"@babel/preset-react": "^7.24.1",
|
||||||
"@types/jest": "^29.5.12",
|
"@playwright/test": "1.50.1",
|
||||||
"@types/lodash": "^4.17.4",
|
"@types/lodash": "^4.17.4",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^17.0.80",
|
"@types/react": "^17.0.80",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/react-redux": "^7.1.33",
|
"@types/react-redux": "^7.1.33",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-table": "^7.7.20",
|
"@types/react-table": "^7.7.20",
|
||||||
"@types/redux-actions": "^2.6.5",
|
"@types/redux-actions": "^2.6.5",
|
||||||
"@types/redux-form": "^8.3.10",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||||
"@typescript-eslint/parser": "^7.10.0",
|
"@typescript-eslint/parser": "^7.10.0",
|
||||||
"babel-loader": "^9.1.3",
|
"babel-loader": "^9.1.3",
|
||||||
@@ -85,8 +89,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"html-webpack-plugin": "^5.6.0",
|
"html-webpack-plugin": "^5.6.0",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
|
||||||
"jscodeshift": "^0.15.2",
|
"jscodeshift": "^0.15.2",
|
||||||
"mini-css-extract-plugin": "^2.9.0",
|
"mini-css-extract-plugin": "^2.9.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
@@ -97,6 +99,7 @@
|
|||||||
"stylelint": "^16.5.0",
|
"stylelint": "^16.5.0",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
|
"vitest": "^3.0.4",
|
||||||
"webpack": "^5.91.0",
|
"webpack": "^5.91.0",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^5.0.4",
|
"webpack-dev-server": "^5.0.4",
|
||||||
|
|||||||
52
client/playwright.config.ts
vendored
Normal file
52
client/playwright.config.ts
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import { CONFIG_FILE_PATH } from './tests/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
globalSetup: path.resolve('./tests/e2e/globalSetup.ts'),
|
||||||
|
globalTeardown: path.resolve('./tests/e2e/globalTeardown.ts'),
|
||||||
|
timeout: 5000,
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: 'http://127.0.0.1:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
launchOptions: {
|
||||||
|
headless: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
webServer: {
|
||||||
|
stdout: process.env.CI ? 'pipe' : 'ignore',
|
||||||
|
command: `${!process.env.CI ? 'sudo ' : ''}./AdGuardHome --local-frontend -v -c ${CONFIG_FILE_PATH}`,
|
||||||
|
url: 'http://127.0.0.1:3000',
|
||||||
|
cwd: '..',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 10000,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -327,10 +327,10 @@
|
|||||||
"rate_limit_whitelist_placeholder": "Voer één IP-adres per regel in",
|
"rate_limit_whitelist_placeholder": "Voer één IP-adres per regel in",
|
||||||
"blocking_ipv4_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
"blocking_ipv4_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
||||||
"blocking_ipv6_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
"blocking_ipv6_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
||||||
"blocking_mode_default": "Standaard: Reageer met een nul IP adres (0.0.0.0 for A; :: voor AAAA) wanneer geblokkeerd door een Adblock-type regel; reageer met het IP-adres dat is opgegeven in de regel wanneer geblokkeerd door een /etc/hosts type regel",
|
"blocking_mode_default": "Standaard: Reageer met een nul IP-adres (0.0.0.0 for A; :: voor AAAA) wanneer geblokkeerd door een Adblock-type regel; reageer met het IP-adres dat is opgegeven in de regel wanneer geblokkeerd door een /etc/hosts type regel",
|
||||||
"blocking_mode_refused": "REFUSED: Antwoorden met REFUSED code",
|
"blocking_mode_refused": "REFUSED: Antwoorden met REFUSED code",
|
||||||
"blocking_mode_nxdomain": "NXDOMAIN: Reageer met NXDOMAIN code",
|
"blocking_mode_nxdomain": "NXDOMAIN: Reageer met NXDOMAIN code",
|
||||||
"blocking_mode_null_ip": "Nul IP: Reageer met een nul IP address (0.0.0.0 voor A; :: voor AAAA)",
|
"blocking_mode_null_ip": "Nul IP: Reageer met een nul IP-adres (0.0.0.0 voor A; :: voor AAAA)",
|
||||||
"blocking_mode_custom_ip": "Aangepast IP: Reageer met een handmatige ingesteld IP adres",
|
"blocking_mode_custom_ip": "Aangepast IP: Reageer met een handmatige ingesteld IP adres",
|
||||||
"theme_auto": "Automatisch",
|
"theme_auto": "Automatisch",
|
||||||
"theme_light": "Licht",
|
"theme_light": "Licht",
|
||||||
|
|||||||
@@ -40,11 +40,11 @@
|
|||||||
"dhcp_ipv4_settings": "DHCP IPv4 Ayarları",
|
"dhcp_ipv4_settings": "DHCP IPv4 Ayarları",
|
||||||
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
|
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
|
||||||
"form_error_required": "Gerekli alan",
|
"form_error_required": "Gerekli alan",
|
||||||
"form_error_ip4_format": "Geçersiz IPv4 adresi",
|
"form_error_ip4_format": "IPv4 adresi geçersiz",
|
||||||
"form_error_ip4_gateway_format": "Geçersiz ağ geçidi IPv4 adresi",
|
"form_error_ip4_gateway_format": "Ağ geçidi IPv4 adresi geçersiz",
|
||||||
"form_error_ip6_format": "Geçersiz IPv6 adresi",
|
"form_error_ip6_format": "IPv6 adresi geçersiz",
|
||||||
"form_error_ip_format": "Geçersiz IP adresi",
|
"form_error_ip_format": "IP adresi geçersiz",
|
||||||
"form_error_mac_format": "Geçersiz MAC adresi",
|
"form_error_mac_format": "MAC adresi geçersiz",
|
||||||
"form_error_client_id_format": "İstemci Kimliği yalnızca sayılar, küçük harfler ve kısa çizgiler içermelidir",
|
"form_error_client_id_format": "İstemci Kimliği yalnızca sayılar, küçük harfler ve kısa çizgiler içermelidir",
|
||||||
"form_error_server_name": "Sunucu adı geçersiz",
|
"form_error_server_name": "Sunucu adı geçersiz",
|
||||||
"form_error_subnet": "\"{{cidr}}\" alt ağı, \"{{ip}}\" IP adresini içermiyor",
|
"form_error_subnet": "\"{{cidr}}\" alt ağı, \"{{ip}}\" IP adresini içermiyor",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"dhcp_table_hostname": "Ana makine Adı",
|
"dhcp_table_hostname": "Ana makine Adı",
|
||||||
"dhcp_table_expires": "Bitiş tarihi",
|
"dhcp_table_expires": "Bitiş tarihi",
|
||||||
"dhcp_warning": "DHCP sunucusunu yine de etkinleştirmek istiyorsanız, ağınızda başka aktif DHCP sunucusu olmadığından emin olun, aksi takdirde ağa bağlı cihazların internet bağlantısı kesilebilir!",
|
"dhcp_warning": "DHCP sunucusunu yine de etkinleştirmek istiyorsanız, ağınızda başka bir aktif DHCP sunucusu olmadığından emin olun, aksi takdirde ağa bağlı cihazların internet bağlantısı kesilebilir!",
|
||||||
"dhcp_error": "AdGuard Home, ağda başka bir etkin DHCP sunucusu olup olmadığını belirleyemedi",
|
"dhcp_error": "AdGuard Home, ağda başka bir etkin DHCP sunucusu olup olmadığını belirleyemedi",
|
||||||
"dhcp_static_ip_error": "DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. AdGuard Home, bu ağ arayüzünün sabit bir IP adresi kullanılarak yapılandırılıp yapılandırılmadığını belirleyemedi. Lütfen sabit IP adresini elle ayarlayın.",
|
"dhcp_static_ip_error": "DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. AdGuard Home, bu ağ arayüzünün sabit bir IP adresi kullanılarak yapılandırılıp yapılandırılmadığını belirleyemedi. Lütfen sabit IP adresini elle ayarlayın.",
|
||||||
"dhcp_dynamic_ip_found": "Sisteminiz, <0>{{interfaceName}}</0> arayüzü için dinamik IP adresi yapılandırması kullanıyor. DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. Geçerli olan IP adresiniz <0>{{ipAddress}}</0>. \"DHCP sunucusunu etkinleştir\" düğmesine basarsanız, AdGuard Home bu IP adresini otomatik bir şekilde sabit olarak ayarlayacaktır.",
|
"dhcp_dynamic_ip_found": "Sisteminiz, <0>{{interfaceName}}</0> arayüzü için dinamik IP adresi yapılandırması kullanıyor. DHCP sunucusunu kullanmak için sabit bir IP adresi ayarlanmalıdır. Geçerli olan IP adresiniz <0>{{ipAddress}}</0>. \"DHCP sunucusunu etkinleştir\" düğmesine basarsanız, AdGuard Home bu IP adresini otomatik bir şekilde sabit olarak ayarlayacaktır.",
|
||||||
@@ -147,13 +147,13 @@
|
|||||||
"average_upstream_response_time": "Ortalama üst kaynak yanıt süresi",
|
"average_upstream_response_time": "Ortalama üst kaynak yanıt süresi",
|
||||||
"response_time": "Yanıt süresi",
|
"response_time": "Yanıt süresi",
|
||||||
"average_processing_time_hint": "Bir DNS isteğinin milisaniye cinsinden ortalama işlem süresi",
|
"average_processing_time_hint": "Bir DNS isteğinin milisaniye cinsinden ortalama işlem süresi",
|
||||||
"block_domain_use_filters_and_hosts": "Filtre ve hosts dosyalarını kullanarak alan adlarını engelle",
|
"block_domain_use_filters_and_hosts": "Filtre ve ana bilgisayar dosyalarını kullanarak alan adlarını engelle",
|
||||||
"filters_block_toggle_hint": "<a>Filtreler</a> ayarlarında engelleme kuralları oluşturabilirsiniz.",
|
"filters_block_toggle_hint": "<a>Filtreler</a> ayarlarında engelleme kuralları oluşturabilirsiniz.",
|
||||||
"use_adguard_browsing_sec": "AdGuard gezinti koruması web hizmetini kullan",
|
"use_adguard_browsing_sec": "AdGuard gezinti koruması web hizmetini kullan",
|
||||||
"use_adguard_browsing_sec_hint": "AdGuard Home, alan adının gezinti koruması web hizmeti tarafından engellenip engellenmediğini kontrol eder. Kontrolü gerçekleştirmek için gizlilik dostu arama API'sini kullanır: sunucuya yalnızca SHA256 karma alan adının kısa bir ön eki gönderilir.",
|
"use_adguard_browsing_sec_hint": "AdGuard Home, alan adının gezinti koruması web hizmeti tarafından engellenip engellenmediğini kontrol eder. Kontrolü gerçekleştirmek için gizlilik dostu arama API'sini kullanır: sunucuya yalnızca SHA256 karma alan adının kısa bir ön eki gönderilir.",
|
||||||
"use_adguard_parental": "AdGuard ebeveyn denetimi web hizmetini kullan",
|
"use_adguard_parental": "AdGuard ebeveyn denetimi web hizmetini kullan",
|
||||||
"use_adguard_parental_hint": "AdGuard Home, alan adının yetişkin içerik bulundurup bulundurmadığını kontrol eder. Gezinti koruması web hizmeti ile kullandığımız aynı gizlilik dostu API'yi kullanır.",
|
"use_adguard_parental_hint": "AdGuard Home, alan adının yetişkin içerik bulundurup bulundurmadığını kontrol eder. Gezinti koruması web hizmeti ile kullandığımız aynı gizlilik dostu API'yi kullanır.",
|
||||||
"enforce_safe_search": "Güvenli Aramayı kullan",
|
"enforce_safe_search": "Güvenli aramayı kullan",
|
||||||
"enforce_save_search_hint": "AdGuard Home, şu arama motorlarında güvenli aramayı uygular: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
|
"enforce_save_search_hint": "AdGuard Home, şu arama motorlarında güvenli aramayı uygular: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex, Pixabay.",
|
||||||
"no_servers_specified": "Sunucu belirtilmedi",
|
"no_servers_specified": "Sunucu belirtilmedi",
|
||||||
"general_settings": "Genel ayarlar",
|
"general_settings": "Genel ayarlar",
|
||||||
@@ -294,6 +294,9 @@
|
|||||||
"blocked_response_ttl": "Engellenen yanıt kullanım süresi",
|
"blocked_response_ttl": "Engellenen yanıt kullanım süresi",
|
||||||
"blocked_response_ttl_desc": "İstemcilerin filtrelenmiş bir yanıtı kaç saniye süreyle önbelleğe alması gerektiğini belirtir",
|
"blocked_response_ttl_desc": "İstemcilerin filtrelenmiş bir yanıtı kaç saniye süreyle önbelleğe alması gerektiğini belirtir",
|
||||||
"form_enter_blocked_response_ttl": "Engellenen yanıt kullanım süresini girin (saniye)",
|
"form_enter_blocked_response_ttl": "Engellenen yanıt kullanım süresini girin (saniye)",
|
||||||
|
"upstream_timeout": "Üst kaynak zaman aşımı",
|
||||||
|
"upstream_timeout_desc": "Üst kaynak sunucusundan yanıt almak için kaç saniye bekleneceğini belirtir",
|
||||||
|
"form_enter_upstream_timeout": "Üst kaynak sunucusu zaman aşımı süresini saniye cinsinden girin",
|
||||||
"dnscrypt": "DNSCrypt",
|
"dnscrypt": "DNSCrypt",
|
||||||
"dns_over_https": "DNS-over-HTTPS",
|
"dns_over_https": "DNS-over-HTTPS",
|
||||||
"dns_over_tls": "DNS-over-TLS",
|
"dns_over_tls": "DNS-over-TLS",
|
||||||
@@ -308,7 +311,7 @@
|
|||||||
"form_enter_rate_limit": "Sıklık limitini girin",
|
"form_enter_rate_limit": "Sıklık limitini girin",
|
||||||
"rate_limit": "Sıklık limiti",
|
"rate_limit": "Sıklık limiti",
|
||||||
"edns_enable": "EDNS istemci alt ağını etkinleştir",
|
"edns_enable": "EDNS istemci alt ağını etkinleştir",
|
||||||
"edns_cs_desc": "Kaynak yönü isteklerine EDNS İstemci Alt Ağı seçeneğini (ECS) ekleyin ve istemciler tarafından gönderilen değerleri sorgu günlüğüne kaydedin.",
|
"edns_cs_desc": "Üst sunucu isteklerine ECS (EDNS İstemci Alt Ağı) seçeneğini ekler ve istemciler tarafından gönderilen değerleri sorgu günlüğünde kaydeder.",
|
||||||
"edns_use_custom_ip": "EDNS için özel IP kullan",
|
"edns_use_custom_ip": "EDNS için özel IP kullan",
|
||||||
"edns_use_custom_ip_desc": "EDNS için özel IP kullanımına izin ver",
|
"edns_use_custom_ip_desc": "EDNS için özel IP kullanımına izin ver",
|
||||||
"rate_limit_desc": "İstemci başına izin verilen saniyedeki istek sayısı. 0 olarak ayarlamak, sınır olmadığı anlamına gelir.",
|
"rate_limit_desc": "İstemci başına izin verilen saniyedeki istek sayısı. 0 olarak ayarlamak, sınır olmadığı anlamına gelir.",
|
||||||
@@ -342,17 +345,17 @@
|
|||||||
"unknown_filter": "Bilinmeyen filtre {{filterId}}",
|
"unknown_filter": "Bilinmeyen filtre {{filterId}}",
|
||||||
"known_tracker": "Bilinen izleyici",
|
"known_tracker": "Bilinen izleyici",
|
||||||
"install_welcome_title": "AdGuard Home'a hoş geldiniz!",
|
"install_welcome_title": "AdGuard Home'a hoş geldiniz!",
|
||||||
"install_welcome_desc": "AdGuard Home, ağ genelinde reklamları ve izleyicileri engelleyen bir DNS sunucusudur. Tüm ağınızı ve tüm cihazlarınızı kontrol etmenizi sağlar, istemci tarafında herhangi bir program kullanmanıza gerek duymaz.",
|
"install_welcome_desc": "AdGuard Home, ağ genelinde reklam ve izleyici engelleyen bir DNS sunucusudur. Tüm ağınızı ve cihazlarınızı kontrol etmenizi sağlar ve istemci tarafında ek bir yazılım kullanmanıza gerek duymaz.",
|
||||||
"install_settings_title": "Yönetici Web Arayüzü",
|
"install_settings_title": "Yönetici Web Arayüzü",
|
||||||
"install_settings_listen": "Dinleme arayüzü",
|
"install_settings_listen": "Dinleme arayüzü",
|
||||||
"install_settings_port": "Bağlantı noktası",
|
"install_settings_port": "Bağlantı noktası",
|
||||||
"install_settings_interface_link": "AdGuard Home yönetici web arayüzünüz aşağıdaki adreslerde bulunacaktır:",
|
"install_settings_interface_link": "AdGuard Home yönetici web arayüzüne aşağıdaki adreslerden erişebilirsiniz:",
|
||||||
"form_error_port": "Geçerli bir bağlantı noktası değeri girin",
|
"form_error_port": "Geçerli bir bağlantı noktası değeri girin",
|
||||||
"install_settings_dns": "DNS sunucusu",
|
"install_settings_dns": "DNS sunucusu",
|
||||||
"install_settings_dns_desc": "Aşağıdaki adreslerde DNS sunucusunu kullanmak için cihazlarınızı veya yönlendiricinizi yapılandırmanız gerekir:",
|
"install_settings_dns_desc": "Cihazlarınızı veya yönlendiricinizi aşağıdaki adreslerdeki DNS sunucusunu kullanacak şekilde yapılandırmanız gerekir:",
|
||||||
"install_settings_all_interfaces": "Tüm arayüzler",
|
"install_settings_all_interfaces": "Tüm arayüzler",
|
||||||
"install_auth_title": "Kimlik Doğrulama",
|
"install_auth_title": "Kimlik Doğrulama",
|
||||||
"install_auth_desc": "AdGuard Home yönetim web arayüzü için şifre doğrulaması yapılandırılmalıdır. AdGuard Home'a yalnızca yerel ağınızdan erişilebilir olsa bile, onu sınırsız erişimden korumak yine de önemlidir.",
|
"install_auth_desc": "AdGuard Home yönetici web arayüzüne parola ile kimlik doğrulama yapılandırılmalıdır. AdGuard Home yalnızca yerel ağınızdan erişilebilir olsa bile, yine de yetkisiz erişime karşı korunması önemlidir.",
|
||||||
"install_auth_username": "Kullanıcı adı",
|
"install_auth_username": "Kullanıcı adı",
|
||||||
"install_auth_password": "Parola",
|
"install_auth_password": "Parola",
|
||||||
"install_auth_confirm": "Parolayı onayla",
|
"install_auth_confirm": "Parolayı onayla",
|
||||||
@@ -366,10 +369,10 @@
|
|||||||
"install_devices_router": "Yönlendirici",
|
"install_devices_router": "Yönlendirici",
|
||||||
"install_devices_router_desc": "Bu kurulum, ev yönlendiricinize bağlı tüm cihazları otomatik olarak kapsar ve her birini elle yapılandırmanıza gerek yoktur.",
|
"install_devices_router_desc": "Bu kurulum, ev yönlendiricinize bağlı tüm cihazları otomatik olarak kapsar ve her birini elle yapılandırmanıza gerek yoktur.",
|
||||||
"install_devices_address": "AdGuard Home DNS sunucusu aşağıdaki adresleri dinliyor",
|
"install_devices_address": "AdGuard Home DNS sunucusu aşağıdaki adresleri dinliyor",
|
||||||
"install_devices_router_list_1": "Yönlendiricinizin ayarlarına gidin. Genellikle tarayıcınızdan http://192.168.0.1/ veya http://192.168.1.1/ gibi bir URL aracılığıyla erişebilirsiniz. Bir parola girmeniz istenebilir. Hatırlamıyorsanız, genellikle yönlendiricinin üzerindeki bir düğmeye basarak parolayı sıfırlayabilirsiniz, ancak bu işlemin seçilmesi durumunda yüksek ihtimalle tüm yönlendirici yapılandırmasını kaybedeceğinizi unutmayın. Yönlendiricinizin kurulumu için bir uygulama gerekiyorsa, lütfen uygulamayı telefonunuza veya PC'nize yükleyin ve yönlendiricinin ayarlarına erişmek için kullanın.",
|
"install_devices_router_list_1": "Yönlendiricinizin ayarlarına gidin. Genellikle, tarayıcınızdan http://192.168.0.1/ veya http://192.168.1.1/ gibi bir URL üzerinden erişebilirsiniz. Giriş yaparken bir parola girmeniz istenebilir. Parolanızı hatırlamıyorsanız, genellikle yönlendiricinin üzerindeki bir düğmeye basarak parolayı sıfırlayabilirsiniz, ancak bu işlemi seçerseniz yönlendiricinin tüm yapılandırmasını kaybedebileceğinizi unutmayın. Yönlendiricinizin kurulumu için bir uygulama gerekiyorsa, lütfen uygulamayı telefonunuza veya bilgisayarınıza yükleyin ve yönlendiricinin ayarlarına erişmek için bu uygulamayı kullanın.",
|
||||||
"install_devices_router_list_2": "DHCP/DNS ayarlarını bulun. DNS satırlarını arayın, genelde iki veya üç tanedir, üç rakam girilebilen dört ayrı grup içeren satırdır.",
|
"install_devices_router_list_2": "DHCP/DNS ayarlarını bulun. DNS satırlarını arayın, genelde iki veya üç tanedir, üç rakam girilebilen dört ayrı grup içeren satırdır.",
|
||||||
"install_devices_router_list_3": "AdGuard Home sunucu adreslerinizi oraya girin.",
|
"install_devices_router_list_3": "AdGuard Home sunucu adreslerinizi oraya girin.",
|
||||||
"install_devices_router_list_4": "Bazı yönlendirici türlerinde özel bir DNS sunucusu ayarlanamaz. Bu durumda, AdGuard Home'u <0>DHCP sunucusu</0> olarak ayarlamak yardımcı olabilir. Aksi takdirde, yönlendirici modeliniz için DNS sunucularını nasıl ayarlayacağınız konusunda yönlendirici kılavuzuna bakmalısınız.",
|
"install_devices_router_list_4": "Bazı yönlendirici türlerinde özel bir DNS sunucusu yapılandırılamaz. Bu durumda, AdGuard Home'u bir <0>DHCP sunucusu</0> olarak yapılandırmak yardımcı olabilir. Aksi takdirde, yönlendirici modelinizde DNS sunucularını nasıl özelleştireceğinizi öğrenmek için yönlendirici kılavuzunu kontrol etmelisiniz.",
|
||||||
"install_devices_windows_list_1": "Başlat menüsünden veya Windows araması aracılığıyla Denetim Masası'nı açın.",
|
"install_devices_windows_list_1": "Başlat menüsünden veya Windows araması aracılığıyla Denetim Masası'nı açın.",
|
||||||
"install_devices_windows_list_2": "Ağ ve İnternet kategorisine girin ve ardından Ağ ve Paylaşım Merkezi'ne girin.",
|
"install_devices_windows_list_2": "Ağ ve İnternet kategorisine girin ve ardından Ağ ve Paylaşım Merkezi'ne girin.",
|
||||||
"install_devices_windows_list_3": "Sol panelde \"Bağdaştırıcı ayarlarını değiştirin\" öğesine tıklayın.",
|
"install_devices_windows_list_3": "Sol panelde \"Bağdaştırıcı ayarlarını değiştirin\" öğesine tıklayın.",
|
||||||
@@ -389,7 +392,7 @@
|
|||||||
"install_devices_ios_list_2": "Sol menüde bulunan Wi-Fi bölümüne girin (telefon ağlar için özel DNS sunucusu ayarlanamaz).",
|
"install_devices_ios_list_2": "Sol menüde bulunan Wi-Fi bölümüne girin (telefon ağlar için özel DNS sunucusu ayarlanamaz).",
|
||||||
"install_devices_ios_list_3": "O anda aktif olan ağın adına dokunun.",
|
"install_devices_ios_list_3": "O anda aktif olan ağın adına dokunun.",
|
||||||
"install_devices_ios_list_4": "DNS alanına AdGuard Home sunucunuzun adreslerini girin.",
|
"install_devices_ios_list_4": "DNS alanına AdGuard Home sunucunuzun adreslerini girin.",
|
||||||
"get_started": "Başlayın",
|
"get_started": "Başla",
|
||||||
"next": "Sonraki",
|
"next": "Sonraki",
|
||||||
"open_dashboard": "Panoyu Aç",
|
"open_dashboard": "Panoyu Aç",
|
||||||
"install_saved": "Başarıyla kaydedildi",
|
"install_saved": "Başarıyla kaydedildi",
|
||||||
@@ -452,14 +455,14 @@
|
|||||||
"settings_global": "Genel",
|
"settings_global": "Genel",
|
||||||
"settings_custom": "Özel",
|
"settings_custom": "Özel",
|
||||||
"table_client": "İstemci",
|
"table_client": "İstemci",
|
||||||
"table_name": "AdAdı",
|
"table_name": "Ad",
|
||||||
"save_btn": "Kaydet",
|
"save_btn": "Kaydet",
|
||||||
"client_add": "İstemci Ekle",
|
"client_add": "İstemci Ekle",
|
||||||
"client_new": "Yeni İstemci",
|
"client_new": "Yeni İstemci",
|
||||||
"client_edit": "İstemciyi Düzenle",
|
"client_edit": "İstemciyi Düzenle",
|
||||||
"client_identifier": "Tanımlayıcı",
|
"client_identifier": "Tanımlayıcı",
|
||||||
"ip_address": "IP adresi",
|
"ip_address": "IP adresi",
|
||||||
"client_identifier_desc": "İstemciler IP adresleri, CIDR, MAC adresleri veya ClientID (DoT/DoH/DoQ için kullanılabilir) ile tanımlanabilir. İstemcileri nasıl tanımlayacağınız hakkında daha fazla bilgiyi <0>buradan</0> edinebilirsiniz.",
|
"client_identifier_desc": "İstemciler, IP adresi, CIDR, MAC adresi veya ClientID (DoT/DoH/DoQ için kullanılabilir) ile tanımlanabilir. İstemcileri nasıl tanımlayacağınız hakkında daha fazla bilgiye <0>buradan</0> ulaşabilirsiniz.",
|
||||||
"form_enter_ip": "IP girin",
|
"form_enter_ip": "IP girin",
|
||||||
"form_enter_subnet_ip": "\"{{cidr}}\" alt ağına bir IP adresi girin",
|
"form_enter_subnet_ip": "\"{{cidr}}\" alt ağına bir IP adresi girin",
|
||||||
"form_enter_mac": "MAC adresi girin",
|
"form_enter_mac": "MAC adresi girin",
|
||||||
@@ -476,7 +479,7 @@
|
|||||||
"client_confirm_delete": "\"{{key}}\" istemcisini silmek istediğinizden emin misiniz?",
|
"client_confirm_delete": "\"{{key}}\" istemcisini silmek istediğinizden emin misiniz?",
|
||||||
"list_confirm_delete": "Bu listeyi silmek istediğinizden emin misiniz?",
|
"list_confirm_delete": "Bu listeyi silmek istediğinizden emin misiniz?",
|
||||||
"auto_clients_title": "Çalışma zamanı istemcileri",
|
"auto_clients_title": "Çalışma zamanı istemcileri",
|
||||||
"auto_clients_desc": "AdGuard Home'u kullanan veya kullanabilecek cihazların IP adresleri hakkında bilgiler. Bu bilgiler, hosts dosyaları, ters DNS, vb. dâhil olmak üzere çeşitli kaynaklardan toplanır.",
|
"auto_clients_desc": "AdGuard Home'u kullanan veya kullanabilecek cihazların IP adresleri hakkında bilgiler. Bu bilgiler, ana bilgisayar dosyaları, ters DNS sorguları ve çeşitli diğer kaynaklardan toplanmaktadır.",
|
||||||
"access_title": "Erişim ayarları",
|
"access_title": "Erişim ayarları",
|
||||||
"access_desc": "AdGuard Home DNS sunucusu için erişim kurallarını buradan yapılandırabilirsiniz",
|
"access_desc": "AdGuard Home DNS sunucusu için erişim kurallarını buradan yapılandırabilirsiniz",
|
||||||
"access_allowed_title": "İzin verilen istemciler",
|
"access_allowed_title": "İzin verilen istemciler",
|
||||||
@@ -598,12 +601,12 @@
|
|||||||
"disable_ipv6": "IPv6 adreslerinin çözümlenmesini devre dışı bırak",
|
"disable_ipv6": "IPv6 adreslerinin çözümlenmesini devre dışı bırak",
|
||||||
"disable_ipv6_desc": "IPv6 adresleri için tüm DNS sorgularını bırakın (AAAA yazın) ve HTTPS yanıtlarından IPv6 ipuçlarını kaldırın.",
|
"disable_ipv6_desc": "IPv6 adresleri için tüm DNS sorgularını bırakın (AAAA yazın) ve HTTPS yanıtlarından IPv6 ipuçlarını kaldırın.",
|
||||||
"fastest_addr": "En hızlı IP adresi",
|
"fastest_addr": "En hızlı IP adresi",
|
||||||
"fastest_addr_desc": "Tüm DNS sunucularını sorgulayın ve tüm yanıtlar arasından en hızlı olan IP adresini döndürün. AdGuard Home'un tüm DNS sunucularından yanıt beklemesi gerektiği için DNS sorgularını yavaşlatır, ancak genel bağlantıyı iyileştirir.",
|
"fastest_addr_desc": "<b>Tüm</b> DNS sunucularından yanıt bekler, her sunucu için TCP bağlantı hızını ölçer ve en hızlı bağlantı hızına sahip sunucunun IP adresini döndürür.<br/>Bu yapılandırma, bir veya daha fazla üst kaynak sunucusu yanıt vermediğinde, DNS sorgularını önemli ölçüde yavaşlatabilir. Üst kaynak sunucularınızın kararlı olduğundan ve üst kaynak zaman aşım sürenizin düşük olduğundan emin olun.",
|
||||||
"autofix_warning_text": "\"Düzelt\" seçeneğine tıklarsanız, AdGuard Home, sisteminizi AdGuard Home DNS sunucusunu kullanacak şekilde yapılandırır.",
|
"autofix_warning_text": "\"Düzelt\" seçeneğine tıklarsanız, AdGuard Home, sisteminizi AdGuard Home DNS sunucusunu kullanacak şekilde yapılandırır.",
|
||||||
"autofix_warning_list": "Bu görevleri gerçekleştirir: <0>Sistem DNSStubListener'ı devre dışı bırakın</0> <0>DNS sunucusu adresini 127.0.0.1 olarak ayarlayın</0> <0>/etc/resolv.conf'un sembolik bağlantı hedefini /run/systemd/resolve/resolv.conf ile değiştirin<0> <0>DNSStubListener'ı durdurun (systemd çözümlenmiş hizmeti yeniden yükleyin)</0>",
|
"autofix_warning_list": "Bu görevleri gerçekleştirir: <0>Sistem DNSStubListener'ı devre dışı bırakın</0> <0>DNS sunucusu adresini 127.0.0.1 olarak ayarlayın</0> <0>/etc/resolv.conf'un sembolik bağlantı hedefini /run/systemd/resolve/resolv.conf ile değiştirin<0> <0>DNSStubListener'ı durdurun (systemd çözümlenmiş hizmeti yeniden yükleyin)</0>",
|
||||||
"autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.",
|
"autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.",
|
||||||
"tags_title": "Etiketler",
|
"tags_title": "Etiketler",
|
||||||
"tags_desc": "İstemciye karşılık gelen etiketleri seçebilirsiniz. Etiketleri daha kesin olarak uygulamak için filtreleme kurallarına dâhil edin. <0>Daha fazla bilgi edinin</0>.",
|
"tags_desc": "İstemciyi tanımlayan etiketleri seçebilirsiniz. Filtreleme kurallarına etiketleri dahil ederek daha hassas bir şekilde uygulayabilirsiniz. <0>Daha fazla bilgi edinin</0>.",
|
||||||
"form_select_tags": "İstemci etiketlerini seçin",
|
"form_select_tags": "İstemci etiketlerini seçin",
|
||||||
"check_title": "Filtrelemeyi denetleyin",
|
"check_title": "Filtrelemeyi denetleyin",
|
||||||
"check_desc": "Ana makine adının filtreleme durumunu kontrol edin.",
|
"check_desc": "Ana makine adının filtreleme durumunu kontrol edin.",
|
||||||
@@ -624,11 +627,11 @@
|
|||||||
"client_blocked": "\"{{ip}}\" istemcisi başarıyla engellendi",
|
"client_blocked": "\"{{ip}}\" istemcisi başarıyla engellendi",
|
||||||
"client_unblocked": "\"{{ip}}\" istemcinin engellemesi başarıyla kaldırıldı",
|
"client_unblocked": "\"{{ip}}\" istemcinin engellemesi başarıyla kaldırıldı",
|
||||||
"static_ip": "Sabit IP adresi",
|
"static_ip": "Sabit IP adresi",
|
||||||
"static_ip_desc": "AdGuard Home bir sunucudur, bu nedenle düzgün çalışması için sabit bir IP adresine ihtiyacı vardır. Aksi takdirde, yönlendiriciniz bir zaman sonra bu cihaza farklı bir IP adresi atayabilir.",
|
"static_ip_desc": "AdGuard Home bir sunucudur, bu nedenle düzgün çalışabilmesi için sabit bir IP adresine ihtiyaç duyar. Aksi takdirde, yönlendiriciniz bu cihaza farklı bir IP adresi atayabilir.",
|
||||||
"set_static_ip": "Sabit IP adresi ayarla",
|
"set_static_ip": "Sabit IP adresi ayarla",
|
||||||
"install_static_ok": "Güzel haber! Sabit IP adresi zaten yapılandırılmış",
|
"install_static_ok": "Güzel haber! Sabit IP adresi zaten yapılandırılmış",
|
||||||
"install_static_error": "AdGuard Home, bu ağ arayüzü için otomatik olarak yapılandıramıyor. Lütfen bunu elle nasıl yapacağınızla ilgili talimatlara bakın.",
|
"install_static_error": "AdGuard Home, bu ağ arayüzü için otomatik olarak yapılandıramıyor. Lütfen bunu elle nasıl yapacağınızla ilgili talimatlara bakın.",
|
||||||
"install_static_configure": "AdGuard Home, <0>{{ip}}</0> dinamik IP adresinin kullanıldığını tespit etti. Sabit adresiniz olarak ayarlanmasını ister misiniz?",
|
"install_static_configure": "AdGuard Home, <0>{{ip}}</0> sabit IP adresinin kullanıldığını tespit etti. Sabit adresiniz olarak ayarlanmasını ister misiniz?",
|
||||||
"confirm_static_ip": "AdGuard Home, {{ip}} adresini sabit IP adresiniz olacak şekilde yapılandırır. Devam etmek istiyor musunuz?",
|
"confirm_static_ip": "AdGuard Home, {{ip}} adresini sabit IP adresiniz olacak şekilde yapılandırır. Devam etmek istiyor musunuz?",
|
||||||
"list_updated": "{{count}} liste güncellendi",
|
"list_updated": "{{count}} liste güncellendi",
|
||||||
"list_updated_plural": "{{count}} liste güncellendi",
|
"list_updated_plural": "{{count}} liste güncellendi",
|
||||||
@@ -707,8 +710,8 @@
|
|||||||
"custom_rotation_input": "Rotasyonu saat cinsinden girin",
|
"custom_rotation_input": "Rotasyonu saat cinsinden girin",
|
||||||
"protection_section_label": "Koruma",
|
"protection_section_label": "Koruma",
|
||||||
"log_and_stats_section_label": "Sorgu günlüğü ve istatistikler",
|
"log_and_stats_section_label": "Sorgu günlüğü ve istatistikler",
|
||||||
"ignore_query_log": "Sorgu günlüğünde bu istemciyi yoksay",
|
"ignore_query_log": "Sorgu günlüğünde bu istemciyi gösterme",
|
||||||
"ignore_statistics": "İstatistiklerde bu istemciyi yoksay",
|
"ignore_statistics": "İstatistiklerde bu istemciyi gösterme",
|
||||||
"schedule_services": "Hizmet engellemeyi duraklat",
|
"schedule_services": "Hizmet engellemeyi duraklat",
|
||||||
"schedule_services_desc": "Hizmet engelleme filtresinin duraklatma planını yapılandırın",
|
"schedule_services_desc": "Hizmet engelleme filtresinin duraklatma planını yapılandırın",
|
||||||
"schedule_services_desc_client": "Bu istemci için hizmet engelleme filtresinin duraklatma planını yapılandırın",
|
"schedule_services_desc_client": "Bu istemci için hizmet engelleme filtresinin duraklatma planını yapılandırın",
|
||||||
@@ -742,6 +745,6 @@
|
|||||||
"friday_short": "Cum",
|
"friday_short": "Cum",
|
||||||
"saturday_short": "Cmt",
|
"saturday_short": "Cmt",
|
||||||
"upstream_dns_cache_configuration": "Üst kaynak DNS önbellek yapılandırması",
|
"upstream_dns_cache_configuration": "Üst kaynak DNS önbellek yapılandırması",
|
||||||
"enable_upstream_dns_cache": "Bu istemcinin özel üst kaynak yapılandırması için DNS önbelleğe almayı etkinleştir",
|
"enable_upstream_dns_cache": "Bu istemcinin özel üst kaynak yapılandırması için DNS önbelleğini etkinleştir",
|
||||||
"dns_cache_size": "DNS önbellek boyutu, bayt cinsinden"
|
"dns_cache_size": "DNS önbellek boyutu, bayt cinsinden"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { describe, expect, test, afterEach, vi, beforeEach, it } from 'vitest';
|
||||||
|
|
||||||
import { sortIp, countClientsStatistics, findAddressType, subnetMaskToBitMask } from '../helpers/helpers';
|
import { sortIp, countClientsStatistics, findAddressType, subnetMaskToBitMask } from '../helpers/helpers';
|
||||||
import { ADDRESS_TYPES } from '../helpers/constants';
|
import { ADDRESS_TYPES } from '../helpers/constants';
|
||||||
|
|
||||||
@@ -259,7 +261,7 @@ describe('sortIp', () => {
|
|||||||
const originalWarn = console.warn;
|
const originalWarn = console.warn;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
console.warn = jest.fn();
|
console.warn = vi.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -347,15 +349,15 @@ describe('sortIp', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('findAddressType', () => {
|
describe('findAddressType', () => {
|
||||||
describe('ip', () => {
|
it('should return IP type for IP addresses', () => {
|
||||||
expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP);
|
expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cidr', () => {
|
it('should return CIDR type for CIDR addresses', () => {
|
||||||
expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR);
|
expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mac', () => {
|
it('should return UNKNOWN type for MAC addresses', () => {
|
||||||
expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);
|
expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
CHECK_TIMEOUT,
|
CHECK_TIMEOUT,
|
||||||
STATUS_RESPONSE,
|
STATUS_RESPONSE,
|
||||||
SETTINGS_NAMES,
|
SETTINGS_NAMES,
|
||||||
FORM_NAME,
|
|
||||||
MANUAL_UPDATE_LINK,
|
MANUAL_UPDATE_LINK,
|
||||||
DISABLE_PROTECTION_TIMINGS,
|
DISABLE_PROTECTION_TIMINGS,
|
||||||
} from '../helpers/constants';
|
} from '../helpers/constants';
|
||||||
@@ -424,10 +423,9 @@ export const testUpstream =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const testUpstreamWithFormValues = () => async (dispatch: any, getState: any) => {
|
export const testUpstreamWithFormValues = (formValues: any) => async (dispatch: any, getState: any) => {
|
||||||
const { upstream_dns_file } = getState().dnsConfig;
|
const { upstream_dns_file } = getState().dnsConfig;
|
||||||
const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } =
|
const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } = formValues;
|
||||||
getState().form[FORM_NAME.UPSTREAM].values;
|
|
||||||
|
|
||||||
return dispatch(
|
return dispatch(
|
||||||
testUpstream(
|
testUpstream(
|
||||||
@@ -512,16 +510,15 @@ export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST');
|
|||||||
export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');
|
export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');
|
||||||
export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
|
export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
|
||||||
|
|
||||||
export const findActiveDhcp = (name: any) => async (dispatch: any, getState: any) => {
|
export const findActiveDhcp = (selectedInterface: any) => async (dispatch: any, getState: any) => {
|
||||||
dispatch(findActiveDhcpRequest());
|
dispatch(findActiveDhcpRequest());
|
||||||
try {
|
try {
|
||||||
const req = {
|
const req = {
|
||||||
interface: name,
|
interface: selectedInterface,
|
||||||
};
|
};
|
||||||
const activeDhcp = await apiClient.findActiveDhcp(req);
|
const activeDhcp = await apiClient.findActiveDhcp(req);
|
||||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
dispatch(findActiveDhcpSuccess(activeDhcp));
|
||||||
const { check, interface_name, interfaces } = getState().dhcp;
|
const { check, interface_name, interfaces } = getState().dhcp;
|
||||||
const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name;
|
|
||||||
const v4 = check?.v4 ?? { static_ip: {}, other_server: {} };
|
const v4 = check?.v4 ?? { static_ip: {}, other_server: {} };
|
||||||
const v6 = check?.v6 ?? { other_server: {} };
|
const v6 = check?.v6 ?? { other_server: {} };
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export const setAllSettingsSuccess = createAction('SET_ALL_SETTINGS_SUCCESS');
|
|||||||
export const setAllSettings = (values: any) => async (dispatch: any) => {
|
export const setAllSettings = (values: any) => async (dispatch: any) => {
|
||||||
dispatch(setAllSettingsRequest());
|
dispatch(setAllSettingsRequest());
|
||||||
try {
|
try {
|
||||||
const { confirm_password, ...config } = values;
|
const config = { ...values };
|
||||||
|
delete config.confirm_password;
|
||||||
|
|
||||||
await apiClient.setAllSettings(config);
|
await apiClient.setAllSettings(config);
|
||||||
dispatch(setAllSettingsSuccess());
|
dispatch(setAllSettingsSuccess());
|
||||||
@@ -48,7 +49,11 @@ export const checkConfig = (values: any) => async (dispatch: any) => {
|
|||||||
dispatch(checkConfigRequest());
|
dispatch(checkConfigRequest());
|
||||||
try {
|
try {
|
||||||
const check = await apiClient.checkConfig(values);
|
const check = await apiClient.checkConfig(values);
|
||||||
dispatch(checkConfigSuccess(check));
|
dispatch(checkConfigSuccess({
|
||||||
|
web: { ...values.web, ...check.web },
|
||||||
|
dns: { ...values.dns, ...check.dns },
|
||||||
|
static_ip: check.static_ip,
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(checkConfigFailure());
|
dispatch(checkConfigFailure());
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { createAction } from 'redux-actions';
|
|||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
|
|
||||||
import { normalizeLogs } from '../helpers/helpers';
|
import { normalizeLogs } from '../helpers/helpers';
|
||||||
import { DEFAULT_LOGS_FILTER, FORM_NAME, QUERY_LOGS_PAGE_LIMIT } from '../helpers/constants';
|
import { DEFAULT_LOGS_FILTER, QUERY_LOGS_PAGE_LIMIT } from '../helpers/constants';
|
||||||
import { addErrorToast, addSuccessToast } from './toasts';
|
import { addErrorToast, addSuccessToast } from './toasts';
|
||||||
|
import { SearchFormValues } from '../components/Logs';
|
||||||
|
|
||||||
const getLogsWithParams = async (config: any) => {
|
const getLogsWithParams = async (config: any) => {
|
||||||
const { older_than, filter, ...values } = config;
|
const { older_than, filter, ...values } = config;
|
||||||
@@ -27,12 +28,10 @@ export const getAdditionalLogsRequest = createAction('GET_ADDITIONAL_LOGS_REQUES
|
|||||||
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
|
export const getAdditionalLogsFailure = createAction('GET_ADDITIONAL_LOGS_FAILURE');
|
||||||
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
|
export const getAdditionalLogsSuccess = createAction('GET_ADDITIONAL_LOGS_SUCCESS');
|
||||||
|
|
||||||
const shortPollQueryLogs = async (data: any, filter: any, dispatch: any, getState: any, total?: any) => {
|
const shortPollQueryLogs = async (data: any, filter: any, dispatch: any, currentQuery?: string, total?: any) => {
|
||||||
const { logs, oldest } = data;
|
const { logs, oldest } = data;
|
||||||
const totalData = total || { logs };
|
const totalData = total || { logs };
|
||||||
|
|
||||||
const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
|
|
||||||
const currentQuery = queryForm && queryForm.values.search;
|
|
||||||
const previousQuery = filter?.search;
|
const previousQuery = filter?.search;
|
||||||
const isQueryTheSame =
|
const isQueryTheSame =
|
||||||
typeof previousQuery === 'string' && typeof currentQuery === 'string' && previousQuery === currentQuery;
|
typeof previousQuery === 'string' && typeof currentQuery === 'string' && previousQuery === currentQuery;
|
||||||
@@ -51,7 +50,7 @@ const shortPollQueryLogs = async (data: any, filter: any, dispatch: any, getStat
|
|||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
if (additionalLogs.oldest.length > 0) {
|
if (additionalLogs.oldest.length > 0) {
|
||||||
return await shortPollQueryLogs(additionalLogs, filter, dispatch, getState, {
|
return await shortPollQueryLogs(additionalLogs, filter, dispatch, currentQuery, {
|
||||||
logs: [...totalData.logs, ...additionalLogs.logs],
|
logs: [...totalData.logs, ...additionalLogs.logs],
|
||||||
oldest: additionalLogs.oldest,
|
oldest: additionalLogs.oldest,
|
||||||
});
|
});
|
||||||
@@ -91,17 +90,18 @@ export const updateLogs = () => async (dispatch: any, getState: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogs = () => async (dispatch: any, getState: any) => {
|
export const getLogs = (currentQuery?: string) => async (dispatch: any, getState: any) => {
|
||||||
dispatch(getLogsRequest());
|
dispatch(getLogsRequest());
|
||||||
try {
|
try {
|
||||||
const { isFiltered, filter, oldest } = getState().queryLogs;
|
const { isFiltered, filter, oldest } = getState().queryLogs;
|
||||||
|
|
||||||
const data = await getLogsWithParams({
|
const data = await getLogsWithParams({
|
||||||
older_than: oldest,
|
older_than: oldest,
|
||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFiltered) {
|
if (isFiltered) {
|
||||||
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
const additionalData = await shortPollQueryLogs(data, filter, dispatch, currentQuery);
|
||||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||||
dispatch(getLogsSuccess(updatedData));
|
dispatch(getLogsSuccess(updatedData));
|
||||||
} else {
|
} else {
|
||||||
@@ -122,13 +122,13 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
|
|||||||
* @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object
|
* @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object
|
||||||
* @returns function
|
* @returns function
|
||||||
*/
|
*/
|
||||||
export const setLogsFilter = (filter: any) => setLogsFilterRequest(filter);
|
export const setLogsFilter = (filter: SearchFormValues) => setLogsFilterRequest(filter);
|
||||||
|
|
||||||
export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST');
|
export const setFilteredLogsRequest = createAction('SET_FILTERED_LOGS_REQUEST');
|
||||||
export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
|
export const setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
|
||||||
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
|
export const setFilteredLogsSuccess = createAction('SET_FILTERED_LOGS_SUCCESS');
|
||||||
|
|
||||||
export const setFilteredLogs = (filter?: any) => async (dispatch: any, getState: any) => {
|
export const setFilteredLogs = (filter?: SearchFormValues) => async (dispatch: any) => {
|
||||||
dispatch(setFilteredLogsRequest());
|
dispatch(setFilteredLogsRequest());
|
||||||
try {
|
try {
|
||||||
const data = await getLogsWithParams({
|
const data = await getLogsWithParams({
|
||||||
@@ -136,7 +136,9 @@ export const setFilteredLogs = (filter?: any) => async (dispatch: any, getState:
|
|||||||
filter,
|
filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const additionalData = await shortPollQueryLogs(data, filter, dispatch, getState);
|
const currentQuery = filter?.search;
|
||||||
|
|
||||||
|
const additionalData = await shortPollQueryLogs(data, filter, dispatch, currentQuery);
|
||||||
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
const updatedData = additionalData.logs ? { ...data, ...additionalData } : data;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
|
|||||||
@@ -1,62 +1,74 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
|
|
||||||
import { renderInputField } from '../../../helpers/form';
|
|
||||||
|
|
||||||
import Info from './Info';
|
import Info from './Info';
|
||||||
import { FORM_NAME } from '../../../helpers/constants';
|
|
||||||
import { RootState } from '../../../initialState';
|
|
||||||
|
|
||||||
interface CheckProps {
|
import { RootState } from '../../../initialState';
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
import { validateRequiredValue } from '../../../helpers/validators';
|
||||||
pristine: boolean;
|
import { Input } from '../../ui/Controls/Input';
|
||||||
invalid: boolean;
|
|
||||||
|
interface FormValues {
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Check = (props: CheckProps) => {
|
type Props = {
|
||||||
const { pristine, invalid, handleSubmit } = props;
|
onSubmit?: (data: FormValues) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Check = ({ onSubmit }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const processingCheck = useSelector((state: RootState) => state.filtering.processingCheck);
|
const processingCheck = useSelector((state: RootState) => state.filtering.processingCheck);
|
||||||
|
|
||||||
const hostname = useSelector((state: RootState) => state.filtering.check.hostname);
|
const hostname = useSelector((state: RootState) => state.filtering.check.hostname);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('check_title')} subtitle={t('check_desc')}>
|
<Card title={t('check_title')} subtitle={t('check_desc')}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 col-md-6">
|
||||||
<div className="input-group">
|
<Controller
|
||||||
<Field
|
name="name"
|
||||||
id="name"
|
control={control}
|
||||||
name="name"
|
rules={{ validate: validateRequiredValue }}
|
||||||
component={renderInputField}
|
render={({ field, fieldState }) => (
|
||||||
type="text"
|
<Input
|
||||||
className="form-control"
|
{...field}
|
||||||
placeholder={t('form_enter_host')}
|
type="text"
|
||||||
/>
|
data-testid="check_domain_name"
|
||||||
|
placeholder={t('form_enter_host')}
|
||||||
<span className="input-group-append">
|
error={fieldState.error?.message}
|
||||||
<button
|
rightAddon={
|
||||||
className="btn btn-success btn-standard btn-large"
|
<span className="input-group-append">
|
||||||
type="submit"
|
<button
|
||||||
onClick={handleSubmit}
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={pristine || invalid || processingCheck}>
|
type="submit"
|
||||||
{t('check')}
|
data-testid="check_domain_submit"
|
||||||
</button>
|
disabled={!isDirty || !isValid || processingCheck}>
|
||||||
</span>
|
{t('check')}
|
||||||
</div>
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{hostname && (
|
{hostname && (
|
||||||
<>
|
<>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Info />
|
<Info />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -67,4 +79,4 @@ const Check = (props: CheckProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({ form: FORM_NAME.DOMAIN_CHECK })(Check);
|
export default Check;
|
||||||
|
|||||||
94
client/src/components/Filters/FiltersList.tsx
Normal file
94
client/src/components/Filters/FiltersList.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Checkbox } from '../ui/Controls/Checkbox';
|
||||||
|
|
||||||
|
const getIconsData = (homepage: string, source: string) => [
|
||||||
|
{
|
||||||
|
iconName: 'dashboard',
|
||||||
|
href: homepage,
|
||||||
|
className: 'ml-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'info',
|
||||||
|
href: source,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderIcons = (iconsData: { iconName: string; href: string; className?: string }[]) =>
|
||||||
|
iconsData.map(({ iconName, href, className = '' }) => (
|
||||||
|
<a
|
||||||
|
key={iconName}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={classNames('d-flex align-items-center', className)}>
|
||||||
|
<svg className="icon icon--15 mr-1 icon--gray">
|
||||||
|
<use xlinkHref={`#${iconName}`} />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
));
|
||||||
|
|
||||||
|
type Filter = {
|
||||||
|
categoryId: string;
|
||||||
|
homepage: string;
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Category = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
categories: Record<string, Category>;
|
||||||
|
filters: Record<string, Filter>;
|
||||||
|
selectedSources: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersList = ({ categories, filters, selectedSources }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.entries(categories).map(([categoryId, category]) => {
|
||||||
|
const categoryFilters = Object.entries(filters)
|
||||||
|
.filter(([, filter]) => filter.categoryId === categoryId)
|
||||||
|
.map(([key, filter]) => ({ ...filter, id: key }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category.name} className="modal-body__item">
|
||||||
|
<h6 className="font-weight-bold mb-1">{t(category.name)}</h6>
|
||||||
|
<p className="mb-3">{t(category.description)}</p>
|
||||||
|
{categoryFilters.map((filter) => {
|
||||||
|
const { homepage, source, name, id } = filter;
|
||||||
|
const isSelected = selectedSources[source];
|
||||||
|
const iconsData = getIconsData(homepage, source);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={name} className="d-flex align-items-center pb-1">
|
||||||
|
<Controller
|
||||||
|
name={id}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid={`filters_${id}`}
|
||||||
|
title={name}
|
||||||
|
disabled={isSelected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{renderIcons(iconsData)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,208 +1,152 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { validatePath, validateRequiredValue } from '../../helpers/validators';
|
import { validatePath, validateRequiredValue } from '../../helpers/validators';
|
||||||
|
|
||||||
import { CheckboxField, renderInputField } from '../../helpers/form';
|
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE } from '../../helpers/constants';
|
||||||
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants';
|
|
||||||
import filtersCatalog from '../../helpers/filters/filters';
|
import filtersCatalog from '../../helpers/filters/filters';
|
||||||
|
import { FiltersList } from './FiltersList';
|
||||||
|
import { Input } from '../ui/Controls/Input';
|
||||||
|
|
||||||
const getIconsData = (homepage: any, source: any) => [
|
type FormValues = {
|
||||||
{
|
enabled: boolean;
|
||||||
iconName: 'dashboard',
|
name: string;
|
||||||
href: homepage,
|
url: string;
|
||||||
className: 'ml-1',
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
iconName: 'info',
|
|
||||||
href: source,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const renderIcons = (iconsData: any) =>
|
const defaultValues: FormValues = {
|
||||||
iconsData.map(({ iconName, href, className = '' }: any) => (
|
enabled: true,
|
||||||
<a
|
name: '',
|
||||||
key={iconName}
|
url: '',
|
||||||
href={href}
|
};
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={classNames('d-flex align-items-center', className)}>
|
|
||||||
<svg className="icon icon--15 mr-1 icon--gray">
|
|
||||||
<use xlinkHref={`#${iconName}`} />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
));
|
|
||||||
|
|
||||||
interface renderCheckboxFieldProps {
|
type Props = {
|
||||||
// https://redux-form.com/8.3.0/docs/api/field.md/#props
|
closeModal: () => void;
|
||||||
input: {
|
onSubmit: (values: FormValues) => void;
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
checked: boolean;
|
|
||||||
onChange: (...args: unknown[]) => unknown;
|
|
||||||
};
|
|
||||||
disabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderCheckboxField = (props: renderCheckboxFieldProps) => (
|
|
||||||
<CheckboxField
|
|
||||||
{...props}
|
|
||||||
meta={{ touched: false, error: null }}
|
|
||||||
input={{
|
|
||||||
...props.input,
|
|
||||||
checked: props.disabled || props.input.checked,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFilters = ({ categories, filters }: any, selectedSources: any, t: any) =>
|
|
||||||
Object.keys(categories).map((categoryId) => {
|
|
||||||
const category = categories[categoryId];
|
|
||||||
const categoryFilters: any = [];
|
|
||||||
Object.keys(filters)
|
|
||||||
.sort()
|
|
||||||
.forEach((key) => {
|
|
||||||
const filter = filters[key];
|
|
||||||
filter.id = key;
|
|
||||||
if (filter.categoryId === categoryId) {
|
|
||||||
categoryFilters.push(filter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={category.name} className="modal-body__item">
|
|
||||||
<h6 className="font-weight-bold mb-1">{t(category.name)}</h6>
|
|
||||||
|
|
||||||
<p className="mb-3">{t(category.description)}</p>
|
|
||||||
|
|
||||||
{categoryFilters.map((filter) => {
|
|
||||||
const { homepage, source, name } = filter;
|
|
||||||
|
|
||||||
const isSelected = Object.prototype.hasOwnProperty.call(selectedSources, source);
|
|
||||||
|
|
||||||
const iconsData = getIconsData(homepage, source);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={name} className="d-flex align-items-center pb-1">
|
|
||||||
<Field
|
|
||||||
name={filter.id}
|
|
||||||
type="checkbox"
|
|
||||||
component={renderCheckboxField}
|
|
||||||
placeholder={t(name)}
|
|
||||||
disabled={isSelected}
|
|
||||||
/>
|
|
||||||
{renderIcons(iconsData)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
interface FormProps {
|
|
||||||
t: (...args: unknown[]) => string;
|
|
||||||
closeModal: (...args: unknown[]) => unknown;
|
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
|
||||||
processingAddFilter: boolean;
|
processingAddFilter: boolean;
|
||||||
processingConfigFilter: boolean;
|
processingConfigFilter: boolean;
|
||||||
whitelist?: boolean;
|
whitelist?: boolean;
|
||||||
modalType: string;
|
modalType: string;
|
||||||
toggleFilteringModal: (...args: unknown[]) => unknown;
|
toggleFilteringModal: ({ type }: { type?: keyof typeof MODAL_TYPE }) => void;
|
||||||
selectedSources?: object;
|
selectedSources?: Record<string, boolean>;
|
||||||
}
|
initialValues?: FormValues;
|
||||||
|
};
|
||||||
|
|
||||||
const Form = (props: FormProps) => {
|
export const Form = ({
|
||||||
const {
|
closeModal,
|
||||||
t,
|
processingAddFilter,
|
||||||
closeModal,
|
processingConfigFilter,
|
||||||
handleSubmit,
|
whitelist,
|
||||||
processingAddFilter,
|
modalType,
|
||||||
processingConfigFilter,
|
toggleFilteringModal,
|
||||||
whitelist,
|
selectedSources,
|
||||||
modalType,
|
onSubmit,
|
||||||
toggleFilteringModal,
|
initialValues,
|
||||||
selectedSources,
|
}: Props) => {
|
||||||
} = props;
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const openModal = (modalType: any, timeout = MODAL_OPEN_TIMEOUT) => {
|
const methods = useForm({
|
||||||
toggleFilteringModal();
|
defaultValues: {
|
||||||
|
...defaultValues,
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
mode: 'onBlur',
|
||||||
|
});
|
||||||
|
const { handleSubmit, control } = methods;
|
||||||
|
|
||||||
|
const openModal = (modalType: keyof typeof MODAL_TYPE, timeout = MODAL_OPEN_TIMEOUT) => {
|
||||||
|
toggleFilteringModal(undefined);
|
||||||
setTimeout(() => toggleFilteringModal({ type: modalType }), timeout);
|
setTimeout(() => toggleFilteringModal({ type: modalType }), timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFilteringListModal = () => openModal(MODAL_TYPE.CHOOSE_FILTERING_LIST);
|
const openFilteringListModal = () => openModal('CHOOSE_FILTERING_LIST');
|
||||||
|
|
||||||
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
|
const openAddFiltersModal = () => openModal('ADD_FILTERS');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<FormProvider {...methods}>
|
||||||
<div className="modal-body modal-body--filters">
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
<div className="modal-body modal-body--filters">
|
||||||
<div className="d-flex justify-content-around">
|
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||||
<button
|
<div className="d-flex justify-content-around">
|
||||||
onClick={openFilteringListModal}
|
<button
|
||||||
className="btn btn-success btn-standard mr-2 btn-large">
|
onClick={openFilteringListModal}
|
||||||
{t('choose_from_list')}
|
className="btn btn-success btn-standard mr-2 btn-large">
|
||||||
</button>
|
{t('choose_from_list')}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
||||||
{t('add_custom_list')}
|
{t('add_custom_list')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST && renderFilters(filtersCatalog, selectedSources, t)}
|
|
||||||
{modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST && modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
|
||||||
<>
|
|
||||||
<div className="form__group">
|
|
||||||
<Field
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
component={renderInputField}
|
|
||||||
className="form-control"
|
|
||||||
placeholder={t('enter_name_hint')}
|
|
||||||
normalizeOnBlur={(data: any) => data.trim()}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{modalType === MODAL_TYPE.CHOOSE_FILTERING_LIST && (
|
||||||
|
<FiltersList
|
||||||
|
categories={filtersCatalog.categories}
|
||||||
|
filters={filtersCatalog.filters}
|
||||||
|
selectedSources={selectedSources}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{modalType !== MODAL_TYPE.CHOOSE_FILTERING_LIST && modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||||
|
<>
|
||||||
|
<div className="form__group">
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="filters_name"
|
||||||
|
placeholder={t('enter_name_hint')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
id="url"
|
name="url"
|
||||||
name="url"
|
control={control}
|
||||||
type="text"
|
rules={{ validate: { validateRequiredValue, validatePath } }}
|
||||||
component={renderInputField}
|
render={({ field, fieldState }) => (
|
||||||
className="form-control"
|
<Input
|
||||||
placeholder={t('enter_url_or_path_hint')}
|
{...field}
|
||||||
validate={[validateRequiredValue, validatePath]}
|
type="text"
|
||||||
normalizeOnBlur={(data: any) => data.trim()}
|
data-testid="filters_url"
|
||||||
/>
|
placeholder={t('enter_url_or_path_hint')}
|
||||||
</div>
|
error={fieldState.error?.message}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form__description">
|
<div className="form__description">
|
||||||
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
||||||
{t('cancel_btn')}
|
{t('cancel_btn')}
|
||||||
</button>
|
|
||||||
|
|
||||||
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-success"
|
|
||||||
disabled={processingAddFilter || processingConfigFilter}>
|
|
||||||
{t('save_btn')}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||||
</form>
|
<button
|
||||||
|
type="submit"
|
||||||
|
data-testid="filters_save"
|
||||||
|
className="btn btn-success"
|
||||||
|
disabled={processingAddFilter || processingConfigFilter}>
|
||||||
|
{t('save_btn')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.FILTER })])(Form);
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { withTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { MODAL_TYPE } from '../../helpers/constants';
|
import { MODAL_TYPE } from '../../helpers/constants';
|
||||||
|
|
||||||
import Form from './Form';
|
import { Form } from './Form';
|
||||||
import '../ui/Modal.css';
|
import '../ui/Modal.css';
|
||||||
|
|
||||||
import { getMap } from '../../helpers/helpers';
|
import { getMap } from '../../helpers/helpers';
|
||||||
@@ -75,25 +75,15 @@ class Modal extends Component<ModalProps> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|
||||||
processingAddFilter,
|
processingAddFilter,
|
||||||
|
|
||||||
processingConfigFilter,
|
processingConfigFilter,
|
||||||
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
||||||
modalType,
|
modalType,
|
||||||
|
|
||||||
currentFilterData,
|
currentFilterData,
|
||||||
|
|
||||||
whitelist,
|
whitelist,
|
||||||
|
|
||||||
toggleFilteringModal,
|
toggleFilteringModal,
|
||||||
|
|
||||||
filters,
|
filters,
|
||||||
|
|
||||||
t,
|
t,
|
||||||
|
|
||||||
filtersCatalog,
|
filtersCatalog,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,69 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import { renderInputField } from '../../../helpers/form';
|
|
||||||
import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators';
|
import { validateAnswer, validateDomain, validateRequiredValue } from '../../../helpers/validators';
|
||||||
import { FORM_NAME } from '../../../helpers/constants';
|
import { Input } from '../../ui/Controls/Input';
|
||||||
|
|
||||||
interface FormProps {
|
interface RewriteFormValues {
|
||||||
pristine: boolean;
|
domain: string;
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
answer: string;
|
||||||
reset: (...args: unknown[]) => string;
|
|
||||||
toggleRewritesModal: (...args: unknown[]) => unknown;
|
|
||||||
submitting: boolean;
|
|
||||||
processingAdd: boolean;
|
|
||||||
t: (...args: unknown[]) => string;
|
|
||||||
initialValues?: object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Form = (props: FormProps) => {
|
type Props = {
|
||||||
const { t, handleSubmit, reset, pristine, submitting, toggleRewritesModal, processingAdd } = props;
|
processingAdd: boolean;
|
||||||
|
currentRewrite?: RewriteFormValues;
|
||||||
|
toggleRewritesModal: () => void;
|
||||||
|
onSubmit?: (data: RewriteFormValues) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Form = ({ processingAdd, currentRewrite, toggleRewritesModal, onSubmit }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
formState: { isDirty, isSubmitting },
|
||||||
|
} = useForm<RewriteFormValues>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
domain: currentRewrite?.domain || '',
|
||||||
|
answer: currentRewrite?.answer || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFormSubmit = async (data: RewriteFormValues) => {
|
||||||
|
if (onSubmit) {
|
||||||
|
await onSubmit(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
<Trans>domain_desc</Trans>
|
<Trans>domain_desc</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
id="domain"
|
|
||||||
name="domain"
|
name="domain"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t('form_domain')}
|
validate: validateDomain,
|
||||||
validate={[validateRequiredValue, validateDomain]}
|
required: validateRequiredValue,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="rewrites_domain"
|
||||||
|
placeholder={t('form_domain')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Trans>examples_title</Trans>:
|
<Trans>examples_title</Trans>:
|
||||||
@@ -44,7 +71,6 @@ const Form = (props: FormProps) => {
|
|||||||
<li>
|
<li>
|
||||||
<code>example.org</code> – <Trans>example_rewrite_domain</Trans>
|
<code>example.org</code> – <Trans>example_rewrite_domain</Trans>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<code>*.example.org</code> –
|
<code>*.example.org</code> –
|
||||||
<span>
|
<span>
|
||||||
@@ -53,14 +79,24 @@ const Form = (props: FormProps) => {
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
id="answer"
|
|
||||||
name="answer"
|
name="answer"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t('form_answer')}
|
validate: validateAnswer,
|
||||||
validate={[validateRequiredValue, validateAnswer]}
|
required: validateRequiredValue,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="rewrites_answer"
|
||||||
|
placeholder={t('form_answer')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,8 +113,9 @@ const Form = (props: FormProps) => {
|
|||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="rewrites_cancel"
|
||||||
className="btn btn-secondary btn-standard"
|
className="btn btn-secondary btn-standard"
|
||||||
disabled={submitting || processingAdd}
|
disabled={isSubmitting || processingAdd}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset();
|
reset();
|
||||||
toggleRewritesModal();
|
toggleRewritesModal();
|
||||||
@@ -88,8 +125,9 @@ const Form = (props: FormProps) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="rewrites_save"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={submitting || pristine || processingAdd}>
|
disabled={isSubmitting || !isDirty || processingAdd}>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,10 +136,4 @@ const Form = (props: FormProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default flow([
|
export default Form;
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.REWRITES,
|
|
||||||
enableReinitialize: true,
|
|
||||||
}),
|
|
||||||
])(Form);
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface ModalProps {
|
|||||||
processingAdd: boolean;
|
processingAdd: boolean;
|
||||||
processingDelete: boolean;
|
processingDelete: boolean;
|
||||||
modalType: string;
|
modalType: string;
|
||||||
currentRewrite?: object;
|
currentRewrite?: { answer: string, domain: string; };
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal = (props: ModalProps) => {
|
const Modal = (props: ModalProps) => {
|
||||||
@@ -23,7 +23,6 @@ const Modal = (props: ModalProps) => {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
toggleRewritesModal,
|
toggleRewritesModal,
|
||||||
processingAdd,
|
processingAdd,
|
||||||
processingDelete,
|
|
||||||
modalType,
|
modalType,
|
||||||
currentRewrite,
|
currentRewrite,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -50,11 +49,10 @@ const Modal = (props: ModalProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
initialValues={{ ...currentRewrite }}
|
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
toggleRewritesModal={toggleRewritesModal}
|
toggleRewritesModal={toggleRewritesModal}
|
||||||
processingAdd={processingAdd}
|
processingAdd={processingAdd}
|
||||||
processingDelete={processingDelete}
|
currentRewrite={currentRewrite}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
|
|||||||
@@ -1,38 +1,55 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { Trans } from 'react-i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import { toggleAllServices } from '../../../helpers/helpers';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { renderServiceField } from '../../../helpers/form';
|
import { ServiceField } from './ServiceField';
|
||||||
import { FORM_NAME } from '../../../helpers/constants';
|
|
||||||
|
export type BlockedService = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon_svg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
blocked_services: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
interface FormProps {
|
interface FormProps {
|
||||||
blockedServices: unknown[];
|
initialValues: Record<string, boolean>;
|
||||||
pristine: boolean;
|
blockedServices: BlockedService[];
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
onSubmit: (values: FormValues) => void;
|
||||||
change: (...args: unknown[]) => unknown;
|
|
||||||
submitting: boolean;
|
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
processingSet: boolean;
|
processingSet: boolean;
|
||||||
t: (...args: unknown[]) => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Form = (props: FormProps) => {
|
export const Form = ({ initialValues, blockedServices, processing, processingSet, onSubmit }: FormProps) => {
|
||||||
const { blockedServices, handleSubmit, change, pristine, submitting, processing, processingSet } = props;
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleAllServices = async (isSelected: boolean) => {
|
||||||
|
blockedServices.forEach((service: BlockedService) => setValue(`blocked_services.${service.id}`, isSelected));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<div className="row mb-4">
|
<div className="row mb-4">
|
||||||
<div className="col-6">
|
<div className="col-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="blocked_services_block_all"
|
||||||
className="btn btn-secondary btn-block"
|
className="btn btn-secondary btn-block"
|
||||||
disabled={processing || processingSet}
|
disabled={processing || processingSet}
|
||||||
onClick={() => toggleAllServices(blockedServices, change, true)}>
|
onClick={() => handleToggleAllServices(true)}>
|
||||||
<Trans>block_all</Trans>
|
<Trans>block_all</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,24 +57,30 @@ const Form = (props: FormProps) => {
|
|||||||
<div className="col-6">
|
<div className="col-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="blocked_services_unblock_all"
|
||||||
className="btn btn-secondary btn-block"
|
className="btn btn-secondary btn-block"
|
||||||
disabled={processing || processingSet}
|
disabled={processing || processingSet}
|
||||||
onClick={() => toggleAllServices(blockedServices, change, false)}>
|
onClick={() => handleToggleAllServices(false)}>
|
||||||
<Trans>unblock_all</Trans>
|
<Trans>unblock_all</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="services">
|
<div className="services">
|
||||||
{blockedServices.map((service: any) => (
|
{blockedServices.map((service: BlockedService) => (
|
||||||
<Field
|
<Controller
|
||||||
key={service.id}
|
key={service.id}
|
||||||
icon={service.icon_svg}
|
|
||||||
name={`blocked_services.${service.id}`}
|
name={`blocked_services.${service.id}`}
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={renderServiceField}
|
render={({ field }) => (
|
||||||
placeholder={service.name}
|
<ServiceField
|
||||||
disabled={processing || processingSet}
|
{...field}
|
||||||
|
data-testid={`blocked_services_${service.id}`}
|
||||||
|
placeholder={service.name}
|
||||||
|
disabled={processing || processingSet}
|
||||||
|
icon={service.icon_svg}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -66,19 +89,12 @@ const Form = (props: FormProps) => {
|
|||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="blocked_services_save"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={submitting || pristine || processing || processingSet}>
|
disabled={isSubmitting || processing || processingSet}>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.SERVICES,
|
|
||||||
enableReinitialize: true,
|
|
||||||
}),
|
|
||||||
])(Form);
|
|
||||||
|
|||||||
42
client/src/components/Filters/Services/ServiceField.tsx
Normal file
42
client/src/components/Filters/Services/ServiceField.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import { FieldValues, ControllerRenderProps } from 'react-hook-form';
|
||||||
|
|
||||||
|
type Props = ControllerRenderProps<FieldValues> & {
|
||||||
|
placeholder: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
icon?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServiceField = React.forwardRef<HTMLInputElement, Props>(
|
||||||
|
({ name, value, onChange, onBlur, placeholder, disabled, className, icon, error, ...rest }, ref) => (
|
||||||
|
<>
|
||||||
|
<label className={cn('service custom-switch', className)}>
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
type="checkbox"
|
||||||
|
className="custom-switch-input"
|
||||||
|
checked={!!value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
ref={ref}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="service__switch custom-switch-indicator"></span>
|
||||||
|
|
||||||
|
<span className="service__text" title={placeholder}>
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
{icon && <div dangerouslySetInnerHTML={{ __html: window.atob(icon) }} className="service__icon" />}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{!disabled && error && <span className="form__message form__message--error">{error}</span>}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
ServiceField.displayName = 'ServiceField';
|
||||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import Form from './Form';
|
import { Form } from './Form';
|
||||||
|
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
|
import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
|
||||||
@@ -86,7 +86,8 @@ const Services = () => {
|
|||||||
<Card
|
<Card
|
||||||
title={t('schedule_services')}
|
title={t('schedule_services')}
|
||||||
subtitle={t('schedule_services_desc')}
|
subtitle={t('schedule_services_desc')}
|
||||||
bodyType="card-body box-body--settings">
|
bodyType="card-body box-body--settings"
|
||||||
|
>
|
||||||
<ScheduleForm schedule={services.list.schedule} onScheduleSubmit={handleScheduleSubmit} />
|
<ScheduleForm schedule={services.list.schedule} onScheduleSubmit={handleScheduleSubmit} />
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,158 +1,71 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { Field, type InjectedFormProps, reduxForm } from 'redux-form';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
DEBOUNCE_FILTER_TIMEOUT,
|
DEBOUNCE_FILTER_TIMEOUT,
|
||||||
DEFAULT_LOGS_FILTER,
|
DEFAULT_LOGS_FILTER,
|
||||||
FORM_NAME,
|
|
||||||
RESPONSE_FILTER,
|
RESPONSE_FILTER,
|
||||||
RESPONSE_FILTER_QUERIES,
|
RESPONSE_FILTER_QUERIES,
|
||||||
} from '../../../helpers/constants';
|
} from '../../../helpers/constants';
|
||||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||||
import useDebounce from '../../../helpers/useDebounce';
|
import useDebounce from '../../../helpers/useDebounce';
|
||||||
|
|
||||||
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
|
import { getLogsUrlParams } from '../../../helpers/helpers';
|
||||||
|
|
||||||
import Tooltip from '../../ui/Tooltip';
|
import { SearchField } from './SearchField';
|
||||||
import { RootState } from '../../../initialState';
|
import { SearchFormValues } from '..';
|
||||||
|
|
||||||
interface renderFilterFieldProps {
|
type Props = {
|
||||||
input: {
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
id: string;
|
|
||||||
onClearInputClick: (...args: unknown[]) => unknown;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
placeholder?: string;
|
setIsLoading: (value: boolean) => void;
|
||||||
type?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
autoComplete?: string;
|
|
||||||
tooltip?: string;
|
|
||||||
onKeyDown?: (...args: unknown[]) => unknown;
|
|
||||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
|
||||||
meta: {
|
|
||||||
touched?: boolean;
|
|
||||||
error?: object;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderFilterField = ({
|
|
||||||
input,
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
placeholder,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
autoComplete,
|
|
||||||
tooltip,
|
|
||||||
meta: { touched, error },
|
|
||||||
onClearInputClick,
|
|
||||||
onKeyDown,
|
|
||||||
normalizeOnBlur,
|
|
||||||
}: renderFilterFieldProps) => {
|
|
||||||
const onBlur = (event: any) => createOnBlurHandler(event, input, normalizeOnBlur);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="input-group-search input-group-search__icon--magnifier">
|
|
||||||
<svg className="icons icon--24 icon--gray">
|
|
||||||
<use xlinkHref="#magnifier" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={id}
|
|
||||||
placeholder={placeholder}
|
|
||||||
type={type}
|
|
||||||
className={className}
|
|
||||||
disabled={disabled}
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
aria-label={placeholder}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames('input-group-search input-group-search__icon--cross', {
|
|
||||||
invisible: input.value.length < 1,
|
|
||||||
})}>
|
|
||||||
<svg className="icons icon--20 icon--gray" onClick={onClearInputClick}>
|
|
||||||
<use xlinkHref="#cross" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="input-group-search input-group-search__icon--tooltip">
|
|
||||||
<Tooltip content={tooltip} className="tooltip-container">
|
|
||||||
<svg className="icons icon--20 icon--gray">
|
|
||||||
<use xlinkHref="#question" />
|
|
||||||
</svg>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
{!disabled && touched && error && <span className="form__message form__message--error">{error}</span>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FORM_NAMES = {
|
export const Form = ({ className, setIsLoading }: Props) => {
|
||||||
search: 'search',
|
|
||||||
response_status: 'response_status',
|
|
||||||
};
|
|
||||||
|
|
||||||
type FiltersFormProps = {
|
|
||||||
className?: string;
|
|
||||||
responseStatusClass?: string;
|
|
||||||
setIsLoading: (...args: unknown[]) => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Form = (props: FiltersFormProps & InjectedFormProps) => {
|
|
||||||
const { className = '', responseStatusClass, setIsLoading, change } = props;
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { response_status, search } = useSelector(
|
const { register, watch, setValue } = useFormContext<SearchFormValues>();
|
||||||
(state: RootState) => state?.form[FORM_NAME.LOGS_FILTER].values,
|
|
||||||
shallowEqual,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [debouncedSearch, setDebouncedSearch] = useDebounce(search.trim(), DEBOUNCE_FILTER_TIMEOUT);
|
const searchValue = watch('search');
|
||||||
|
const responseStatusValue = watch('response_status');
|
||||||
|
|
||||||
|
const [debouncedSearch, setDebouncedSearch] = useDebounce(searchValue.trim(), DEBOUNCE_FILTER_TIMEOUT);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setLogsFilter({
|
setLogsFilter({
|
||||||
response_status,
|
response_status: responseStatusValue,
|
||||||
search: debouncedSearch,
|
search: debouncedSearch,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`);
|
history.replace(`${getLogsUrlParams(debouncedSearch, responseStatusValue)}`);
|
||||||
}, [response_status, debouncedSearch]);
|
}, [responseStatusValue, debouncedSearch]);
|
||||||
|
|
||||||
if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) {
|
useEffect(() => {
|
||||||
change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]);
|
if (responseStatusValue && !(responseStatusValue in RESPONSE_FILTER_QUERIES)) {
|
||||||
}
|
setValue('response_status', DEFAULT_LOGS_FILTER.response_status);
|
||||||
|
}
|
||||||
|
}, [responseStatusValue, setValue]);
|
||||||
|
|
||||||
const onInputClear = async () => {
|
const onInputClear = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
setValue('search', DEFAULT_LOGS_FILTER.search);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEnterPress = (e: any) => {
|
const onEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
setDebouncedSearch(search);
|
setDebouncedSearch(searchValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeOnBlur = (data: any) => data.trim();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="d-flex flex-wrap form-control--container"
|
className="d-flex flex-wrap form-control--container"
|
||||||
@@ -160,40 +73,28 @@ const Form = (props: FiltersFormProps & InjectedFormProps) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}>
|
}}>
|
||||||
<div className="field__search">
|
<div className="field__search">
|
||||||
<Field
|
<SearchField
|
||||||
id={FORM_NAMES.search}
|
value={searchValue}
|
||||||
name={FORM_NAMES.search}
|
handleChange={(val) => setValue('search', val)}
|
||||||
component={renderFilterField}
|
onKeyDown={onEnterPress}
|
||||||
type="text"
|
onClear={onInputClear}
|
||||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
|
||||||
placeholder={t('domain_or_client')}
|
placeholder={t('domain_or_client')}
|
||||||
tooltip={t('query_log_strict_search')}
|
tooltip={t('query_log_strict_search')}
|
||||||
onClearInputClick={onInputClear}
|
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||||
onKeyDown={onEnterPress}
|
|
||||||
normalizeOnBlur={normalizeOnBlur}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="field__select">
|
<div className="field__select">
|
||||||
<Field
|
<select
|
||||||
name={FORM_NAMES.response_status}
|
{...register('response_status')}
|
||||||
component="select"
|
className="form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block">
|
||||||
className={classNames(
|
|
||||||
'form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent',
|
|
||||||
responseStatusClass,
|
|
||||||
)}>
|
|
||||||
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
|
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
|
||||||
<option key={LABEL} value={QUERY} disabled={disabled}>
|
<option key={LABEL} value={QUERY} disabled={disabled}>
|
||||||
{t(LABEL)}
|
{t(LABEL)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Field>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FiltersForm = reduxForm<Record<string, any>, FiltersFormProps>({
|
|
||||||
form: FORM_NAME.LOGS_FILTER,
|
|
||||||
enableReinitialize: true,
|
|
||||||
})(Form);
|
|
||||||
|
|||||||
62
client/src/components/Logs/Filters/SearchField.tsx
Normal file
62
client/src/components/Logs/Filters/SearchField.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { ComponentProps } from 'react';
|
||||||
|
import Tooltip from '../../ui/Tooltip';
|
||||||
|
|
||||||
|
interface Props extends ComponentProps<'input'> {
|
||||||
|
handleChange: (newValue: string) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
tooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchField = ({
|
||||||
|
handleChange,
|
||||||
|
onClear,
|
||||||
|
value,
|
||||||
|
tooltip,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: Props) => {
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
handleChange(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
e.target.value = e.target.value.trim();
|
||||||
|
handleChange(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="input-group-search input-group-search__icon--magnifier">
|
||||||
|
<svg className="icons icon--24 icon--gray">
|
||||||
|
<use xlinkHref="#magnifier" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className={className}
|
||||||
|
value={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{typeof value === 'string' && value.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="input-group-search input-group-search__icon--cross"
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
<svg className="icons icon--20 icon--gray">
|
||||||
|
<use xlinkHref="#cross" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tooltip && (
|
||||||
|
<span className="input-group-search input-group-search__icon--tooltip">
|
||||||
|
<Tooltip content={tooltip} className="tooltip-container">
|
||||||
|
<svg className="icons icon--20 icon--gray">
|
||||||
|
<use xlinkHref="#question" />
|
||||||
|
</svg>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,17 +2,16 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { FiltersForm } from './Form';
|
import { Form } from './Form';
|
||||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||||
import { addSuccessToast } from '../../../actions/toasts';
|
import { addSuccessToast } from '../../../actions/toasts';
|
||||||
|
|
||||||
interface FiltersProps {
|
interface FiltersProps {
|
||||||
filter: object;
|
|
||||||
processingGetLogs: boolean;
|
processingGetLogs: boolean;
|
||||||
setIsLoading: (...args: unknown[]) => unknown;
|
setIsLoading: (...args: unknown[]) => unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
const Filters = ({ setIsLoading }: FiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -38,7 +37,9 @@ const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</h1>
|
</h1>
|
||||||
<FiltersForm responseStatusClass="d-sm-block" setIsLoading={setIsLoading} initialValues={filter} />
|
<Form
|
||||||
|
setIsLoading={setIsLoading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface InfiniteTableProps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
items: unknown[];
|
items: unknown[];
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
|
currentQuery: string;
|
||||||
setDetailedDataCurrent: Dispatch<SetStateAction<any>>;
|
setDetailedDataCurrent: Dispatch<SetStateAction<any>>;
|
||||||
setButtonType: (...args: unknown[]) => unknown;
|
setButtonType: (...args: unknown[]) => unknown;
|
||||||
setModalOpened: (...args: unknown[]) => unknown;
|
setModalOpened: (...args: unknown[]) => unknown;
|
||||||
@@ -27,6 +28,7 @@ const InfiniteTable = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
items,
|
items,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
|
currentQuery,
|
||||||
setDetailedDataCurrent,
|
setDetailedDataCurrent,
|
||||||
setButtonType,
|
setButtonType,
|
||||||
setModalOpened,
|
setModalOpened,
|
||||||
@@ -43,7 +45,7 @@ const InfiniteTable = ({
|
|||||||
|
|
||||||
const listener = useCallback(() => {
|
const listener = useCallback(() => {
|
||||||
if (!loadingRef.current && loader.current && isScrolledIntoView(loader.current)) {
|
if (!loadingRef.current && loader.current && isScrolledIntoView(loader.current)) {
|
||||||
dispatch(getLogs());
|
dispatch(getLogs(currentQuery));
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { BLOCK_ACTIONS, MEDIUM_SCREEN_SIZE } from '../../helpers/constants';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { BLOCK_ACTIONS, DEFAULT_LOGS_FILTER, MEDIUM_SCREEN_SIZE } from '../../helpers/constants';
|
||||||
|
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
|
|
||||||
@@ -29,7 +30,12 @@ import { BUTTON_PREFIX } from './Cells/helpers';
|
|||||||
import AnonymizerNotification from './AnonymizerNotification';
|
import AnonymizerNotification from './AnonymizerNotification';
|
||||||
import { RootState } from '../../initialState';
|
import { RootState } from '../../initialState';
|
||||||
|
|
||||||
const processContent = (data: any, buttonType: string) =>
|
export type SearchFormValues = {
|
||||||
|
search: string;
|
||||||
|
response_status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processContent = (data: any, _buttonType: string) =>
|
||||||
Object.entries(data).map(([key, value]) => {
|
Object.entries(data).map(([key, value]) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
@@ -76,7 +82,6 @@ const Logs = () => {
|
|||||||
const {
|
const {
|
||||||
enabled,
|
enabled,
|
||||||
processingGetConfig,
|
processingGetConfig,
|
||||||
// processingAdditionalLogs,
|
|
||||||
processingGetLogs,
|
processingGetLogs,
|
||||||
anonymize_client_ip: anonymizeClientIp,
|
anonymize_client_ip: anonymizeClientIp,
|
||||||
} = useSelector((state: RootState) => state.queryLogs, shallowEqual);
|
} = useSelector((state: RootState) => state.queryLogs, shallowEqual);
|
||||||
@@ -88,6 +93,17 @@ const Logs = () => {
|
|||||||
const search = search_url_param || filter?.search || '';
|
const search = search_url_param || filter?.search || '';
|
||||||
const response_status = response_status_url_param || filter?.response_status || '';
|
const response_status = response_status_url_param || filter?.response_status || '';
|
||||||
|
|
||||||
|
const formMethods = useForm<SearchFormValues>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
search: search || DEFAULT_LOGS_FILTER.search,
|
||||||
|
response_status: response_status || DEFAULT_LOGS_FILTER.response_status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { watch } = formMethods;
|
||||||
|
const currentQuery = watch('search');
|
||||||
|
|
||||||
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth <= MEDIUM_SCREEN_SIZE);
|
const [isSmallScreen, setIsSmallScreen] = useState(window.innerWidth <= MEDIUM_SCREEN_SIZE);
|
||||||
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
const [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
||||||
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
||||||
@@ -174,15 +190,12 @@ const Logs = () => {
|
|||||||
|
|
||||||
const renderPage = () => (
|
const renderPage = () => (
|
||||||
<>
|
<>
|
||||||
<Filters
|
<FormProvider {...formMethods}>
|
||||||
filter={{
|
<Filters
|
||||||
response_status,
|
setIsLoading={setIsLoading}
|
||||||
search,
|
processingGetLogs={processingGetLogs}
|
||||||
}}
|
/>
|
||||||
setIsLoading={setIsLoading}
|
</FormProvider>
|
||||||
processingGetLogs={processingGetLogs}
|
|
||||||
// processingAdditionalLogs={processingAdditionalLogs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InfiniteTable
|
<InfiniteTable
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -191,6 +204,7 @@ const Logs = () => {
|
|||||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||||
setButtonType={setButtonType}
|
setButtonType={setButtonType}
|
||||||
setModalOpened={setModalOpened}
|
setModalOpened={setModalOpened}
|
||||||
|
currentQuery={currentQuery}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import whoisCell from './whoisCell';
|
|||||||
|
|
||||||
import LogsSearchLink from '../../ui/LogsSearchLink';
|
import LogsSearchLink from '../../ui/LogsSearchLink';
|
||||||
|
|
||||||
import { sortIp } from '../../../helpers/helpers';
|
import { sortIp, formatNumber } from '../../../helpers/helpers';
|
||||||
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
|
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
|
||||||
import { TABLES_MIN_ROWS } from '../../../helpers/constants';
|
import { TABLES_MIN_ROWS } from '../../../helpers/constants';
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ class AutoClients extends Component<AutoClientsProps> {
|
|||||||
return (
|
return (
|
||||||
<div className="logs__row">
|
<div className="logs__row">
|
||||||
<div className="logs__text" title={clientStats}>
|
<div className="logs__text" title={clientStats}>
|
||||||
<LogsSearchLink search={row.original.ip}>{clientStats}</LogsSearchLink>
|
<LogsSearchLink search={row.original.ip}>{formatNumber(clientStats)}</LogsSearchLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import ReactTable from 'react-table';
|
|||||||
import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
|
import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
|
||||||
|
|
||||||
import { initSettings } from '../../../../actions';
|
import { initSettings } from '../../../../actions';
|
||||||
import { splitByNewLine, countClientsStatistics, sortIp, getService } from '../../../../helpers/helpers';
|
import { splitByNewLine, countClientsStatistics, sortIp, getService, formatNumber } from '../../../../helpers/helpers';
|
||||||
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE, TABLES_MIN_ROWS } from '../../../../helpers/constants';
|
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE, TABLES_MIN_ROWS } from '../../../../helpers/constants';
|
||||||
|
|
||||||
import Card from '../../../ui/Card';
|
import Card from '../../../ui/Card';
|
||||||
@@ -111,6 +111,12 @@ const ClientsTable = ({
|
|||||||
config.tags = [];
|
config.tags = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (values.ids) {
|
||||||
|
config.ids = values.ids.map((id) => id.name);
|
||||||
|
} else {
|
||||||
|
config.ids = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof values.upstreams_cache_size === 'string') {
|
if (typeof values.upstreams_cache_size === 'string') {
|
||||||
config.upstreams_cache_size = 0;
|
config.upstreams_cache_size = 0;
|
||||||
}
|
}
|
||||||
@@ -300,12 +306,15 @@ const ClientsTable = ({
|
|||||||
sortMethod: (a: any, b: any) => b - a,
|
sortMethod: (a: any, b: any) => b - a,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
Cell: (row: any) => {
|
Cell: (row: any) => {
|
||||||
const content = CellWrap(row);
|
let content = row.value;
|
||||||
|
if (typeof content === "number") {
|
||||||
if (!row.value) {
|
content = formatNumber(content);
|
||||||
|
} else {
|
||||||
|
content = CellWrap(row);
|
||||||
|
}
|
||||||
|
if (!content) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <LogsSearchLink search={row.original.name}>{content}</LogsSearchLink>;
|
return <LogsSearchLink search={row.original.name}>{content}</LogsSearchLink>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { ClientForm } from '../types';
|
||||||
|
import { BlockedService } from '../../../../Filters/Services/Form';
|
||||||
|
import { ServiceField } from '../../../../Filters/Services/ServiceField';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
services: BlockedService[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlockedServices = ({ services }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { watch, setValue, control } = useFormContext<ClientForm>();
|
||||||
|
|
||||||
|
const useGlobalServices = watch('use_global_blocked_services');
|
||||||
|
|
||||||
|
const handleToggleAllServices = (isSelected: boolean) => {
|
||||||
|
services.forEach((service: BlockedService) => setValue(`blocked_services.${service.id}`, isSelected));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div title={t('block_services')}>
|
||||||
|
<div className="form__group">
|
||||||
|
<Controller
|
||||||
|
name="use_global_blocked_services"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<ServiceField
|
||||||
|
{...field}
|
||||||
|
data-testid="clients_use_global_blocked_services"
|
||||||
|
placeholder={t('blocked_services_global')}
|
||||||
|
className="service--global"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row mb-4">
|
||||||
|
<div className="col-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="clients_block_all"
|
||||||
|
className="btn btn-secondary btn-block"
|
||||||
|
disabled={useGlobalServices}
|
||||||
|
onClick={() => handleToggleAllServices(true)}>
|
||||||
|
<Trans>block_all</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="clients_unblock_all"
|
||||||
|
className="btn btn-secondary btn-block"
|
||||||
|
disabled={useGlobalServices}
|
||||||
|
onClick={() => handleToggleAllServices(false)}>
|
||||||
|
<Trans>unblock_all</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{services.length > 0 && (
|
||||||
|
<div className="services">
|
||||||
|
{services.map((service: BlockedService) => (
|
||||||
|
<Controller
|
||||||
|
key={service.id}
|
||||||
|
name={`blocked_services.${service.id}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<ServiceField
|
||||||
|
{...field}
|
||||||
|
data-testid={`clients_service_${service.id}`}
|
||||||
|
placeholder={service.name}
|
||||||
|
disabled={useGlobalServices}
|
||||||
|
icon={service.icon_svg}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ClientForm } from '../types';
|
||||||
|
import { Input } from '../../../../ui/Controls/Input';
|
||||||
|
import { validateClientId, validateRequiredValue } from '../../../../../helpers/validators';
|
||||||
|
|
||||||
|
export const ClientIds = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { control } = useFormContext<ClientForm>();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray<ClientForm>({
|
||||||
|
control,
|
||||||
|
name: 'ids',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form__group">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="mb-1">
|
||||||
|
<Controller
|
||||||
|
name={`ids.${index}.name`}
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
validate: {
|
||||||
|
required: (value) => validateRequiredValue(value),
|
||||||
|
validId: (value) => validateClientId(value),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid={`clients_id_${index}`}
|
||||||
|
placeholder={t('form_enter_id')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onBlur={(event) => {
|
||||||
|
const trimmedValue = event.target.value.trim();
|
||||||
|
field.onBlur();
|
||||||
|
field.onChange(trimmedValue);
|
||||||
|
}}
|
||||||
|
rightAddon={
|
||||||
|
index !== 0 && (
|
||||||
|
<span className="input-group-append">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid={`clients_id_remove_${index}`}
|
||||||
|
className="btn btn-secondary btn-icon btn-icon--green"
|
||||||
|
onClick={() => remove(index)}>
|
||||||
|
<svg className="icon icon--24">
|
||||||
|
<use xlinkHref="#cross" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="clients_id_add"
|
||||||
|
className="btn btn-link btn-block btn-sm"
|
||||||
|
onClick={() => append({ name: '' })}
|
||||||
|
title={t('form_add_id')}>
|
||||||
|
<svg className="icon icon--24">
|
||||||
|
<use xlinkHref="#plus" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
import { captitalizeWords } from '../../../../../helpers/helpers';
|
||||||
|
import { ClientForm } from '../types';
|
||||||
|
import { Checkbox } from '../../../../ui/Controls/Checkbox';
|
||||||
|
|
||||||
|
type ProtectionSettings = 'use_global_settings' | 'filtering_enabled' | 'safebrowsing_enabled' | 'parental_enabled';
|
||||||
|
|
||||||
|
const settingsCheckboxes: {
|
||||||
|
name: ProtectionSettings;
|
||||||
|
placeholder: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: 'use_global_settings',
|
||||||
|
placeholder: i18next.t('client_global_settings'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'filtering_enabled',
|
||||||
|
placeholder: i18next.t('block_domain_use_filters_and_hosts'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'safebrowsing_enabled',
|
||||||
|
placeholder: i18next.t('use_adguard_browsing_sec'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parental_enabled',
|
||||||
|
placeholder: i18next.t('use_adguard_parental'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type LogsStatsSettings = 'ignore_querylog' | 'ignore_statistics';
|
||||||
|
|
||||||
|
const logAndStatsCheckboxes: { name: LogsStatsSettings; placeholder: string }[] = [
|
||||||
|
{
|
||||||
|
name: 'ignore_querylog',
|
||||||
|
placeholder: i18next.t('ignore_query_log'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ignore_statistics',
|
||||||
|
placeholder: i18next.t('ignore_statistics'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
safeSearchServices: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MainSettings = ({ safeSearchServices }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { watch, control } = useFormContext<ClientForm>();
|
||||||
|
|
||||||
|
const useGlobalSettings = watch('use_global_settings');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div title={t('main_settings')}>
|
||||||
|
<div className="form__label--bot form__label--bold">{t('protection_section_label')}</div>
|
||||||
|
{settingsCheckboxes.map((setting) => (
|
||||||
|
<div className="form__group" key={setting.name}>
|
||||||
|
<Controller
|
||||||
|
name={setting.name}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid={`clients_${setting.name}`}
|
||||||
|
title={setting.placeholder}
|
||||||
|
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<Controller
|
||||||
|
name="safe_search.enabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
data-testid="clients_safe_search"
|
||||||
|
{...field}
|
||||||
|
title={t('enforce_safe_search')}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group--inner">
|
||||||
|
{Object.keys(safeSearchServices).map((searchKey) => (
|
||||||
|
<div key={searchKey}>
|
||||||
|
<Controller
|
||||||
|
name={`safe_search.${searchKey}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid={`clients_safe_search_${searchKey}`}
|
||||||
|
title={captitalizeWords(searchKey)}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__label--bold form__label--top form__label--bot">
|
||||||
|
{t('log_and_stats_section_label')}
|
||||||
|
</div>
|
||||||
|
{logAndStatsCheckboxes.map((setting) => (
|
||||||
|
<div className="form__group" key={setting.name}>
|
||||||
|
<Controller
|
||||||
|
name={setting.name}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox {...field} data-testid={`clients_${setting.name}`} title={setting.placeholder} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { ScheduleForm } from '../../../../Filters/Services/ScheduleForm';
|
||||||
|
import { ClientForm } from '../types';
|
||||||
|
|
||||||
|
export const ScheduleServices = () => {
|
||||||
|
const { watch, setValue } = useFormContext<ClientForm>();
|
||||||
|
|
||||||
|
const blockedServicesSchedule = watch('blocked_services_schedule');
|
||||||
|
|
||||||
|
const handleScheduleSubmit = (values: any) => {
|
||||||
|
setValue('blocked_services_schedule', values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="form__desc mb-4">
|
||||||
|
<Trans>schedule_services_desc_client</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScheduleForm schedule={blockedServicesSchedule} onScheduleSubmit={handleScheduleSubmit} clientForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import Examples from '../../../Dns/Upstream/Examples';
|
||||||
|
import { UINT32_RANGE } from '../../../../../helpers/constants';
|
||||||
|
import { Textarea } from '../../../../ui/Controls/Textarea';
|
||||||
|
import { ClientForm } from '../types';
|
||||||
|
import { Checkbox } from '../../../../ui/Controls/Checkbox';
|
||||||
|
import { Input } from '../../../../ui/Controls/Input';
|
||||||
|
import { toNumber } from '../../../../../helpers/form';
|
||||||
|
|
||||||
|
export const UpstreamDns = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { control } = useFormContext<ClientForm>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div title={t('upstream_dns')}>
|
||||||
|
<div className="form__desc mb-3">
|
||||||
|
<Trans components={[<a href="#dns" key="0" />]}>upstream_dns_client_desc</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="upstreams"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
data-testid="clients_upstreams"
|
||||||
|
className="form-control form-control--textarea mb-5"
|
||||||
|
placeholder={t('upstream_dns')}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Examples />
|
||||||
|
|
||||||
|
<div className="form__label--bold mt-5 mb-3">{t('upstream_dns_cache_configuration')}</div>
|
||||||
|
|
||||||
|
<div className="form__group mb-2">
|
||||||
|
<Controller
|
||||||
|
name="upstreams_cache_enabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid="clients_upstreams_cache_enabled"
|
||||||
|
title={t('enable_upstream_dns_cache')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<label htmlFor="upstreams_cache_size" className="form__label">
|
||||||
|
{t('dns_cache_size')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="upstreams_cache_size"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
data-testid="clients_upstreams_cache_size"
|
||||||
|
placeholder={t('enter_cache_size')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
min={0}
|
||||||
|
max={UINT32_RANGE.MAX}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { BlockedServices } from './BlockedServices';
|
||||||
|
export { ClientIds } from './ClientIds';
|
||||||
|
export { ScheduleServices } from './ScheduleServices';
|
||||||
|
export { MainSettings } from './MainSettings';
|
||||||
|
export { UpstreamDns } from './UpstreamDns';
|
||||||
223
client/src/components/Settings/Clients/Form/index.tsx
Normal file
223
client/src/components/Settings/Clients/Form/index.tsx
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import Select from 'react-select';
|
||||||
|
|
||||||
|
import Tabs from '../../../ui/Tabs';
|
||||||
|
import { CLIENT_ID_LINK, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
|
||||||
|
import { RootState } from '../../../../initialState';
|
||||||
|
import { Input } from '../../../ui/Controls/Input';
|
||||||
|
import { validateRequiredValue } from '../../../../helpers/validators';
|
||||||
|
import { ClientForm } from './types';
|
||||||
|
import { BlockedServices, ClientIds, MainSettings, ScheduleServices, UpstreamDns } from './components';
|
||||||
|
|
||||||
|
import '../Service.css';
|
||||||
|
|
||||||
|
const defaultFormValues: ClientForm = {
|
||||||
|
ids: [{ name: '' }],
|
||||||
|
name: '',
|
||||||
|
tags: [],
|
||||||
|
use_global_settings: false,
|
||||||
|
filtering_enabled: false,
|
||||||
|
safebrowsing_enabled: false,
|
||||||
|
parental_enabled: false,
|
||||||
|
ignore_querylog: false,
|
||||||
|
ignore_statistics: false,
|
||||||
|
blocked_services: {},
|
||||||
|
safe_search: { enabled: false },
|
||||||
|
upstreams: '',
|
||||||
|
upstreams_cache_enabled: false,
|
||||||
|
upstreams_cache_size: 0,
|
||||||
|
use_global_blocked_services: false,
|
||||||
|
blocked_services_schedule: {
|
||||||
|
time_zone: LOCAL_TIMEZONE_VALUE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSubmit: (values: ClientForm) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
useGlobalSettings?: boolean;
|
||||||
|
useGlobalServices?: boolean;
|
||||||
|
blockedServicesSchedule?: {
|
||||||
|
time_zone: string;
|
||||||
|
};
|
||||||
|
processingAdding: boolean;
|
||||||
|
processingUpdating: boolean;
|
||||||
|
tagsOptions: { label: string; value: string }[];
|
||||||
|
initialValues?: ClientForm;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Form = ({
|
||||||
|
onSubmit,
|
||||||
|
onClose,
|
||||||
|
processingAdding,
|
||||||
|
processingUpdating,
|
||||||
|
tagsOptions,
|
||||||
|
initialValues,
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const methods = useForm<ClientForm>({
|
||||||
|
defaultValues: {
|
||||||
|
...defaultFormValues,
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
mode: 'onBlur',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting, isValid },
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const services = useSelector((store: RootState) => store?.services);
|
||||||
|
const { safe_search } = initialValues;
|
||||||
|
const safeSearchServices = { ...safe_search };
|
||||||
|
delete safeSearchServices.enabled;
|
||||||
|
|
||||||
|
const [activeTabLabel, setActiveTabLabel] = useState('settings');
|
||||||
|
|
||||||
|
const tabs = {
|
||||||
|
settings: {
|
||||||
|
title: 'settings',
|
||||||
|
component: <MainSettings safeSearchServices={safeSearchServices} />,
|
||||||
|
},
|
||||||
|
block_services: {
|
||||||
|
title: 'block_services',
|
||||||
|
component: <BlockedServices services={services?.allServices} />,
|
||||||
|
},
|
||||||
|
schedule_services: {
|
||||||
|
title: 'schedule_services',
|
||||||
|
component: <ScheduleServices />,
|
||||||
|
},
|
||||||
|
upstream_dns: {
|
||||||
|
title: 'upstream_dns',
|
||||||
|
component: <UpstreamDns />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeTab = tabs[activeTabLabel].component;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="form__group mb-0">
|
||||||
|
<div className="form__group">
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
rules={{ validate: validateRequiredValue }}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="clients_name"
|
||||||
|
placeholder={t('form_client_name')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onBlur={(event) => {
|
||||||
|
const trimmedValue = event.target.value.trim();
|
||||||
|
field.onBlur();
|
||||||
|
field.onChange(trimmedValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group mb-4">
|
||||||
|
<div className="form__label">
|
||||||
|
<strong className="mr-3">
|
||||||
|
<Trans>tags_title</Trans>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__desc mt-0 mb-2">
|
||||||
|
<Trans
|
||||||
|
components={[
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://link.adtidy.org/forward.html?action=dns_kb_filtering_syntax_ctag&from=ui&app=home"
|
||||||
|
key="0"
|
||||||
|
/>,
|
||||||
|
]}>
|
||||||
|
tags_desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="tags"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="clients_tags"
|
||||||
|
options={tagsOptions}
|
||||||
|
className="basic-multi-select"
|
||||||
|
classNamePrefix="select"
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<div className="form__label">
|
||||||
|
<strong className="mr-3">
|
||||||
|
<Trans>client_identifier</Trans>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__desc mt-0">
|
||||||
|
<Trans
|
||||||
|
components={[
|
||||||
|
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" key="0" />,
|
||||||
|
]}>
|
||||||
|
client_identifier_desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form__group">
|
||||||
|
<ClientIds />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
controlClass="form"
|
||||||
|
tabs={tabs}
|
||||||
|
activeTabLabel={activeTabLabel}
|
||||||
|
setActiveTabLabel={setActiveTabLabel}>
|
||||||
|
{activeTab}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-standard"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
onClose();
|
||||||
|
}}>
|
||||||
|
<Trans>cancel_btn</Trans>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={isSubmitting || !isValid || processingAdding || processingUpdating}>
|
||||||
|
<Trans>save_btn</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
28
client/src/components/Settings/Clients/Form/types.ts
Normal file
28
client/src/components/Settings/Clients/Form/types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export type ClientForm = {
|
||||||
|
name: string;
|
||||||
|
tags: { value: string; label: string }[];
|
||||||
|
ids: { name: string }[];
|
||||||
|
use_global_settings: boolean;
|
||||||
|
use_global_blocked_services: boolean;
|
||||||
|
blocked_services_schedule: {
|
||||||
|
time_zone: string;
|
||||||
|
};
|
||||||
|
safe_search: {
|
||||||
|
enabled: boolean;
|
||||||
|
[key: string]: boolean;
|
||||||
|
};
|
||||||
|
upstreams: string;
|
||||||
|
upstreams_cache_enabled: boolean;
|
||||||
|
upstreams_cache_size: number;
|
||||||
|
blocked_services: Record<string, boolean>;
|
||||||
|
filtering_enabled: boolean;
|
||||||
|
safebrowsing_enabled: boolean;
|
||||||
|
parental_enabled: boolean;
|
||||||
|
ignore_querylog: boolean;
|
||||||
|
ignore_statistics: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubmitClientForm = Omit<ClientForm, 'ids' | 'tags'> & {
|
||||||
|
ids: string[];
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
@@ -4,8 +4,15 @@ import { Trans, withTranslation } from 'react-i18next';
|
|||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
|
|
||||||
import { MODAL_TYPE } from '../../../helpers/constants';
|
import { MODAL_TYPE } from '../../../helpers/constants';
|
||||||
|
import { Form } from './Form';
|
||||||
|
|
||||||
import Form from './Form';
|
const normalizeIds = (initialIds?: string[]): { name: string }[] => {
|
||||||
|
if (!initialIds || initialIds.length === 0) {
|
||||||
|
return [{ name: '' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialIds.map((id: string) => ({ name: id }));
|
||||||
|
};
|
||||||
|
|
||||||
const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
|
const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
|
||||||
if (initial && initial.blocked_services) {
|
if (initial && initial.blocked_services) {
|
||||||
@@ -19,6 +26,7 @@ const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
|
|||||||
return {
|
return {
|
||||||
...initial,
|
...initial,
|
||||||
blocked_services: blocked,
|
blocked_services: blocked,
|
||||||
|
ids: normalizeIds(initial.ids),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +34,14 @@ const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
|
|||||||
return {
|
return {
|
||||||
...initial,
|
...initial,
|
||||||
name: clientName,
|
name: clientName,
|
||||||
ids: [clientId],
|
ids: [{ name: clientId }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return initial;
|
return {
|
||||||
|
...initial,
|
||||||
|
ids: normalizeIds(initial.ids),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
@@ -41,7 +52,7 @@ interface ModalProps {
|
|||||||
handleClose: (...args: unknown[]) => unknown;
|
handleClose: (...args: unknown[]) => unknown;
|
||||||
processingAdding: boolean;
|
processingAdding: boolean;
|
||||||
processingUpdating: boolean;
|
processingUpdating: boolean;
|
||||||
tagsOptions: unknown[];
|
tagsOptions: { label: string; value: string }[];
|
||||||
t: (...args: unknown[]) => string;
|
t: (...args: unknown[]) => string;
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,7 @@ const Modal = ({
|
|||||||
<Form
|
<Form
|
||||||
initialValues={{ ...initialData }}
|
initialValues={{ ...initialData }}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
handleClose={handleClose}
|
onClose={handleClose}
|
||||||
processingAdding={processingAdding}
|
processingAdding={processingAdding}
|
||||||
processingUpdating={processingUpdating}
|
processingUpdating={processingUpdating}
|
||||||
tagsOptions={tagsOptions}
|
tagsOptions={tagsOptions}
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { renderInputField, toNumber } from '../../../helpers/form';
|
import { UINT32_RANGE } from '../../../helpers/constants';
|
||||||
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
|
|
||||||
import {
|
import {
|
||||||
validateIpv4,
|
|
||||||
validateRequiredValue,
|
|
||||||
validateIpv4RangeEnd,
|
|
||||||
validateGatewaySubnetMask,
|
validateGatewaySubnetMask,
|
||||||
validateIpForGatewaySubnetMask,
|
validateIpForGatewaySubnetMask,
|
||||||
|
validateIpv4,
|
||||||
|
validateIpv4RangeEnd,
|
||||||
validateNotInRange,
|
validateNotInRange,
|
||||||
|
validateRequiredValue,
|
||||||
} from '../../../helpers/validators';
|
} from '../../../helpers/validators';
|
||||||
import { RootState } from '../../../initialState';
|
import { DhcpFormValues } from '.';
|
||||||
|
import { Input } from '../../ui/Controls/Input';
|
||||||
|
import { toNumber } from '../../../helpers/form';
|
||||||
|
|
||||||
interface FormDHCPv4Props {
|
type FormDHCPv4Props = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
|
||||||
submitting: boolean;
|
|
||||||
initialValues: { v4?: any };
|
|
||||||
processingConfig?: boolean;
|
processingConfig?: boolean;
|
||||||
change: (field: string, value: any) => void;
|
|
||||||
reset: () => void;
|
|
||||||
ipv4placeholders?: {
|
ipv4placeholders?: {
|
||||||
gateway_ip: string;
|
gateway_ip: string;
|
||||||
subnet_mask: string;
|
subnet_mask: string;
|
||||||
@@ -30,127 +24,179 @@ interface FormDHCPv4Props {
|
|||||||
range_end: string;
|
range_end: string;
|
||||||
lease_duration: string;
|
lease_duration: string;
|
||||||
};
|
};
|
||||||
}
|
interfaces: any;
|
||||||
|
onSubmit?: (data: DhcpFormValues) => void;
|
||||||
|
};
|
||||||
|
|
||||||
const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholders }: FormDHCPv4Props) => {
|
const FormDHCPv4 = ({ processingConfig, ipv4placeholders, interfaces, onSubmit }: FormDHCPv4Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
} = useFormContext<DhcpFormValues>();
|
||||||
|
|
||||||
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
|
const interfaceName = watch('interface_name');
|
||||||
const interface_name = interfaces?.values?.interface_name;
|
const isInterfaceIncludesIpv4 = interfaces?.[interfaceName]?.ipv4_addresses;
|
||||||
|
|
||||||
const isInterfaceIncludesIpv4 = useSelector(
|
const formValues = watch('v4');
|
||||||
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
|
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
|
||||||
);
|
const hasV4Errors = errors.v4 && Object.keys(errors.v4).length > 0;
|
||||||
|
|
||||||
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
|
const isDisabled = useMemo(() => {
|
||||||
|
return isSubmitting || hasV4Errors || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig;
|
||||||
const invalid =
|
}, [isSubmitting, hasV4Errors, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]);
|
||||||
dhcp?.syncErrors ||
|
|
||||||
interfaces?.syncErrors ||
|
|
||||||
!isInterfaceIncludesIpv4 ||
|
|
||||||
isEmptyConfig ||
|
|
||||||
submitting ||
|
|
||||||
processingConfig;
|
|
||||||
|
|
||||||
const validateRequired = useCallback(
|
|
||||||
(value) => {
|
|
||||||
if (isEmptyConfig) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return validateRequiredValue(value);
|
|
||||||
},
|
|
||||||
[isEmptyConfig],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_gateway_input')}</label>
|
<Controller
|
||||||
|
|
||||||
<Field
|
|
||||||
name="v4.gateway_ip"
|
name="v4.gateway_ip"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t(ipv4placeholders.gateway_ip)}
|
ipv4: validateIpv4,
|
||||||
validate={[validateIpv4, validateRequired, validateNotInRange]}
|
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
|
||||||
disabled={!isInterfaceIncludesIpv4}
|
notInRange: validateNotInRange,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="v4_gateway_ip"
|
||||||
|
label={t('dhcp_form_gateway_input')}
|
||||||
|
placeholder={t(ipv4placeholders.gateway_ip)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isInterfaceIncludesIpv4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_subnet_input')}</label>
|
<Controller
|
||||||
|
|
||||||
<Field
|
|
||||||
name="v4.subnet_mask"
|
name="v4.subnet_mask"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t(ipv4placeholders.subnet_mask)}
|
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
|
||||||
validate={[validateRequired, validateGatewaySubnetMask]}
|
subnet: validateGatewaySubnetMask,
|
||||||
disabled={!isInterfaceIncludesIpv4}
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="v4_subnet_mask"
|
||||||
|
label={t('dhcp_form_subnet_input')}
|
||||||
|
placeholder={t(ipv4placeholders.subnet_mask)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isInterfaceIncludesIpv4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group mb-0">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<label>{t('dhcp_form_range_title')}</label>
|
<label>{t('dhcp_form_range_title')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Field
|
<Controller
|
||||||
name="v4.range_start"
|
name="v4.range_start"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t(ipv4placeholders.range_start)}
|
ipv4: validateIpv4,
|
||||||
validate={[validateIpv4, validateIpForGatewaySubnetMask]}
|
gateway: validateIpForGatewaySubnetMask,
|
||||||
disabled={!isInterfaceIncludesIpv4}
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="v4_range_start"
|
||||||
|
placeholder={t(ipv4placeholders.range_start)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isInterfaceIncludesIpv4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Field
|
<Controller
|
||||||
name="v4.range_end"
|
name="v4.range_end"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t(ipv4placeholders.range_end)}
|
ipv4: validateIpv4,
|
||||||
validate={[validateIpv4, validateIpv4RangeEnd, validateIpForGatewaySubnetMask]}
|
rangeEnd: validateIpv4RangeEnd,
|
||||||
disabled={!isInterfaceIncludesIpv4}
|
gateway: validateIpForGatewaySubnetMask,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="v4_range_end"
|
||||||
|
placeholder={t(ipv4placeholders.range_end)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isInterfaceIncludesIpv4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_lease_title')}</label>
|
<Controller
|
||||||
|
|
||||||
<Field
|
|
||||||
name="v4.lease_duration"
|
name="v4.lease_duration"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="number"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t(ipv4placeholders.lease_duration)}
|
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
|
||||||
validate={validateRequired}
|
},
|
||||||
normalize={toNumber}
|
}}
|
||||||
min={1}
|
render={({ field, fieldState }) => (
|
||||||
max={UINT32_RANGE.MAX}
|
<Input
|
||||||
disabled={!isInterfaceIncludesIpv4}
|
{...field}
|
||||||
|
type="number"
|
||||||
|
data-testid="v4_lease_duration"
|
||||||
|
label={t('dhcp_form_lease_title')}
|
||||||
|
placeholder={t(ipv4placeholders.lease_duration)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isInterfaceIncludesIpv4}
|
||||||
|
min={1}
|
||||||
|
max={UINT32_RANGE.MAX}
|
||||||
|
value={field.value ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
|
<button
|
||||||
|
data-testid="v4_save"
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={isDisabled}>
|
||||||
{t('save_config')}
|
{t('save_config')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,9 +204,4 @@ const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholde
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm<
|
export default FormDHCPv4;
|
||||||
Record<string, any>,
|
|
||||||
Omit<FormDHCPv4Props, 'submitting' | 'handleSubmit' | 'reset' | 'change'>
|
|
||||||
>({
|
|
||||||
form: FORM_NAME.DHCPv4,
|
|
||||||
})(FormDHCPv4);
|
|
||||||
|
|||||||
@@ -1,93 +1,92 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { renderInputField, toNumber } from '../../../helpers/form';
|
import { UINT32_RANGE } from '../../../helpers/constants';
|
||||||
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
|
|
||||||
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
|
import { validateIpv6, validateRequiredValue } from '../../../helpers/validators';
|
||||||
import { RootState } from '../../../initialState';
|
import { DhcpFormValues } from '.';
|
||||||
|
import { Input } from '../../ui/Controls/Input';
|
||||||
|
import { toNumber } from '../../../helpers/form';
|
||||||
|
|
||||||
interface FormDHCPv6Props {
|
type FormDHCPv6Props = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
|
||||||
submitting: boolean;
|
|
||||||
initialValues: {
|
|
||||||
v6?: any;
|
|
||||||
};
|
|
||||||
change: (field: string, value: any) => void;
|
|
||||||
reset: () => void;
|
|
||||||
processingConfig?: boolean;
|
processingConfig?: boolean;
|
||||||
ipv6placeholders?: {
|
ipv6placeholders?: {
|
||||||
range_start: string;
|
range_start: string;
|
||||||
range_end: string;
|
range_end: string;
|
||||||
lease_duration: string;
|
lease_duration: string;
|
||||||
};
|
};
|
||||||
}
|
interfaces: any;
|
||||||
|
onSubmit?: (data: DhcpFormValues) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholders }: FormDHCPv6Props) => {
|
const FormDHCPv6 = ({ processingConfig, ipv6placeholders, interfaces, onSubmit }: FormDHCPv6Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isValid },
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
} = useFormContext<DhcpFormValues>();
|
||||||
|
|
||||||
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv6], shallowEqual);
|
const interfaceName = watch('interface_name');
|
||||||
|
const isInterfaceIncludesIpv6 = interfaces?.[interfaceName]?.ipv6_addresses;
|
||||||
|
|
||||||
const interfaces = useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual);
|
const formValues = watch('v6');
|
||||||
const interface_name = interfaces?.values?.interface_name;
|
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
|
||||||
|
|
||||||
const isInterfaceIncludesIpv6 = useSelector(
|
const isDisabled = useMemo(() => {
|
||||||
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses,
|
return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig;
|
||||||
);
|
}, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv6, isEmptyConfig]);
|
||||||
|
|
||||||
const isEmptyConfig = !Object.values(dhcp?.values?.v6 ?? {}).some(Boolean);
|
|
||||||
|
|
||||||
const invalid =
|
|
||||||
dhcp?.syncErrors ||
|
|
||||||
interfaces?.syncErrors ||
|
|
||||||
!isInterfaceIncludesIpv6 ||
|
|
||||||
isEmptyConfig ||
|
|
||||||
submitting ||
|
|
||||||
processingConfig;
|
|
||||||
|
|
||||||
const validateRequired = useCallback(
|
|
||||||
(value) => {
|
|
||||||
if (isEmptyConfig) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return validateRequiredValue(value);
|
|
||||||
},
|
|
||||||
[isEmptyConfig],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group mb-0">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<label>{t('dhcp_form_range_title')}</label>
|
<label>{t('dhcp_form_range_title')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Field
|
<Controller
|
||||||
name="v6.range_start"
|
name="v6.range_start"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: isInterfaceIncludesIpv6
|
||||||
placeholder={t(ipv6placeholders.range_start)}
|
? {
|
||||||
validate={[validateIpv6, validateRequired]}
|
ipv6: validateIpv6,
|
||||||
disabled={!isInterfaceIncludesIpv6}
|
required: validateRequiredValue,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="v6_range_start"
|
||||||
|
placeholder={t(ipv6placeholders.range_start)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isInterfaceIncludesIpv6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Field
|
<Controller
|
||||||
name="v6.range_end"
|
name="v6.range_end"
|
||||||
component="input"
|
control={control}
|
||||||
type="text"
|
render={({ field, fieldState }) => (
|
||||||
className="form-control disabled cursor--not-allowed"
|
<Input
|
||||||
placeholder={t(ipv6placeholders.range_end)}
|
{...field}
|
||||||
value={t(ipv6placeholders.range_end)}
|
type="text"
|
||||||
disabled
|
data-testid="v6_range_end"
|
||||||
|
placeholder={t(ipv6placeholders.range_end)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,25 +96,43 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
|
|||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6 form__group form__group--settings">
|
<div className="col-lg-6 form__group form__group--settings">
|
||||||
<label>{t('dhcp_form_lease_title')}</label>
|
<Controller
|
||||||
|
|
||||||
<Field
|
|
||||||
name="v6.lease_duration"
|
name="v6.lease_duration"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="number"
|
rules={{
|
||||||
className="form-control"
|
validate: isInterfaceIncludesIpv6
|
||||||
placeholder={t(ipv6placeholders.lease_duration)}
|
? {
|
||||||
validate={validateRequired}
|
required: validateRequiredValue,
|
||||||
normalizeOnBlur={toNumber}
|
}
|
||||||
min={1}
|
: undefined,
|
||||||
max={UINT32_RANGE.MAX}
|
}}
|
||||||
disabled={!isInterfaceIncludesIpv6}
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
data-testid="v6_lease_duration"
|
||||||
|
label={t('dhcp_form_lease_title')}
|
||||||
|
placeholder={t(ipv6placeholders.lease_duration)}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isInterfaceIncludesIpv6}
|
||||||
|
min={1}
|
||||||
|
max={UINT32_RANGE.MAX}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button type="submit" className="btn btn-success btn-standard" disabled={invalid}>
|
<button
|
||||||
|
data-testid="v6_save"
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={isDisabled}>
|
||||||
{t('save_config')}
|
{t('save_config')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,9 +140,4 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm<
|
export default FormDHCPv6;
|
||||||
Record<string, any>,
|
|
||||||
Omit<FormDHCPv6Props, 'handleSubmit' | 'change' | 'submitting' | 'reset'>
|
|
||||||
>({
|
|
||||||
form: FORM_NAME.DHCPv6,
|
|
||||||
})(FormDHCPv6);
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { renderSelectField } from '../../../helpers/form';
|
|
||||||
import { validateRequiredValue } from '../../../helpers/validators';
|
import { validateRequiredValue } from '../../../helpers/validators';
|
||||||
import { FORM_NAME } from '../../../helpers/constants';
|
|
||||||
import { RootState } from '../../../initialState';
|
import { RootState } from '../../../initialState';
|
||||||
|
import { DhcpFormValues } from '.';
|
||||||
|
|
||||||
const renderInterfaces = (interfaces: any) =>
|
const renderInterfaces = (interfaces: any) =>
|
||||||
Object.keys(interfaces).map((item) => {
|
Object.keys(interfaces).map((item) => {
|
||||||
@@ -47,13 +45,13 @@ const getInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: any)
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface renderInterfaceValuesProps {
|
interface RenderInterfaceValuesProps {
|
||||||
gateway_ip: string;
|
gateway_ip: string;
|
||||||
hardware_address: string;
|
hardware_address: string;
|
||||||
ip_addresses: string[];
|
ip_addresses: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: renderInterfaceValuesProps) => (
|
const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: RenderInterfaceValuesProps) => (
|
||||||
<div className="d-flex align-items-end dhcp__interfaces-info">
|
<div className="d-flex align-items-end dhcp__interfaces-info">
|
||||||
<ul className="list-unstyled m-0">
|
<ul className="list-unstyled m-0">
|
||||||
{getInterfaceValues({
|
{getInterfaceValues({
|
||||||
@@ -77,11 +75,15 @@ const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: r
|
|||||||
|
|
||||||
const Interfaces = () => {
|
const Interfaces = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<DhcpFormValues>();
|
||||||
|
|
||||||
const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp, shallowEqual);
|
const { processingInterfaces, interfaces, enabled } = useSelector((store: RootState) => store.dhcp);
|
||||||
|
|
||||||
const interface_name =
|
const interface_name = watch('interface_name');
|
||||||
useSelector((store: RootState) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
|
|
||||||
|
|
||||||
if (processingInterfaces || !interfaces) {
|
if (processingInterfaces || !interfaces) {
|
||||||
return null;
|
return null;
|
||||||
@@ -92,27 +94,34 @@ const Interfaces = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="row dhcp__interfaces">
|
<div className="row dhcp__interfaces">
|
||||||
<div className="col col__dhcp">
|
<div className="col col__dhcp">
|
||||||
<Field
|
<label htmlFor="interface_name" className="form__label">
|
||||||
name="interface_name"
|
{t('dhcp_interface_select')}
|
||||||
component={renderSelectField}
|
</label>
|
||||||
|
<select
|
||||||
|
id="interface_name"
|
||||||
|
data-testid="interface_name"
|
||||||
className="form-control custom-select pl-4 col-md"
|
className="form-control custom-select pl-4 col-md"
|
||||||
validate={[validateRequiredValue]}
|
disabled={enabled}
|
||||||
label="dhcp_interface_select">
|
{...register('interface_name', {
|
||||||
|
validate: validateRequiredValue,
|
||||||
|
})}>
|
||||||
<option value="" disabled={enabled}>
|
<option value="" disabled={enabled}>
|
||||||
{t('dhcp_interface_select')}
|
{t('dhcp_interface_select')}
|
||||||
</option>
|
</option>
|
||||||
{renderInterfaces(interfaces)}
|
{renderInterfaces(interfaces)}
|
||||||
</Field>
|
</select>
|
||||||
|
{errors.interface_name && (
|
||||||
|
<div className="form__message form__message--error">{t(errors.interface_name.message)}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{interfaceValue && renderInterfaceValues({
|
{interfaceValue &&
|
||||||
gateway_ip: interfaceValue.gateway_ip,
|
renderInterfaceValues({
|
||||||
hardware_address: interfaceValue.hardware_address,
|
gateway_ip: interfaceValue.gateway_ip,
|
||||||
ip_addresses: interfaceValue.ip_addresses
|
hardware_address: interfaceValue.hardware_address,
|
||||||
})}
|
ip_addresses: interfaceValue.ip_addresses,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({
|
export default Interfaces;
|
||||||
form: FORM_NAME.DHCP_INTERFACES,
|
|
||||||
})(Interfaces);
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||||
|
|
||||||
import { renderInputField, normalizeMac } from '../../../../helpers/form';
|
import { normalizeMac } from '../../../../helpers/form';
|
||||||
import {
|
import {
|
||||||
validateIpv4,
|
validateIpv4,
|
||||||
validateMac,
|
validateMac,
|
||||||
@@ -12,12 +11,12 @@ import {
|
|||||||
validateIpv4InCidr,
|
validateIpv4InCidr,
|
||||||
validateIpGateway,
|
validateIpGateway,
|
||||||
} from '../../../../helpers/validators';
|
} from '../../../../helpers/validators';
|
||||||
import { FORM_NAME } from '../../../../helpers/constants';
|
|
||||||
|
|
||||||
import { toggleLeaseModal } from '../../../../actions';
|
import { toggleLeaseModal } from '../../../../actions';
|
||||||
import { RootState } from '../../../../initialState';
|
import { RootState } from '../../../../initialState';
|
||||||
|
import { Input } from '../../../ui/Controls/Input';
|
||||||
|
|
||||||
interface FormStaticLeaseProps {
|
type Props = {
|
||||||
initialValues?: {
|
initialValues?: {
|
||||||
mac?: string;
|
mac?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
@@ -25,63 +24,91 @@ interface FormStaticLeaseProps {
|
|||||||
cidr?: string;
|
cidr?: string;
|
||||||
gatewayIp?: string;
|
gatewayIp?: string;
|
||||||
};
|
};
|
||||||
pristine: boolean;
|
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
|
||||||
reset: () => void;
|
|
||||||
submitting: boolean;
|
|
||||||
processingAdding?: boolean;
|
processingAdding?: boolean;
|
||||||
cidr?: string;
|
cidr?: string;
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
}
|
onSubmit: (data: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cidr, isEdit }: FormStaticLeaseProps) => {
|
export const Form = ({ initialValues, processingAdding, cidr, isEdit, onSubmit }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const dynamicLease = useSelector((store: RootState) => store.dhcp.leaseModalConfig, shallowEqual);
|
const dynamicLease = useSelector((store: RootState) => store.dhcp.leaseModalConfig, shallowEqual);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
} = useForm({
|
||||||
|
defaultValues: initialValues,
|
||||||
|
mode: 'onBlur',
|
||||||
|
});
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
reset();
|
reset();
|
||||||
dispatch(toggleLeaseModal());
|
dispatch(toggleLeaseModal());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
id="mac"
|
|
||||||
name="mac"
|
name="mac"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{ validate: { required: validateRequiredValue, mac: validateMac } }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('form_enter_mac')}
|
<Input
|
||||||
normalize={normalizeMac}
|
{...field}
|
||||||
validate={[validateRequiredValue, validateMac]}
|
type="text"
|
||||||
disabled={isEdit}
|
data-testid="static_lease_mac"
|
||||||
|
placeholder={t('form_enter_mac')}
|
||||||
|
disabled={isEdit}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onChange={(e) => field.onChange(normalizeMac(e.target.value))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
id="ip"
|
|
||||||
name="ip"
|
name="ip"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t('form_enter_subnet_ip', { cidr })}
|
required: validateRequiredValue,
|
||||||
validate={[validateRequiredValue, validateIpv4, validateIpv4InCidr, validateIpGateway]}
|
ipv4: validateIpv4,
|
||||||
|
inCidr: validateIpv4InCidr,
|
||||||
|
gateway: validateIpGateway,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="static_lease_ip"
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
placeholder={t('form_enter_subnet_ip', { cidr })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
id="hostname"
|
|
||||||
name="hostname"
|
name="hostname"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
render={({ field, fieldState }) => (
|
||||||
className="form-control"
|
<Input
|
||||||
placeholder={t('form_enter_hostname')}
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="static_lease_hostname"
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
placeholder={t('form_enter_hostname')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,16 +117,18 @@ const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cid
|
|||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="static_lease_cancel"
|
||||||
className="btn btn-secondary btn-standard"
|
className="btn btn-secondary btn-standard"
|
||||||
disabled={submitting}
|
disabled={isSubmitting}
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
<Trans>cancel_btn</Trans>
|
<Trans>cancel_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="static_lease_save"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={submitting || processingAdding || (pristine && !dynamicLease)}>
|
disabled={isSubmitting || processingAdding || (!isDirty && !dynamicLease)}>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,8 +136,3 @@ const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cid
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm<
|
|
||||||
Record<string, any>,
|
|
||||||
Omit<FormStaticLeaseProps, 'submitting' | 'handleSubmit' | 'reset' | 'pristine'>
|
|
||||||
>({ form: FORM_NAME.LEASE })(Form);
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Trans, withTranslation } from 'react-i18next';
|
|||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import Form from './Form';
|
import { Form } from './Form';
|
||||||
|
|
||||||
import { toggleLeaseModal } from '../../../../actions';
|
import { toggleLeaseModal } from '../../../../actions';
|
||||||
import { MODAL_TYPE } from '../../../../helpers/constants';
|
import { MODAL_TYPE } from '../../../../helpers/constants';
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { destroy } from 'redux-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { DHCP_DESCRIPTION_PLACEHOLDERS, DHCP_FORM_NAMES, STATUS_RESPONSE, FORM_NAME } from '../../../helpers/constants';
|
import { DHCP_DESCRIPTION_PLACEHOLDERS, STATUS_RESPONSE } from '../../../helpers/constants';
|
||||||
|
|
||||||
import Leases from './Leases';
|
import Leases from './Leases';
|
||||||
|
|
||||||
@@ -40,6 +40,55 @@ import {
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import { RootState } from '../../../initialState';
|
import { RootState } from '../../../initialState';
|
||||||
|
|
||||||
|
type IPv4FormValues = {
|
||||||
|
gateway_ip?: string;
|
||||||
|
subnet_mask?: string;
|
||||||
|
range_start?: string;
|
||||||
|
range_end?: string;
|
||||||
|
lease_duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPv6FormValues = {
|
||||||
|
range_start?: string;
|
||||||
|
range_end?: string;
|
||||||
|
lease_duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefaultV4Values = (v4: IPv4FormValues) => {
|
||||||
|
const emptyForm = Object.entries(v4).every(
|
||||||
|
([key, value]) => key === 'lease_duration' || value === ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (emptyForm) {
|
||||||
|
return {
|
||||||
|
...v4,
|
||||||
|
lease_duration: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v4;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DhcpFormValues = {
|
||||||
|
v4?: IPv4FormValues;
|
||||||
|
v6?: IPv6FormValues;
|
||||||
|
interface_name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_V4_VALUES = {
|
||||||
|
gateway_ip: '',
|
||||||
|
subnet_mask: '',
|
||||||
|
range_start: '',
|
||||||
|
range_end: '',
|
||||||
|
lease_duration: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_V6_VALUES = {
|
||||||
|
range_start: '',
|
||||||
|
range_end: '',
|
||||||
|
lease_duration: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const Dhcp = () => {
|
const Dhcp = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -65,12 +114,21 @@ const Dhcp = () => {
|
|||||||
modalType,
|
modalType,
|
||||||
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
|
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
|
||||||
|
|
||||||
const interface_name =
|
const methods = useForm<DhcpFormValues>({
|
||||||
useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
|
mode: 'onBlur',
|
||||||
const isInterfaceIncludesIpv4 =
|
defaultValues: {
|
||||||
useSelector((state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses);
|
v4: getDefaultV4Values(v4),
|
||||||
|
v6,
|
||||||
|
interface_name: interfaceName || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { watch, reset } = methods;
|
||||||
|
|
||||||
const dhcp = useSelector((state: RootState) => state.form[FORM_NAME.DHCPv4], shallowEqual);
|
const interface_name = watch('interface_name');
|
||||||
|
const isInterfaceIncludesIpv4 = useSelector(
|
||||||
|
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
|
||||||
|
);
|
||||||
|
const ipv4Config = watch('v4');
|
||||||
|
|
||||||
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
|
const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4);
|
||||||
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
|
const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
|
||||||
@@ -85,6 +143,22 @@ const Dhcp = () => {
|
|||||||
}
|
}
|
||||||
}, [dhcp_available]);
|
}, [dhcp_available]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (v4 || v6 || interfaceName) {
|
||||||
|
reset({
|
||||||
|
v4: {
|
||||||
|
...DEFAULT_V4_VALUES,
|
||||||
|
...getDefaultV4Values(v4),
|
||||||
|
},
|
||||||
|
v6: {
|
||||||
|
...DEFAULT_V6_VALUES,
|
||||||
|
...v6,
|
||||||
|
},
|
||||||
|
interface_name: interfaceName || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [v4, v6, interfaceName, reset]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
|
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
|
||||||
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
|
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
|
||||||
@@ -103,13 +177,17 @@ const Dhcp = () => {
|
|||||||
const clear = () => {
|
const clear = () => {
|
||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
if (window.confirm(t('dhcp_reset'))) {
|
if (window.confirm(t('dhcp_reset'))) {
|
||||||
Object.values(DHCP_FORM_NAMES).forEach((formName: any) => dispatch(destroy(formName)));
|
reset({
|
||||||
|
v4: DEFAULT_V4_VALUES,
|
||||||
|
v6: DEFAULT_V6_VALUES,
|
||||||
|
interface_name: '',
|
||||||
|
});
|
||||||
dispatch(resetDhcp());
|
dispatch(resetDhcp());
|
||||||
dispatch(getDhcpStatus());
|
dispatch(getDhcpStatus());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (values: any) => {
|
const handleSubmit = (values: DhcpFormValues) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setDhcpConfig({
|
setDhcpConfig({
|
||||||
interface_name,
|
interface_name,
|
||||||
@@ -130,12 +208,7 @@ const Dhcp = () => {
|
|||||||
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
|
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
|
||||||
|
|
||||||
const getToggleDhcpButton = () => {
|
const getToggleDhcpButton = () => {
|
||||||
const filledConfig =
|
const filledConfig = interface_name && (Object.values(v4).every(Boolean) || Object.values(v6).every(Boolean));
|
||||||
interface_name &&
|
|
||||||
(Object.values(v4)
|
|
||||||
|
|
||||||
.every(Boolean) ||
|
|
||||||
Object.values(v6).every(Boolean));
|
|
||||||
|
|
||||||
const className = classNames('btn btn-sm', {
|
const className = classNames('btn btn-sm', {
|
||||||
'btn-gray': enabled,
|
'btn-gray': enabled,
|
||||||
@@ -173,9 +246,6 @@ const Dhcp = () => {
|
|||||||
|
|
||||||
const toggleModal = () => dispatch(toggleLeaseModal());
|
const toggleModal = () => dispatch(toggleLeaseModal());
|
||||||
|
|
||||||
const initialV4 = enteredSomeV4Value ? v4 : {};
|
|
||||||
const initialV6 = enteredSomeV6Value ? v6 : {};
|
|
||||||
|
|
||||||
if (processing || processingInterfaces) {
|
if (processing || processingInterfaces) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
@@ -196,19 +266,13 @@ const Dhcp = () => {
|
|||||||
|
|
||||||
const toggleDhcpButton = getToggleDhcpButton();
|
const toggleDhcpButton = getToggleDhcpButton();
|
||||||
|
|
||||||
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
|
const inputtedIPv4values = ipv4Config.gateway_ip && ipv4Config.subnet_mask;
|
||||||
|
|
||||||
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
|
const isEmptyConfig = !Object.values(ipv4Config).some(Boolean);
|
||||||
const disabledLeasesButton = Boolean(
|
const disabledLeasesButton = Boolean(
|
||||||
dhcp?.syncErrors ||
|
!isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values,
|
||||||
!isInterfaceIncludesIpv4 ||
|
|
||||||
isEmptyConfig ||
|
|
||||||
processingConfig ||
|
|
||||||
!inputtedIPv4values,
|
|
||||||
);
|
);
|
||||||
const cidr = inputtedIPv4values
|
const cidr = inputtedIPv4values ? `${ipv4Config.gateway_ip}/${subnetMaskToBitMask(ipv4Config.subnet_mask)}` : '';
|
||||||
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -246,29 +310,30 @@ const Dhcp = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Interfaces initialValues={{ interface_name: interfaceName }} />
|
<FormProvider {...methods}>
|
||||||
|
<Interfaces />
|
||||||
|
<Card title={t('dhcp_ipv4_settings')} bodyType="card-body box-body--settings">
|
||||||
|
<div>
|
||||||
|
<FormDHCPv4
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
processingConfig={processingConfig}
|
||||||
|
ipv4placeholders={ipv4placeholders}
|
||||||
|
interfaces={interfaces}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card title={t('dhcp_ipv6_settings')} bodyType="card-body box-body--settings">
|
||||||
|
<div>
|
||||||
|
<FormDHCPv6
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
processingConfig={processingConfig}
|
||||||
|
ipv6placeholders={ipv6placeholders}
|
||||||
|
interfaces={interfaces}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
<Card title={t('dhcp_ipv4_settings')} bodyType="card-body box-body--settings">
|
|
||||||
<div>
|
|
||||||
<FormDHCPv4
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
initialValues={{ v4: initialV4 }}
|
|
||||||
processingConfig={processingConfig}
|
|
||||||
ipv4placeholders={ipv4placeholders}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title={t('dhcp_ipv6_settings')} bodyType="card-body box-body--settings">
|
|
||||||
<div>
|
|
||||||
<FormDHCPv6
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
initialValues={{ v6: initialV6 }}
|
|
||||||
processingConfig={processingConfig}
|
|
||||||
ipv6placeholders={ipv6placeholders}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<Card title={t('dhcp_leases')} bodyType="card-body box-body--settings">
|
<Card title={t('dhcp_leases')} bodyType="card-body box-body--settings">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@@ -290,7 +355,7 @@ const Dhcp = () => {
|
|||||||
processingDeleting={processingDeleting}
|
processingDeleting={processingDeleting}
|
||||||
processingUpdating={processingUpdating}
|
processingUpdating={processingUpdating}
|
||||||
cidr={cidr}
|
cidr={cidr}
|
||||||
gatewayIp={dhcp?.values?.v4?.gateway_ip}
|
gatewayIp={ipv4Config.gateway_ip}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="btn-list mt-2">
|
<div className="btn-list mt-2">
|
||||||
|
|||||||
@@ -1,118 +1,140 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
import i18next from 'i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { CLIENT_ID_LINK } from '../../../../helpers/constants';
|
||||||
import flow from 'lodash/flow';
|
import { removeEmptyLines, trimMultilineString } from '../../../../helpers/helpers';
|
||||||
|
import { Textarea } from '../../../ui/Controls/Textarea';
|
||||||
|
|
||||||
import { renderTextareaField } from '../../../../helpers/form';
|
type FormData = {
|
||||||
import { trimMultilineString, removeEmptyLines } from '../../../../helpers/helpers';
|
allowed_clients: string;
|
||||||
import { CLIENT_ID_LINK, FORM_NAME } from '../../../../helpers/constants';
|
disallowed_clients: string;
|
||||||
|
blocked_hosts: string;
|
||||||
|
};
|
||||||
|
|
||||||
const fields = [
|
const fields: {
|
||||||
|
id: keyof FormData;
|
||||||
|
title: string;
|
||||||
|
subtitle: ReactNode;
|
||||||
|
normalizeOnBlur: (value: string) => string;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
id: 'allowed_clients',
|
id: 'allowed_clients',
|
||||||
title: 'access_allowed_title',
|
title: i18next.t('access_allowed_title'),
|
||||||
subtitle: 'access_allowed_desc',
|
subtitle: (
|
||||||
|
<Trans
|
||||||
|
components={{
|
||||||
|
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||||
|
}}>
|
||||||
|
access_allowed_desc
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
normalizeOnBlur: removeEmptyLines,
|
normalizeOnBlur: removeEmptyLines,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'disallowed_clients',
|
id: 'disallowed_clients',
|
||||||
title: 'access_disallowed_title',
|
title: i18next.t('access_disallowed_title'),
|
||||||
subtitle: 'access_disallowed_desc',
|
subtitle: (
|
||||||
|
<Trans
|
||||||
|
components={{
|
||||||
|
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||||
|
}}>
|
||||||
|
access_disallowed_desc
|
||||||
|
</Trans>
|
||||||
|
),
|
||||||
normalizeOnBlur: trimMultilineString,
|
normalizeOnBlur: trimMultilineString,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'blocked_hosts',
|
id: 'blocked_hosts',
|
||||||
title: 'access_blocked_title',
|
title: i18next.t('access_blocked_title'),
|
||||||
subtitle: 'access_blocked_desc',
|
subtitle: i18next.t('access_blocked_desc'),
|
||||||
normalizeOnBlur: removeEmptyLines,
|
normalizeOnBlur: removeEmptyLines,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface FormProps {
|
type FormProps = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
initialValues?: {
|
||||||
submitting: boolean;
|
allowed_clients?: string;
|
||||||
invalid: boolean;
|
disallowed_clients?: string;
|
||||||
initialValues: object;
|
blocked_hosts?: string;
|
||||||
|
};
|
||||||
|
onSubmit: (data: FormData) => void;
|
||||||
processingSet: boolean;
|
processingSet: boolean;
|
||||||
t: (...args: unknown[]) => string;
|
};
|
||||||
textarea?: boolean;
|
|
||||||
allowedClients?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface renderFieldProps {
|
const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
||||||
id?: string;
|
const { t } = useTranslation();
|
||||||
title?: string;
|
|
||||||
subtitle?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
processingSet?: boolean;
|
|
||||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Form = (props: FormProps) => {
|
const {
|
||||||
const { allowedClients, handleSubmit, submitting, invalid, processingSet } = props;
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
allowed_clients: initialValues?.allowed_clients || '',
|
||||||
|
disallowed_clients: initialValues?.disallowed_clients || '',
|
||||||
|
blocked_hosts: initialValues?.blocked_hosts || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const allowedClients = watch('allowed_clients');
|
||||||
|
|
||||||
const renderField = ({
|
const renderField = ({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
disabled = false,
|
|
||||||
processingSet,
|
|
||||||
normalizeOnBlur,
|
normalizeOnBlur,
|
||||||
}: renderFieldProps) => (
|
}: {
|
||||||
<div key={id} className="form__group mb-5">
|
id: keyof FormData;
|
||||||
<label className="form__label form__label--with-desc" htmlFor={id}>
|
title: string;
|
||||||
<Trans>{title}</Trans>
|
subtitle: ReactNode;
|
||||||
|
normalizeOnBlur: (value: string) => string;
|
||||||
|
}) => {
|
||||||
|
const disabled = allowedClients && id === 'disallowed_clients';
|
||||||
|
|
||||||
{disabled && (
|
return (
|
||||||
<>
|
<div key={id} className="form__group mb-5">
|
||||||
<span> </span>(<Trans>disabled</Trans>)
|
<label className="form__label form__label--with-desc" htmlFor={id}>
|
||||||
</>
|
{title}
|
||||||
)}
|
{disabled && <> ({t('disabled')})</>}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">{subtitle}</div>
|
||||||
<Trans
|
|
||||||
components={{
|
<Controller
|
||||||
a: (
|
name={id}
|
||||||
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
|
control={control}
|
||||||
text
|
render={({ field }) => (
|
||||||
</a>
|
<Textarea
|
||||||
),
|
{...field}
|
||||||
}}>
|
id={id}
|
||||||
{subtitle}
|
data-testid={id}
|
||||||
</Trans>
|
disabled={disabled || processingSet}
|
||||||
|
onBlur={(e) => {
|
||||||
|
field.onChange(normalizeOnBlur(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<Field
|
};
|
||||||
id={id}
|
|
||||||
name={id}
|
|
||||||
component={renderTextareaField}
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control--textarea font-monospace"
|
|
||||||
disabled={disabled || processingSet}
|
|
||||||
normalizeOnBlur={normalizeOnBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
{fields.map((f) => {
|
{fields.map((f) => renderField(f))}
|
||||||
return renderField({
|
|
||||||
...f,
|
|
||||||
disabled: allowedClients && f.id === 'disallowed_clients' || false
|
|
||||||
});
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className="card-actions">
|
<div className="card-actions">
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="access_save"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={submitting || invalid || processingSet}>
|
disabled={isSubmitting || !isDirty || processingSet}>
|
||||||
<Trans>save_config</Trans>
|
{t('save_config')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,18 +142,4 @@ let Form = (props: FormProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = formValueSelector(FORM_NAME.ACCESS);
|
export default Form;
|
||||||
|
|
||||||
Form = connect((state) => {
|
|
||||||
const allowedClients = selector(state, 'allowed_clients');
|
|
||||||
return {
|
|
||||||
allowedClients,
|
|
||||||
};
|
|
||||||
})(Form);
|
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.ACCESS,
|
|
||||||
}),
|
|
||||||
])(Form);
|
|
||||||
|
|||||||
@@ -1,52 +1,72 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import i18next from 'i18next';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
|
||||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { renderInputField, toNumber, CheckboxField } from '../../../../helpers/form';
|
|
||||||
import { CACHE_CONFIG_FIELDS, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
|
|
||||||
|
|
||||||
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
|
|
||||||
import { clearDnsCache } from '../../../../actions/dnsConfig';
|
import { clearDnsCache } from '../../../../actions/dnsConfig';
|
||||||
|
import { CACHE_CONFIG_FIELDS, UINT32_RANGE } from '../../../../helpers/constants';
|
||||||
|
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
|
||||||
import { RootState } from '../../../../initialState';
|
import { RootState } from '../../../../initialState';
|
||||||
|
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||||
|
|
||||||
const INPUTS_FIELDS = [
|
const INPUTS_FIELDS = [
|
||||||
{
|
{
|
||||||
name: CACHE_CONFIG_FIELDS.cache_size,
|
name: CACHE_CONFIG_FIELDS.cache_size,
|
||||||
title: 'cache_size',
|
title: i18next.t('cache_size'),
|
||||||
description: 'cache_size_desc',
|
description: i18next.t('cache_size_desc'),
|
||||||
placeholder: 'enter_cache_size',
|
placeholder: i18next.t('enter_cache_size'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
|
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
|
||||||
title: 'cache_ttl_min_override',
|
title: i18next.t('cache_ttl_min_override'),
|
||||||
description: 'cache_ttl_min_override_desc',
|
description: i18next.t('cache_ttl_min_override_desc'),
|
||||||
placeholder: 'enter_cache_ttl_min_override',
|
placeholder: i18next.t('enter_cache_ttl_min_override'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
|
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
|
||||||
title: 'cache_ttl_max_override',
|
title: i18next.t('cache_ttl_max_override'),
|
||||||
description: 'cache_ttl_max_override_desc',
|
description: i18next.t('cache_ttl_max_override_desc'),
|
||||||
placeholder: 'enter_cache_ttl_max_override',
|
placeholder: i18next.t('enter_cache_ttl_max_override'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface CacheFormProps {
|
type FormData = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
cache_size: number;
|
||||||
submitting: boolean;
|
cache_ttl_min: number;
|
||||||
invalid: boolean;
|
cache_ttl_max: number;
|
||||||
}
|
cache_optimistic: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
|
type CacheFormProps = {
|
||||||
|
initialValues?: Partial<FormData>;
|
||||||
|
onSubmit: (data: FormData) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { processingSetConfig } = useSelector((state: RootState) => state.dnsConfig, shallowEqual);
|
const { processingSetConfig } = useSelector((state: RootState) => state.dnsConfig);
|
||||||
const { cache_ttl_max, cache_ttl_min } = useSelector(
|
|
||||||
(state: RootState) => state.form[FORM_NAME.CACHE].values,
|
const {
|
||||||
shallowEqual,
|
register,
|
||||||
);
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
cache_size: initialValues?.cache_size || 0,
|
||||||
|
cache_ttl_min: initialValues?.cache_ttl_min || 0,
|
||||||
|
cache_ttl_max: initialValues?.cache_ttl_max || 0,
|
||||||
|
cache_optimistic: initialValues?.cache_optimistic || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cache_ttl_min = watch('cache_ttl_min');
|
||||||
|
const cache_ttl_max = watch('cache_ttl_max');
|
||||||
|
|
||||||
const minExceedsMax = cache_ttl_min > 0 && cache_ttl_max > 0 && cache_ttl_min > cache_ttl_max;
|
const minExceedsMax = cache_ttl_min > 0 && cache_ttl_max > 0 && cache_ttl_min > cache_ttl_max;
|
||||||
|
|
||||||
@@ -57,29 +77,30 @@ const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{INPUTS_FIELDS.map(({ name, title, description, placeholder }) => (
|
{INPUTS_FIELDS.map(({ name, title, description, placeholder }) => (
|
||||||
<div className="col-12" key={name}>
|
<div className="col-12" key={name}>
|
||||||
<div className="col-12 col-md-7 p-0">
|
<div className="col-12 col-md-7 p-0">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor={name} className="form__label form__label--with-desc">
|
<label htmlFor={name} className="form__label form__label--with-desc">
|
||||||
{t(title)}
|
{title}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">{t(description)}</div>
|
<div className="form__desc form__desc--top">{description}</div>
|
||||||
|
|
||||||
<Field
|
<input
|
||||||
name={name}
|
|
||||||
type="number"
|
type="number"
|
||||||
component={renderInputField}
|
data-testid={`dns_${name}`}
|
||||||
placeholder={t(placeholder)}
|
|
||||||
disabled={processingSetConfig}
|
|
||||||
className="form-control"
|
className="form-control"
|
||||||
normalizeOnBlur={replaceZeroWithEmptyString}
|
placeholder={placeholder}
|
||||||
normalize={toNumber}
|
disabled={processingSetConfig}
|
||||||
min={0}
|
min={0}
|
||||||
max={UINT32_RANGE.MAX}
|
max={UINT32_RANGE.MAX}
|
||||||
|
{...register(name as keyof FormData, {
|
||||||
|
valueAsNumber: true,
|
||||||
|
setValueAs: (value) => replaceZeroWithEmptyString(value),
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,13 +112,18 @@ const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="cache_optimistic"
|
name="cache_optimistic"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('cache_optimistic')}
|
<Checkbox
|
||||||
disabled={processingSetConfig}
|
{...field}
|
||||||
subtitle={t('cache_optimistic_desc')}
|
data-testid="dns_cache_optimistic"
|
||||||
|
title={t('cache_optimistic')}
|
||||||
|
subtitle={t('cache_optimistic_desc')}
|
||||||
|
disabled={processingSetConfig}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,19 +131,21 @@ const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="dns_save"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={submitting || invalid || processingSetConfig || minExceedsMax}>
|
disabled={isSubmitting || !isDirty || processingSetConfig || minExceedsMax}>
|
||||||
<Trans>save_btn</Trans>
|
{t('save_btn')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="dns_clear"
|
||||||
className="btn btn-outline-secondary btn-standard form__button"
|
className="btn btn-outline-secondary btn-standard form__button"
|
||||||
onClick={handleClearCache}>
|
onClick={handleClearCache}>
|
||||||
<Trans>clear_cache</Trans>
|
{t('clear_cache')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({ form: FORM_NAME.CACHE })(Form);
|
export default Form;
|
||||||
|
|||||||
@@ -1,211 +1,279 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallowEqual, useSelector } from 'react-redux';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import i18next from 'i18next';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { validateIp, validateIpv4, validateIpv6, validateRequiredValue } from '../../../../helpers/validators';
|
||||||
import {
|
|
||||||
renderInputField,
|
|
||||||
renderRadioField,
|
|
||||||
renderTextareaField,
|
|
||||||
CheckboxField,
|
|
||||||
toNumber,
|
|
||||||
} from '../../../../helpers/form';
|
|
||||||
import {
|
|
||||||
validateIpv4,
|
|
||||||
validateIpv6,
|
|
||||||
validateRequiredValue,
|
|
||||||
validateIp,
|
|
||||||
validateIPv4Subnet,
|
|
||||||
validateIPv6Subnet,
|
|
||||||
} from '../../../../helpers/validators';
|
|
||||||
|
|
||||||
import { removeEmptyLines } from '../../../../helpers/helpers';
|
import { BLOCKING_MODES, UINT32_RANGE } from '../../../../helpers/constants';
|
||||||
import { BLOCKING_MODES, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
|
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||||
import { RootState } from '../../../../initialState';
|
import { Input } from '../../../ui/Controls/Input';
|
||||||
|
import { toNumber } from '../../../../helpers/form';
|
||||||
|
import { Textarea } from '../../../ui/Controls/Textarea';
|
||||||
|
import { Radio } from '../../../ui/Controls/Radio';
|
||||||
|
|
||||||
const checkboxes = [
|
const checkboxes: {
|
||||||
|
name: 'dnssec_enabled' | 'disable_ipv6';
|
||||||
|
placeholder: string;
|
||||||
|
subtitle: string;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
name: 'dnssec_enabled',
|
name: 'dnssec_enabled',
|
||||||
placeholder: 'dnssec_enable',
|
placeholder: i18next.t('dnssec_enable'),
|
||||||
subtitle: 'dnssec_enable_desc',
|
subtitle: i18next.t('dnssec_enable_desc'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'disable_ipv6',
|
name: 'disable_ipv6',
|
||||||
placeholder: 'disable_ipv6',
|
placeholder: i18next.t('disable_ipv6'),
|
||||||
subtitle: 'disable_ipv6_desc',
|
subtitle: i18next.t('disable_ipv6_desc'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const customIps = [
|
const customIps: {
|
||||||
|
name: 'blocking_ipv4' | 'blocking_ipv6';
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
validateIp: (value: string) => string;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
description: 'blocking_ipv4_desc',
|
|
||||||
name: 'blocking_ipv4',
|
name: 'blocking_ipv4',
|
||||||
|
label: i18next.t('blocking_ipv4'),
|
||||||
|
description: i18next.t('blocking_ipv4_desc'),
|
||||||
validateIp: validateIpv4,
|
validateIp: validateIpv4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'blocking_ipv6_desc',
|
|
||||||
name: 'blocking_ipv6',
|
name: 'blocking_ipv6',
|
||||||
|
label: i18next.t('blocking_ipv6'),
|
||||||
|
description: i18next.t('blocking_ipv6_desc'),
|
||||||
validateIp: validateIpv6,
|
validateIp: validateIpv6,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getFields = (processing: any, t: any) =>
|
const blockingModeOptions = [
|
||||||
Object.values(BLOCKING_MODES)
|
{
|
||||||
|
value: BLOCKING_MODES.default,
|
||||||
|
label: i18next.t('default'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.refused,
|
||||||
|
label: i18next.t('refused'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.nxdomain,
|
||||||
|
label: i18next.t('nxdomain'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.null_ip,
|
||||||
|
label: i18next.t('null_ip'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: BLOCKING_MODES.custom_ip,
|
||||||
|
label: i18next.t('custom_ip'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
.map((mode: any) => (
|
const blockingModeDescriptions = [
|
||||||
<Field
|
i18next.t(`blocking_mode_default`),
|
||||||
key={mode}
|
i18next.t(`blocking_mode_refused`),
|
||||||
name="blocking_mode"
|
i18next.t(`blocking_mode_nxdomain`),
|
||||||
type="radio"
|
i18next.t(`blocking_mode_null_ip`),
|
||||||
component={renderRadioField}
|
i18next.t(`blocking_mode_custom_ip`),
|
||||||
value={mode}
|
];
|
||||||
placeholder={t(mode)}
|
|
||||||
disabled={processing}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
interface ConfigFormProps {
|
type FormData = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
ratelimit: number;
|
||||||
submitting: boolean;
|
ratelimit_subnet_len_ipv4: number;
|
||||||
invalid: boolean;
|
ratelimit_subnet_len_ipv6: number;
|
||||||
|
ratelimit_whitelist: string;
|
||||||
|
edns_cs_enabled: boolean;
|
||||||
|
edns_cs_use_custom: boolean;
|
||||||
|
edns_cs_custom_ip?: string;
|
||||||
|
dnssec_enabled: boolean;
|
||||||
|
disable_ipv6: boolean;
|
||||||
|
blocking_mode: string;
|
||||||
|
blocking_ipv4?: string;
|
||||||
|
blocking_ipv6?: string;
|
||||||
|
blocked_response_ttl: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
processing?: boolean;
|
processing?: boolean;
|
||||||
}
|
initialValues?: Partial<FormData>;
|
||||||
|
onSubmit: (data: FormData) => void;
|
||||||
|
};
|
||||||
|
|
||||||
const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps) => {
|
const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { blocking_mode, edns_cs_enabled, edns_cs_use_custom } = useSelector(
|
|
||||||
(state: RootState) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {},
|
const {
|
||||||
shallowEqual,
|
handleSubmit,
|
||||||
);
|
watch,
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blocking_mode = watch('blocking_mode');
|
||||||
|
const edns_cs_enabled = watch('edns_cs_enabled');
|
||||||
|
const edns_cs_use_custom = watch('edns_cs_use_custom');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit" className="form__label form__label--with-desc">
|
<Controller
|
||||||
<Trans>rate_limit</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
|
||||||
<Trans>rate_limit_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="ratelimit"
|
name="ratelimit"
|
||||||
type="number"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{ validate: validateRequiredValue }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('form_enter_rate_limit')}
|
<Input
|
||||||
normalize={toNumber}
|
{...field}
|
||||||
validate={validateRequiredValue}
|
data-testid="dns_config_ratelimit"
|
||||||
min={UINT32_RANGE.MIN}
|
type="number"
|
||||||
max={UINT32_RANGE.MAX}
|
label={t('rate_limit')}
|
||||||
|
desc={t('rate_limit_desc')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
min={UINT32_RANGE.MIN}
|
||||||
|
max={UINT32_RANGE.MAX}
|
||||||
|
disabled={processing}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit_subnet_len_ipv4" className="form__label form__label--with-desc">
|
<Controller
|
||||||
<Trans>rate_limit_subnet_len_ipv4</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
|
||||||
<Trans>rate_limit_subnet_len_ipv4_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="ratelimit_subnet_len_ipv4"
|
name="ratelimit_subnet_len_ipv4"
|
||||||
type="number"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{ validate: validateRequiredValue }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('form_enter_rate_limit_subnet_len')}
|
<Input
|
||||||
normalize={toNumber}
|
{...field}
|
||||||
validate={[validateRequiredValue, validateIPv4Subnet]}
|
data-testid="dns_config_subnet_ipv4"
|
||||||
min={0}
|
type="number"
|
||||||
max={32}
|
label={t('rate_limit_subnet_len_ipv4')}
|
||||||
|
desc={t('rate_limit_subnet_len_ipv4_desc')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
min={0}
|
||||||
|
max={32}
|
||||||
|
disabled={processing}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit_subnet_len_ipv6" className="form__label form__label--with-desc">
|
<Controller
|
||||||
<Trans>rate_limit_subnet_len_ipv6</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
|
||||||
<Trans>rate_limit_subnet_len_ipv6_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="ratelimit_subnet_len_ipv6"
|
name="ratelimit_subnet_len_ipv6"
|
||||||
type="number"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{ validate: validateRequiredValue }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('form_enter_rate_limit_subnet_len')}
|
<Input
|
||||||
normalize={toNumber}
|
{...field}
|
||||||
validate={[validateRequiredValue, validateIPv6Subnet]}
|
data-testid="dns_config_subnet_ipv6"
|
||||||
min={0}
|
type="number"
|
||||||
max={128}
|
label={t('rate_limit_subnet_len_ipv6')}
|
||||||
|
desc={t('rate_limit_subnet_len_ipv6_desc')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
min={0}
|
||||||
|
max={128}
|
||||||
|
disabled={processing}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="ratelimit_whitelist" className="form__label form__label--with-desc">
|
<Controller
|
||||||
<Trans>rate_limit_whitelist</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
|
||||||
<Trans>rate_limit_whitelist_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="ratelimit_whitelist"
|
name="ratelimit_whitelist"
|
||||||
component={renderTextareaField}
|
control={control}
|
||||||
type="text"
|
render={({ field, fieldState }) => (
|
||||||
className="form-control"
|
<Textarea
|
||||||
placeholder={t('rate_limit_whitelist_placeholder')}
|
{...field}
|
||||||
normalizeOnBlur={removeEmptyLines}
|
data-testid="dns_config_subnet_ipv6"
|
||||||
|
label={t('rate_limit_whitelist')}
|
||||||
|
desc={t('rate_limit_whitelist_desc')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={processing}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="edns_cs_enabled"
|
name="edns_cs_enabled"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('edns_enable')}
|
<Checkbox
|
||||||
disabled={processing}
|
{...field}
|
||||||
subtitle={t('edns_cs_desc')}
|
data-testid="dns_config_edns_cs_enabled"
|
||||||
|
title={t('edns_enable')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 form__group form__group--inner">
|
<div className="col-12 form__group form__group--inner">
|
||||||
<div className="form__group ">
|
<div className="form__group">
|
||||||
<Field
|
<Controller
|
||||||
name="edns_cs_use_custom"
|
name="edns_cs_use_custom"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('edns_use_custom_ip')}
|
<Checkbox
|
||||||
disabled={processing || !edns_cs_enabled}
|
{...field}
|
||||||
subtitle={t('edns_use_custom_ip_desc')}
|
data-testid="dns_config_edns_use_custom_ip"
|
||||||
|
title={t('edns_use_custom_ip')}
|
||||||
|
disabled={processing || !edns_cs_enabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{edns_cs_use_custom && (
|
{edns_cs_use_custom && (
|
||||||
<Field
|
<Controller
|
||||||
name="edns_cs_custom_ip"
|
name="edns_cs_custom_ip"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
className="form-control"
|
rules={{
|
||||||
placeholder={t('form_enter_ip')}
|
validate: {
|
||||||
validate={[validateIp, validateRequiredValue]}
|
required: validateRequiredValue,
|
||||||
|
id: validateIp,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
data-testid="dns_config_edns_cs_custom_ip"
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={processing || !edns_cs_enabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -213,13 +281,18 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
|||||||
{checkboxes.map(({ name, placeholder, subtitle }) => (
|
{checkboxes.map(({ name, placeholder, subtitle }) => (
|
||||||
<div className="col-12" key={name}>
|
<div className="col-12" key={name}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name={name}
|
name={name}
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t(placeholder)}
|
<Checkbox
|
||||||
disabled={processing}
|
{...field}
|
||||||
subtitle={t(subtitle)}
|
data-testid={`dns_config_${name}`}
|
||||||
|
title={placeholder}
|
||||||
|
subtitle={subtitle}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,42 +300,50 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
|||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="form__group form__group--settings mb-4">
|
<div className="form__group form__group--settings mb-4">
|
||||||
<label className="form__label form__label--with-desc">
|
<label className="form__label form__label--with-desc">{t('blocking_mode')}</label>
|
||||||
<Trans>blocking_mode</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
{Object.values(BLOCKING_MODES)
|
{blockingModeDescriptions.map((desc: string) => (
|
||||||
|
<li key={desc}>{desc}</li>
|
||||||
.map((mode: any) => (
|
))}
|
||||||
<li key={mode}>
|
|
||||||
<Trans>{`blocking_mode_${mode}`}</Trans>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="custom-controls-stacked">{getFields(processing, t)}</div>
|
<div className="custom-controls-stacked">
|
||||||
|
<Controller
|
||||||
|
name="blocking_mode"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Radio {...field} options={blockingModeOptions} disabled={processing} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{blocking_mode === BLOCKING_MODES.custom_ip && (
|
{blocking_mode === BLOCKING_MODES.custom_ip && (
|
||||||
<>
|
<>
|
||||||
{customIps.map(({ description, name, validateIp }) => (
|
{customIps.map(({ label, description, name, validateIp }) => (
|
||||||
<div className="col-12 col-sm-6" key={name}>
|
<div className="col-12 col-sm-6" key={name}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label className="form__label form__label--with-desc" htmlFor={name}>
|
<Controller
|
||||||
<Trans>{name}</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
|
||||||
<Trans>{description}</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name={name}
|
name={name}
|
||||||
component={renderInputField}
|
control={control}
|
||||||
className="form-control"
|
rules={{
|
||||||
placeholder={t('form_enter_ip')}
|
validate: {
|
||||||
validate={[validateIp, validateRequiredValue]}
|
required: validateRequiredValue,
|
||||||
|
ip: validateIp,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
data-testid="dns_config_blocked_response_ttl"
|
||||||
|
type="text"
|
||||||
|
label={label}
|
||||||
|
desc={description}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,24 +353,27 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
|||||||
|
|
||||||
<div className="col-12 col-md-7">
|
<div className="col-12 col-md-7">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="blocked_response_ttl" className="form__label form__label--with-desc">
|
<Controller
|
||||||
<Trans>blocked_response_ttl</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
|
||||||
<Trans>blocked_response_ttl_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="blocked_response_ttl"
|
name="blocked_response_ttl"
|
||||||
type="number"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{ validate: validateRequiredValue }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('form_enter_blocked_response_ttl')}
|
<Input
|
||||||
normalize={toNumber}
|
{...field}
|
||||||
validate={validateRequiredValue}
|
data-testid="dns_config_blocked_response_ttl"
|
||||||
min={UINT32_RANGE.MIN}
|
type="number"
|
||||||
max={UINT32_RANGE.MAX}
|
label={t('blocked_response_ttl')}
|
||||||
|
desc={t('blocked_response_ttl_desc')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
min={UINT32_RANGE.MIN}
|
||||||
|
max={UINT32_RANGE.MAX}
|
||||||
|
disabled={processing}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,14 +381,13 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="dns_config_save"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={submitting || invalid || processing}>
|
disabled={isSubmitting || !isDirty || processing}>
|
||||||
<Trans>save_btn</Trans>
|
{t('save_btn')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm<Record<string, any>, Omit<ConfigFormProps, 'invalid' | 'submitting' | 'handleSubmit'>>({
|
export default Form;
|
||||||
form: FORM_NAME.BLOCKING_MODE,
|
|
||||||
})(Form);
|
|
||||||
|
|||||||
@@ -1,185 +1,110 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import i18next from 'i18next';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import clsx from 'clsx';
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
import Examples from './Examples';
|
|
||||||
|
|
||||||
import {
|
|
||||||
renderRadioField,
|
|
||||||
renderTextareaField,
|
|
||||||
CheckboxField,
|
|
||||||
renderInputField,
|
|
||||||
toNumber,
|
|
||||||
} from '../../../../helpers/form';
|
|
||||||
import {
|
|
||||||
DNS_REQUEST_OPTIONS,
|
|
||||||
FORM_NAME,
|
|
||||||
UINT32_RANGE,
|
|
||||||
UPSTREAM_CONFIGURATION_WIKI_LINK,
|
|
||||||
} from '../../../../helpers/constants';
|
|
||||||
|
|
||||||
import { testUpstreamWithFormValues } from '../../../../actions';
|
import { testUpstreamWithFormValues } from '../../../../actions';
|
||||||
|
import { DNS_REQUEST_OPTIONS, UINT32_RANGE, UPSTREAM_CONFIGURATION_WIKI_LINK } from '../../../../helpers/constants';
|
||||||
import { removeEmptyLines, trimLinesAndRemoveEmpty } from '../../../../helpers/helpers';
|
import { removeEmptyLines } from '../../../../helpers/helpers';
|
||||||
|
|
||||||
import { getTextareaCommentsHighlight, syncScroll } from '../../../../helpers/highlightTextareaComments';
|
import { getTextareaCommentsHighlight, syncScroll } from '../../../../helpers/highlightTextareaComments';
|
||||||
import '../../../ui/texareaCommentsHighlight.css';
|
|
||||||
import { RootState } from '../../../../initialState';
|
import { RootState } from '../../../../initialState';
|
||||||
|
import '../../../ui/texareaCommentsHighlight.css';
|
||||||
|
import Examples from './Examples';
|
||||||
|
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||||
|
import { Textarea } from '../../../ui/Controls/Textarea';
|
||||||
|
import { Radio } from '../../../ui/Controls/Radio';
|
||||||
|
import { Input } from '../../../ui/Controls/Input';
|
||||||
import { validateRequiredValue } from '../../../../helpers/validators';
|
import { validateRequiredValue } from '../../../../helpers/validators';
|
||||||
|
import { toNumber } from '../../../../helpers/form';
|
||||||
|
|
||||||
const UPSTREAM_DNS_NAME = 'upstream_dns';
|
const UPSTREAM_DNS_NAME = 'upstream_dns';
|
||||||
const UPSTREAM_MODE_NAME = 'upstream_mode';
|
|
||||||
|
|
||||||
interface renderFieldProps {
|
type FormData = {
|
||||||
name: string;
|
upstream_dns: string;
|
||||||
component: any;
|
upstream_mode: string;
|
||||||
type: string;
|
fallback_dns: string;
|
||||||
className?: string;
|
bootstrap_dns: string;
|
||||||
placeholder: string;
|
local_ptr_upstreams: string;
|
||||||
subtitle?: string;
|
use_private_ptr_resolvers: boolean;
|
||||||
value?: string;
|
resolve_clients: boolean;
|
||||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
upstream_timeout: number;
|
||||||
containerClass?: string;
|
|
||||||
onScroll?: (...args: unknown[]) => unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderField = ({
|
|
||||||
name,
|
|
||||||
component,
|
|
||||||
type,
|
|
||||||
className,
|
|
||||||
placeholder,
|
|
||||||
subtitle,
|
|
||||||
value,
|
|
||||||
normalizeOnBlur,
|
|
||||||
containerClass,
|
|
||||||
onScroll,
|
|
||||||
}: renderFieldProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const processingTestUpstream = useSelector((state: RootState) => state.settings.processingTestUpstream);
|
|
||||||
|
|
||||||
const processingSetConfig = useSelector((state: RootState) => state.dnsConfig.processingSetConfig);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={placeholder} className={classnames('col-12 mb-4', containerClass)}>
|
|
||||||
<Field
|
|
||||||
id={name}
|
|
||||||
value={value}
|
|
||||||
name={name}
|
|
||||||
component={component}
|
|
||||||
type={type}
|
|
||||||
className={className}
|
|
||||||
placeholder={t(placeholder)}
|
|
||||||
subtitle={t(subtitle)}
|
|
||||||
disabled={processingSetConfig || processingTestUpstream}
|
|
||||||
normalizeOnBlur={normalizeOnBlur}
|
|
||||||
onScroll={onScroll}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface renderTextareaWithHighlightFieldProps {
|
type FormProps = {
|
||||||
className: string;
|
initialValues?: Partial<FormData>;
|
||||||
disabled?: boolean;
|
onSubmit: (data: FormData) => void;
|
||||||
id: string;
|
|
||||||
input?: object;
|
|
||||||
meta?: object;
|
|
||||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
|
||||||
onScroll?: (...args: unknown[]) => unknown;
|
|
||||||
placeholder: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderTextareaWithHighlightField = (props: renderTextareaWithHighlightFieldProps) => {
|
|
||||||
const upstream_dns = useSelector((store: RootState) => store.form[FORM_NAME.UPSTREAM].values.upstream_dns);
|
|
||||||
|
|
||||||
const upstream_dns_file = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);
|
|
||||||
const ref = useRef(null);
|
|
||||||
|
|
||||||
const onScroll = (e: any) => syncScroll(e, ref);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderTextareaField({
|
|
||||||
...props,
|
|
||||||
disabled: !!upstream_dns_file,
|
|
||||||
onScroll,
|
|
||||||
normalizeOnBlur: trimLinesAndRemoveEmpty,
|
|
||||||
})}
|
|
||||||
|
|
||||||
{getTextareaCommentsHighlight(ref, upstream_dns)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const INPUT_FIELDS = [
|
const upstreamModeOptions = [
|
||||||
{
|
{
|
||||||
name: UPSTREAM_MODE_NAME,
|
label: i18next.t('load_balancing'),
|
||||||
type: 'radio',
|
desc: <Trans components={{ br: <br />, b: <b /> }}>load_balancing_desc</Trans>,
|
||||||
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
||||||
component: renderRadioField,
|
|
||||||
subtitle: 'load_balancing_desc',
|
|
||||||
placeholder: 'load_balancing',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: UPSTREAM_MODE_NAME,
|
label: i18next.t('parallel_requests'),
|
||||||
type: 'radio',
|
desc: <Trans components={{ br: <br />, b: <b /> }}>upstream_parallel</Trans>,
|
||||||
value: DNS_REQUEST_OPTIONS.PARALLEL,
|
value: DNS_REQUEST_OPTIONS.PARALLEL,
|
||||||
component: renderRadioField,
|
|
||||||
subtitle: 'upstream_parallel',
|
|
||||||
placeholder: 'parallel_requests',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: UPSTREAM_MODE_NAME,
|
label: i18next.t('fastest_addr'),
|
||||||
type: 'radio',
|
desc: <Trans components={{ br: <br />, b: <b /> }}>fastest_addr_desc</Trans>,
|
||||||
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
|
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
|
||||||
component: renderRadioField,
|
|
||||||
subtitle: 'fastest_addr_desc',
|
|
||||||
placeholder: 'fastest_addr',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface FormProps {
|
const Form = ({ initialValues, onSubmit }: FormProps) => {
|
||||||
handleSubmit?: (...args: unknown[]) => string;
|
|
||||||
submitting?: boolean;
|
|
||||||
invalid?: boolean;
|
|
||||||
initialValues?: object;
|
|
||||||
upstream_dns?: string;
|
|
||||||
fallback_dns?: string;
|
|
||||||
bootstrap_dns?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const upstream_dns = useSelector((store: RootState) => store.form[FORM_NAME.UPSTREAM].values.upstream_dns);
|
const {
|
||||||
|
control,
|
||||||
const processingTestUpstream = useSelector((state: RootState) => state.settings.processingTestUpstream);
|
handleSubmit,
|
||||||
|
watch,
|
||||||
const processingSetConfig = useSelector((state: RootState) => state.dnsConfig.processingSetConfig);
|
formState: { isSubmitting, isDirty },
|
||||||
const defaultLocalPtrUpstreams = useSelector((state: RootState) => state.dnsConfig.default_local_ptr_upstreams);
|
} = useForm<FormData>({
|
||||||
|
mode: 'onBlur',
|
||||||
const handleUpstreamTest = () => dispatch(testUpstreamWithFormValues());
|
defaultValues: {
|
||||||
|
upstream_dns: initialValues?.upstream_dns || '',
|
||||||
const testButtonClass = classnames('btn btn-primary btn-standard mr-2', {
|
upstream_mode: initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
||||||
'btn-loading': processingTestUpstream,
|
fallback_dns: initialValues?.fallback_dns || '',
|
||||||
|
bootstrap_dns: initialValues?.bootstrap_dns || '',
|
||||||
|
local_ptr_upstreams: initialValues?.local_ptr_upstreams || '',
|
||||||
|
use_private_ptr_resolvers: initialValues?.use_private_ptr_resolvers || false,
|
||||||
|
resolve_clients: initialValues?.resolve_clients || false,
|
||||||
|
upstream_timeout: initialValues?.upstream_timeout || 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const components = {
|
const upstream_dns = watch('upstream_dns');
|
||||||
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
|
const processingTestUpstream = useSelector((state: RootState) => state.settings.processingTestUpstream);
|
||||||
|
const processingSetConfig = useSelector((state: RootState) => state.dnsConfig.processingSetConfig);
|
||||||
|
const defaultLocalPtrUpstreams = useSelector((state: RootState) => state.dnsConfig.default_local_ptr_upstreams);
|
||||||
|
const upstream_dns_file = useSelector((state: RootState) => state.dnsConfig.upstream_dns_file);
|
||||||
|
|
||||||
|
const handleUpstreamTest = () => {
|
||||||
|
const formValues = {
|
||||||
|
bootstrap_dns: watch('bootstrap_dns'),
|
||||||
|
upstream_dns: watch('upstream_dns'),
|
||||||
|
local_ptr_upstreams: watch('local_ptr_upstreams'),
|
||||||
|
fallback_dns: watch('fallback_dns'),
|
||||||
|
};
|
||||||
|
dispatch(testUpstreamWithFormValues(formValues));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="form--upstream">
|
<form onSubmit={handleSubmit(onSubmit)} className="form--upstream">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<label className="col form__label" htmlFor={UPSTREAM_DNS_NAME}>
|
<label className="col form__label" htmlFor="upstream_dns">
|
||||||
<Trans components={components}>upstream_dns_help</Trans>{' '}
|
<Trans
|
||||||
|
components={{
|
||||||
|
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||||
|
}}>
|
||||||
|
upstream_dns_help
|
||||||
|
</Trans>{' '}
|
||||||
<Trans
|
<Trans
|
||||||
components={[
|
components={[
|
||||||
<a
|
<a
|
||||||
@@ -196,44 +121,69 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|||||||
|
|
||||||
<div className="col-12 mb-4">
|
<div className="col-12 mb-4">
|
||||||
<div className="text-edit-container">
|
<div className="text-edit-container">
|
||||||
<Field
|
<Controller
|
||||||
id={UPSTREAM_DNS_NAME}
|
name="upstream_dns"
|
||||||
name={UPSTREAM_DNS_NAME}
|
control={control}
|
||||||
component={renderTextareaWithHighlightField}
|
render={({ field }) => (
|
||||||
type="text"
|
<>
|
||||||
className="form-control form-control--textarea font-monospace text-input"
|
<Textarea
|
||||||
placeholder={t('upstream_dns')}
|
{...field}
|
||||||
disabled={processingSetConfig || processingTestUpstream}
|
id={UPSTREAM_DNS_NAME}
|
||||||
normalizeOnBlur={removeEmptyLines}
|
data-testid="upstream_dns"
|
||||||
|
className="form-control--textarea-large text-input"
|
||||||
|
wrapperClassName="mb-0"
|
||||||
|
placeholder={t('upstream_dns')}
|
||||||
|
disabled={!!upstream_dns_file || processingSetConfig || processingTestUpstream}
|
||||||
|
onScroll={(e) => syncScroll(e, textareaRef)}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
{getTextareaCommentsHighlight(textareaRef, upstream_dns)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{INPUT_FIELDS.map(renderField)}
|
|
||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<Examples />
|
<Examples />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 mb-4">
|
||||||
|
<Controller
|
||||||
|
name="upstream_mode"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Radio
|
||||||
|
{...field}
|
||||||
|
options={upstreamModeOptions}
|
||||||
|
disabled={processingSetConfig || processingTestUpstream}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<label className="form__label form__label--with-desc" htmlFor="fallback_dns">
|
<label className="form__label form__label--with-desc" htmlFor="fallback_dns">
|
||||||
<Trans>fallback_dns_title</Trans>
|
{t('fallback_dns_title')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">{t('fallback_dns_desc')}</div>
|
||||||
<Trans>fallback_dns_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
id="fallback_dns"
|
|
||||||
name="fallback_dns"
|
name="fallback_dns"
|
||||||
component={renderTextareaField}
|
control={control}
|
||||||
type="text"
|
render={({ field }) => (
|
||||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
<Textarea
|
||||||
placeholder={t('fallback_dns_placeholder')}
|
{...field}
|
||||||
disabled={processingSetConfig}
|
id="fallback_dns"
|
||||||
normalizeOnBlur={removeEmptyLines}
|
data-testid="fallback_dns"
|
||||||
|
wrapperClassName="mb-0"
|
||||||
|
placeholder={t('fallback_dns_placeholder')}
|
||||||
|
disabled={processingSetConfig}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -241,24 +191,30 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 mb-2">
|
<div className="col-12">
|
||||||
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
|
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
|
||||||
<Trans>bootstrap_dns</Trans>
|
{t('bootstrap_dns')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">{t('bootstrap_dns_desc')}</div>
|
||||||
<Trans>bootstrap_dns_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
id="bootstrap_dns"
|
|
||||||
name="bootstrap_dns"
|
name="bootstrap_dns"
|
||||||
component={renderTextareaField}
|
control={control}
|
||||||
type="text"
|
render={({ field }) => (
|
||||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
<Textarea
|
||||||
placeholder={t('bootstrap_dns')}
|
{...field}
|
||||||
disabled={processingSetConfig}
|
id="bootstrap_dns"
|
||||||
normalizeOnBlur={removeEmptyLines}
|
data-testid="bootstrap_dns"
|
||||||
|
placeholder={t('bootstrap_dns')}
|
||||||
|
wrapperClassName="mb-0"
|
||||||
|
disabled={processingSetConfig}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = removeEmptyLines(e.target.value);
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -268,43 +224,47 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<label className="form__label form__label--with-desc" htmlFor="local_ptr">
|
<label className="form__label form__label--with-desc" htmlFor="local_ptr">
|
||||||
<Trans>local_ptr_title</Trans>
|
{t('local_ptr_title')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">{t('local_ptr_desc')}</div>
|
||||||
<Trans>local_ptr_desc</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
{/** TODO: Add internazionalization for "" */}
|
{defaultLocalPtrUpstreams?.length > 0
|
||||||
{defaultLocalPtrUpstreams?.length > 0 ? (
|
? t('local_ptr_default_resolver', {
|
||||||
<Trans values={{ ip: defaultLocalPtrUpstreams.map((s: any) => `"${s}"`).join(', ') }}>
|
ip: defaultLocalPtrUpstreams.map((s: any) => `"${s}"`).join(', '),
|
||||||
local_ptr_default_resolver
|
})
|
||||||
</Trans>
|
: t('local_ptr_no_default_resolver')}
|
||||||
) : (
|
|
||||||
<Trans>local_ptr_no_default_resolver</Trans>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
id="local_ptr_upstreams"
|
|
||||||
name="local_ptr_upstreams"
|
name="local_ptr_upstreams"
|
||||||
component={renderTextareaField}
|
control={control}
|
||||||
type="text"
|
render={({ field }) => (
|
||||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
<Textarea
|
||||||
placeholder={t('local_ptr_placeholder')}
|
{...field}
|
||||||
disabled={processingSetConfig}
|
id="local_ptr_upstreams"
|
||||||
normalizeOnBlur={removeEmptyLines}
|
data-testid="local_ptr_upstreams"
|
||||||
|
placeholder={t('local_ptr_placeholder')}
|
||||||
|
disabled={processingSetConfig}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Field
|
<Controller
|
||||||
name="use_private_ptr_resolvers"
|
name="use_private_ptr_resolvers"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('use_private_ptr_resolvers_title')}
|
<Checkbox
|
||||||
subtitle={t('use_private_ptr_resolvers_desc')}
|
{...field}
|
||||||
disabled={processingSetConfig}
|
data-testid="dns_use_private_ptr_resolvers"
|
||||||
|
title={t('use_private_ptr_resolvers_title')}
|
||||||
|
subtitle={t('use_private_ptr_resolvers_desc')}
|
||||||
|
disabled={processingSetConfig}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,14 +273,19 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12">
|
<div className="col-12 mb-4">
|
||||||
<Field
|
<Controller
|
||||||
name="resolve_clients"
|
name="resolve_clients"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('resolve_clients_title')}
|
<Checkbox
|
||||||
subtitle={t('resolve_clients_desc')}
|
{...field}
|
||||||
disabled={processingSetConfig}
|
data-testid="dns_resolve_clients"
|
||||||
|
title={t('resolve_clients_title')}
|
||||||
|
subtitle={t('resolve_clients_desc')}
|
||||||
|
disabled={processingSetConfig}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -338,16 +303,26 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|||||||
<Trans>upstream_timeout_desc</Trans>
|
<Trans>upstream_timeout_desc</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
name="upstream_timeout"
|
name="upstream_timeout"
|
||||||
type="number"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{ validate: validateRequiredValue }}
|
||||||
className="form-control"
|
render={({ field }) => (
|
||||||
placeholder={t('form_enter_upstream_timeout')}
|
<Input
|
||||||
normalize={toNumber}
|
{...field}
|
||||||
validate={validateRequiredValue}
|
type="number"
|
||||||
min={1}
|
id="upstream_timeout"
|
||||||
max={UINT32_RANGE.MAX}
|
data-testid="upstream_timeout"
|
||||||
|
placeholder={t('form_enter_upstream_timeout')}
|
||||||
|
disabled={processingSetConfig}
|
||||||
|
min={1}
|
||||||
|
max={UINT32_RANGE.MAX}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -357,17 +332,21 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={testButtonClass}
|
data-testid="dns_upstream_test"
|
||||||
|
className={clsx('btn btn-primary btn-standard mr-2', {
|
||||||
|
'btn-loading': processingTestUpstream,
|
||||||
|
})}
|
||||||
onClick={handleUpstreamTest}
|
onClick={handleUpstreamTest}
|
||||||
disabled={!upstream_dns || processingTestUpstream}>
|
disabled={!upstream_dns || processingTestUpstream}>
|
||||||
<Trans>test_upstream_btn</Trans>
|
{t('test_upstream_btn')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="dns_upstream_save"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={submitting || invalid || processingSetConfig || processingTestUpstream}>
|
disabled={isSubmitting || !isDirty || processingSetConfig || processingTestUpstream}>
|
||||||
<Trans>apply_btn</Trans>
|
{t('apply_btn')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,4 +354,4 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({ form: FORM_NAME.UPSTREAM })(Form);
|
export default Form;
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import { renderInputField, CheckboxField, renderRadioField, toNumber } from '../../../helpers/form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import i18next from 'i18next';
|
||||||
import {
|
import {
|
||||||
validateServerName,
|
validateServerName,
|
||||||
validateIsSafePort,
|
validateIsSafePort,
|
||||||
@@ -14,7 +12,6 @@ import {
|
|||||||
validatePortTLS,
|
validatePortTLS,
|
||||||
validatePlainDns,
|
validatePlainDns,
|
||||||
} from '../../../helpers/validators';
|
} from '../../../helpers/validators';
|
||||||
import i18n from '../../../i18n';
|
|
||||||
|
|
||||||
import KeyStatus from './KeyStatus';
|
import KeyStatus from './KeyStatus';
|
||||||
|
|
||||||
@@ -22,51 +19,39 @@ import CertificateStatus from './CertificateStatus';
|
|||||||
import {
|
import {
|
||||||
DNS_OVER_QUIC_PORT,
|
DNS_OVER_QUIC_PORT,
|
||||||
DNS_OVER_TLS_PORT,
|
DNS_OVER_TLS_PORT,
|
||||||
FORM_NAME,
|
|
||||||
STANDARD_HTTPS_PORT,
|
STANDARD_HTTPS_PORT,
|
||||||
ENCRYPTION_SOURCE,
|
ENCRYPTION_SOURCE,
|
||||||
} from '../../../helpers/constants';
|
} from '../../../helpers/constants';
|
||||||
|
import { Checkbox } from '../../ui/Controls/Checkbox';
|
||||||
|
import { Radio } from '../../ui/Controls/Radio';
|
||||||
|
import { Input } from '../../ui/Controls/Input';
|
||||||
|
import { Textarea } from '../../ui/Controls/Textarea';
|
||||||
|
import { EncryptionData } from '../../../initialState';
|
||||||
|
import { toNumber } from '../../../helpers/form';
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const certificateSourceOptions = [
|
||||||
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
|
{
|
||||||
|
label: i18next.t('encryption_certificates_source_path'),
|
||||||
|
value: ENCRYPTION_SOURCE.PATH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18next.t('encryption_certificates_source_content'),
|
||||||
|
value: ENCRYPTION_SOURCE.CONTENT,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (values.port_dns_over_tls && values.port_https) {
|
const keySourceOptions = [
|
||||||
if (values.port_dns_over_tls === values.port_https) {
|
{
|
||||||
errors.port_dns_over_tls = i18n.t('form_error_equal');
|
label: i18next.t('encryption_key_source_path'),
|
||||||
|
value: ENCRYPTION_SOURCE.PATH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18next.t('encryption_key_source_content'),
|
||||||
|
value: ENCRYPTION_SOURCE.CONTENT,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
errors.port_https = i18n.t('form_error_equal');
|
const validationMessage = (warningValidation: string, isWarning: boolean) => {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFields = (change: any, setTlsConfig: any, validateTlsConfig: any, t: any) => {
|
|
||||||
const fields = {
|
|
||||||
private_key: '',
|
|
||||||
certificate_chain: '',
|
|
||||||
private_key_path: '',
|
|
||||||
certificate_path: '',
|
|
||||||
port_https: STANDARD_HTTPS_PORT,
|
|
||||||
port_dns_over_tls: DNS_OVER_TLS_PORT,
|
|
||||||
port_dns_over_quic: DNS_OVER_QUIC_PORT,
|
|
||||||
server_name: '',
|
|
||||||
force_https: false,
|
|
||||||
enabled: false,
|
|
||||||
private_key_saved: false,
|
|
||||||
serve_plain_dns: true,
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line no-alert
|
|
||||||
if (window.confirm(t('encryption_reset'))) {
|
|
||||||
Object.keys(fields)
|
|
||||||
|
|
||||||
.forEach((field) => change(field, fields[field]));
|
|
||||||
setTlsConfig(fields);
|
|
||||||
validateTlsConfig(fields);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validationMessage = (warningValidation: any, isWarning: any) => {
|
|
||||||
if (!warningValidation) {
|
if (!warningValidation) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -88,56 +73,60 @@ const validationMessage = (warningValidation: any, isWarning: any) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FormProps {
|
export type EncryptionFormValues = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
enabled?: boolean;
|
||||||
handleChange?: (...args: unknown[]) => unknown;
|
serve_plain_dns?: boolean;
|
||||||
isEnabled: boolean;
|
server_name?: string;
|
||||||
servePlainDns: boolean;
|
force_https?: boolean;
|
||||||
certificateChain: string;
|
port_https?: number;
|
||||||
privateKey: string;
|
port_dns_over_tls?: number;
|
||||||
certificatePath: string;
|
port_dns_over_quic?: number;
|
||||||
privateKeyPath: string;
|
certificate_chain?: string;
|
||||||
change: (...args: unknown[]) => unknown;
|
private_key?: string;
|
||||||
submitting: boolean;
|
certificate_path?: string;
|
||||||
invalid: boolean;
|
private_key_path?: string;
|
||||||
initialValues: object;
|
certificate_source?: string;
|
||||||
processingConfig: boolean;
|
key_source?: string;
|
||||||
processingValidate: boolean;
|
private_key_saved?: boolean;
|
||||||
status_key?: string;
|
};
|
||||||
not_after?: string;
|
|
||||||
warning_validation?: string;
|
type Props = {
|
||||||
valid_chain?: boolean;
|
initialValues: EncryptionFormValues;
|
||||||
valid_key?: boolean;
|
encryption: EncryptionData;
|
||||||
valid_cert?: boolean;
|
onSubmit: (values: EncryptionFormValues) => void;
|
||||||
valid_pair?: boolean;
|
debouncedConfigValidation: (values: EncryptionFormValues) => void;
|
||||||
dns_names?: string[];
|
setTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
key_type?: string;
|
validateTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
issuer?: string;
|
};
|
||||||
subject?: string;
|
|
||||||
t: (...args: unknown[]) => string;
|
const defaultValues = {
|
||||||
setTlsConfig: (...args: unknown[]) => unknown;
|
enabled: false,
|
||||||
validateTlsConfig: (...args: unknown[]) => unknown;
|
serve_plain_dns: true,
|
||||||
certificateSource?: string;
|
server_name: '',
|
||||||
privateKeySource?: string;
|
force_https: false,
|
||||||
privateKeySaved?: boolean;
|
port_https: STANDARD_HTTPS_PORT,
|
||||||
}
|
port_dns_over_tls: DNS_OVER_TLS_PORT,
|
||||||
|
port_dns_over_quic: DNS_OVER_QUIC_PORT,
|
||||||
|
certificate_chain: '',
|
||||||
|
private_key: '',
|
||||||
|
certificate_path: '',
|
||||||
|
private_key_path: '',
|
||||||
|
certificate_source: ENCRYPTION_SOURCE.PATH,
|
||||||
|
key_source: ENCRYPTION_SOURCE.PATH,
|
||||||
|
private_key_saved: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Form = ({
|
||||||
|
initialValues,
|
||||||
|
encryption,
|
||||||
|
onSubmit,
|
||||||
|
setTlsConfig,
|
||||||
|
debouncedConfigValidation,
|
||||||
|
validateTlsConfig,
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
let Form = (props: FormProps) => {
|
|
||||||
const {
|
const {
|
||||||
t,
|
|
||||||
handleSubmit,
|
|
||||||
handleChange,
|
|
||||||
isEnabled,
|
|
||||||
servePlainDns,
|
|
||||||
certificateChain,
|
|
||||||
privateKey,
|
|
||||||
certificatePath,
|
|
||||||
privateKeyPath,
|
|
||||||
change,
|
|
||||||
invalid,
|
|
||||||
submitting,
|
|
||||||
processingConfig,
|
|
||||||
processingValidate,
|
|
||||||
not_after,
|
not_after,
|
||||||
valid_chain,
|
valid_chain,
|
||||||
valid_key,
|
valid_key,
|
||||||
@@ -148,37 +137,100 @@ let Form = (props: FormProps) => {
|
|||||||
issuer,
|
issuer,
|
||||||
subject,
|
subject,
|
||||||
warning_validation,
|
warning_validation,
|
||||||
setTlsConfig,
|
processingConfig,
|
||||||
validateTlsConfig,
|
processingValidate,
|
||||||
certificateSource,
|
} = encryption;
|
||||||
privateKeySource,
|
|
||||||
privateKeySaved,
|
const {
|
||||||
} = props;
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
setError,
|
||||||
|
getValues,
|
||||||
|
formState: { isSubmitting, isValid },
|
||||||
|
} = useForm<EncryptionFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
...defaultValues,
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
mode: 'onBlur',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
enabled: isEnabled,
|
||||||
|
serve_plain_dns: servePlainDns,
|
||||||
|
certificate_chain: certificateChain,
|
||||||
|
private_key: privateKey,
|
||||||
|
private_key_path: privateKeyPath,
|
||||||
|
key_source: privateKeySource,
|
||||||
|
private_key_saved: privateKeySaved,
|
||||||
|
certificate_path: certificatePath,
|
||||||
|
certificate_source: certificateSource,
|
||||||
|
} = watch();
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
debouncedConfigValidation(getValues());
|
||||||
|
};
|
||||||
|
|
||||||
const isSavingDisabled = () => {
|
const isSavingDisabled = () => {
|
||||||
const processing = submitting || processingConfig || processingValidate;
|
const processing = isSubmitting || processingConfig || processingValidate;
|
||||||
|
|
||||||
if (servePlainDns && !isEnabled) {
|
if (servePlainDns && !isEnabled) {
|
||||||
return invalid || processing;
|
return !isValid || processing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return invalid || processing || !valid_key || !valid_cert || !valid_pair;
|
return !isValid || processing || !valid_key || !valid_cert || !valid_pair;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFields = () => {
|
||||||
|
if (window.confirm(t('encryption_reset'))) {
|
||||||
|
reset();
|
||||||
|
setTlsConfig(defaultValues);
|
||||||
|
validateTlsConfig(defaultValues);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePorts = (values: EncryptionFormValues) => {
|
||||||
|
const errors: { port_dns_over_tls?: string; port_https?: string } = {};
|
||||||
|
|
||||||
|
if (values.port_dns_over_tls && values.port_https) {
|
||||||
|
if (values.port_dns_over_tls === values.port_https) {
|
||||||
|
errors.port_dns_over_tls = i18next.t('form_error_equal');
|
||||||
|
errors.port_https = i18next.t('form_error_equal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = (data: EncryptionFormValues) => {
|
||||||
|
const validationErrors = validatePorts(data);
|
||||||
|
|
||||||
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
|
Object.entries(validationErrors).forEach(([field, message]) => {
|
||||||
|
setError(field as keyof EncryptionFormValues, { type: 'manual', message });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onSubmit(data);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDisabled = isSavingDisabled();
|
const isDisabled = isSavingDisabled();
|
||||||
const isWarning = valid_key && valid_cert && valid_pair;
|
const isWarning = valid_key && valid_cert && valid_pair;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="form__group form__group--settings mb-3">
|
<div className="form__group form__group--settings mb-3">
|
||||||
<Field
|
<Controller
|
||||||
name="enabled"
|
name="enabled"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('encryption_enable')}
|
<Checkbox {...field} title={t('encryption_enable')} onBlur={handleBlur} />
|
||||||
onChange={handleChange}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -187,13 +239,13 @@ let Form = (props: FormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group mb-3 mt-5">
|
<div className="form__group mb-3 mt-5">
|
||||||
<Field
|
<Controller
|
||||||
name="serve_plain_dns"
|
name="serve_plain_dns"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
rules={{
|
||||||
placeholder={t('encryption_plain_dns_enable')}
|
validate: (value) => validatePlainDns(value, getValues()),
|
||||||
onChange={handleChange}
|
}}
|
||||||
validate={validatePlainDns}
|
render={({ field }) => <Checkbox {...field} title={t('encryption_plain_dns_enable')} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -212,16 +264,20 @@ let Form = (props: FormProps) => {
|
|||||||
|
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
id="server_name"
|
|
||||||
name="server_name"
|
name="server_name"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{ validate: validateServerName }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('encryption_server_enter')}
|
<Input
|
||||||
onChange={handleChange}
|
{...field}
|
||||||
disabled={!isEnabled}
|
type="text"
|
||||||
validate={validateServerName}
|
placeholder={t('encryption_server_enter')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form__desc">
|
<div className="form__desc">
|
||||||
@@ -232,13 +288,12 @@ let Form = (props: FormProps) => {
|
|||||||
|
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="force_https"
|
name="force_https"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('encryption_redirect')}
|
<Checkbox {...field} title={t('encryption_redirect')} disabled={!isEnabled} />
|
||||||
onChange={handleChange}
|
)}
|
||||||
disabled={!isEnabled}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form__desc">
|
<div className="form__desc">
|
||||||
@@ -255,17 +310,24 @@ let Form = (props: FormProps) => {
|
|||||||
<Trans>encryption_https</Trans>
|
<Trans>encryption_https</Trans>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
id="port_https"
|
|
||||||
name="port_https"
|
name="port_https"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="number"
|
rules={{ validate: { validatePort, validateIsSafePort } }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('encryption_https')}
|
<Input
|
||||||
validate={[validatePort, validateIsSafePort]}
|
{...field}
|
||||||
normalize={toNumber}
|
type="number"
|
||||||
onChange={handleChange}
|
placeholder={t('encryption_https')}
|
||||||
disabled={!isEnabled}
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form__desc">
|
<div className="form__desc">
|
||||||
@@ -280,17 +342,24 @@ let Form = (props: FormProps) => {
|
|||||||
<Trans>encryption_dot</Trans>
|
<Trans>encryption_dot</Trans>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
id="port_dns_over_tls"
|
|
||||||
name="port_dns_over_tls"
|
name="port_dns_over_tls"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="number"
|
rules={{ validate: validatePortTLS }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('encryption_dot')}
|
<Input
|
||||||
validate={[validatePortTLS]}
|
{...field}
|
||||||
normalize={toNumber}
|
type="number"
|
||||||
onChange={handleChange}
|
placeholder={t('encryption_dot')}
|
||||||
disabled={!isEnabled}
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form__desc">
|
<div className="form__desc">
|
||||||
@@ -305,17 +374,24 @@ let Form = (props: FormProps) => {
|
|||||||
<Trans>encryption_doq</Trans>
|
<Trans>encryption_doq</Trans>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
id="port_dns_over_quic"
|
|
||||||
name="port_dns_over_quic"
|
name="port_dns_over_quic"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="number"
|
rules={{ validate: validatePortQuic }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('encryption_doq')}
|
<Input
|
||||||
validate={[validatePortQuic]}
|
{...field}
|
||||||
normalize={toNumber}
|
type="number"
|
||||||
onChange={handleChange}
|
placeholder={t('encryption_doq')}
|
||||||
disabled={!isEnabled}
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="form__desc">
|
<div className="form__desc">
|
||||||
@@ -352,50 +428,44 @@ let Form = (props: FormProps) => {
|
|||||||
|
|
||||||
<div className="form__inline mb-2">
|
<div className="form__inline mb-2">
|
||||||
<div className="custom-controls-stacked">
|
<div className="custom-controls-stacked">
|
||||||
<Field
|
<Controller
|
||||||
name="certificate_source"
|
name="certificate_source"
|
||||||
component={renderRadioField}
|
control={control}
|
||||||
type="radio"
|
render={({ field }) => (
|
||||||
className="form-control mr-2"
|
<Radio {...field} options={certificateSourceOptions} disabled={!isEnabled} />
|
||||||
value="path"
|
)}
|
||||||
placeholder={t('encryption_certificates_source_path')}
|
|
||||||
disabled={!isEnabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="certificate_source"
|
|
||||||
component={renderRadioField}
|
|
||||||
type="radio"
|
|
||||||
className="form-control mr-2"
|
|
||||||
value="content"
|
|
||||||
placeholder={t('encryption_certificates_source_content')}
|
|
||||||
disabled={!isEnabled}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{certificateSource === ENCRYPTION_SOURCE.CONTENT && (
|
{certificateSource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||||
<Field
|
<Controller
|
||||||
id="certificate_chain"
|
|
||||||
name="certificate_chain"
|
name="certificate_chain"
|
||||||
component="textarea"
|
control={control}
|
||||||
type="text"
|
render={({ field, fieldState }) => (
|
||||||
className="form-control form-control--textarea"
|
<Textarea
|
||||||
placeholder={t('encryption_certificates_input')}
|
{...field}
|
||||||
onChange={handleChange}
|
placeholder={t('encryption_certificates_input')}
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
) : (
|
||||||
{certificateSource === ENCRYPTION_SOURCE.PATH && (
|
<Controller
|
||||||
<Field
|
|
||||||
id="certificate_path"
|
|
||||||
name="certificate_path"
|
name="certificate_path"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
render={({ field, fieldState }) => (
|
||||||
className="form-control"
|
<Input
|
||||||
placeholder={t('encryption_certificate_path')}
|
{...field}
|
||||||
onChange={handleChange}
|
type="text"
|
||||||
disabled={!isEnabled}
|
placeholder={t('encryption_certificate_path')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -424,70 +494,67 @@ let Form = (props: FormProps) => {
|
|||||||
|
|
||||||
<div className="form__inline mb-2">
|
<div className="form__inline mb-2">
|
||||||
<div className="custom-controls-stacked">
|
<div className="custom-controls-stacked">
|
||||||
<Field
|
<Controller
|
||||||
name="key_source"
|
name="key_source"
|
||||||
component={renderRadioField}
|
control={control}
|
||||||
type="radio"
|
render={({ field }) => (
|
||||||
className="form-control mr-2"
|
<Radio {...field} options={keySourceOptions} disabled={!isEnabled} />
|
||||||
value={ENCRYPTION_SOURCE.PATH}
|
)}
|
||||||
placeholder={t('encryption_key_source_path')}
|
|
||||||
disabled={!isEnabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="key_source"
|
|
||||||
component={renderRadioField}
|
|
||||||
type="radio"
|
|
||||||
className="form-control mr-2"
|
|
||||||
value={ENCRYPTION_SOURCE.CONTENT}
|
|
||||||
placeholder={t('encryption_key_source_content')}
|
|
||||||
disabled={!isEnabled}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{privateKeySource === ENCRYPTION_SOURCE.PATH && (
|
{privateKeySource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||||
<Field
|
<>
|
||||||
|
<Controller
|
||||||
|
name="private_key_saved"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
title={t('use_saved_key')}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
onChange={(checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setValue('private_key', '');
|
||||||
|
}
|
||||||
|
field.onChange(checked);
|
||||||
|
}}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="private_key"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder={t('encryption_key_input')}
|
||||||
|
disabled={!isEnabled || privateKeySaved}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
name="private_key_path"
|
name="private_key_path"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
render={({ field, fieldState }) => (
|
||||||
className="form-control"
|
<Input
|
||||||
placeholder={t('encryption_private_key_path')}
|
{...field}
|
||||||
onChange={handleChange}
|
type="text"
|
||||||
disabled={!isEnabled}
|
placeholder={t('encryption_private_key_path')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{privateKeySource === ENCRYPTION_SOURCE.CONTENT && [
|
|
||||||
<Field
|
|
||||||
key="private_key_saved"
|
|
||||||
name="private_key_saved"
|
|
||||||
type="checkbox"
|
|
||||||
className="form__group form__group--settings mb-2"
|
|
||||||
component={CheckboxField}
|
|
||||||
disabled={!isEnabled}
|
|
||||||
placeholder={t('use_saved_key')}
|
|
||||||
onChange={(event: any) => {
|
|
||||||
if (event.target.checked) {
|
|
||||||
change('private_key', '');
|
|
||||||
}
|
|
||||||
if (handleChange) {
|
|
||||||
handleChange(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
|
|
||||||
<Field
|
|
||||||
id="private_key"
|
|
||||||
key="private_key"
|
|
||||||
name="private_key"
|
|
||||||
component="textarea"
|
|
||||||
type="text"
|
|
||||||
className="form-control form-control--textarea"
|
|
||||||
placeholder={t('encryption_key_input')}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!isEnabled || privateKeySaved}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__status">
|
<div className="form__status">
|
||||||
@@ -505,44 +572,11 @@ let Form = (props: FormProps) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary btn-standart"
|
className="btn btn-secondary btn-standart"
|
||||||
disabled={submitting || processingConfig}
|
disabled={isSubmitting || processingConfig}
|
||||||
onClick={() => clearFields(change, setTlsConfig, validateTlsConfig, t)}>
|
onClick={clearFields}>
|
||||||
<Trans>reset_settings</Trans>
|
<Trans>reset_settings</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = formValueSelector(FORM_NAME.ENCRYPTION);
|
|
||||||
|
|
||||||
Form = connect((state) => {
|
|
||||||
const isEnabled = selector(state, 'enabled');
|
|
||||||
const servePlainDns = selector(state, 'serve_plain_dns');
|
|
||||||
const certificateChain = selector(state, 'certificate_chain');
|
|
||||||
const privateKey = selector(state, 'private_key');
|
|
||||||
const certificatePath = selector(state, 'certificate_path');
|
|
||||||
const privateKeyPath = selector(state, 'private_key_path');
|
|
||||||
const certificateSource = selector(state, 'certificate_source');
|
|
||||||
const privateKeySource = selector(state, 'key_source');
|
|
||||||
const privateKeySaved = selector(state, 'private_key_saved');
|
|
||||||
return {
|
|
||||||
isEnabled,
|
|
||||||
servePlainDns,
|
|
||||||
certificateChain,
|
|
||||||
privateKey,
|
|
||||||
certificatePath,
|
|
||||||
privateKeyPath,
|
|
||||||
certificateSource,
|
|
||||||
privateKeySource,
|
|
||||||
privateKeySaved,
|
|
||||||
};
|
|
||||||
})(Form);
|
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.ENCRYPTION,
|
|
||||||
validate,
|
|
||||||
}),
|
|
||||||
])(Form);
|
|
||||||
|
|||||||
@@ -1,61 +1,60 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import debounce from 'lodash/debounce';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
|
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
|
||||||
|
|
||||||
import Form from './Form';
|
import { EncryptionFormValues, Form } from './Form';
|
||||||
|
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
|
|
||||||
import PageTitle from '../../ui/PageTitle';
|
import PageTitle from '../../ui/PageTitle';
|
||||||
|
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import { EncryptionData } from '../../../initialState';
|
import { EncryptionData } from '../../../initialState';
|
||||||
|
|
||||||
interface EncryptionProps {
|
type Props = {
|
||||||
setTlsConfig: (...args: unknown[]) => unknown;
|
|
||||||
validateTlsConfig: (...args: unknown[]) => unknown;
|
|
||||||
encryption: EncryptionData;
|
encryption: EncryptionData;
|
||||||
t: (...args: unknown[]) => string;
|
setTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
}
|
validateTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
class Encryption extends Component<EncryptionProps> {
|
export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {
|
||||||
componentDidMount() {
|
const { t } = useTranslation();
|
||||||
const { validateTlsConfig, encryption } = this.props;
|
|
||||||
|
|
||||||
if (encryption.enabled) {
|
const initialValues = useMemo((): EncryptionFormValues => {
|
||||||
validateTlsConfig(encryption);
|
const {
|
||||||
}
|
enabled,
|
||||||
}
|
serve_plain_dns,
|
||||||
|
server_name,
|
||||||
handleFormSubmit = (values: any) => {
|
force_https,
|
||||||
const submitValues = this.getSubmitValues(values);
|
port_https,
|
||||||
|
port_dns_over_tls,
|
||||||
this.props.setTlsConfig(submitValues);
|
port_dns_over_quic,
|
||||||
};
|
certificate_chain,
|
||||||
|
private_key,
|
||||||
handleFormChange = debounce((values) => {
|
certificate_path,
|
||||||
const submitValues = this.getSubmitValues(values);
|
private_key_path,
|
||||||
|
private_key_saved,
|
||||||
if (submitValues.enabled) {
|
} = encryption;
|
||||||
this.props.validateTlsConfig(submitValues);
|
|
||||||
}
|
|
||||||
}, DEBOUNCE_TIMEOUT);
|
|
||||||
|
|
||||||
getInitialValues = (data: any) => {
|
|
||||||
const { certificate_chain, private_key, private_key_saved } = data;
|
|
||||||
const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
const certificate_source = certificate_chain ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
||||||
const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
const key_source = private_key || private_key_saved ? ENCRYPTION_SOURCE.CONTENT : ENCRYPTION_SOURCE.PATH;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
enabled,
|
||||||
|
serve_plain_dns,
|
||||||
|
server_name,
|
||||||
|
force_https,
|
||||||
|
port_https,
|
||||||
|
port_dns_over_tls,
|
||||||
|
port_dns_over_quic,
|
||||||
|
certificate_chain,
|
||||||
|
private_key,
|
||||||
|
certificate_path,
|
||||||
|
private_key_path,
|
||||||
|
private_key_saved,
|
||||||
certificate_source,
|
certificate_source,
|
||||||
key_source,
|
key_source,
|
||||||
};
|
};
|
||||||
};
|
}, [encryption]);
|
||||||
|
|
||||||
getSubmitValues = (values: any) => {
|
const getSubmitValues = useCallback((values: any) => {
|
||||||
const { certificate_source, key_source, private_key_saved, ...config } = values;
|
const { certificate_source, key_source, private_key_saved, ...config } = values;
|
||||||
|
|
||||||
if (certificate_source === ENCRYPTION_SOURCE.PATH) {
|
if (certificate_source === ENCRYPTION_SOURCE.PATH) {
|
||||||
@@ -76,63 +75,47 @@ class Encryption extends Component<EncryptionProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
render() {
|
const handleFormSubmit = useCallback(
|
||||||
const { encryption, t } = this.props;
|
(values: any) => {
|
||||||
const {
|
const submitValues = getSubmitValues(values);
|
||||||
enabled,
|
setTlsConfig(submitValues);
|
||||||
server_name,
|
},
|
||||||
force_https,
|
[getSubmitValues, setTlsConfig],
|
||||||
port_https,
|
);
|
||||||
port_dns_over_tls,
|
|
||||||
port_dns_over_quic,
|
|
||||||
certificate_chain,
|
|
||||||
private_key,
|
|
||||||
certificate_path,
|
|
||||||
private_key_path,
|
|
||||||
private_key_saved,
|
|
||||||
serve_plain_dns,
|
|
||||||
} = encryption;
|
|
||||||
|
|
||||||
const initialValues = this.getInitialValues({
|
const validateConfig = useCallback((values) => {
|
||||||
enabled,
|
const submitValues = getSubmitValues(values);
|
||||||
server_name,
|
|
||||||
force_https,
|
|
||||||
port_https,
|
|
||||||
port_dns_over_tls,
|
|
||||||
port_dns_over_quic,
|
|
||||||
certificate_chain,
|
|
||||||
private_key,
|
|
||||||
certificate_path,
|
|
||||||
private_key_path,
|
|
||||||
private_key_saved,
|
|
||||||
serve_plain_dns,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
if (submitValues.enabled) {
|
||||||
<div className="encryption">
|
validateTlsConfig(submitValues);
|
||||||
<PageTitle title={t('encryption_settings')} />
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
{encryption.processing && <Loading />}
|
const debouncedConfigValidation = useMemo(() => debounce(validateConfig, DEBOUNCE_TIMEOUT), [validateConfig]);
|
||||||
{!encryption.processing && (
|
|
||||||
<Card
|
|
||||||
title={t('encryption_title')}
|
|
||||||
subtitle={t('encryption_desc')}
|
|
||||||
bodyType="card-body box-body--settings">
|
|
||||||
<Form
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={this.handleFormSubmit}
|
|
||||||
onChange={this.handleFormChange}
|
|
||||||
setTlsConfig={this.props.setTlsConfig}
|
|
||||||
validateTlsConfig={this.props.validateTlsConfig}
|
|
||||||
{...this.props.encryption}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation()(Encryption);
|
return (
|
||||||
|
<div className="encryption">
|
||||||
|
<PageTitle title={t('encryption_settings')} />
|
||||||
|
|
||||||
|
{encryption.processing ? (
|
||||||
|
<Loading />
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
title={t('encryption_title')}
|
||||||
|
subtitle={t('encryption_desc')}
|
||||||
|
bodyType="card-body box-body--settings">
|
||||||
|
<Form
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
debouncedConfigValidation={debouncedConfigValidation}
|
||||||
|
setTlsConfig={setTlsConfig}
|
||||||
|
validateTlsConfig={validateTlsConfig}
|
||||||
|
encryption={encryption}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,39 +1,115 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import debounce from 'lodash/debounce';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
|
import i18next from 'i18next';
|
||||||
|
import { toNumber } from '../../../helpers/form';
|
||||||
|
import { DAY, FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK } from '../../../helpers/constants';
|
||||||
|
import { Checkbox } from '../../ui/Controls/Checkbox';
|
||||||
|
import { Select } from '../../ui/Controls/Select';
|
||||||
|
|
||||||
import Form from './Form';
|
const THREE_DAYS_INTERVAL = DAY * 3;
|
||||||
|
const SEVEN_DAYS_INTERVAL = DAY * 7;
|
||||||
|
|
||||||
import { getObjDiff } from '../../../helpers/helpers';
|
const getTitleForInterval = (interval: number) => {
|
||||||
|
if (interval === 0) {
|
||||||
|
return i18next.t('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
interface FiltersConfigProps {
|
if (interval === THREE_DAYS_INTERVAL || interval === SEVEN_DAYS_INTERVAL) {
|
||||||
initialValues: object;
|
return i18next.t('interval_days', { count: interval / DAY });
|
||||||
processing: boolean;
|
}
|
||||||
setFiltersConfig: (...args: unknown[]) => unknown;
|
|
||||||
t: (...args: unknown[]) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FiltersConfig = (props: FiltersConfigProps) => {
|
return i18next.t('interval_hours', { count: interval });
|
||||||
const { initialValues, processing } = props;
|
|
||||||
|
|
||||||
const handleFormChange = debounce((values) => {
|
|
||||||
const diff = getObjDiff(initialValues, values);
|
|
||||||
|
|
||||||
if (Object.values(diff).length > 0) {
|
|
||||||
props.setFiltersConfig(values);
|
|
||||||
}
|
|
||||||
}, DEBOUNCE_TIMEOUT);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={handleFormChange}
|
|
||||||
onChange={handleFormChange}
|
|
||||||
processing={processing}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTranslation()(FiltersConfig);
|
export type FormValues = {
|
||||||
|
enabled: boolean;
|
||||||
|
interval: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValues: FormValues;
|
||||||
|
setFiltersConfig: (values: FormValues) => void;
|
||||||
|
processing: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersConfig = ({ initialValues, setFiltersConfig, processing }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const prevFormValuesRef = useRef<FormValues>(initialValues);
|
||||||
|
|
||||||
|
const { watch, control } = useForm({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formValues = watch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const prevFormValues = prevFormValuesRef.current;
|
||||||
|
|
||||||
|
if (JSON.stringify(prevFormValues) !== JSON.stringify(formValues)) {
|
||||||
|
setFiltersConfig(formValues);
|
||||||
|
prevFormValuesRef.current = formValues;
|
||||||
|
}
|
||||||
|
}, [formValues]);
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
a: <a href={FILTERS_RELATIVE_LINK} rel="noopener noreferrer" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<Controller
|
||||||
|
name="enabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
data-testid="filters_enabled"
|
||||||
|
title={t('block_domain_use_filters_and_hosts')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans components={components}>filters_block_toggle_hint</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-md-5">
|
||||||
|
<div className="form__group form__group--inner mb-5">
|
||||||
|
<label className="form__label">
|
||||||
|
<Trans>filters_interval</Trans>
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="interval"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="filters_interval"
|
||||||
|
disabled={processing}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}>
|
||||||
|
{FILTERS_INTERVALS_HOURS.map((interval) => (
|
||||||
|
<option value={interval} key={interval}>
|
||||||
|
{getTitleForInterval(interval)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,147 +1,182 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { change, Field, formValueSelector, reduxForm } from 'redux-form';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import i18next from 'i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CheckboxField,
|
|
||||||
toFloatNumber,
|
|
||||||
renderTextareaField,
|
|
||||||
renderInputField,
|
|
||||||
renderRadioField,
|
|
||||||
} from '../../../helpers/form';
|
|
||||||
|
|
||||||
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||||
import {
|
import { QUERY_LOG_INTERVALS_DAYS, HOUR, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
|
||||||
FORM_NAME,
|
|
||||||
QUERY_LOG_INTERVALS_DAYS,
|
|
||||||
HOUR,
|
|
||||||
DAY,
|
|
||||||
RETENTION_CUSTOM,
|
|
||||||
RETENTION_CUSTOM_INPUT,
|
|
||||||
RETENTION_RANGE,
|
|
||||||
CUSTOM_INTERVAL,
|
|
||||||
} from '../../../helpers/constants';
|
|
||||||
import '../FormButton.css';
|
import '../FormButton.css';
|
||||||
|
import { Checkbox } from '../../ui/Controls/Checkbox';
|
||||||
|
import { Input } from '../../ui/Controls/Input';
|
||||||
|
import { toNumber } from '../../../helpers/form';
|
||||||
|
import { Textarea } from '../../ui/Controls/Textarea';
|
||||||
|
|
||||||
const getIntervalTitle = (interval: any, t: any) => {
|
const getIntervalTitle = (interval: number) => {
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case RETENTION_CUSTOM:
|
case RETENTION_CUSTOM:
|
||||||
return t('settings_custom');
|
return i18next.t('settings_custom');
|
||||||
case 6 * HOUR:
|
case 6 * HOUR:
|
||||||
return t('interval_6_hour');
|
return i18next.t('interval_6_hour');
|
||||||
case DAY:
|
case DAY:
|
||||||
return t('interval_24_hour');
|
return i18next.t('interval_24_hour');
|
||||||
default:
|
default:
|
||||||
return t('interval_days', { count: interval / DAY });
|
return i18next.t('interval_days', { count: interval / DAY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIntervalFields = (processing: any, t: any, toNumber: any) =>
|
export type FormValues = {
|
||||||
QUERY_LOG_INTERVALS_DAYS.map((interval) => (
|
enabled: boolean;
|
||||||
<Field
|
anonymize_client_ip: boolean;
|
||||||
key={interval}
|
interval: number;
|
||||||
name="interval"
|
customInterval?: number | null;
|
||||||
type="radio"
|
ignored: string;
|
||||||
component={renderRadioField}
|
};
|
||||||
value={interval}
|
|
||||||
placeholder={getIntervalTitle(interval, t)}
|
|
||||||
normalize={toNumber}
|
|
||||||
disabled={processing}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
interface FormProps {
|
type Props = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
initialValues: Partial<FormValues>;
|
||||||
handleClear: (...args: unknown[]) => unknown;
|
|
||||||
submitting: boolean;
|
|
||||||
invalid: boolean;
|
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
processingClear: boolean;
|
processingReset: boolean;
|
||||||
t: (...args: unknown[]) => string;
|
onSubmit: (values: FormValues) => void;
|
||||||
interval?: number;
|
onReset: () => void;
|
||||||
customInterval?: number;
|
};
|
||||||
dispatch: (...args: unknown[]) => unknown;
|
|
||||||
}
|
export const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
let Form = (props: FormProps) => {
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
submitting,
|
watch,
|
||||||
invalid,
|
setValue,
|
||||||
processing,
|
control,
|
||||||
processingClear,
|
formState: { isSubmitting },
|
||||||
handleClear,
|
} = useForm<FormValues>({
|
||||||
t,
|
mode: 'onBlur',
|
||||||
interval,
|
defaultValues: {
|
||||||
customInterval,
|
enabled: initialValues.enabled || false,
|
||||||
dispatch,
|
anonymize_client_ip: initialValues.anonymize_client_ip || false,
|
||||||
} = props;
|
interval: initialValues.interval || DAY,
|
||||||
|
customInterval: initialValues.customInterval || null,
|
||||||
|
ignored: initialValues.ignored || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const intervalValue = watch('interval');
|
||||||
|
const customIntervalValue = watch('customInterval');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) {
|
if (QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)) {
|
||||||
dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null));
|
setValue('customInterval', null);
|
||||||
}
|
}
|
||||||
}, [interval]);
|
}, [intervalValue]);
|
||||||
|
|
||||||
|
const onSubmitForm = (data: FormValues) => {
|
||||||
|
onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIgnoredBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
const trimmed = trimLinesAndRemoveEmpty(e.target.value);
|
||||||
|
setValue('ignored', trimmed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="enabled"
|
name="enabled"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('query_log_enable')}
|
<Checkbox
|
||||||
disabled={processing}
|
{...field}
|
||||||
|
data-testid="logs_enabled"
|
||||||
|
title={t('query_log_enable')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="anonymize_client_ip"
|
name="anonymize_client_ip"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('anonymize_client_ip')}
|
<Checkbox
|
||||||
subtitle={t('anonymize_client_ip_desc')}
|
{...field}
|
||||||
disabled={processing}
|
data-testid="logs_anonymize_client_ip"
|
||||||
|
title={t('anonymize_client_ip')}
|
||||||
|
subtitle={t('anonymize_client_ip_desc')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="form__label">
|
<div className="form__label">
|
||||||
<Trans>query_log_retention</Trans>
|
<Trans>query_log_retention</Trans>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<div className="custom-controls-stacked">
|
<div className="custom-controls-stacked">
|
||||||
<Field
|
<label className="custom-control custom-radio">
|
||||||
key={RETENTION_CUSTOM}
|
<input
|
||||||
name="interval"
|
type="radio"
|
||||||
type="radio"
|
data-testid="logs_config_interval"
|
||||||
component={renderRadioField}
|
className="custom-control-input"
|
||||||
value={QUERY_LOG_INTERVALS_DAYS.includes(interval) ? RETENTION_CUSTOM : interval}
|
disabled={processing}
|
||||||
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
|
checked={!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)}
|
||||||
normalize={toFloatNumber}
|
value={RETENTION_CUSTOM}
|
||||||
disabled={processing}
|
onChange={(e) => {
|
||||||
/>
|
setValue('interval', parseInt(e.target.value, 10));
|
||||||
{!QUERY_LOG_INTERVALS_DAYS.includes(interval) && (
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="custom-control-label">{getIntervalTitle(RETENTION_CUSTOM)}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue) && (
|
||||||
<div className="form__group--input">
|
<div className="form__group--input">
|
||||||
<div className="form__desc form__desc--top">{t('custom_rotation_input')}</div>
|
<div className="form__desc form__desc--top">{t('custom_rotation_input')}</div>
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
key={RETENTION_CUSTOM_INPUT}
|
name="customInterval"
|
||||||
name={CUSTOM_INTERVAL}
|
control={control}
|
||||||
type="number"
|
render={({ field, fieldState }) => (
|
||||||
className="form-control"
|
<Input
|
||||||
component={renderInputField}
|
{...field}
|
||||||
disabled={processing}
|
data-testid="logs_config_custom_interval"
|
||||||
normalize={toFloatNumber}
|
disabled={processing}
|
||||||
min={RETENTION_RANGE.MIN}
|
error={fieldState.error?.message}
|
||||||
max={RETENTION_RANGE.MAX}
|
min={RETENTION_RANGE.MIN}
|
||||||
|
max={RETENTION_RANGE.MAX}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{getIntervalFields(processing, t, toFloatNumber)}
|
|
||||||
|
{QUERY_LOG_INTERVALS_DAYS.map((interval) => (
|
||||||
|
<label key={interval} className="custom-control custom-radio">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="custom-control-input"
|
||||||
|
data-testid={`logs_config_${interval}`}
|
||||||
|
disabled={processing}
|
||||||
|
value={interval}
|
||||||
|
checked={intervalValue === interval}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue('interval', parseInt(e.target.value, 10));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="custom-control-label">{getIntervalTitle(interval)}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,51 +189,41 @@ let Form = (props: FormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="ignored"
|
name="ignored"
|
||||||
type="textarea"
|
control={control}
|
||||||
className="form-control form-control--textarea font-monospace text-input"
|
render={({ field, fieldState }) => (
|
||||||
component={renderTextareaField}
|
<Textarea
|
||||||
placeholder={t('ignore_domains')}
|
{...field}
|
||||||
disabled={processing}
|
data-testid="logs_config_ingored"
|
||||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
placeholder={t('ignore_domains')}
|
||||||
|
className="text-input"
|
||||||
|
disabled={processing}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onBlur={handleIgnoredBlur}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="logs_config_save"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={
|
disabled={disableSubmit}>
|
||||||
submitting ||
|
|
||||||
invalid ||
|
|
||||||
processing ||
|
|
||||||
(!QUERY_LOG_INTERVALS_DAYS.includes(interval) && !customInterval)
|
|
||||||
}>
|
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="logs_config_clear"
|
||||||
className="btn btn-outline-secondary btn-standard form__button"
|
className="btn btn-outline-secondary btn-standard form__button"
|
||||||
onClick={() => handleClear()}
|
onClick={onReset}
|
||||||
disabled={processingClear}>
|
disabled={processingReset}>
|
||||||
<Trans>query_log_clear</Trans>
|
<Trans>query_log_clear</Trans>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = formValueSelector(FORM_NAME.LOG_CONFIG);
|
|
||||||
|
|
||||||
Form = connect((state) => {
|
|
||||||
const interval = selector(state, 'interval');
|
|
||||||
const customInterval = selector(state, CUSTOM_INTERVAL);
|
|
||||||
return {
|
|
||||||
interval,
|
|
||||||
customInterval,
|
|
||||||
};
|
|
||||||
})(Form);
|
|
||||||
|
|
||||||
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.LOG_CONFIG })])(Form);
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { withTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
|
|
||||||
import Form from './Form';
|
import { Form, FormValues } from './Form';
|
||||||
import { HOUR } from '../../../helpers/constants';
|
import { HOUR } from '../../../helpers/constants';
|
||||||
|
|
||||||
interface LogsConfigProps {
|
interface LogsConfigProps {
|
||||||
@@ -20,7 +20,7 @@ interface LogsConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LogsConfig extends Component<LogsConfigProps> {
|
class LogsConfig extends Component<LogsConfigProps> {
|
||||||
handleFormSubmit = (values: any) => {
|
handleFormSubmit = (values: FormValues) => {
|
||||||
const { t, interval: prevInterval } = this.props;
|
const { t, interval: prevInterval } = this.props;
|
||||||
const { interval, customInterval, ...rest } = values;
|
const { interval, customInterval, ...rest } = values;
|
||||||
|
|
||||||
@@ -53,19 +53,12 @@ class LogsConfig extends Component<LogsConfigProps> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
t,
|
t,
|
||||||
|
|
||||||
enabled,
|
enabled,
|
||||||
|
|
||||||
interval,
|
interval,
|
||||||
|
|
||||||
processing,
|
processing,
|
||||||
|
|
||||||
processingClear,
|
processingClear,
|
||||||
|
|
||||||
anonymize_client_ip,
|
anonymize_client_ip,
|
||||||
|
|
||||||
ignored,
|
ignored,
|
||||||
|
|
||||||
customInterval,
|
customInterval,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -80,10 +73,10 @@ class LogsConfig extends Component<LogsConfigProps> {
|
|||||||
anonymize_client_ip,
|
anonymize_client_ip,
|
||||||
ignored: ignored?.join('\n'),
|
ignored: ignored?.join('\n'),
|
||||||
}}
|
}}
|
||||||
onSubmit={this.handleFormSubmit}
|
|
||||||
processing={processing}
|
processing={processing}
|
||||||
processingClear={processingClear}
|
processingReset={processingClear}
|
||||||
handleClear={this.handleClear}
|
onSubmit={this.handleFormSubmit}
|
||||||
|
onReset={this.handleClear}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form__message {
|
.form__message {
|
||||||
|
margin-top: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +98,10 @@
|
|||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__label {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.form__label--bold {
|
.form__label--bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +1,101 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { change, Field, formValueSelector, reduxForm } from 'redux-form';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import i18next from 'i18next';
|
||||||
import flow from 'lodash/flow';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import {
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
renderRadioField,
|
import { STATS_INTERVALS_DAYS, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
|
||||||
toNumber,
|
|
||||||
CheckboxField,
|
|
||||||
renderTextareaField,
|
|
||||||
toFloatNumber,
|
|
||||||
renderInputField,
|
|
||||||
} from '../../../helpers/form';
|
|
||||||
import {
|
|
||||||
FORM_NAME,
|
|
||||||
STATS_INTERVALS_DAYS,
|
|
||||||
DAY,
|
|
||||||
RETENTION_CUSTOM,
|
|
||||||
RETENTION_CUSTOM_INPUT,
|
|
||||||
CUSTOM_INTERVAL,
|
|
||||||
RETENTION_RANGE,
|
|
||||||
} from '../../../helpers/constants';
|
|
||||||
|
|
||||||
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
|
||||||
import '../FormButton.css';
|
import '../FormButton.css';
|
||||||
|
import { Checkbox } from '../../ui/Controls/Checkbox';
|
||||||
|
import { Input } from '../../ui/Controls/Input';
|
||||||
|
import { toNumber } from '../../../helpers/form';
|
||||||
|
import { Textarea } from '../../ui/Controls/Textarea';
|
||||||
|
|
||||||
const getIntervalTitle = (intervalMs: any, t: any) => {
|
const getIntervalTitle = (interval: any) => {
|
||||||
switch (intervalMs) {
|
switch (interval) {
|
||||||
case RETENTION_CUSTOM:
|
case RETENTION_CUSTOM:
|
||||||
return t('settings_custom');
|
return i18next.t('settings_custom');
|
||||||
case DAY:
|
case DAY:
|
||||||
return t('interval_24_hour');
|
return i18next.t('interval_24_hour');
|
||||||
default:
|
default:
|
||||||
return t('interval_days', { count: intervalMs / DAY });
|
return i18next.t('interval_days', { count: interval / DAY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FormProps {
|
export type FormValues = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
enabled: boolean;
|
||||||
handleReset: (...args: unknown[]) => string;
|
interval: number;
|
||||||
change: (...args: unknown[]) => unknown;
|
customInterval?: number | null;
|
||||||
submitting: boolean;
|
ignored: string;
|
||||||
invalid: boolean;
|
};
|
||||||
|
|
||||||
|
const defaultFormValues = {
|
||||||
|
enabled: false,
|
||||||
|
interval: DAY,
|
||||||
|
customInterval: null,
|
||||||
|
ignored: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValues: FormValues;
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
processingReset: boolean;
|
processingReset: boolean;
|
||||||
t: (...args: unknown[]) => string;
|
onSubmit: (values: FormValues) => void;
|
||||||
interval?: number;
|
onReset: () => void;
|
||||||
customInterval?: number;
|
};
|
||||||
dispatch: (...args: unknown[]) => unknown;
|
|
||||||
}
|
export const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
let Form = (props: FormProps) => {
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
processing,
|
watch,
|
||||||
submitting,
|
setValue,
|
||||||
invalid,
|
control,
|
||||||
handleReset,
|
formState: { isSubmitting },
|
||||||
processingReset,
|
} = useForm<FormValues>({
|
||||||
t,
|
mode: 'onBlur',
|
||||||
interval,
|
defaultValues: {
|
||||||
customInterval,
|
...defaultFormValues,
|
||||||
dispatch,
|
...initialValues,
|
||||||
} = props;
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const intervalValue = watch('interval');
|
||||||
|
const customIntervalValue = watch('customInterval');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (STATS_INTERVALS_DAYS.includes(interval)) {
|
if (STATS_INTERVALS_DAYS.includes(intervalValue)) {
|
||||||
dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null));
|
setValue('customInterval', null);
|
||||||
}
|
}
|
||||||
}, [interval]);
|
}, [intervalValue]);
|
||||||
|
|
||||||
|
const onSubmitForm = (data: FormValues) => {
|
||||||
|
onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="enabled"
|
name="enabled"
|
||||||
type="checkbox"
|
control={control}
|
||||||
component={CheckboxField}
|
render={({ field }) => (
|
||||||
placeholder={t('statistics_enable')}
|
<Checkbox
|
||||||
disabled={processing}
|
{...field}
|
||||||
|
data-testid="stats_config_enabled"
|
||||||
|
title={t('statistics_enable')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="form__label form__label--with-desc">
|
<div className="form__label form__label--with-desc">
|
||||||
<Trans>statistics_retention</Trans>
|
<Trans>statistics_retention</Trans>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
<Trans>statistics_retention_desc</Trans>
|
<Trans>statistics_retention_desc</Trans>
|
||||||
@@ -92,85 +103,105 @@ let Form = (props: FormProps) => {
|
|||||||
|
|
||||||
<div className="form__group form__group--settings mt-2">
|
<div className="form__group form__group--settings mt-2">
|
||||||
<div className="custom-controls-stacked">
|
<div className="custom-controls-stacked">
|
||||||
<Field
|
<label className="custom-control custom-radio">
|
||||||
key={RETENTION_CUSTOM}
|
<input
|
||||||
name="interval"
|
type="radio"
|
||||||
type="radio"
|
data-testid="stats_config_interval"
|
||||||
component={renderRadioField}
|
className="custom-control-input"
|
||||||
value={STATS_INTERVALS_DAYS.includes(interval) ? RETENTION_CUSTOM : interval}
|
disabled={processing}
|
||||||
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
|
checked={!STATS_INTERVALS_DAYS.includes(intervalValue)}
|
||||||
normalize={toFloatNumber}
|
value={RETENTION_CUSTOM}
|
||||||
disabled={processing}
|
onChange={(e) => {
|
||||||
/>
|
setValue('interval', parseInt(e.target.value, 10));
|
||||||
{!STATS_INTERVALS_DAYS.includes(interval) && (
|
}}
|
||||||
<div className="form__group--input">
|
/>
|
||||||
<div className="form__desc form__desc--top">{t('custom_retention_input')}</div>
|
|
||||||
|
|
||||||
<Field
|
<span className="custom-control-label">{getIntervalTitle(RETENTION_CUSTOM)}</span>
|
||||||
key={RETENTION_CUSTOM_INPUT}
|
</label>
|
||||||
name={CUSTOM_INTERVAL}
|
|
||||||
type="number"
|
{!STATS_INTERVALS_DAYS.includes(intervalValue) && (
|
||||||
className="form-control"
|
<div className="form__group--input">
|
||||||
component={renderInputField}
|
<div className="form__desc form__desc--top">{i18next.t('custom_retention_input')}</div>
|
||||||
disabled={processing}
|
|
||||||
normalize={toFloatNumber}
|
<Controller
|
||||||
min={RETENTION_RANGE.MIN}
|
name="customInterval"
|
||||||
max={RETENTION_RANGE.MAX}
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
data-testid="stats_config_custom_interval"
|
||||||
|
disabled={processing}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
min={RETENTION_RANGE.MIN}
|
||||||
|
max={RETENTION_RANGE.MAX}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{STATS_INTERVALS_DAYS.map((interval) => (
|
{STATS_INTERVALS_DAYS.map((interval) => (
|
||||||
<Field
|
<label key={interval} className="custom-control custom-radio">
|
||||||
key={interval}
|
<input
|
||||||
name="interval"
|
type="radio"
|
||||||
type="radio"
|
className="custom-control-input"
|
||||||
component={renderRadioField}
|
disabled={processing}
|
||||||
value={interval}
|
value={interval}
|
||||||
placeholder={getIntervalTitle(interval, t)}
|
checked={intervalValue === interval}
|
||||||
normalize={toNumber}
|
onChange={(e) => {
|
||||||
disabled={processing}
|
setValue('interval', parseInt(e.target.value, 10));
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="custom-control-label">{getIntervalTitle(interval)}</span>
|
||||||
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="form__label form__label--with-desc">
|
<div className="form__label form__label--with-desc">
|
||||||
<Trans>ignore_domains_title</Trans>
|
<Trans>ignore_domains_title</Trans>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
<Trans>ignore_domains_desc_stats</Trans>
|
<Trans>ignore_domains_desc_stats</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Controller
|
||||||
name="ignored"
|
name="ignored"
|
||||||
type="textarea"
|
control={control}
|
||||||
className="form-control form-control--textarea font-monospace text-input"
|
render={({ field, fieldState }) => (
|
||||||
component={renderTextareaField}
|
<Textarea
|
||||||
placeholder={t('ignore_domains')}
|
{...field}
|
||||||
disabled={processing}
|
data-testid="stats_config_ignored"
|
||||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
placeholder={t('ignore_domains')}
|
||||||
|
className="text-input"
|
||||||
|
disabled={processing}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
trimOnBlur
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
data-testid="stats_config_save"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={
|
disabled={disableSubmit}>
|
||||||
submitting ||
|
|
||||||
invalid ||
|
|
||||||
processing ||
|
|
||||||
(!STATS_INTERVALS_DAYS.includes(interval) && !customInterval)
|
|
||||||
}>
|
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="stats_config_clear"
|
||||||
className="btn btn-outline-secondary btn-standard form__button"
|
className="btn btn-outline-secondary btn-standard form__button"
|
||||||
onClick={() => handleReset()}
|
onClick={onReset}
|
||||||
disabled={processingReset}>
|
disabled={processingReset}>
|
||||||
<Trans>statistics_clear</Trans>
|
<Trans>statistics_clear</Trans>
|
||||||
</button>
|
</button>
|
||||||
@@ -178,16 +209,3 @@ let Form = (props: FormProps) => {
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = formValueSelector(FORM_NAME.STATS_CONFIG);
|
|
||||||
|
|
||||||
Form = connect((state) => {
|
|
||||||
const interval = selector(state, 'interval');
|
|
||||||
const customInterval = selector(state, CUSTOM_INTERVAL);
|
|
||||||
return {
|
|
||||||
interval,
|
|
||||||
customInterval,
|
|
||||||
};
|
|
||||||
})(Form);
|
|
||||||
|
|
||||||
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.STATS_CONFIG })])(Form);
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { withTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
|
|
||||||
import Form from './Form';
|
import { Form, FormValues } from './Form';
|
||||||
import { HOUR } from '../../../helpers/constants';
|
import { HOUR } from '../../../helpers/constants';
|
||||||
|
|
||||||
interface StatsConfigProps {
|
interface StatsConfigProps {
|
||||||
@@ -19,7 +19,7 @@ interface StatsConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class StatsConfig extends Component<StatsConfigProps> {
|
class StatsConfig extends Component<StatsConfigProps> {
|
||||||
handleFormSubmit = ({ enabled, interval, ignored, customInterval }: any) => {
|
handleFormSubmit = ({ enabled, interval, ignored, customInterval }: FormValues) => {
|
||||||
const { t, interval: prevInterval } = this.props;
|
const { t, interval: prevInterval } = this.props;
|
||||||
const newInterval = customInterval ? customInterval * HOUR : interval;
|
const newInterval = customInterval ? customInterval * HOUR : interval;
|
||||||
|
|
||||||
@@ -49,17 +49,11 @@ class StatsConfig extends Component<StatsConfigProps> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
t,
|
t,
|
||||||
|
|
||||||
interval,
|
interval,
|
||||||
|
|
||||||
customInterval,
|
customInterval,
|
||||||
|
|
||||||
processing,
|
processing,
|
||||||
|
|
||||||
processingReset,
|
processingReset,
|
||||||
|
|
||||||
ignored,
|
ignored,
|
||||||
|
|
||||||
enabled,
|
enabled,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -73,10 +67,10 @@ class StatsConfig extends Component<StatsConfigProps> {
|
|||||||
enabled,
|
enabled,
|
||||||
ignored: ignored.join('\n'),
|
ignored: ignored.join('\n'),
|
||||||
}}
|
}}
|
||||||
onSubmit={this.handleFormSubmit}
|
|
||||||
processing={processing}
|
processing={processing}
|
||||||
processingReset={processingReset}
|
processingReset={processingReset}
|
||||||
handleReset={this.handleReset}
|
onSubmit={this.handleFormSubmit}
|
||||||
|
onReset={this.handleReset}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import i18next from 'i18next';
|
||||||
import StatsConfig from './StatsConfig';
|
import StatsConfig from './StatsConfig';
|
||||||
|
|
||||||
import LogsConfig from './LogsConfig';
|
import LogsConfig from './LogsConfig';
|
||||||
|
|
||||||
import FiltersConfig from './FiltersConfig';
|
import { FiltersConfig } from './FiltersConfig';
|
||||||
|
|
||||||
import Checkbox from '../ui/Checkbox';
|
import { Checkbox } from '../ui/Controls/Checkbox';
|
||||||
|
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
|
|
||||||
@@ -24,14 +25,14 @@ const ORDER_KEY = 'order';
|
|||||||
const SETTINGS = {
|
const SETTINGS = {
|
||||||
safebrowsing: {
|
safebrowsing: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
title: 'use_adguard_browsing_sec',
|
title: i18next.t('use_adguard_browsing_sec'),
|
||||||
subtitle: 'use_adguard_browsing_sec_hint',
|
subtitle: i18next.t('use_adguard_browsing_sec_hint'),
|
||||||
[ORDER_KEY]: 0,
|
[ORDER_KEY]: 0,
|
||||||
},
|
},
|
||||||
parental: {
|
parental: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
title: 'use_adguard_parental',
|
title: i18next.t('use_adguard_parental'),
|
||||||
subtitle: 'use_adguard_parental_hint',
|
subtitle: i18next.t('use_adguard_parental_hint'),
|
||||||
[ORDER_KEY]: 1,
|
[ORDER_KEY]: 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -89,9 +90,18 @@ class Settings extends Component<SettingsProps> {
|
|||||||
renderSettings = (settings: any) =>
|
renderSettings = (settings: any) =>
|
||||||
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
|
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
|
||||||
const setting = settings[key];
|
const setting = settings[key];
|
||||||
const { enabled } = setting;
|
const { enabled, title, subtitle } = setting;
|
||||||
|
|
||||||
return <Checkbox {...setting} key={key} handleChange={() => this.props.toggleSetting(key, enabled)} />;
|
return (
|
||||||
|
<div key={key} className="form__group form__group--checkbox">
|
||||||
|
<Checkbox
|
||||||
|
value={enabled}
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
onChange={(checked) => this.props.toggleSetting(key, !checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
renderSafeSearch = () => {
|
renderSafeSearch = () => {
|
||||||
@@ -106,27 +116,29 @@ class Settings extends Component<SettingsProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Checkbox
|
<div className="form__group form__group--checkbox">
|
||||||
enabled={enabled}
|
<Checkbox
|
||||||
title="enforce_safe_search"
|
value={enabled}
|
||||||
subtitle="enforce_save_search_hint"
|
title={i18next.t('enforce_safe_search')}
|
||||||
handleChange={({ target: { checked: enabled } }) =>
|
subtitle={i18next.t('enforce_save_search_hint')}
|
||||||
this.props.toggleSetting('safesearch', { ...safesearch, enabled })
|
onChange={(checked) =>
|
||||||
}
|
this.props.toggleSetting('safesearch', { ...safesearch, enabled: checked })
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form__group--inner">
|
<div className="form__group--inner">
|
||||||
{Object.keys(searches).map((searchKey) => (
|
{Object.keys(searches).map((searchKey) => (
|
||||||
<Checkbox
|
<div key={searchKey} className="form__group form__group--checkbox">
|
||||||
key={searchKey}
|
<Checkbox
|
||||||
enabled={searches[searchKey]}
|
value={searches[searchKey]}
|
||||||
title={captitalizeWords(searchKey)}
|
title={captitalizeWords(searchKey)}
|
||||||
subtitle=""
|
disabled={!safesearch.enabled}
|
||||||
disabled={!safesearch.enabled}
|
onChange={(checked) =>
|
||||||
handleChange={({ target: { checked } }: any) =>
|
this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })
|
||||||
this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -136,23 +148,14 @@ class Settings extends Component<SettingsProps> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
setStatsConfig,
|
setStatsConfig,
|
||||||
|
|
||||||
resetStats,
|
resetStats,
|
||||||
|
|
||||||
stats,
|
stats,
|
||||||
|
|
||||||
queryLogs,
|
queryLogs,
|
||||||
|
|
||||||
setLogsConfig,
|
setLogsConfig,
|
||||||
|
|
||||||
clearLogs,
|
clearLogs,
|
||||||
|
|
||||||
filtering,
|
filtering,
|
||||||
|
|
||||||
setFiltersConfig,
|
setFiltersConfig,
|
||||||
|
|
||||||
t,
|
t,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -163,6 +166,7 @@ class Settings extends Component<SettingsProps> {
|
|||||||
<PageTitle title={t('general_settings')} />
|
<PageTitle title={t('general_settings')} />
|
||||||
|
|
||||||
{!isDataReady && <Loading />}
|
{!isDataReady && <Loading />}
|
||||||
|
|
||||||
{isDataReady && (
|
{isDataReady && (
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Guide from '../ui/Guide';
|
import { Guide } from '../ui/Guide';
|
||||||
|
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
|
|
||||||
@@ -14,10 +14,7 @@ interface SetupGuideProps {
|
|||||||
t: (id: string) => string;
|
t: (id: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SetupGuide = ({
|
const SetupGuide = ({ t, dashboard: { dnsAddresses } }: SetupGuideProps) => (
|
||||||
t,
|
|
||||||
dashboard: { dnsAddresses },
|
|
||||||
}: SetupGuideProps) => (
|
|
||||||
<div className="guide">
|
<div className="guide">
|
||||||
<PageTitle title={t('setup_guide')} />
|
<PageTitle title={t('setup_guide')} />
|
||||||
|
|
||||||
|
|||||||
113
client/src/components/ui/Controls/Checkbox/checkbox.css
Normal file
113
client/src/components/ui/Controls/Checkbox/checkbox.css
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
.checkbox {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox--single {
|
||||||
|
display: block;
|
||||||
|
margin: 2px auto 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox--single .checkbox__label:before {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox--settings .checkbox__label:before {
|
||||||
|
top: 2px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox--settings .checkbox__label-title {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox--form .checkbox__label:before {
|
||||||
|
top: 1px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label:before {
|
||||||
|
content: '';
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
background-color: var(--checkbox-bg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: 12px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition:
|
||||||
|
0.3s ease-in-out box-shadow,
|
||||||
|
0.3s ease-in-out opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label .checkbox__label-text {
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label .checkbox__label-text .md__paragraph {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: baseline;
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:checked + .checkbox__label:before {
|
||||||
|
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMi4zIDkuMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxwYXRoIGQ9Ik0xMS44IDAuNUw1LjMgOC41IDAuNSA0LjIiLz48L3N2Zz4=);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:focus + .checkbox__label:before {
|
||||||
|
box-shadow: 0 0 1px 1px rgba(74, 74, 74, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:disabled + .checkbox__label {
|
||||||
|
cursor: default;
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__input:disabled + .checkbox__label:before {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label-text {
|
||||||
|
max-width: 515px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label-text--long {
|
||||||
|
max-width: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label-title {
|
||||||
|
display: block;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox__label-subtitle {
|
||||||
|
display: block;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--scolor);
|
||||||
|
}
|
||||||
50
client/src/components/ui/Controls/Checkbox/index.tsx
Normal file
50
client/src/components/ui/Controls/Checkbox/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { forwardRef, ReactNode } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import './checkbox.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
value: boolean;
|
||||||
|
name?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
error?: string;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Checkbox = forwardRef<HTMLInputElement, Props>(
|
||||||
|
(
|
||||||
|
{ title, subtitle, value, name, disabled, error, className = 'checkbox--form', onChange, onBlur, ...rest },
|
||||||
|
ref,
|
||||||
|
) => (
|
||||||
|
<>
|
||||||
|
<label className={clsx('checkbox', className)}>
|
||||||
|
<span className="checkbox__marker" />
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox__input"
|
||||||
|
disabled={disabled}
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
onBlur={onBlur}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<span className="checkbox__label">
|
||||||
|
<span className="checkbox__label-text checkbox__label-text--long">
|
||||||
|
<span className="checkbox__label-title">{title}</span>
|
||||||
|
|
||||||
|
{subtitle && <span className="checkbox__label-subtitle">{subtitle}</span>}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{error && <div className="form__message form__message--error">{error}</div>}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Checkbox.displayName = 'Checkbox';
|
||||||
45
client/src/components/ui/Controls/Input.tsx
Normal file
45
client/src/components/ui/Controls/Input.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { ComponentProps, forwardRef, ReactNode } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
type Props = ComponentProps<'input'> & {
|
||||||
|
label?: string;
|
||||||
|
desc?: string;
|
||||||
|
leftAddon?: ReactNode;
|
||||||
|
rightAddon?: ReactNode;
|
||||||
|
error?: string;
|
||||||
|
trimOnBlur?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Input = forwardRef<HTMLInputElement, Props>(
|
||||||
|
({ name, label, desc, className, leftAddon, rightAddon, error, trimOnBlur, onBlur, ...rest }, ref) => (
|
||||||
|
<div className={clsx('form-group', { 'has-error': !!error })}>
|
||||||
|
{label && (
|
||||||
|
<label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{desc && <div className="form__desc form__desc--top">{desc}</div>}
|
||||||
|
<div className="input-group">
|
||||||
|
{leftAddon && <div>{leftAddon}</div>}
|
||||||
|
<input
|
||||||
|
className={clsx('form-control', { 'is-invalid': !!error }, className)}
|
||||||
|
ref={ref}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (trimOnBlur) {
|
||||||
|
e.target.value = e.target.value.trim();
|
||||||
|
rest.onChange(e);
|
||||||
|
}
|
||||||
|
if (onBlur) {
|
||||||
|
onBlur(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{rightAddon && <div>{rightAddon}</div>}
|
||||||
|
</div>
|
||||||
|
{error && <div className="form__message form__message--error mt-1">{error}</div>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Input.displayName = 'Input';
|
||||||
50
client/src/components/ui/Controls/Radio.tsx
Normal file
50
client/src/components/ui/Controls/Radio.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { forwardRef, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type Props<T> = {
|
||||||
|
name: string;
|
||||||
|
value: T;
|
||||||
|
onChange: (e: T) => void;
|
||||||
|
options: { label: string; desc?: ReactNode; value: T }[];
|
||||||
|
disabled?: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Radio = forwardRef<HTMLInputElement, Props<string | boolean | number | undefined>>(
|
||||||
|
({ disabled, onChange, value, options, name, error, ...rest }, ref) => {
|
||||||
|
const getId = (label: string) => (name ? `${label}_${name}` : label);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{options.map((o) => {
|
||||||
|
const checked = value === o.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={`${getId(o.label)}`}
|
||||||
|
htmlFor={getId(o.label)}
|
||||||
|
className="custom-control custom-radio">
|
||||||
|
<input
|
||||||
|
id={getId(o.label)}
|
||||||
|
data-testid={o.value}
|
||||||
|
type="radio"
|
||||||
|
className="custom-control-input"
|
||||||
|
onChange={() => onChange(o.value)}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="custom-control-label">{o.label}</span>
|
||||||
|
|
||||||
|
{o.desc && <span className="checkbox__label-subtitle">{o.desc}</span>}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!disabled && error && <span className="form__message form__message--error">{error}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Radio.displayName = 'Radio';
|
||||||
27
client/src/components/ui/Controls/Select.tsx
Normal file
27
client/src/components/ui/Controls/Select.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { ComponentProps, forwardRef } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
type SelectProps = ComponentProps<'select'> & {
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
|
({ name, label, className, error, children, ...rest }, ref) => (
|
||||||
|
<div className={clsx('form-group', { 'has-error': !!error })}>
|
||||||
|
{label && (
|
||||||
|
<label className="form__label" htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<div className="input-group">
|
||||||
|
<select className={clsx('form-control custom-select', className)} ref={ref} {...rest}>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{error && <div className="form__message form__message--error mt-1">{error}</div>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Select.displayName = 'Select';
|
||||||
45
client/src/components/ui/Controls/Textarea.tsx
Normal file
45
client/src/components/ui/Controls/Textarea.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { ComponentProps, forwardRef } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||||
|
|
||||||
|
type Props = ComponentProps<'textarea'> & {
|
||||||
|
className?: string;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
label?: string;
|
||||||
|
desc?: string;
|
||||||
|
error?: string;
|
||||||
|
trimOnBlur?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Textarea = forwardRef<HTMLTextAreaElement, Props>(
|
||||||
|
({ name, label, desc, className, wrapperClassName, error, trimOnBlur, onBlur, ...rest }, ref) => (
|
||||||
|
<div className={clsx('form-group', wrapperClassName, { 'has-error': !!error })}>
|
||||||
|
{label && (
|
||||||
|
<label className={clsx('form__label', { 'form__label--with-desc': !!desc })} htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
{desc && <div className="form__desc form__desc--top">{desc}</div>}
|
||||||
|
<textarea
|
||||||
|
className={clsx(
|
||||||
|
'form-control form-control--textarea form-control--textarea-small font-monospace',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (trimOnBlur) {
|
||||||
|
const normalizedValue = trimLinesAndRemoveEmpty(e.target.value);
|
||||||
|
rest.onChange(normalizedValue);
|
||||||
|
}
|
||||||
|
if (onBlur) {
|
||||||
|
onBlur(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
{error && <div className="form__message form__message--error">{error}</div>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Textarea.displayName = 'Textarea';
|
||||||
@@ -7,7 +7,7 @@ import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
|
|||||||
|
|
||||||
import Tabs from '../Tabs';
|
import Tabs from '../Tabs';
|
||||||
|
|
||||||
import MobileConfigForm from './MobileConfigForm';
|
import { MobileConfigForm } from './MobileConfigForm';
|
||||||
import { RootState } from '../../../initialState';
|
import { RootState } from '../../../initialState';
|
||||||
|
|
||||||
interface renderLiProps {
|
interface renderLiProps {
|
||||||
@@ -346,7 +346,7 @@ interface GuideProps {
|
|||||||
dnsAddresses?: unknown[];
|
dnsAddresses?: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Guide = ({ dnsAddresses }: GuideProps) => {
|
export const Guide = ({ dnsAddresses }: GuideProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const serverName = useSelector((state: RootState) => state.encryption?.server_name);
|
const serverName = useSelector((state: RootState) => state.encryption?.server_name);
|
||||||
@@ -381,5 +381,3 @@ const Guide = ({ dnsAddresses }: GuideProps) => {
|
|||||||
Guide.defaultProps = {
|
Guide.defaultProps = {
|
||||||
dnsAddresses: [],
|
dnsAddresses: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Guide;
|
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { Field, reduxForm } from 'redux-form';
|
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
|
|
||||||
import { getPathWithQueryString } from '../../../helpers/helpers';
|
import { getPathWithQueryString } from '../../../helpers/helpers';
|
||||||
import { CLIENT_ID_LINK, FORM_NAME, MOBILE_CONFIG_LINKS, STANDARD_HTTPS_PORT } from '../../../helpers/constants';
|
import { CLIENT_ID_LINK, MOBILE_CONFIG_LINKS, STANDARD_HTTPS_PORT } from '../../../helpers/constants';
|
||||||
import { renderInputField, renderSelectField, toNumber } from '../../../helpers/form';
|
import { toNumber } from '../../../helpers/form';
|
||||||
import {
|
import {
|
||||||
validateConfigClientId,
|
validateConfigClientId,
|
||||||
validateServerName,
|
validateServerName,
|
||||||
validatePort,
|
validatePort,
|
||||||
validateIsSafePort,
|
validateIsSafePort,
|
||||||
} from '../../../helpers/validators';
|
} from '../../../helpers/validators';
|
||||||
import { RootState } from '../../../initialState';
|
import { Input } from '../Controls/Input';
|
||||||
|
import { Select } from '../Controls/Select';
|
||||||
|
|
||||||
const getDownloadLink = (host: any, clientId: any, protocol: any, invalid: any) => {
|
const getDownloadLink = (host: string, clientId: string, protocol: string, invalid: boolean) => {
|
||||||
if (!host || invalid) {
|
if (!host || invalid) {
|
||||||
return (
|
return (
|
||||||
<button type="button" className="btn btn-success btn-standard btn-large disabled">
|
<button type="button" className="btn btn-success btn-standard btn-large disabled">
|
||||||
<Trans>download_mobileconfig</Trans>
|
{i18next.t('download_mobileconfig')}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkParams: { host: string, client_id?: string } = { host };
|
const linkParams: { host: string; client_id?: string } = { host };
|
||||||
|
|
||||||
if (clientId) {
|
if (clientId) {
|
||||||
linkParams.client_id = clientId;
|
linkParams.client_id = clientId;
|
||||||
@@ -37,29 +36,48 @@ const getDownloadLink = (host: any, clientId: any, protocol: any, invalid: any)
|
|||||||
href={getPathWithQueryString(protocol, linkParams)}
|
href={getPathWithQueryString(protocol, linkParams)}
|
||||||
className={cn('btn btn-success btn-standard btn-large')}
|
className={cn('btn btn-success btn-standard btn-large')}
|
||||||
download>
|
download>
|
||||||
<Trans>download_mobileconfig</Trans>
|
{i18next.t('download_mobileconfig')}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MobileConfigFormProps {
|
type FormValues = {
|
||||||
invalid: boolean;
|
host: string;
|
||||||
}
|
clientId: string;
|
||||||
|
protocol: string;
|
||||||
|
port?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const MobileConfigForm = ({ invalid }: MobileConfigFormProps) => {
|
type Props = {
|
||||||
const formValues = useSelector((state: RootState) => state.form[FORM_NAME.MOBILE_CONFIG]?.values);
|
initialValues?: FormValues;
|
||||||
|
};
|
||||||
|
|
||||||
if (!formValues) {
|
const defaultFormValues = {
|
||||||
return null;
|
host: '',
|
||||||
}
|
clientId: '',
|
||||||
|
protocol: MOBILE_CONFIG_LINKS.DOT,
|
||||||
|
port: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const { host, clientId, protocol, port } = formValues;
|
export const MobileConfigForm = ({ initialValues }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const githubLink = (
|
const {
|
||||||
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
|
watch,
|
||||||
text
|
control,
|
||||||
</a>
|
formState: { isValid },
|
||||||
);
|
} = useForm<FormValues>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
...defaultFormValues,
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const protocol = watch('protocol');
|
||||||
|
const host = watch('host');
|
||||||
|
const clientId = watch('clientId');
|
||||||
|
const port = watch('port');
|
||||||
|
|
||||||
const getHostName = () => {
|
const getHostName = () => {
|
||||||
if (port && port !== STANDARD_HTTPS_PORT && protocol === MOBILE_CONFIG_LINKS.DOH) {
|
if (port && port !== STANDARD_HTTPS_PORT && protocol === MOBILE_CONFIG_LINKS.DOH) {
|
||||||
@@ -75,33 +93,47 @@ const MobileConfigForm = ({ invalid }: MobileConfigFormProps) => {
|
|||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<label htmlFor="host" className="form__label">
|
<Controller
|
||||||
{i18next.t('dhcp_table_hostname')}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="host"
|
name="host"
|
||||||
type="text"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{ validate: validateServerName }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={i18next.t('form_enter_hostname')}
|
<Input
|
||||||
validate={validateServerName}
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="mobile_config_host"
|
||||||
|
label={t('dhcp_table_hostname')}
|
||||||
|
placeholder={t('form_enter_hostname')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{protocol === MOBILE_CONFIG_LINKS.DOH && (
|
{protocol === MOBILE_CONFIG_LINKS.DOH && (
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<label htmlFor="port" className="form__label">
|
<Controller
|
||||||
{i18next.t('encryption_https')}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="port"
|
name="port"
|
||||||
type="number"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={i18next.t('encryption_https')}
|
range: (value) => validatePort(value) || true,
|
||||||
validate={[validatePort, validateIsSafePort]}
|
safety: (value) => validateIsSafePort(value) || true,
|
||||||
normalize={toNumber}
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
data-testid="mobile_config_port"
|
||||||
|
label={t('encryption_https')}
|
||||||
|
placeholder={t('encryption_https')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -110,39 +142,49 @@ const MobileConfigForm = ({ invalid }: MobileConfigFormProps) => {
|
|||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="clientId" className="form__label form__label--with-desc">
|
<label htmlFor="clientId" className="form__label form__label--with-desc">
|
||||||
{i18next.t('client_id')}
|
{t('client_id')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="form__desc form__desc--top">
|
<div className="form__desc form__desc--top">
|
||||||
<Trans components={{ a: githubLink }}>client_id_desc</Trans>
|
<Trans
|
||||||
|
components={{ a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" /> }}>
|
||||||
|
client_id_desc
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field
|
<Controller
|
||||||
name="clientId"
|
name="clientId"
|
||||||
type="text"
|
control={control}
|
||||||
component={renderInputField}
|
rules={{
|
||||||
className="form-control"
|
validate: validateConfigClientId,
|
||||||
placeholder={i18next.t('client_id_placeholder')}
|
}}
|
||||||
validate={validateConfigClientId}
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
data-testid="mobile_config_client_id"
|
||||||
|
placeholder={t('client_id_placeholder')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label htmlFor="protocol" className="form__label">
|
<Controller
|
||||||
{i18next.t('protocol')}
|
name="protocol"
|
||||||
</label>
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
<Field name="protocol" type="text" component={renderSelectField} className="form-control">
|
<Select {...field} label={t('protocol')} data-testid="mobile_config_protocol">
|
||||||
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
|
<option value={MOBILE_CONFIG_LINKS.DOT}>{t('dns_over_tls')}</option>
|
||||||
|
<option value={MOBILE_CONFIG_LINKS.DOH}>{t('dns_over_https')}</option>
|
||||||
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
|
</Select>
|
||||||
</Field>
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{getDownloadLink(getHostName(), clientId, protocol, invalid)}
|
{getDownloadLink(getHostName(), clientId, protocol, !isValid)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({ form: FORM_NAME.MOBILE_CONFIG })(MobileConfigForm);
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { default } from './Guide';
|
export * from './Guide';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { toggleProtection, getClients } from '../actions';
|
import { toggleProtection, getClients } from '../actions';
|
||||||
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
|
import { getStats, getStatsConfig } from '../actions/stats';
|
||||||
import { getAccessList } from '../actions/access';
|
import { getAccessList } from '../actions/access';
|
||||||
|
|
||||||
import Dashboard from '../components/Dashboard';
|
import Dashboard from '../components/Dashboard';
|
||||||
@@ -19,7 +19,7 @@ type DispatchProps = {
|
|||||||
getStats: (...args: unknown[]) => unknown;
|
getStats: (...args: unknown[]) => unknown;
|
||||||
getStatsConfig: (...args: unknown[]) => unknown;
|
getStatsConfig: (...args: unknown[]) => unknown;
|
||||||
getAccessList: () => (dispatch: any) => void;
|
getAccessList: () => (dispatch: any) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const mapDispatchToProps: DispatchProps = {
|
const mapDispatchToProps: DispatchProps = {
|
||||||
toggleProtection,
|
toggleProtection,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
|
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
|
||||||
|
|
||||||
import Encryption from '../components/Settings/Encryption';
|
import { Encryption } from '../components/Settings/Encryption';
|
||||||
|
|
||||||
const mapStateToProps = (state: any) => {
|
const mapStateToProps = (state: any) => {
|
||||||
const { encryption } = state;
|
const { encryption } = state;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export const STANDARD_WEB_PORT = 80;
|
|||||||
export const STANDARD_HTTPS_PORT = 443;
|
export const STANDARD_HTTPS_PORT = 443;
|
||||||
export const DNS_OVER_TLS_PORT = 853;
|
export const DNS_OVER_TLS_PORT = 853;
|
||||||
export const DNS_OVER_QUIC_PORT = 853;
|
export const DNS_OVER_QUIC_PORT = 853;
|
||||||
|
export const MIN_PORT = 1;
|
||||||
export const MAX_PORT = 65535;
|
export const MAX_PORT = 65535;
|
||||||
|
|
||||||
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
export const EMPTY_DATE = '0001-01-01T00:00:00Z';
|
||||||
@@ -209,7 +210,7 @@ export const WHOIS_ICONS = {
|
|||||||
|
|
||||||
export const DEFAULT_LOGS_FILTER = {
|
export const DEFAULT_LOGS_FILTER = {
|
||||||
search: '',
|
search: '',
|
||||||
response_status: '',
|
response_status: 'all',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_LANGUAGE = 'en';
|
export const DEFAULT_LANGUAGE = 'en';
|
||||||
|
|||||||
@@ -28,12 +28,6 @@ export default {
|
|||||||
"homepage": "https://badmojr.github.io/1Hosts/",
|
"homepage": "https://badmojr.github.io/1Hosts/",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
|
||||||
},
|
},
|
||||||
"1hosts_mini": {
|
|
||||||
"name": "1Hosts (mini)",
|
|
||||||
"categoryId": "general",
|
|
||||||
"homepage": "https://badmojr.github.io/1Hosts/",
|
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_38.txt"
|
|
||||||
},
|
|
||||||
"CHN_adrules": {
|
"CHN_adrules": {
|
||||||
"name": "CHN: AdRules DNS List",
|
"name": "CHN: AdRules DNS List",
|
||||||
"categoryId": "regional",
|
"categoryId": "regional",
|
||||||
|
|||||||
@@ -1,304 +1,5 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import { createOnBlurHandler } from './helpers';
|
|
||||||
import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from './constants';
|
import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from './constants';
|
||||||
|
|
||||||
interface renderFieldProps {
|
|
||||||
id: string;
|
|
||||||
input: object;
|
|
||||||
className?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
type?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
autoComplete?: string;
|
|
||||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
step?: number;
|
|
||||||
onScroll?: (...args: unknown[]) => unknown;
|
|
||||||
meta: {
|
|
||||||
touched?: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderField = (props: renderFieldProps, elementType: any) => {
|
|
||||||
const {
|
|
||||||
input,
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
placeholder,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
normalizeOnBlur,
|
|
||||||
onScroll,
|
|
||||||
autoComplete,
|
|
||||||
meta: { touched, error },
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
step,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const onBlur = (event: any) => createOnBlurHandler(event, input, normalizeOnBlur);
|
|
||||||
|
|
||||||
const element = React.createElement(elementType, {
|
|
||||||
...input,
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
placeholder,
|
|
||||||
autoComplete,
|
|
||||||
disabled,
|
|
||||||
type,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
step,
|
|
||||||
onBlur,
|
|
||||||
onScroll,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{element}
|
|
||||||
{!disabled && touched && error && (
|
|
||||||
<span className="form__message form__message--error">
|
|
||||||
<Trans>{error}</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renderTextareaField = (props: any) => renderField(props, 'textarea');
|
|
||||||
|
|
||||||
export const renderInputField = (props: any) => renderField(props, 'input');
|
|
||||||
|
|
||||||
interface renderGroupFieldProps {
|
|
||||||
input: object;
|
|
||||||
id?: string;
|
|
||||||
className?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
type?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
autoComplete?: string;
|
|
||||||
isActionAvailable?: boolean;
|
|
||||||
removeField?: (...args: unknown[]) => unknown;
|
|
||||||
meta: {
|
|
||||||
touched?: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderGroupField = ({
|
|
||||||
input,
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
placeholder,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
autoComplete,
|
|
||||||
isActionAvailable,
|
|
||||||
removeField,
|
|
||||||
meta: { touched, error },
|
|
||||||
normalizeOnBlur,
|
|
||||||
}: renderGroupFieldProps) => {
|
|
||||||
const onBlur = (event: any) => createOnBlurHandler(event, input, normalizeOnBlur);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="input-group">
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={id}
|
|
||||||
placeholder={placeholder}
|
|
||||||
type={type}
|
|
||||||
className={className}
|
|
||||||
disabled={disabled}
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
{isActionAvailable && (
|
|
||||||
<span className="input-group-append">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-icon btn-icon--green"
|
|
||||||
onClick={removeField}>
|
|
||||||
<svg className="icon icon--24">
|
|
||||||
<use xlinkHref="#cross" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!disabled && touched && error && (
|
|
||||||
<span className="form__message form__message--error">
|
|
||||||
<Trans>{error}</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface renderRadioFieldProps {
|
|
||||||
input: object;
|
|
||||||
placeholder?: string;
|
|
||||||
subtitle?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
meta: {
|
|
||||||
touched?: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderRadioField = ({
|
|
||||||
input,
|
|
||||||
placeholder,
|
|
||||||
subtitle,
|
|
||||||
disabled,
|
|
||||||
meta: { touched, error },
|
|
||||||
}: renderRadioFieldProps) => (
|
|
||||||
<Fragment>
|
|
||||||
<label className="custom-control custom-radio">
|
|
||||||
<input {...input} type="radio" className="custom-control-input" disabled={disabled} />
|
|
||||||
|
|
||||||
<span className="custom-control-label">{placeholder}</span>
|
|
||||||
|
|
||||||
{subtitle && <span className="checkbox__label-subtitle" dangerouslySetInnerHTML={{ __html: subtitle }} />}
|
|
||||||
</label>
|
|
||||||
{!disabled && touched && error && (
|
|
||||||
<span className="form__message form__message--error">
|
|
||||||
<Trans>{error}</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface CheckboxFieldProps {
|
|
||||||
input: object;
|
|
||||||
placeholder?: string;
|
|
||||||
subtitle?: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
onClick?: (...args: unknown[]) => unknown;
|
|
||||||
modifier?: string;
|
|
||||||
checked?: boolean;
|
|
||||||
meta: {
|
|
||||||
touched?: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CheckboxField = ({
|
|
||||||
input,
|
|
||||||
placeholder,
|
|
||||||
subtitle,
|
|
||||||
disabled,
|
|
||||||
onClick,
|
|
||||||
modifier = 'checkbox--form',
|
|
||||||
meta: { touched, error },
|
|
||||||
}: CheckboxFieldProps) => (
|
|
||||||
<>
|
|
||||||
<label className={`checkbox ${modifier}`} onClick={onClick}>
|
|
||||||
<span className="checkbox__marker" />
|
|
||||||
|
|
||||||
<input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
|
|
||||||
|
|
||||||
<span className="checkbox__label">
|
|
||||||
<span className="checkbox__label-text checkbox__label-text--long">
|
|
||||||
<span className="checkbox__label-title">{placeholder}</span>
|
|
||||||
|
|
||||||
{subtitle && <span className="checkbox__label-subtitle">{subtitle}</span>}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
{!disabled && touched && error && (
|
|
||||||
<div className="form__message form__message--error mt-1">
|
|
||||||
<Trans>{error}</Trans>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface renderSelectFieldProps {
|
|
||||||
input: object;
|
|
||||||
disabled?: boolean;
|
|
||||||
label?: string;
|
|
||||||
children: unknown[] | React.ReactElement;
|
|
||||||
meta: {
|
|
||||||
touched?: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderSelectField = ({ input, meta: { touched, error }, children, label }: renderSelectFieldProps) => {
|
|
||||||
const showWarning = touched && error;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{label && (
|
|
||||||
<label>
|
|
||||||
<Trans>{label}</Trans>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<select {...input} className="form-control custom-select">
|
|
||||||
{children}
|
|
||||||
</select>
|
|
||||||
{showWarning && (
|
|
||||||
<span className="form__message form__message--error form__message--left-pad">
|
|
||||||
<Trans>{error}</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface renderServiceFieldProps {
|
|
||||||
input: object;
|
|
||||||
placeholder?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
modifier?: string;
|
|
||||||
icon?: string;
|
|
||||||
meta: {
|
|
||||||
touched?: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderServiceField = ({
|
|
||||||
input,
|
|
||||||
placeholder,
|
|
||||||
disabled,
|
|
||||||
modifier,
|
|
||||||
icon,
|
|
||||||
meta: { touched, error },
|
|
||||||
}: renderServiceFieldProps) => (
|
|
||||||
<>
|
|
||||||
<label className={cn('service custom-switch', { [modifier]: modifier })}>
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
type="checkbox"
|
|
||||||
className="custom-switch-input"
|
|
||||||
value={placeholder.toLowerCase()}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="service__switch custom-switch-indicator"></span>
|
|
||||||
|
|
||||||
<span className="service__text" title={placeholder}>
|
|
||||||
{placeholder}
|
|
||||||
</span>
|
|
||||||
{icon && <div dangerouslySetInnerHTML={{ __html: window.atob(icon) }} className="service__icon" />}
|
|
||||||
</label>
|
|
||||||
{!disabled && touched && error && (
|
|
||||||
<span className="form__message form__message--error">
|
|
||||||
<Trans>{error}</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} ip
|
* @param {string} ip
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
THEMES,
|
THEMES,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from './localStorageHelper';
|
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from './localStorageHelper';
|
||||||
import { DhcpInterface } from '../initialState';
|
import { DhcpInterface, InstallInterface } from '../initialState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param time {string} The time to format
|
* @param time {string} The time to format
|
||||||
@@ -217,9 +217,9 @@ export const getInterfaceIp = (option: any) => {
|
|||||||
return interfaceIP;
|
return interfaceIP;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getIpList = (interfaces: DhcpInterface[]) =>
|
export const getIpList = (interfaces: InstallInterface[]) =>
|
||||||
Object.values(interfaces)
|
Object.values(interfaces)
|
||||||
.reduce((acc: string[], curr: DhcpInterface) => acc.concat(curr.ip_addresses), [] as string[])
|
.reduce((acc: string[], curr: InstallInterface) => acc.concat(curr.ip_addresses), [] as string[])
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -468,8 +468,6 @@ export const getParamsForClientsSearch = (data: any, param: any, additionalParam
|
|||||||
* @param {function} [normalizeOnBlur]
|
* @param {function} [normalizeOnBlur]
|
||||||
* @returns {function}
|
* @returns {function}
|
||||||
*/
|
*/
|
||||||
export const createOnBlurHandler = (event: any, input: any, normalizeOnBlur: any) =>
|
|
||||||
normalizeOnBlur ? input.onBlur(normalizeOnBlur(event.target.value)) : input.onBlur();
|
|
||||||
|
|
||||||
export const checkFiltered = (reason: any) => reason.indexOf(FILTERED) === 0;
|
export const checkFiltered = (reason: any) => reason.indexOf(FILTERED) === 0;
|
||||||
export const checkRewrite = (reason: any) => reason === FILTERED_STATUS.REWRITE;
|
export const checkRewrite = (reason: any) => reason === FILTERED_STATUS.REWRITE;
|
||||||
@@ -671,15 +669,17 @@ export const countClientsStatistics = (ids: any, autoClients: any) => {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const formatElapsedMs = (elapsedMs: string, t: (key: string) => string) => {
|
export const formatElapsedMs = (elapsedMs: string, t: (key: string) => string) => {
|
||||||
const parsedElapsedMs = parseInt(elapsedMs, 10);
|
const parsedElapsedMs = parseFloat(elapsedMs);
|
||||||
|
|
||||||
if (Number.isNaN(parsedElapsedMs)) {
|
if (Number.isNaN(parsedElapsedMs)) {
|
||||||
return elapsedMs;
|
return elapsedMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedMs = formatNumber(parsedElapsedMs);
|
const formattedValue = parsedElapsedMs < 1
|
||||||
|
? parsedElapsedMs.toFixed(2)
|
||||||
|
: Math.floor(parsedElapsedMs).toString();
|
||||||
|
|
||||||
return `${formattedMs} ${t('milliseconds_abbreviation')}`;
|
return `${formattedValue} ${t('milliseconds_abbreviation')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"timeUpdated": "2025-01-13T10:04:54.031Z",
|
"timeUpdated": "2025-03-10T15:13:28.992Z",
|
||||||
"categories": {
|
"categories": {
|
||||||
"0": "audio_video_player",
|
"0": "audio_video_player",
|
||||||
"1": "comments",
|
"1": "comments",
|
||||||
@@ -5940,7 +5940,8 @@
|
|||||||
"name": "Digioh",
|
"name": "Digioh",
|
||||||
"categoryId": 4,
|
"categoryId": 4,
|
||||||
"url": "https://digioh.com/",
|
"url": "https://digioh.com/",
|
||||||
"companyId": null
|
"companyId": "digioh",
|
||||||
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
"digital.gov": {
|
"digital.gov": {
|
||||||
"name": "Digital.gov",
|
"name": "Digital.gov",
|
||||||
@@ -8261,8 +8262,8 @@
|
|||||||
},
|
},
|
||||||
"google_marketing": {
|
"google_marketing": {
|
||||||
"name": "Google Marketing",
|
"name": "Google Marketing",
|
||||||
"categoryId": 6,
|
"categoryId": 4,
|
||||||
"url": "https://marketingplatform.google.com/",
|
"url": "https://marketingplatform.google.com/about/enterprise",
|
||||||
"companyId": "google",
|
"companyId": "google",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
@@ -9058,6 +9059,13 @@
|
|||||||
"url": "https://www.ippen-digital.de/",
|
"url": "https://www.ippen-digital.de/",
|
||||||
"companyId": null
|
"companyId": null
|
||||||
},
|
},
|
||||||
|
"id5-sync": {
|
||||||
|
"name": "ID5 Sync",
|
||||||
|
"categoryId": 4,
|
||||||
|
"url": "https://id5.io/",
|
||||||
|
"companyId": "id5-sync",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"id_services": {
|
"id_services": {
|
||||||
"name": "ID Services",
|
"name": "ID Services",
|
||||||
"categoryId": 6,
|
"categoryId": 6,
|
||||||
@@ -20948,6 +20956,8 @@
|
|||||||
"wunderloop.net": "audience_science",
|
"wunderloop.net": "audience_science",
|
||||||
"12mlbe.com": "audiencerate",
|
"12mlbe.com": "audiencerate",
|
||||||
"audiencesquare.com": "audiencesquare.com",
|
"audiencesquare.com": "audiencesquare.com",
|
||||||
|
"ad.gt": "audiencesquare.com",
|
||||||
|
"audigent.com": "audiencesquare.com",
|
||||||
"auditude.com": "auditude",
|
"auditude.com": "auditude",
|
||||||
"audtd.com": "audtd.com",
|
"audtd.com": "audtd.com",
|
||||||
"cdn.augur.io": "augur",
|
"cdn.augur.io": "augur",
|
||||||
@@ -21682,9 +21692,6 @@
|
|||||||
"dtmpub.com": "dotomi",
|
"dtmpub.com": "dotomi",
|
||||||
"double.net": "double.net",
|
"double.net": "double.net",
|
||||||
"2mdn.net": "doubleclick",
|
"2mdn.net": "doubleclick",
|
||||||
"doubleclick.net": "doubleclick",
|
|
||||||
"invitemedia.com": "doubleclick",
|
|
||||||
"doubleclick.com": "doubleclick",
|
|
||||||
"doublepimp.com": "doublepimp",
|
"doublepimp.com": "doublepimp",
|
||||||
"doublepimpssl.com": "doublepimp",
|
"doublepimpssl.com": "doublepimp",
|
||||||
"redcourtside.com": "doublepimp",
|
"redcourtside.com": "doublepimp",
|
||||||
@@ -21988,6 +21995,7 @@
|
|||||||
"cdn.foxpush.net": "foxpush",
|
"cdn.foxpush.net": "foxpush",
|
||||||
"foxpush.com": "foxpush",
|
"foxpush.com": "foxpush",
|
||||||
"foxtel.com.au": "foxtel",
|
"foxtel.com.au": "foxtel",
|
||||||
|
"foxtelgroupcdn.net.au": "foxtel",
|
||||||
"foxydeal.com": "foxydeal_com",
|
"foxydeal.com": "foxydeal_com",
|
||||||
"yabidos.com": "fraudlogix",
|
"yabidos.com": "fraudlogix",
|
||||||
"besucherstatistiken.com": "free_counter",
|
"besucherstatistiken.com": "free_counter",
|
||||||
@@ -22389,6 +22397,8 @@
|
|||||||
"maps.google.es": "google_maps",
|
"maps.google.es": "google_maps",
|
||||||
"maps.google.se": "google_maps",
|
"maps.google.se": "google_maps",
|
||||||
"maps.gstatic.com": "google_maps",
|
"maps.gstatic.com": "google_maps",
|
||||||
|
"doubleclick.net": "google_marketing",
|
||||||
|
"invitemedia.com": "google_marketing",
|
||||||
"adsense.google.com": "google_marketing",
|
"adsense.google.com": "google_marketing",
|
||||||
"adservice.google.ca": "google_marketing",
|
"adservice.google.ca": "google_marketing",
|
||||||
"adservice.google.co.in": "google_marketing",
|
"adservice.google.co.in": "google_marketing",
|
||||||
@@ -22419,6 +22429,7 @@
|
|||||||
"adservice.google.vg": "google_marketing",
|
"adservice.google.vg": "google_marketing",
|
||||||
"adtrafficquality.google": "google_marketing",
|
"adtrafficquality.google": "google_marketing",
|
||||||
"dai.google.com": "google_marketing",
|
"dai.google.com": "google_marketing",
|
||||||
|
"doubleclick.com": "google_marketing",
|
||||||
"doubleclickbygoogle.com": "google_marketing",
|
"doubleclickbygoogle.com": "google_marketing",
|
||||||
"googlesyndication-cn.com": "google_marketing",
|
"googlesyndication-cn.com": "google_marketing",
|
||||||
"duo.google.com": "google_meet",
|
"duo.google.com": "google_meet",
|
||||||
@@ -22604,6 +22615,9 @@
|
|||||||
"icuazeczpeoohx.com": "icuazeczpeoohx.com",
|
"icuazeczpeoohx.com": "icuazeczpeoohx.com",
|
||||||
"id-news.net": "id-news.net",
|
"id-news.net": "id-news.net",
|
||||||
"idcdn.de": "id-news.net",
|
"idcdn.de": "id-news.net",
|
||||||
|
"eu-1-id5-sync.com": "id5-sync",
|
||||||
|
"id5-sync.com": "id5-sync",
|
||||||
|
"id5.io": "id5-sync",
|
||||||
"cdn.id.services": "id_services",
|
"cdn.id.services": "id_services",
|
||||||
"e-generator.com": "ideal_media",
|
"e-generator.com": "ideal_media",
|
||||||
"idealo.com": "idealo_com",
|
"idealo.com": "idealo_com",
|
||||||
@@ -23656,6 +23670,7 @@
|
|||||||
"blockmetrics.com": "pagefair",
|
"blockmetrics.com": "pagefair",
|
||||||
"pagefair.com": "pagefair",
|
"pagefair.com": "pagefair",
|
||||||
"pagefair.net": "pagefair",
|
"pagefair.net": "pagefair",
|
||||||
|
"btloader.com": "pagefair",
|
||||||
"ghmedia.com": "pagescience",
|
"ghmedia.com": "pagescience",
|
||||||
"777seo.com": "paid-to-promote",
|
"777seo.com": "paid-to-promote",
|
||||||
"paid-to-promote.net": "paid-to-promote",
|
"paid-to-promote.net": "paid-to-promote",
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { ip4ToInt, isValidAbsolutePath } from './form';
|
|||||||
import { isIpInCidr, parseSubnetMask } from './helpers';
|
import { isIpInCidr, parseSubnetMask } from './helpers';
|
||||||
|
|
||||||
// Validation functions
|
// Validation functions
|
||||||
// https://redux-form.com/8.3.0/examples/fieldlevelvalidation/
|
|
||||||
// If the value is valid, the validation function should return undefined.
|
// If the value is valid, the validation function should return undefined.
|
||||||
/**
|
/**
|
||||||
* @param value {string|number}
|
* @param value {string|number}
|
||||||
@@ -35,7 +34,7 @@ export const validateRequiredValue = (value: any) => {
|
|||||||
if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) {
|
if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return 'form_error_required';
|
return i18next.t('form_error_required');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +50,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
|
|||||||
const { range_end, range_start } = allValues.v4;
|
const { range_end, range_start } = allValues.v4;
|
||||||
|
|
||||||
if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {
|
if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {
|
||||||
return 'greater_range_start_error';
|
return i18next.t('greater_range_start_error');
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -63,7 +62,7 @@ export const validateIpv4RangeEnd = (_: any, allValues: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateIpv4 = (value: any) => {
|
export const validateIpv4 = (value: any) => {
|
||||||
if (value && !R_IPV4.test(value)) {
|
if (value && !R_IPV4.test(value)) {
|
||||||
return 'form_error_ip4_format';
|
return i18next.t('form_error_ip4_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -108,16 +107,16 @@ export const validateNotInRange = (value: any, allValues: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateGatewaySubnetMask = (_: any, allValues: any) => {
|
export const validateGatewaySubnetMask = (_: any, allValues: any) => {
|
||||||
if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {
|
if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {
|
||||||
return 'gateway_or_subnet_invalid';
|
return i18next.t('gateway_or_subnet_invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { subnet_mask, gateway_ip } = allValues.v4;
|
const { subnet_mask, gateway_ip } = allValues.v4;
|
||||||
|
|
||||||
if (validateIpv4(gateway_ip)) {
|
if (validateIpv4(gateway_ip)) {
|
||||||
return 'gateway_or_subnet_invalid';
|
return i18next.t('gateway_or_subnet_invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseSubnetMask(subnet_mask) ? undefined : 'gateway_or_subnet_invalid';
|
return parseSubnetMask(subnet_mask) ? undefined : i18next.t('gateway_or_subnet_invalid');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,7 +125,7 @@ export const validateGatewaySubnetMask = (_: any, allValues: any) => {
|
|||||||
* @param allValues
|
* @param allValues
|
||||||
*/
|
*/
|
||||||
export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
||||||
if (!allValues || !allValues.v4 || !value) {
|
if (!allValues || !allValues.v4 || !value || !allValues.gateway_ip || !allValues.subnet_mask) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +138,7 @@ export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
|||||||
const subnetPrefix = parseSubnetMask(subnet_mask);
|
const subnetPrefix = parseSubnetMask(subnet_mask);
|
||||||
|
|
||||||
if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {
|
if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {
|
||||||
return 'subnet_error';
|
return i18next.t('subnet_error');
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -149,7 +148,7 @@ export const validateIpForGatewaySubnetMask = (value: any, allValues: any) => {
|
|||||||
* @param value {string}
|
* @param value {string}
|
||||||
* @returns {undefined|string}
|
* @returns {undefined|string}
|
||||||
*/
|
*/
|
||||||
export const validateClientId = (value: any) => {
|
export const validateClientId = (value: string) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -165,7 +164,7 @@ export const validateClientId = (value: any) => {
|
|||||||
R_CLIENT_ID.test(formattedValue)
|
R_CLIENT_ID.test(formattedValue)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return 'form_error_client_id_format';
|
return i18next.t('form_error_client_id_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -180,7 +179,7 @@ export const validateConfigClientId = (value: any) => {
|
|||||||
}
|
}
|
||||||
const formattedValue = value.trim();
|
const formattedValue = value.trim();
|
||||||
if (formattedValue && !R_CLIENT_ID.test(formattedValue)) {
|
if (formattedValue && !R_CLIENT_ID.test(formattedValue)) {
|
||||||
return 'form_error_client_id_format';
|
return i18next.t('form_error_client_id_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -195,7 +194,7 @@ export const validateServerName = (value: any) => {
|
|||||||
}
|
}
|
||||||
const formattedValue = value ? value.trim() : value;
|
const formattedValue = value ? value.trim() : value;
|
||||||
if (formattedValue && !R_DOMAIN.test(formattedValue)) {
|
if (formattedValue && !R_DOMAIN.test(formattedValue)) {
|
||||||
return 'form_error_server_name';
|
return i18next.t('form_error_server_name');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -206,7 +205,7 @@ export const validateServerName = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateIpv6 = (value: any) => {
|
export const validateIpv6 = (value: any) => {
|
||||||
if (value && !R_IPV6.test(value)) {
|
if (value && !R_IPV6.test(value)) {
|
||||||
return 'form_error_ip6_format';
|
return i18next.t('form_error_ip6_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -217,7 +216,7 @@ export const validateIpv6 = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateIp = (value: any) => {
|
export const validateIp = (value: any) => {
|
||||||
if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) {
|
if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) {
|
||||||
return 'form_error_ip_format';
|
return i18next.t('form_error_ip_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -228,7 +227,7 @@ export const validateIp = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateMac = (value: any) => {
|
export const validateMac = (value: any) => {
|
||||||
if (value && !R_MAC.test(value)) {
|
if (value && !R_MAC.test(value)) {
|
||||||
return 'form_error_mac_format';
|
return i18next.t('form_error_mac_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -239,7 +238,7 @@ export const validateMac = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validatePort = (value: any) => {
|
export const validatePort = (value: any) => {
|
||||||
if ((value || value === 0) && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
|
if ((value || value === 0) && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
|
||||||
return 'form_error_port_range';
|
return i18next.t('form_error_port_range');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -250,7 +249,7 @@ export const validatePort = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateInstallPort = (value: any) => {
|
export const validateInstallPort = (value: any) => {
|
||||||
if (value < 1 || value > MAX_PORT) {
|
if (value < 1 || value > MAX_PORT) {
|
||||||
return 'form_error_port';
|
return i18next.t('form_error_port');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -264,7 +263,7 @@ export const validatePortTLS = (value: any) => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
|
if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) {
|
||||||
return 'form_error_port_range';
|
return i18next.t('form_error_port_range');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -281,7 +280,7 @@ export const validatePortQuic = validatePortTLS;
|
|||||||
*/
|
*/
|
||||||
export const validateIsSafePort = (value: any) => {
|
export const validateIsSafePort = (value: any) => {
|
||||||
if (UNSAFE_PORTS.includes(value)) {
|
if (UNSAFE_PORTS.includes(value)) {
|
||||||
return 'form_error_port_unsafe';
|
return i18next.t('form_error_port_unsafe');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -292,7 +291,7 @@ export const validateIsSafePort = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateDomain = (value: any) => {
|
export const validateDomain = (value: any) => {
|
||||||
if (value && !R_HOST.test(value)) {
|
if (value && !R_HOST.test(value)) {
|
||||||
return 'form_error_domain_format';
|
return i18next.t('form_error_domain_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -303,7 +302,7 @@ export const validateDomain = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validateAnswer = (value: any) => {
|
export const validateAnswer = (value: any) => {
|
||||||
if (value && !R_IPV4.test(value) && !R_IPV6.test(value) && !R_HOST.test(value)) {
|
if (value && !R_IPV4.test(value) && !R_IPV6.test(value) && !R_HOST.test(value)) {
|
||||||
return 'form_error_answer_format';
|
return i18next.t('form_error_answer_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -314,7 +313,7 @@ export const validateAnswer = (value: any) => {
|
|||||||
*/
|
*/
|
||||||
export const validatePath = (value: any) => {
|
export const validatePath = (value: any) => {
|
||||||
if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) {
|
if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) {
|
||||||
return 'form_error_url_or_path_format';
|
return i18next.t('form_error_url_or_path_format');
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
@@ -402,7 +401,7 @@ export const validatePlainDns = (value: any, allValues: any) => {
|
|||||||
const { enabled } = allValues;
|
const { enabled } = allValues;
|
||||||
|
|
||||||
if (!enabled && !value) {
|
if (!enabled && !value) {
|
||||||
return 'encryption_plain_dns_error';
|
return i18next.t('encryption_plain_dns_error');
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ALL_INTERFACES_IP,
|
|
||||||
BLOCKING_MODES,
|
BLOCKING_MODES,
|
||||||
DAY,
|
DAY,
|
||||||
DEFAULT_LOGS_FILTER,
|
DEFAULT_LOGS_FILTER,
|
||||||
INSTALL_FIRST_STEP,
|
|
||||||
STANDARD_DNS_PORT,
|
STANDARD_DNS_PORT,
|
||||||
STANDARD_WEB_PORT,
|
STANDARD_WEB_PORT,
|
||||||
TIME_UNITS,
|
TIME_UNITS,
|
||||||
@@ -11,6 +9,14 @@ import {
|
|||||||
import { DEFAULT_BLOCKING_IPV4, DEFAULT_BLOCKING_IPV6 } from './reducers/dnsConfig';
|
import { DEFAULT_BLOCKING_IPV4, DEFAULT_BLOCKING_IPV6 } from './reducers/dnsConfig';
|
||||||
import { Filter } from './helpers/helpers';
|
import { Filter } from './helpers/helpers';
|
||||||
|
|
||||||
|
export type InstallInterface = {
|
||||||
|
flags: string;
|
||||||
|
hardware_address: string;
|
||||||
|
ip_addresses: string[];
|
||||||
|
mtu: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type InstallData = {
|
export type InstallData = {
|
||||||
step: number;
|
step: number;
|
||||||
processingDefault: boolean;
|
processingDefault: boolean;
|
||||||
@@ -33,13 +39,7 @@ export type InstallData = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
interfaces: {
|
interfaces: InstallInterface[];
|
||||||
flags: string;
|
|
||||||
hardware_address: string;
|
|
||||||
ip_addresses: string[];
|
|
||||||
mtu: number;
|
|
||||||
name: string;
|
|
||||||
}[];
|
|
||||||
dnsVersion: string;
|
dnsVersion: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,17 +78,17 @@ export type EncryptionData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Client = {
|
export type Client = {
|
||||||
blocked_services: string[],
|
blocked_services: string[];
|
||||||
blocked_services_schedule: {
|
blocked_services_schedule: {
|
||||||
sun?: { start: number, end: number },
|
sun?: { start: number; end: number };
|
||||||
mon?: { start: number, end: number },
|
mon?: { start: number; end: number };
|
||||||
tue?: { start: number, end: number },
|
tue?: { start: number; end: number };
|
||||||
wed?: { start: number, end: number },
|
wed?: { start: number; end: number };
|
||||||
thu?: { start: number, end: number },
|
thu?: { start: number; end: number };
|
||||||
fri?: { start: number, end: number },
|
fri?: { start: number; end: number };
|
||||||
sat?: { start: number, end: number },
|
sat?: { start: number; end: number };
|
||||||
time_zone: string;
|
time_zone: string;
|
||||||
},
|
};
|
||||||
filtering_enabled: boolean;
|
filtering_enabled: boolean;
|
||||||
ids: string[];
|
ids: string[];
|
||||||
ignore_querylog: boolean;
|
ignore_querylog: boolean;
|
||||||
@@ -104,14 +104,14 @@ export type Client = {
|
|||||||
upstreams_cache_size: number;
|
upstreams_cache_size: number;
|
||||||
use_global_blocked_services: boolean;
|
use_global_blocked_services: boolean;
|
||||||
use_global_settings: boolean;
|
use_global_settings: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type AutoClient = {
|
export type AutoClient = {
|
||||||
ip: string;
|
ip: string;
|
||||||
name: string;
|
name: string;
|
||||||
source: string;
|
source: string;
|
||||||
whois_info: any;
|
whois_info: any;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type DashboardData = {
|
export type DashboardData = {
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
@@ -151,13 +151,13 @@ export type SettingsData = {
|
|||||||
order: number;
|
order: number;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
title: string;
|
title: string;
|
||||||
},
|
};
|
||||||
safebrowsing: {
|
safebrowsing: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
order: number;
|
order: number;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
title: string;
|
title: string;
|
||||||
},
|
};
|
||||||
safesearch: Record<string, boolean>;
|
safesearch: Record<string, boolean>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -182,7 +182,7 @@ export type RewritesData = {
|
|||||||
export type NormalizedTopClients = {
|
export type NormalizedTopClients = {
|
||||||
auto: Record<string, number>;
|
auto: Record<string, number>;
|
||||||
configured: Record<string, number>;
|
configured: Record<string, number>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type StatsData = {
|
export type StatsData = {
|
||||||
processingGetConfig: boolean;
|
processingGetConfig: boolean;
|
||||||
@@ -256,13 +256,13 @@ export type DhcpData = {
|
|||||||
interface_name: string;
|
interface_name: string;
|
||||||
check?: {
|
check?: {
|
||||||
v4?: {
|
v4?: {
|
||||||
other_server?: { found: string; error?: string },
|
other_server?: { found: string; error?: string };
|
||||||
static_ip?: {static: string, ip: string},
|
static_ip?: { static: string; ip: string };
|
||||||
},
|
};
|
||||||
v6?: {
|
v6?: {
|
||||||
other_server?: { found: string; error?: string },
|
other_server?: { found: string; error?: string };
|
||||||
static_ip?: {static: string, ip: string},
|
static_ip?: { static: string; ip: string };
|
||||||
},
|
};
|
||||||
};
|
};
|
||||||
v4: {
|
v4: {
|
||||||
gateway_ip: string;
|
gateway_ip: string;
|
||||||
@@ -321,7 +321,7 @@ export type DnsConfigData = {
|
|||||||
ratelimit_subnet_len_ipv4?: number;
|
ratelimit_subnet_len_ipv4?: number;
|
||||||
ratelimit_subnet_len_ipv6?: number;
|
ratelimit_subnet_len_ipv6?: number;
|
||||||
edns_cs_use_custom?: boolean;
|
edns_cs_use_custom?: boolean;
|
||||||
edns_cs_custom_ip?: boolean;
|
edns_cs_custom_ip?: string;
|
||||||
cache_size?: number;
|
cache_size?: number;
|
||||||
cache_ttl_max?: number;
|
cache_ttl_max?: number;
|
||||||
cache_ttl_min?: number;
|
cache_ttl_min?: number;
|
||||||
@@ -391,7 +391,6 @@ export type RootState = {
|
|||||||
install?: InstallData;
|
install?: InstallData;
|
||||||
toasts: { notices: any[] };
|
toasts: { notices: any[] };
|
||||||
loadingBar: any;
|
loadingBar: any;
|
||||||
form: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InstallState = {
|
export type InstallState = {
|
||||||
@@ -617,5 +616,4 @@ export const initialState: RootState = {
|
|||||||
},
|
},
|
||||||
toasts: { notices: [] },
|
toasts: { notices: [] },
|
||||||
loadingBar: {},
|
loadingBar: {},
|
||||||
form: {},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
|
import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers';
|
||||||
import { ALL_INTERFACES_IP } from '../../helpers/constants';
|
import { ALL_INTERFACES_IP } from '../../helpers/constants';
|
||||||
import { DhcpInterface } from '../../initialState';
|
import { InstallInterface } from '../../initialState';
|
||||||
|
|
||||||
interface renderItemProps {
|
interface renderItemProps {
|
||||||
ip: string;
|
ip: string;
|
||||||
@@ -28,7 +28,7 @@ const renderItem = ({ ip, port, isDns }: renderItemProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface AddressListProps {
|
interface AddressListProps {
|
||||||
interfaces: DhcpInterface[];
|
interfaces: InstallInterface[];
|
||||||
address: string;
|
address: string;
|
||||||
port: number;
|
port: number;
|
||||||
isDns?: boolean;
|
isDns?: boolean;
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { withTranslation, Trans } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import i18n from '../../i18n';
|
|
||||||
|
|
||||||
import Controls from './Controls';
|
import Controls from './Controls';
|
||||||
|
import { validatePasswordLength, validateRequiredValue } from '../../helpers/validators';
|
||||||
|
import { Input } from '../../components/ui/Controls/Input';
|
||||||
|
|
||||||
import { renderInputField } from '../../helpers/form';
|
type AuthFormValues = {
|
||||||
import { FORM_NAME } from '../../helpers/constants';
|
username: string;
|
||||||
import { validatePasswordLength } from '../../helpers/validators';
|
password: string;
|
||||||
|
confirm_password: string;
|
||||||
const required = (value: any) => {
|
|
||||||
if (value || value === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Trans>form_error_required</Trans>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = (values: any) => {
|
type Props = {
|
||||||
const errors: { confirm_password?: string } = {};
|
onAuthSubmit: (values: AuthFormValues) => void;
|
||||||
|
|
||||||
if (values.confirm_password !== values.password) {
|
|
||||||
errors.confirm_password = i18n.t('form_error_password');
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AuthProps {
|
export const Auth = ({ onAuthSubmit }: Props) => {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
const { t } = useTranslation();
|
||||||
pristine: boolean;
|
const {
|
||||||
invalid: boolean;
|
handleSubmit,
|
||||||
t: (...args: unknown[]) => string;
|
watch,
|
||||||
}
|
control,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm<AuthFormValues>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const Auth = (props: AuthProps) => {
|
const password = watch('password');
|
||||||
const { handleSubmit, pristine, invalid, t } = props;
|
|
||||||
|
const validateConfirmPassword = (value: string) => {
|
||||||
|
if (value !== password) {
|
||||||
|
return t('form_error_password');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="setup__step" onSubmit={handleSubmit}>
|
<form className="setup__step" onSubmit={handleSubmit(onAuthSubmit)}>
|
||||||
<div className="setup__group">
|
<div className="setup__group">
|
||||||
<div className="setup__subtitle">
|
<div className="setup__subtitle">
|
||||||
<Trans>install_auth_title</Trans>
|
<Trans>install_auth_title</Trans>
|
||||||
@@ -52,65 +52,74 @@ const Auth = (props: AuthProps) => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>
|
<Controller
|
||||||
<Trans>install_auth_username</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="username"
|
name="username"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="text"
|
rules={{ validate: validateRequiredValue }}
|
||||||
className="form-control"
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('install_auth_username_enter')}
|
<Input
|
||||||
validate={[required]}
|
{...field}
|
||||||
autoComplete="username"
|
type="text"
|
||||||
|
data-testid="install_username"
|
||||||
|
label={t('install_auth_username')}
|
||||||
|
placeholder={t('install_auth_username_enter')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>
|
<Controller
|
||||||
<Trans>install_auth_password</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="password"
|
name="password"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="password"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t('install_auth_password_enter')}
|
required: validateRequiredValue,
|
||||||
validate={[required, validatePasswordLength]}
|
passwordLength: validatePasswordLength,
|
||||||
autoComplete="new-password"
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
data-testid="install_password"
|
||||||
|
label={t('install_auth_password')}
|
||||||
|
placeholder={t('install_auth_password_enter')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>
|
<Controller
|
||||||
<Trans>install_auth_confirm</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="confirm_password"
|
name="confirm_password"
|
||||||
component={renderInputField}
|
control={control}
|
||||||
type="password"
|
rules={{
|
||||||
className="form-control"
|
validate: {
|
||||||
placeholder={t('install_auth_confirm')}
|
required: validateRequiredValue,
|
||||||
validate={[required]}
|
confirmPassword: validateConfirmPassword,
|
||||||
autoComplete="new-password"
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
data-testid="install_confirm_password"
|
||||||
|
label={t('install_auth_confirm')}
|
||||||
|
placeholder={t('install_auth_confirm')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Controls pristine={pristine} invalid={invalid} />
|
<Controls isDirty={isDirty} isValid={isValid} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.INSTALL,
|
|
||||||
destroyOnUnmount: false,
|
|
||||||
forceUnregisterOnUnmount: true,
|
|
||||||
validate,
|
|
||||||
}),
|
|
||||||
])(Auth);
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Controls extends Component<ControlsProps> {
|
|||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
data-testid="install_back"
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary btn-lg setup__button"
|
className="btn btn-secondary btn-lg setup__button"
|
||||||
onClick={this.props.prevStep}>
|
onClick={this.props.prevStep}>
|
||||||
@@ -44,24 +45,16 @@ class Controls extends Component<ControlsProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderNextButton(step: any) {
|
renderNextButton(step: any) {
|
||||||
const {
|
const { nextStep, invalid, pristine, install, ip, port } = this.props;
|
||||||
nextStep,
|
|
||||||
|
|
||||||
invalid,
|
|
||||||
|
|
||||||
pristine,
|
|
||||||
|
|
||||||
install,
|
|
||||||
|
|
||||||
ip,
|
|
||||||
|
|
||||||
port,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<button type="button" className="btn btn-success btn-lg setup__button" onClick={nextStep}>
|
<button
|
||||||
|
data-testid="install_get_started"
|
||||||
|
type="button"
|
||||||
|
className="btn btn-success btn-lg setup__button"
|
||||||
|
onClick={nextStep}>
|
||||||
<Trans>get_started</Trans>
|
<Trans>get_started</Trans>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -69,6 +62,7 @@ class Controls extends Component<ControlsProps> {
|
|||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
data-testid="install_next"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-success btn-lg setup__button"
|
className="btn btn-success btn-lg setup__button"
|
||||||
disabled={invalid || pristine || install.processingSubmit}>
|
disabled={invalid || pristine || install.processingSubmit}>
|
||||||
@@ -77,13 +71,18 @@ class Controls extends Component<ControlsProps> {
|
|||||||
);
|
);
|
||||||
case 4:
|
case 4:
|
||||||
return (
|
return (
|
||||||
<button type="button" className="btn btn-success btn-lg setup__button" onClick={nextStep}>
|
<button
|
||||||
|
data-testid="install_next"
|
||||||
|
type="button"
|
||||||
|
className="btn btn-success btn-lg setup__button"
|
||||||
|
onClick={nextStep}>
|
||||||
<Trans>next</Trans>
|
<Trans>next</Trans>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
case 5:
|
case 5:
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
data-testid="install_open_dashboard"
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-success btn-lg setup__button"
|
className="btn btn-success btn-lg setup__button"
|
||||||
onClick={() => this.props.openDashboard(ip, port)}>
|
onClick={() => this.props.openDashboard(ip, port)}>
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { reduxForm, formValueSelector } from 'redux-form';
|
import { Trans } from 'react-i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import Guide from '../../components/ui/Guide';
|
import { Guide } from '../../components/ui/Guide';
|
||||||
|
|
||||||
import Controls from './Controls';
|
import Controls from './Controls';
|
||||||
|
|
||||||
import AddressList from './AddressList';
|
import AddressList from './AddressList';
|
||||||
import { FORM_NAME } from '../../helpers/constants';
|
import { InstallInterface } from '../../initialState';
|
||||||
import { DhcpInterface } from '../../initialState';
|
import { DnsConfig } from './Settings';
|
||||||
|
|
||||||
interface DevicesProps {
|
type Props = {
|
||||||
interfaces: DhcpInterface[];
|
interfaces: InstallInterface[];
|
||||||
dnsIp: string;
|
dnsConfig: DnsConfig;
|
||||||
dnsPort: number;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let Devices = (props: DevicesProps) => (
|
export const Devices = ({ interfaces, dnsConfig }: Props) => (
|
||||||
<div className="setup__step">
|
<div className="setup__step">
|
||||||
<div className="setup__group">
|
<div className="setup__group">
|
||||||
<div className="setup__subtitle">
|
<div className="setup__subtitle">
|
||||||
@@ -34,7 +30,7 @@ let Devices = (props: DevicesProps) => (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<AddressList interfaces={props.interfaces} address={props.dnsIp} port={props.dnsPort} isDns />
|
<AddressList interfaces={interfaces} address={dnsConfig.ip} port={dnsConfig.port} isDns />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -44,24 +40,3 @@ let Devices = (props: DevicesProps) => (
|
|||||||
<Controls />
|
<Controls />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const selector = formValueSelector('install');
|
|
||||||
|
|
||||||
Devices = connect((state) => {
|
|
||||||
const dnsIp = selector(state, 'dns.ip');
|
|
||||||
const dnsPort = selector(state, 'dns.port');
|
|
||||||
|
|
||||||
return {
|
|
||||||
dnsIp,
|
|
||||||
dnsPort,
|
|
||||||
};
|
|
||||||
})(Devices);
|
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.INSTALL,
|
|
||||||
destroyOnUnmount: false,
|
|
||||||
forceUnregisterOnUnmount: true,
|
|
||||||
}),
|
|
||||||
])(Devices);
|
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
import { INSTALL_TOTAL_STEPS } from '../../helpers/constants';
|
import { INSTALL_TOTAL_STEPS } from '../../helpers/constants';
|
||||||
|
|
||||||
const getProgressPercent = (step: any) => (step / INSTALL_TOTAL_STEPS) * 100;
|
const getProgressPercent = (step: number) => (step / INSTALL_TOTAL_STEPS) * 100;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: number;
|
step: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Progress = (props: Props) => (
|
export const Progress = ({ step }: Props) => (
|
||||||
<div className="setup__progress">
|
<div className="setup__progress">
|
||||||
<Trans>install_step</Trans> {props.step}/{INSTALL_TOTAL_STEPS}
|
<Trans>install_step</Trans> {step}/{INSTALL_TOTAL_STEPS}
|
||||||
<div className="setup__progress-wrap">
|
<div className="setup__progress-wrap">
|
||||||
<div className="setup__progress-inner" style={{ width: `${getProgressPercent(props.step)}%` }} />
|
<div className="setup__progress-inner" style={{ width: `${getProgressPercent(step)}%` }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default withTranslation()(Progress);
|
|
||||||
|
|||||||
@@ -1,32 +1,84 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
import i18n from 'i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
import i18n, { TFunction } from 'i18next';
|
|
||||||
|
|
||||||
import Controls from './Controls';
|
import Controls from './Controls';
|
||||||
|
|
||||||
import AddressList from './AddressList';
|
import AddressList from './AddressList';
|
||||||
|
|
||||||
import { getInterfaceIp } from '../../helpers/helpers';
|
import { getInterfaceIp } from '../../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
ALL_INTERFACES_IP,
|
ALL_INTERFACES_IP,
|
||||||
FORM_NAME,
|
|
||||||
ADDRESS_IN_USE_TEXT,
|
ADDRESS_IN_USE_TEXT,
|
||||||
PORT_53_FAQ_LINK,
|
PORT_53_FAQ_LINK,
|
||||||
STATUS_RESPONSE,
|
STATUS_RESPONSE,
|
||||||
STANDARD_DNS_PORT,
|
STANDARD_DNS_PORT,
|
||||||
STANDARD_WEB_PORT,
|
STANDARD_WEB_PORT,
|
||||||
|
MAX_PORT,
|
||||||
|
MIN_PORT,
|
||||||
} from '../../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
|
|
||||||
import { renderInputField, toNumber } from '../../helpers/form';
|
import { validateRequiredValue } from '../../helpers/validators';
|
||||||
import { validateRequiredValue, validateInstallPort } from '../../helpers/validators';
|
import { InstallInterface } from '../../initialState';
|
||||||
import { DhcpInterface } from '../../initialState';
|
import { Input } from '../../components/ui/Controls/Input';
|
||||||
|
import { Select } from '../../components/ui/Controls/Select';
|
||||||
|
import { toNumber } from '../../helpers/form';
|
||||||
|
|
||||||
const renderInterfaces = (interfaces: DhcpInterface[]) =>
|
const validateInstallPort = (value: number) => {
|
||||||
Object.values(interfaces).map((option: DhcpInterface) => {
|
if (value < MIN_PORT || value > MAX_PORT) {
|
||||||
|
return i18n.t('form_error_port');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebConfig = {
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DnsConfig = {
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingsFormValues = {
|
||||||
|
web: WebConfig;
|
||||||
|
dns: DnsConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StaticIpType = {
|
||||||
|
ip: string;
|
||||||
|
static: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConfigType = {
|
||||||
|
web: {
|
||||||
|
ip: string;
|
||||||
|
port?: number;
|
||||||
|
status: string;
|
||||||
|
can_autofix: boolean;
|
||||||
|
};
|
||||||
|
dns: {
|
||||||
|
ip: string;
|
||||||
|
port?: number;
|
||||||
|
status: string;
|
||||||
|
can_autofix: boolean;
|
||||||
|
};
|
||||||
|
staticIp: StaticIpType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleSubmit: (data: SettingsFormValues) => void;
|
||||||
|
handleChange?: (data: SettingsFormValues) => unknown;
|
||||||
|
handleFix: (web: WebConfig, dns: DnsConfig, set_static_ip: boolean) => void;
|
||||||
|
validateForm: (data: SettingsFormValues) => void;
|
||||||
|
config: ConfigType;
|
||||||
|
interfaces: InstallInterface[];
|
||||||
|
initialValues?: object;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderInterfaces = (interfaces: InstallInterface[]) =>
|
||||||
|
Object.values(interfaces).map((option: InstallInterface) => {
|
||||||
const { name, ip_addresses, flags } = option;
|
const { name, ip_addresses, flags } = option;
|
||||||
|
|
||||||
if (option && ip_addresses?.length > 0) {
|
if (option && ip_addresses?.length > 0) {
|
||||||
@@ -43,113 +95,70 @@ const renderInterfaces = (interfaces: DhcpInterface[]) =>
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
export const Settings = ({ handleSubmit, handleFix, validateForm, config, interfaces }: Props) => {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
const { t } = useTranslation();
|
||||||
handleChange?: (...args: unknown[]) => unknown;
|
|
||||||
handleFix: (...args: unknown[]) => unknown;
|
const defaultValues = {
|
||||||
validateForm?: (...args: unknown[]) => unknown;
|
|
||||||
webIp: string;
|
|
||||||
dnsIp: string;
|
|
||||||
config: {
|
|
||||||
web: {
|
web: {
|
||||||
status: string;
|
ip: config.web.ip || ALL_INTERFACES_IP,
|
||||||
can_autofix: boolean;
|
port: config.web.port || STANDARD_WEB_PORT,
|
||||||
};
|
},
|
||||||
dns: {
|
dns: {
|
||||||
status: string;
|
ip: config.dns.ip || ALL_INTERFACES_IP,
|
||||||
can_autofix: boolean;
|
port: config.dns.port || STANDARD_DNS_PORT,
|
||||||
};
|
},
|
||||||
staticIp: {
|
|
||||||
ip: string;
|
|
||||||
static: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
webPort?: number;
|
|
||||||
dnsPort?: number;
|
|
||||||
interfaces: DhcpInterface[];
|
|
||||||
invalid: boolean;
|
|
||||||
initialValues?: object;
|
|
||||||
t: TFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Settings extends Component<Props> {
|
const {
|
||||||
componentDidMount() {
|
control,
|
||||||
const { webIp, webPort, dnsIp, dnsPort } = this.props;
|
watch,
|
||||||
|
handleSubmit: reactHookFormSubmit,
|
||||||
|
formState: { isValid },
|
||||||
|
} = useForm<SettingsFormValues>({
|
||||||
|
defaultValues,
|
||||||
|
mode: 'onBlur',
|
||||||
|
});
|
||||||
|
|
||||||
this.props.validateForm({
|
const watchFields = watch();
|
||||||
|
|
||||||
|
const { status: webStatus, can_autofix: isWebFixAvailable } = config.web;
|
||||||
|
const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns;
|
||||||
|
const { staticIp } = config;
|
||||||
|
|
||||||
|
const webIpVal = watch('web.ip');
|
||||||
|
const webPortVal = watch('web.port');
|
||||||
|
const dnsIpVal = watch('dns.ip');
|
||||||
|
const dnsPortVal = watch('dns.port');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const webPortError = validateInstallPort(webPortVal);
|
||||||
|
const dnsPortError = validateInstallPort(dnsPortVal);
|
||||||
|
|
||||||
|
if (webPortError || dnsPortError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateForm({
|
||||||
web: {
|
web: {
|
||||||
ip: webIp,
|
ip: webIpVal,
|
||||||
port: webPort,
|
port: webPortVal,
|
||||||
},
|
},
|
||||||
dns: {
|
dns: {
|
||||||
ip: dnsIp,
|
ip: dnsIpVal,
|
||||||
port: dnsPort,
|
port: dnsPortVal,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}, [webIpVal, webPortVal, dnsIpVal, dnsPortVal]);
|
||||||
|
|
||||||
getStaticIpMessage = (staticIp: { ip: string; static: string }) => {
|
|
||||||
const { static: status, ip } = staticIp;
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case STATUS_RESPONSE.NO: {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="mb-2">
|
|
||||||
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
|
|
||||||
install_static_configure
|
|
||||||
</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-primary btn-sm"
|
|
||||||
onClick={() => this.handleStaticIp(ip)}>
|
|
||||||
<Trans>set_static_ip</Trans>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case STATUS_RESPONSE.ERROR: {
|
|
||||||
return (
|
|
||||||
<div className="text-danger">
|
|
||||||
<Trans>install_static_error</Trans>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case STATUS_RESPONSE.YES: {
|
|
||||||
return (
|
|
||||||
<div className="text-success">
|
|
||||||
<Trans>install_static_ok</Trans>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAutofix = (type: any) => {
|
|
||||||
const {
|
|
||||||
webIp,
|
|
||||||
|
|
||||||
webPort,
|
|
||||||
|
|
||||||
dnsIp,
|
|
||||||
|
|
||||||
dnsPort,
|
|
||||||
|
|
||||||
handleFix,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
|
const handleAutofix = (type: string) => {
|
||||||
const web = {
|
const web = {
|
||||||
ip: webIp,
|
ip: watchFields.web?.ip,
|
||||||
port: webPort,
|
port: watchFields.web?.port,
|
||||||
autofix: false,
|
autofix: false,
|
||||||
};
|
};
|
||||||
const dns = {
|
const dns = {
|
||||||
ip: dnsIp,
|
ip: watchFields.dns?.ip,
|
||||||
port: dnsPort,
|
port: watchFields.dns?.port,
|
||||||
autofix: false,
|
autofix: false,
|
||||||
};
|
};
|
||||||
const set_static_ip = false;
|
const set_static_ip = false;
|
||||||
@@ -163,276 +172,292 @@ class Settings extends Component<Props> {
|
|||||||
handleFix(web, dns, set_static_ip);
|
handleFix(web, dns, set_static_ip);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleStaticIp = (ip: any) => {
|
const handleStaticIp = (ip: string) => {
|
||||||
const {
|
|
||||||
webIp,
|
|
||||||
|
|
||||||
webPort,
|
|
||||||
|
|
||||||
dnsIp,
|
|
||||||
|
|
||||||
dnsPort,
|
|
||||||
|
|
||||||
handleFix,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const web = {
|
const web = {
|
||||||
ip: webIp,
|
ip: watchFields.web?.ip,
|
||||||
port: webPort,
|
port: watchFields.web?.port,
|
||||||
autofix: false,
|
autofix: false,
|
||||||
};
|
};
|
||||||
const dns = {
|
const dns = {
|
||||||
ip: dnsIp,
|
ip: watchFields.dns?.ip,
|
||||||
port: dnsPort,
|
port: watchFields.dns?.port,
|
||||||
autofix: false,
|
autofix: false,
|
||||||
};
|
};
|
||||||
const set_static_ip = true;
|
const set_static_ip = true;
|
||||||
|
|
||||||
if (window.confirm(this.props.t('confirm_static_ip', { ip }))) {
|
if (window.confirm(t('confirm_static_ip', { ip }))) {
|
||||||
handleFix(web, dns, set_static_ip);
|
handleFix(web, dns, set_static_ip);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const getStaticIpMessage = useCallback(
|
||||||
const {
|
(staticIp: StaticIpType) => {
|
||||||
handleSubmit,
|
const { static: status, ip } = staticIp;
|
||||||
|
|
||||||
handleChange,
|
switch (status) {
|
||||||
|
case STATUS_RESPONSE.NO:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
|
||||||
|
install_static_configure
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
webIp,
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
onClick={() => handleStaticIp(ip)}>
|
||||||
|
<Trans>set_static_ip</Trans>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case STATUS_RESPONSE.ERROR:
|
||||||
|
return (
|
||||||
|
<div className="text-danger">
|
||||||
|
<Trans>install_static_error</Trans>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case STATUS_RESPONSE.YES:
|
||||||
|
return (
|
||||||
|
<div className="text-success">
|
||||||
|
<Trans>install_static_ok</Trans>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleStaticIp],
|
||||||
|
);
|
||||||
|
|
||||||
webPort,
|
const onSubmit = (data: SettingsFormValues) => {
|
||||||
|
validateForm(data);
|
||||||
|
handleSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
dnsIp,
|
return (
|
||||||
|
<form className="setup__step" onSubmit={reactHookFormSubmit(onSubmit)}>
|
||||||
|
<div className="setup__group">
|
||||||
|
<div className="setup__subtitle">
|
||||||
|
<Trans>install_settings_title</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
dnsPort,
|
<div className="row">
|
||||||
|
<div className="col-8">
|
||||||
interfaces,
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
invalid,
|
<Trans>install_settings_listen</Trans>
|
||||||
|
</label>
|
||||||
config,
|
<Controller
|
||||||
|
name="web.ip"
|
||||||
t,
|
control={control}
|
||||||
} = this.props;
|
render={({ field }) => (
|
||||||
const { status: webStatus, can_autofix: isWebFixAvailable } = config.web;
|
<Select {...field} data-testid="install_web_ip">
|
||||||
const { status: dnsStatus, can_autofix: isDnsFixAvailable } = config.dns;
|
<option value={ALL_INTERFACES_IP}>
|
||||||
const { staticIp } = config;
|
{t('install_settings_all_interfaces')}
|
||||||
|
</option>
|
||||||
return (
|
{renderInterfaces(interfaces)}
|
||||||
<form className="setup__step" onSubmit={handleSubmit}>
|
</Select>
|
||||||
<div className="setup__group">
|
)}
|
||||||
<div className="setup__subtitle">
|
/>
|
||||||
<Trans>install_settings_title</Trans>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row">
|
<div className="col-4">
|
||||||
<div className="col-8">
|
<div className="form-group">
|
||||||
<div className="form-group">
|
<label>
|
||||||
<label>
|
<Trans>install_settings_port</Trans>
|
||||||
<Trans>install_settings_listen</Trans>
|
</label>
|
||||||
</label>
|
<Controller
|
||||||
|
name="web.port"
|
||||||
<Field
|
control={control}
|
||||||
name="web.ip"
|
rules={{
|
||||||
component="select"
|
validate: {
|
||||||
className="form-control custom-select"
|
required: validateRequiredValue,
|
||||||
onChange={handleChange}>
|
installPort: validateInstallPort,
|
||||||
<option value={ALL_INTERFACES_IP}>
|
},
|
||||||
{this.props.t('install_settings_all_interfaces')}
|
}}
|
||||||
</option>
|
render={({ field, fieldState }) => (
|
||||||
{renderInterfaces(interfaces)}
|
<Input
|
||||||
</Field>
|
{...field}
|
||||||
</div>
|
type="number"
|
||||||
|
data-testid="install_web_port"
|
||||||
|
placeholder={STANDARD_WEB_PORT.toString()}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="col-4">
|
<div className="col-12">
|
||||||
<div className="form-group">
|
{webStatus && (
|
||||||
<label>
|
<div className="setup__error text-danger">
|
||||||
<Trans>install_settings_port</Trans>
|
{webStatus}
|
||||||
</label>
|
{isWebFixAvailable && (
|
||||||
|
<button
|
||||||
<Field
|
type="button"
|
||||||
name="web.port"
|
data-testid="install_web_fix"
|
||||||
component={renderInputField}
|
className="btn btn-secondary btn-sm ml-2"
|
||||||
type="number"
|
onClick={() => handleAutofix('web')}>
|
||||||
className="form-control"
|
<Trans>fix</Trans>
|
||||||
placeholder={STANDARD_WEB_PORT.toString()}
|
</button>
|
||||||
validate={[validateInstallPort, validateRequiredValue]}
|
)}
|
||||||
normalize={toNumber}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="col-12">
|
<hr className="divider--small" />
|
||||||
{webStatus && (
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setup__desc">
|
||||||
|
<Trans>install_settings_interface_link</Trans>
|
||||||
|
|
||||||
|
<div className="mt-1">
|
||||||
|
<AddressList
|
||||||
|
interfaces={interfaces}
|
||||||
|
address={watchFields.web?.ip}
|
||||||
|
port={watchFields.web?.port}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setup__group">
|
||||||
|
<div className="setup__subtitle">
|
||||||
|
<Trans>install_settings_dns</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-8">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<Trans>install_settings_listen</Trans>
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="dns.ip"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select {...field} data-testid="install_dns_ip">
|
||||||
|
<option value={ALL_INTERFACES_IP}>
|
||||||
|
{t('install_settings_all_interfaces')}
|
||||||
|
</option>
|
||||||
|
{renderInterfaces(interfaces)}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-4">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
<Trans>install_settings_port</Trans>
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="dns.port"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: t('form_error_required'),
|
||||||
|
validate: {
|
||||||
|
required: validateRequiredValue,
|
||||||
|
installPort: validateInstallPort,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
data-testid="install_dns_port"
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
placeholder={STANDARD_WEB_PORT.toString()}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
field.onChange(toNumber(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12">
|
||||||
|
{dnsStatus && (
|
||||||
|
<>
|
||||||
<div className="setup__error text-danger">
|
<div className="setup__error text-danger">
|
||||||
{webStatus}
|
{dnsStatus}
|
||||||
{isWebFixAvailable && (
|
{isDnsFixAvailable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="install_dns_fix"
|
||||||
className="btn btn-secondary btn-sm ml-2"
|
className="btn btn-secondary btn-sm ml-2"
|
||||||
onClick={() => this.handleAutofix('web')}>
|
onClick={() => handleAutofix('dns')}>
|
||||||
<Trans>fix</Trans>
|
<Trans>fix</Trans>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{isDnsFixAvailable && (
|
||||||
|
<div className="text-muted mb-2">
|
||||||
<hr className="divider--small" />
|
<p className="mb-1">
|
||||||
</div>
|
<Trans>autofix_warning_text</Trans>
|
||||||
</div>
|
</p>
|
||||||
|
<Trans components={[<li key="0">text</li>]}>autofix_warning_list</Trans>
|
||||||
<div className="setup__desc">
|
<p className="mb-1">
|
||||||
<Trans>install_settings_interface_link</Trans>
|
<Trans>autofix_warning_result</Trans>
|
||||||
|
</p>
|
||||||
<div className="mt-1">
|
|
||||||
<AddressList interfaces={interfaces} address={webIp} port={webPort} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="setup__group">
|
|
||||||
<div className="setup__subtitle">
|
|
||||||
<Trans>install_settings_dns</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-8">
|
|
||||||
<div className="form-group">
|
|
||||||
<label>
|
|
||||||
<Trans>install_settings_listen</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="dns.ip"
|
|
||||||
component="select"
|
|
||||||
className="form-control custom-select"
|
|
||||||
onChange={handleChange}>
|
|
||||||
<option value={ALL_INTERFACES_IP}>{t('install_settings_all_interfaces')}</option>
|
|
||||||
{renderInterfaces(interfaces)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-4">
|
|
||||||
<div className="form-group">
|
|
||||||
<label>
|
|
||||||
<Trans>install_settings_port</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="dns.port"
|
|
||||||
component={renderInputField}
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
placeholder={STANDARD_WEB_PORT.toString()}
|
|
||||||
validate={[validateInstallPort, validateRequiredValue]}
|
|
||||||
normalize={toNumber}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-12">
|
|
||||||
{dnsStatus && (
|
|
||||||
<>
|
|
||||||
<div className="setup__error text-danger">
|
|
||||||
{dnsStatus}
|
|
||||||
{isDnsFixAvailable && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-sm ml-2"
|
|
||||||
onClick={() => this.handleAutofix('dns')}>
|
|
||||||
<Trans>fix</Trans>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{isDnsFixAvailable && (
|
|
||||||
<div className="text-muted mb-2">
|
|
||||||
<p className="mb-1">
|
|
||||||
<Trans>autofix_warning_text</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Trans components={[<li key="0">text</li>]}>autofix_warning_list</Trans>
|
|
||||||
|
|
||||||
<p className="mb-1">
|
|
||||||
<Trans>autofix_warning_result</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{dnsPort === STANDARD_DNS_PORT &&
|
|
||||||
!isDnsFixAvailable &&
|
|
||||||
dnsStatus.includes(ADDRESS_IN_USE_TEXT) && (
|
|
||||||
<Trans
|
|
||||||
components={[
|
|
||||||
<a
|
|
||||||
href={PORT_53_FAQ_LINK}
|
|
||||||
key="0"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
link
|
|
||||||
</a>,
|
|
||||||
]}>
|
|
||||||
port_53_faq_link
|
|
||||||
</Trans>
|
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{watchFields.dns?.port === STANDARD_DNS_PORT &&
|
||||||
|
!isDnsFixAvailable &&
|
||||||
|
dnsStatus?.includes(ADDRESS_IN_USE_TEXT) && (
|
||||||
|
<Trans
|
||||||
|
components={[
|
||||||
|
<a href={PORT_53_FAQ_LINK} key="0" target="_blank" rel="noopener noreferrer">
|
||||||
|
link
|
||||||
|
</a>,
|
||||||
|
]}>
|
||||||
|
port_53_faq_link
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
|
||||||
<hr className="divider--small" />
|
<hr className="divider--small" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="setup__desc">
|
|
||||||
<Trans>install_settings_dns_desc</Trans>
|
|
||||||
|
|
||||||
<div className="mt-1">
|
|
||||||
<AddressList interfaces={interfaces} address={dnsIp} port={dnsPort} isDns={true} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="setup__group">
|
<div className="setup__desc">
|
||||||
<div className="setup__subtitle">
|
<Trans>install_settings_dns_desc</Trans>
|
||||||
<Trans>static_ip</Trans>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-2">
|
<div className="mt-1">
|
||||||
<Trans>static_ip_desc</Trans>
|
<AddressList
|
||||||
|
interfaces={interfaces}
|
||||||
|
address={watchFields.dns?.ip}
|
||||||
|
port={watchFields.dns?.port}
|
||||||
|
isDns={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{this.getStaticIpMessage(staticIp)}
|
<div className="setup__group">
|
||||||
|
<div className="setup__subtitle">
|
||||||
|
<Trans>static_ip</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Controls invalid={invalid} />
|
<div className="mb-2">
|
||||||
</form>
|
<Trans>static_ip_desc</Trans>
|
||||||
);
|
</div>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selector = formValueSelector(FORM_NAME.INSTALL);
|
{getStaticIpMessage(staticIp)}
|
||||||
|
</div>
|
||||||
|
|
||||||
const SettingsForm = connect((state) => {
|
<Controls invalid={!isValid} />
|
||||||
const webIp = selector(state, 'web.ip');
|
</form>
|
||||||
const webPort = selector(state, 'web.port');
|
);
|
||||||
const dnsIp = selector(state, 'dns.ip');
|
};
|
||||||
const dnsPort = selector(state, 'dns.port');
|
|
||||||
|
|
||||||
return {
|
|
||||||
webIp,
|
|
||||||
webPort,
|
|
||||||
dnsIp,
|
|
||||||
dnsPort,
|
|
||||||
};
|
|
||||||
})(Settings);
|
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.INSTALL,
|
|
||||||
destroyOnUnmount: false,
|
|
||||||
forceUnregisterOnUnmount: true,
|
|
||||||
}),
|
|
||||||
])(SettingsForm);
|
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { reduxForm, formValueSelector } from 'redux-form';
|
import { Trans } from 'react-i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import Controls from './Controls';
|
import Controls from './Controls';
|
||||||
import { FORM_NAME } from '../../helpers/constants';
|
import { WebConfig } from './Settings';
|
||||||
|
|
||||||
interface SubmitProps {
|
type Props = {
|
||||||
webIp: string;
|
webConfig: WebConfig;
|
||||||
webPort: number;
|
openDashboard: (ip: string, port: number) => void;
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
};
|
||||||
pristine: boolean;
|
|
||||||
submitting: boolean;
|
|
||||||
openDashboard: (...args: unknown[]) => unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Submit = (props: SubmitProps) => (
|
export const Submit = ({ openDashboard, webConfig }: Props) => (
|
||||||
<div className="setup__step">
|
<div className="setup__step">
|
||||||
<div className="setup__group">
|
<div className="setup__group">
|
||||||
<h1 className="setup__title">
|
<h1 className="setup__title">
|
||||||
@@ -29,27 +22,6 @@ let Submit = (props: SubmitProps) => (
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Controls openDashboard={props.openDashboard} ip={props.webIp} port={props.webPort} />
|
<Controls openDashboard={openDashboard} ip={webConfig.ip} port={webConfig.port} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const selector = formValueSelector('install');
|
|
||||||
|
|
||||||
Submit = connect((state) => {
|
|
||||||
const webIp = selector(state, 'web.ip');
|
|
||||||
const webPort = selector(state, 'web.port');
|
|
||||||
|
|
||||||
return {
|
|
||||||
webIp,
|
|
||||||
webPort,
|
|
||||||
};
|
|
||||||
})(Submit);
|
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withTranslation(),
|
|
||||||
reduxForm({
|
|
||||||
form: FORM_NAME.INSTALL,
|
|
||||||
destroyOnUnmount: false,
|
|
||||||
forceUnregisterOnUnmount: true,
|
|
||||||
}),
|
|
||||||
])(Submit);
|
|
||||||
|
|||||||
@@ -1,101 +1,80 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { useEffect, Fragment } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
import * as actionCreators from '../../actions/install';
|
import * as actionCreators from '../../actions/install';
|
||||||
|
|
||||||
import { getWebAddress } from '../../helpers/helpers';
|
import { getWebAddress } from '../../helpers/helpers';
|
||||||
import { INSTALL_FIRST_STEP, INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../helpers/constants';
|
import { INSTALL_TOTAL_STEPS, ALL_INTERFACES_IP, DEBOUNCE_TIMEOUT } from '../../helpers/constants';
|
||||||
|
|
||||||
import Loading from '../../components/ui/Loading';
|
import Loading from '../../components/ui/Loading';
|
||||||
|
|
||||||
import Greeting from './Greeting';
|
import Greeting from './Greeting';
|
||||||
|
import { ConfigType, DnsConfig, Settings, WebConfig } from './Settings';
|
||||||
import Settings from './Settings';
|
import { Devices } from './Devices';
|
||||||
|
import { Submit } from './Submit';
|
||||||
import Auth from './Auth';
|
import { Progress } from './Progress';
|
||||||
|
import { Auth } from './Auth';
|
||||||
import Devices from './Devices';
|
|
||||||
|
|
||||||
import Submit from './Submit';
|
|
||||||
|
|
||||||
import Progress from './Progress';
|
|
||||||
|
|
||||||
import Toasts from '../../components/Toasts';
|
import Toasts from '../../components/Toasts';
|
||||||
|
|
||||||
import Footer from '../../components/ui/Footer';
|
import Footer from '../../components/ui/Footer';
|
||||||
|
|
||||||
import Icons from '../../components/ui/Icons';
|
import Icons from '../../components/ui/Icons';
|
||||||
|
|
||||||
import { Logo } from '../../components/ui/svg/logo';
|
import { Logo } from '../../components/ui/svg/logo';
|
||||||
|
|
||||||
import './Setup.css';
|
import './Setup.css';
|
||||||
import '../../components/ui/Tabler.css';
|
import '../../components/ui/Tabler.css';
|
||||||
|
import { InstallInterface, InstallState } from '../../initialState';
|
||||||
|
|
||||||
interface SetupProps {
|
export const Setup = () => {
|
||||||
getDefaultAddresses: (...args: unknown[]) => unknown;
|
const dispatch = useDispatch();
|
||||||
setAllSettings: (...args: unknown[]) => unknown;
|
|
||||||
checkConfig: (...args: unknown[]) => unknown;
|
|
||||||
nextStep: (...args: unknown[]) => unknown;
|
|
||||||
prevStep: (...args: unknown[]) => unknown;
|
|
||||||
install: {
|
|
||||||
step: number;
|
|
||||||
processingDefault: boolean;
|
|
||||||
web;
|
|
||||||
dns;
|
|
||||||
staticIp;
|
|
||||||
interfaces;
|
|
||||||
};
|
|
||||||
step?: number;
|
|
||||||
web?: object;
|
|
||||||
dns?: object;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Setup extends Component<SetupProps> {
|
const install = useSelector((state: InstallState) => state.install);
|
||||||
componentDidMount() {
|
const { processingDefault, step, web, dns, staticIp, interfaces } = install;
|
||||||
this.props.getDefaultAddresses();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFormSubmit = (values: any) => {
|
useEffect(() => {
|
||||||
const { staticIp, ...config } = values;
|
dispatch(actionCreators.getDefaultAddresses());
|
||||||
|
}, []);
|
||||||
|
|
||||||
this.props.setAllSettings(config);
|
const handleFormSubmit = (values: any) => {
|
||||||
|
const config = { ...values };
|
||||||
|
delete config.staticIp;
|
||||||
|
|
||||||
|
if (web.port && dns.port) {
|
||||||
|
dispatch(
|
||||||
|
actionCreators.setAllSettings({
|
||||||
|
web,
|
||||||
|
dns,
|
||||||
|
...config,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFormChange = debounce((values) => {
|
const checkConfig = debounce((values) => {
|
||||||
const { web, dns } = values;
|
const { web, dns } = values;
|
||||||
|
|
||||||
if (values && web.port && dns.port) {
|
if (values && web.port && dns.port) {
|
||||||
this.props.checkConfig({ web, dns, set_static_ip: false });
|
dispatch(actionCreators.checkConfig({ web, dns, set_static_ip: false }));
|
||||||
}
|
}
|
||||||
}, DEBOUNCE_TIMEOUT);
|
}, DEBOUNCE_TIMEOUT);
|
||||||
|
|
||||||
handleFix = (web: any, dns: any, set_static_ip: any) => {
|
const handleFix = (web: WebConfig, dns: DnsConfig, set_static_ip: boolean) => {
|
||||||
this.props.checkConfig({ web, dns, set_static_ip });
|
dispatch(actionCreators.checkConfig({ web, dns, set_static_ip }));
|
||||||
};
|
};
|
||||||
|
|
||||||
openDashboard = (ip: any, port: any) => {
|
const openDashboard = (ip: string, port: number) => {
|
||||||
let address = getWebAddress(ip, port);
|
let address = getWebAddress(ip, port);
|
||||||
|
|
||||||
if (ip === ALL_INTERFACES_IP) {
|
if (ip === ALL_INTERFACES_IP) {
|
||||||
address = getWebAddress(window.location.hostname, port);
|
address = getWebAddress(window.location.hostname, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.replace(address);
|
window.location.replace(address);
|
||||||
};
|
};
|
||||||
|
|
||||||
nextStep = () => {
|
const handleNextStep = () => {
|
||||||
if (this.props.install.step < INSTALL_TOTAL_STEPS) {
|
if (step < INSTALL_TOTAL_STEPS) {
|
||||||
this.props.nextStep();
|
dispatch(actionCreators.nextStep());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
prevStep = () => {
|
const renderPage = (step: number, config: ConfigType, interfaces: InstallInterface[]) => {
|
||||||
if (this.props.install.step > INSTALL_FIRST_STEP) {
|
|
||||||
this.props.prevStep();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderPage(step: any, config: any, interfaces: any) {
|
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 1:
|
case 1:
|
||||||
return <Greeting />;
|
return <Greeting />;
|
||||||
@@ -105,55 +84,41 @@ class Setup extends Component<SetupProps> {
|
|||||||
config={config}
|
config={config}
|
||||||
initialValues={config}
|
initialValues={config}
|
||||||
interfaces={interfaces}
|
interfaces={interfaces}
|
||||||
onSubmit={this.nextStep}
|
handleSubmit={handleNextStep}
|
||||||
onChange={this.handleFormChange}
|
validateForm={checkConfig}
|
||||||
validateForm={this.handleFormChange}
|
handleFix={handleFix}
|
||||||
handleFix={this.handleFix}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return <Auth onSubmit={this.handleFormSubmit} />;
|
return <Auth onAuthSubmit={handleFormSubmit} />;
|
||||||
case 4:
|
case 4:
|
||||||
return <Devices interfaces={interfaces} />;
|
return <Devices interfaces={interfaces} dnsConfig={dns} />;
|
||||||
case 5:
|
case 5:
|
||||||
return <Submit openDashboard={this.openDashboard} />;
|
return <Submit openDashboard={openDashboard} webConfig={web} />;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (processingDefault) {
|
||||||
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { processingDefault, step, web, dns, staticIp, interfaces } = this.props.install;
|
<>
|
||||||
|
<div className="setup">
|
||||||
|
<div className="setup__container">
|
||||||
|
<Logo className="setup__logo" />
|
||||||
|
{renderPage(step, { web, dns, staticIp }, interfaces)}
|
||||||
|
<Progress step={step} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
<Footer />
|
||||||
<Fragment>
|
|
||||||
{processingDefault && <Loading />}
|
|
||||||
{!processingDefault && (
|
|
||||||
<Fragment>
|
|
||||||
<div className="setup">
|
|
||||||
<div className="setup__container">
|
|
||||||
<Logo className="setup__logo" />
|
|
||||||
{this.renderPage(step, { web, dns, staticIp }, interfaces)}
|
|
||||||
<Progress step={step} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Footer />
|
<Toasts />
|
||||||
|
|
||||||
<Toasts />
|
<Icons />
|
||||||
|
</>
|
||||||
<Icons />
|
);
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: any) => {
|
|
||||||
const { install, toasts } = state;
|
|
||||||
const props = { install, toasts };
|
|
||||||
return props;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, actionCreators)(Setup);
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import configureStore from '../configureStore';
|
|||||||
import reducers from '../reducers/install';
|
import reducers from '../reducers/install';
|
||||||
import '../i18n';
|
import '../i18n';
|
||||||
|
|
||||||
import Setup from './Setup';
|
import { Setup } from './Setup';
|
||||||
import { InstallState } from '../initialState';
|
import { InstallState } from '../initialState';
|
||||||
|
|
||||||
const store = configureStore<InstallState>(reducers, {});
|
const store = configureStore<InstallState>(reducers, {});
|
||||||
|
|||||||
@@ -1,67 +1,84 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Input } from '../../components/ui/Controls/Input';
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
import { renderInputField } from '../../helpers/form';
|
|
||||||
import { validateRequiredValue } from '../../helpers/validators';
|
import { validateRequiredValue } from '../../helpers/validators';
|
||||||
import { FORM_NAME } from '../../helpers/constants';
|
|
||||||
|
|
||||||
interface LoginFormProps {
|
export type LoginFormValues = {
|
||||||
handleSubmit: (...args: unknown[]) => string;
|
username: string;
|
||||||
submitting: boolean;
|
password: string;
|
||||||
invalid: boolean;
|
};
|
||||||
|
|
||||||
|
type LoginFormProps = {
|
||||||
|
onSubmit: (data: LoginFormValues) => void;
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
t: (...args: unknown[]) => string;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const Form = (props: LoginFormProps) => {
|
const Form = ({ onSubmit, processing }: LoginFormProps) => {
|
||||||
const { handleSubmit, processing, invalid, t } = props;
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { isValid },
|
||||||
|
} = useForm<LoginFormValues>({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="card">
|
<form onSubmit={handleSubmit(onSubmit)} className="card">
|
||||||
<div className="card-body p-6">
|
<div className="card-body p-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label className="form__label" htmlFor="username">
|
<Controller
|
||||||
<Trans>username_label</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
id="username1"
|
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
control={control}
|
||||||
className="form-control"
|
rules={{ validate: validateRequiredValue }}
|
||||||
component={renderInputField}
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('username_placeholder')}
|
<Input
|
||||||
autoComplete="username"
|
{...field}
|
||||||
autocapitalize="none"
|
data-testid="username"
|
||||||
disabled={processing}
|
type="text"
|
||||||
validate={[validateRequiredValue]}
|
label={t('username_label')}
|
||||||
|
placeholder={t('username_placeholder')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="username"
|
||||||
|
autoCapitalize="none"
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label className="form__label" htmlFor="password">
|
<Controller
|
||||||
<Trans>password_label</Trans>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
id="password"
|
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
control={control}
|
||||||
className="form-control"
|
rules={{ validate: validateRequiredValue }}
|
||||||
component={renderInputField}
|
render={({ field, fieldState }) => (
|
||||||
placeholder={t('password_placeholder')}
|
<Input
|
||||||
autoComplete="current-password"
|
{...field}
|
||||||
disabled={processing}
|
data-testid="password"
|
||||||
validate={[validateRequiredValue]}
|
type="password"
|
||||||
|
label={t('username_label')}
|
||||||
|
placeholder={t('password_placeholder')}
|
||||||
|
error={fieldState.error?.message}
|
||||||
|
autoComplete="current-password"
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<button type="submit" className="btn btn-success btn-block" disabled={processing || invalid}>
|
<button
|
||||||
<Trans>sign_in</Trans>
|
data-testid="sign_in"
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-block"
|
||||||
|
disabled={processing || !isValid}>
|
||||||
|
{t('sign_in')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,4 +86,4 @@ const Form = (props: LoginFormProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.LOGIN })])(Form);
|
export default Form;
|
||||||
|
|||||||
@@ -1,95 +1,68 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import flow from 'lodash/flow';
|
import { Trans } from 'react-i18next';
|
||||||
import { withTranslation, Trans } from 'react-i18next';
|
|
||||||
|
|
||||||
import * as actionCreators from '../../actions/login';
|
import * as actionCreators from '../../actions/login';
|
||||||
|
|
||||||
import { Logo } from '../../components/ui/svg/logo';
|
import { Logo } from '../../components/ui/svg/logo';
|
||||||
|
|
||||||
import Toasts from '../../components/Toasts';
|
import Toasts from '../../components/Toasts';
|
||||||
|
|
||||||
import Footer from '../../components/ui/Footer';
|
import Footer from '../../components/ui/Footer';
|
||||||
|
|
||||||
import Icons from '../../components/ui/Icons';
|
import Icons from '../../components/ui/Icons';
|
||||||
|
import Form, { LoginFormValues } from './Form';
|
||||||
import Form from './Form';
|
|
||||||
|
|
||||||
import './Login.css';
|
import './Login.css';
|
||||||
import '../../components/ui/Tabler.css';
|
import '../../components/ui/Tabler.css';
|
||||||
|
import { LoginState } from '../../initialState';
|
||||||
|
|
||||||
type LoginProps = {
|
export const Login = () => {
|
||||||
login: {
|
const dispatch = useDispatch();
|
||||||
processingLogin: boolean;
|
const { processingLogin } = useSelector((state: LoginState) => state.login);
|
||||||
};
|
const [isForgotPasswordVisible, setIsForgotPasswordVisible] = useState(false);
|
||||||
processLogin: (args: { name: string; password: string }) => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoginState = {
|
const handleSubmit = ({ username: name, password }: LoginFormValues) => {
|
||||||
isForgotPasswordVisible: boolean;
|
dispatch(actionCreators.processLogin({ name, password }));
|
||||||
};
|
|
||||||
|
|
||||||
class Login extends Component<LoginProps, LoginState> {
|
|
||||||
state = {
|
|
||||||
isForgotPasswordVisible: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = ({ username: name, password }: { username: string; password: string }) => {
|
const toggleText = () => {
|
||||||
this.props.processLogin({ name, password });
|
setIsForgotPasswordVisible((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleText = () => {
|
return (
|
||||||
this.setState((prevState) => ({
|
<div className="login">
|
||||||
isForgotPasswordVisible: !prevState.isForgotPasswordVisible,
|
<div className="login__form">
|
||||||
}));
|
<div className="text-center mb-6">
|
||||||
};
|
<Logo className="h-6 login__logo" />
|
||||||
|
|
||||||
render() {
|
|
||||||
const { processingLogin } = this.props.login;
|
|
||||||
const { isForgotPasswordVisible } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="login">
|
|
||||||
<div className="login__form">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<Logo className="h-6 login__logo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form onSubmit={this.handleSubmit} processing={processingLogin} />
|
|
||||||
|
|
||||||
<div className="login__info">
|
|
||||||
<button type="button" className="btn btn-link login__link" onClick={this.toggleText}>
|
|
||||||
<Trans>forgot_password</Trans>
|
|
||||||
</button>
|
|
||||||
{isForgotPasswordVisible && (
|
|
||||||
<div className="login__message">
|
|
||||||
<Trans
|
|
||||||
components={[
|
|
||||||
<a
|
|
||||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#password-reset"
|
|
||||||
key="0"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
link
|
|
||||||
</a>,
|
|
||||||
]}>
|
|
||||||
forgot_password_desc
|
|
||||||
</Trans>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
<Form onSubmit={handleSubmit} processing={processingLogin} />
|
||||||
|
|
||||||
<Toasts />
|
<div className="login__info">
|
||||||
|
<button type="button" className="btn btn-link login__link" onClick={toggleText}>
|
||||||
|
<Trans>forgot_password</Trans>
|
||||||
|
</button>
|
||||||
|
|
||||||
<Icons />
|
{isForgotPasswordVisible && (
|
||||||
|
<div className="login__message">
|
||||||
|
<Trans
|
||||||
|
components={[
|
||||||
|
<a
|
||||||
|
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#password-reset"
|
||||||
|
key="0"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
link
|
||||||
|
</a>,
|
||||||
|
]}>
|
||||||
|
forgot_password_desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = ({ login, toasts }: any) => ({ login, toasts });
|
<Footer />
|
||||||
|
<Toasts />
|
||||||
export default flow([withTranslation(), connect(mapStateToProps, actionCreators)])(Login);
|
<Icons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import configureStore from '../configureStore';
|
|||||||
import reducers from '../reducers/login';
|
import reducers from '../reducers/login';
|
||||||
import '../i18n';
|
import '../i18n';
|
||||||
|
|
||||||
import Login from './Login';
|
import { Login } from './Login';
|
||||||
import { LoginState } from '../initialState';
|
import { LoginState } from '../initialState';
|
||||||
|
|
||||||
const store = configureStore<LoginState>(reducers, {});
|
const store = configureStore<LoginState>(reducers, {});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { loadingBarReducer } from 'react-redux-loading-bar';
|
import { loadingBarReducer } from 'react-redux-loading-bar';
|
||||||
|
|
||||||
import { reducer as formReducer } from 'redux-form';
|
|
||||||
import toasts from './toasts';
|
import toasts from './toasts';
|
||||||
import encryption from './encryption';
|
import encryption from './encryption';
|
||||||
import clients from './clients';
|
import clients from './clients';
|
||||||
@@ -31,5 +30,4 @@ export default combineReducers({
|
|||||||
stats,
|
stats,
|
||||||
dnsConfig,
|
dnsConfig,
|
||||||
loadingBar: loadingBarReducer,
|
loadingBar: loadingBarReducer,
|
||||||
form: formReducer,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,23 +2,21 @@ import { combineReducers } from 'redux';
|
|||||||
|
|
||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
import { reducer as formReducer } from 'redux-form';
|
|
||||||
|
|
||||||
import * as actions from '../actions/install';
|
import * as actions from '../actions/install';
|
||||||
import toasts from './toasts';
|
import toasts from './toasts';
|
||||||
import { ALL_INTERFACES_IP, INSTALL_FIRST_STEP, STANDARD_DNS_PORT, STANDARD_WEB_PORT } from '../helpers/constants';
|
import { ALL_INTERFACES_IP, INSTALL_FIRST_STEP, STANDARD_DNS_PORT, STANDARD_WEB_PORT } from '../helpers/constants';
|
||||||
|
|
||||||
const install = handleActions(
|
const install = handleActions(
|
||||||
{
|
{
|
||||||
[actions.getDefaultAddressesRequest.toString().toString()]: (state: any) => ({
|
[actions.getDefaultAddressesRequest.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
processingDefault: true,
|
processingDefault: true,
|
||||||
}),
|
}),
|
||||||
[actions.getDefaultAddressesFailure.toString().toString()]: (state: any) => ({
|
[actions.getDefaultAddressesFailure.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
processingDefault: false,
|
processingDefault: false,
|
||||||
}),
|
}),
|
||||||
[actions.getDefaultAddressesSuccess.toString().toString()]: (state: any, { payload }: any) => {
|
[actions.getDefaultAddressesSuccess.toString()]: (state: any, { payload }: any) => {
|
||||||
const { interfaces, version } = payload;
|
const { interfaces, version } = payload;
|
||||||
const web = { ...state.web, port: payload.web_port };
|
const web = { ...state.web, port: payload.web_port };
|
||||||
const dns = { ...state.dns, port: payload.dns_port };
|
const dns = { ...state.dns, port: payload.dns_port };
|
||||||
@@ -35,37 +33,37 @@ const install = handleActions(
|
|||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.nextStep.toString().toString()]: (state: any) => ({
|
[actions.nextStep.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
step: state.step + 1,
|
step: state.step + 1,
|
||||||
}),
|
}),
|
||||||
[actions.prevStep.toString().toString()]: (state: any) => ({
|
[actions.prevStep.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
step: state.step - 1,
|
step: state.step - 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[actions.setAllSettingsRequest.toString().toString()]: (state: any) => ({
|
[actions.setAllSettingsRequest.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
processingSubmit: true,
|
processingSubmit: true,
|
||||||
}),
|
}),
|
||||||
[actions.setAllSettingsFailure.toString().toString()]: (state: any) => ({
|
[actions.setAllSettingsFailure.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
processingSubmit: false,
|
processingSubmit: false,
|
||||||
}),
|
}),
|
||||||
[actions.setAllSettingsSuccess.toString().toString()]: (state: any) => ({
|
[actions.setAllSettingsSuccess.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
processingSubmit: false,
|
processingSubmit: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[actions.checkConfigRequest.toString().toString()]: (state: any) => ({
|
[actions.checkConfigRequest.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
processingCheck: true,
|
processingCheck: true,
|
||||||
}),
|
}),
|
||||||
[actions.checkConfigFailure.toString().toString()]: (state: any) => ({
|
[actions.checkConfigFailure.toString()]: (state: any) => ({
|
||||||
...state,
|
...state,
|
||||||
processingCheck: false,
|
processingCheck: false,
|
||||||
}),
|
}),
|
||||||
[actions.checkConfigSuccess.toString().toString()]: (state: any, { payload }: any) => {
|
[actions.checkConfigSuccess.toString()]: (state: any, { payload }: any) => {
|
||||||
const web = { ...state.web, ...payload.web };
|
const web = { ...state.web, ...payload.web };
|
||||||
const dns = { ...state.dns, ...payload.dns };
|
const dns = { ...state.dns, ...payload.dns };
|
||||||
const staticIp = { ...state.staticIp, ...payload.static_ip };
|
const staticIp = { ...state.staticIp, ...payload.static_ip };
|
||||||
@@ -110,5 +108,4 @@ const install = handleActions(
|
|||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
install,
|
install,
|
||||||
toasts,
|
toasts,
|
||||||
form: formReducer,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { combineReducers } from 'redux';
|
|||||||
|
|
||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
import { reducer as formReducer } from 'redux-form';
|
|
||||||
|
|
||||||
import * as actions from '../actions/login';
|
import * as actions from '../actions/login';
|
||||||
import toasts from './toasts';
|
import toasts from './toasts';
|
||||||
|
|
||||||
@@ -33,5 +31,4 @@ const login = handleActions(
|
|||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
login,
|
login,
|
||||||
toasts,
|
toasts,
|
||||||
form: formReducer,
|
|
||||||
});
|
});
|
||||||
|
|||||||
4
client/tests/constants.ts
Normal file
4
client/tests/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const ADMIN_USERNAME = 'admin';
|
||||||
|
export const ADMIN_PASSWORD = 'superpassword';
|
||||||
|
export const PORT = 3000;
|
||||||
|
export const CONFIG_FILE_PATH = '/tmp/AdGuard.e2e.yaml';
|
||||||
64
client/tests/e2e/dhcp.spec.ts
Normal file
64
client/tests/e2e/dhcp.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { ADMIN_PASSWORD, ADMIN_USERNAME } from '../constants';
|
||||||
|
import { getDHCPConfig } from '../helpers/network';
|
||||||
|
|
||||||
|
const dhcpConfig = getDHCPConfig();
|
||||||
|
const INTERFACE_NAME = dhcpConfig.interfaceName;
|
||||||
|
const RANGE_START = dhcpConfig.rangeStart;
|
||||||
|
const RANGE_END = dhcpConfig.rangeEnd;
|
||||||
|
const SUBNET_MASK = dhcpConfig.subnetMask;
|
||||||
|
const LEASE_TIME = '86400';
|
||||||
|
|
||||||
|
test.describe('DHCP Configuration', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/login.html');
|
||||||
|
await page.getByTestId('username').click();
|
||||||
|
await page.getByTestId('username').fill(ADMIN_USERNAME);
|
||||||
|
await page.getByTestId('password').click();
|
||||||
|
await page.getByTestId('password').fill(ADMIN_PASSWORD);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.getByTestId('sign_in').click();
|
||||||
|
await page.waitForURL((url) => !url.href.endsWith('/login.html'));
|
||||||
|
await page.goto(`/#dhcp`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should select the correct DHCP interface', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
expect(await page.locator('select[name="interface_name"]').inputValue()).toBe(INTERFACE_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should configure DHCP IPv4 settings correctly', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
await page.getByTestId('v4_gateway_ip').click();
|
||||||
|
await page.getByTestId('v4_gateway_ip').fill('192.168.1.99');
|
||||||
|
await page.getByTestId('v4_subnet_mask').click();
|
||||||
|
await page.getByTestId('v4_subnet_mask').fill(SUBNET_MASK);
|
||||||
|
await page.getByTestId('v4_range_start').click();
|
||||||
|
await page.getByTestId('v4_range_start').fill(RANGE_START);
|
||||||
|
await page.getByTestId('v4_range_end').click();
|
||||||
|
await page.getByTestId('v4_range_end').fill(RANGE_END);
|
||||||
|
await page.getByTestId('v4_lease_duration').click();
|
||||||
|
await page.getByTestId('v4_lease_duration').fill(LEASE_TIME);
|
||||||
|
await page.getByTestId('v4_save').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error for invalid DHCP IPv4 range', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
await page.getByTestId('v4_range_start').click();
|
||||||
|
await page.getByTestId('v4_range_start').fill(RANGE_END);
|
||||||
|
await page.getByTestId('v4_range_end').click();
|
||||||
|
await page.getByTestId('v4_range_end').fill(RANGE_START);
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
|
||||||
|
expect(await page.getByText('Must be greater than range').isVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error for invalid DHCP IPv4 address', async ({ page }) => {
|
||||||
|
await page.getByTestId('interface_name').selectOption(INTERFACE_NAME);
|
||||||
|
await page.getByTestId('v4_gateway_ip').click();
|
||||||
|
await page.getByTestId('v4_gateway_ip').fill('192.168.1.200s');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
|
||||||
|
expect(await page.getByText('Invalid IPv4 address').isVisible()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
31
client/tests/e2e/globalSetup.ts
Normal file
31
client/tests/e2e/globalSetup.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { chromium, type FullConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
import { ADMIN_USERNAME, ADMIN_PASSWORD, PORT } from '../constants';
|
||||||
|
|
||||||
|
async function globalSetup(config: FullConfig) {
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
slowMo: 100,
|
||||||
|
});
|
||||||
|
const page = await browser.newPage({ baseURL: config.webServer?.url });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByTestId('install_get_started').click();
|
||||||
|
await page.getByTestId('install_web_port').fill(PORT.toString());
|
||||||
|
await page.getByTestId('install_next').click();
|
||||||
|
await page.getByTestId('install_username').fill(ADMIN_USERNAME);
|
||||||
|
await page.getByTestId('install_password').fill(ADMIN_PASSWORD);
|
||||||
|
await page.getByTestId('install_confirm_password').click();
|
||||||
|
await page.getByTestId('install_confirm_password').fill(ADMIN_PASSWORD);
|
||||||
|
await page.getByTestId('install_next').click();
|
||||||
|
await page.getByTestId('install_next').click();
|
||||||
|
await page.getByTestId('install_open_dashboard').click();
|
||||||
|
await page.waitForURL((url) => !url.href.endsWith('/install.html'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during global setup:', error);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup;
|
||||||
11
client/tests/e2e/globalTeardown.ts
Normal file
11
client/tests/e2e/globalTeardown.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { existsSync, unlinkSync } from 'fs';
|
||||||
|
import { CONFIG_FILE_PATH } from '../constants';
|
||||||
|
|
||||||
|
async function globalTeardown() {
|
||||||
|
// Remove the test config file
|
||||||
|
if (existsSync(CONFIG_FILE_PATH)) {
|
||||||
|
unlinkSync(CONFIG_FILE_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalTeardown;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user