Compare commits
74 Commits
v0.108.0-b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a6c8b4198 | ||
|
|
ae840c9c96 | ||
|
|
187b759fc6 | ||
|
|
8c8323ae68 | ||
|
|
b5c47054ab | ||
|
|
4776255604 | ||
|
|
e5d0f0b119 | ||
|
|
af7c2e3a9d | ||
|
|
2c46bc92fe | ||
|
|
61a1403e4e | ||
|
|
4ccc2a2138 | ||
|
|
72425b80a3 | ||
|
|
c7c62ad3b6 | ||
|
|
003e7ce0d5 | ||
|
|
a8fdf1c553 | ||
|
|
7d479baba6 | ||
|
|
feb9c886d8 | ||
|
|
3521e8ed9f | ||
|
|
4d258972d1 | ||
|
|
9726171f0f | ||
|
|
6d282ae716 | ||
|
|
6a99c39d11 | ||
|
|
1cc6c00e4b | ||
|
|
1cb6634d67 | ||
|
|
106785aab0 | ||
|
|
295b2fefb1 | ||
|
|
8b4768aadd | ||
|
|
810ae94832 | ||
|
|
fd337967f4 | ||
|
|
4adf2bcf00 | ||
|
|
1eb1b1108c | ||
|
|
85e6bf54c9 | ||
|
|
cb5de5c653 | ||
|
|
403fa9b1fe | ||
|
|
2f32b97d2f | ||
|
|
19d2fd47e2 | ||
|
|
f82dee17f0 | ||
|
|
1a3853d52a | ||
|
|
c41af2763f | ||
|
|
ee91a6084f | ||
|
|
ac8a0ec570 | ||
|
|
3255efcaf3 | ||
|
|
bf9be98c71 | ||
|
|
0ed2cd04b2 | ||
|
|
66fba942c8 | ||
|
|
7f9cef948c | ||
|
|
3be90f7b75 | ||
|
|
64994c7fcb | ||
|
|
4b0db6d397 | ||
|
|
777b310a4b | ||
|
|
148a407f6c | ||
|
|
ed947a048e | ||
|
|
74640fb06c | ||
|
|
0d2163c1d6 | ||
|
|
318bd2901a | ||
|
|
61fe269cb8 | ||
|
|
8b2ab8ea87 | ||
|
|
0389515ee3 | ||
|
|
cd5dd1eb16 | ||
|
|
d8ce5b453c | ||
|
|
782a1a9820 | ||
|
|
54cc8f506f | ||
|
|
64fe768725 | ||
|
|
e9d4e76828 | ||
|
|
4d2eba0f29 | ||
|
|
4c65b03844 | ||
|
|
1e0873aa71 | ||
|
|
a5b073d070 | ||
|
|
84d72cb842 | ||
|
|
acbad67f47 | ||
|
|
0211a952ea | ||
|
|
f3161d0c05 | ||
|
|
0c72cde4c3 | ||
|
|
b1657c2b2a |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -1,8 +1,8 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.23.6'
|
||||
'NODE_VERSION': '16'
|
||||
'GO_VERSION': '1.24.2'
|
||||
'NODE_VERSION': '20'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.23.6'
|
||||
'GO_VERSION': '1.24.2'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,6 +19,10 @@
|
||||
/agh-backup/
|
||||
/bin/
|
||||
/build/*
|
||||
/client/blob-report/
|
||||
/client/playwright-report/
|
||||
/client/playwright/.cache/
|
||||
/client/test-results/
|
||||
/data/
|
||||
/dist/
|
||||
/filtering/tests/filtering.TestLotsOfRules*.pprof
|
||||
|
||||
147
CHANGELOG.md
147
CHANGELOG.md
@@ -9,15 +9,135 @@ The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/
|
||||
<!--
|
||||
## [v0.108.0] – TBA
|
||||
|
||||
## [v0.107.57] - 2025-02-10 (APPROX.)
|
||||
## [v0.107.62] - 2025-04-30 (APPROX.)
|
||||
|
||||
See also the [v0.107.57 GitHub milestone][ms-v0.107.57].
|
||||
See also the [v0.107.62 GitHub milestone][ms-v0.107.62].
|
||||
|
||||
[ms-v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/milestone/92?closed=1
|
||||
[ms-v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/milestone/97?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### Fixed
|
||||
|
||||
- Command line option `--update` when the `dns.serve_plain_dns` configuration property was disabled ([7801]).
|
||||
|
||||
- DNS cache not working for custom upstream configurations.
|
||||
|
||||
- Validation process for the DNS-over-TLS, DNS-over-QUIC, and HTTPS ports on the *Encryption Settings* page.
|
||||
|
||||
[#7801]: https://github.com/AdguardTeam/AdGuardHome/issues/7801
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
|
||||
## [v0.107.61] - 2025-04-22
|
||||
|
||||
See also the [v0.107.61 GitHub milestone][ms-v0.107.61].
|
||||
|
||||
### Security
|
||||
|
||||
- Any simultaneous requests that are considered duplicates will now only result in a single request to upstreams, reducing the chance of a cache poisoning attack succeeding. This is controlled by the new configuration object `pending_requests`, which has a single `enabled` property, set to `true` by default.
|
||||
|
||||
**NOTE:** We thank [Xiang Li][mr-xiang-li] for reporting this security issue. It's strongly recommended to leave it enabled, otherwise AdGuard Home will be vulnerable to untrusted clients.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Searching for persistent clients using an exact match for CIDR in the `POST /clients/search HTTP API`.
|
||||
|
||||
[mr-xiang-li]: https://lixiang521.com/
|
||||
[ms-v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/milestone/96?closed=1
|
||||
|
||||
## [v0.107.60] - 2025-04-14
|
||||
|
||||
See also the [v0.107.60 GitHub milestone][ms-v0.107.60].
|
||||
|
||||
### Security
|
||||
|
||||
- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.24.2][go-1.24.2].
|
||||
|
||||
### Changed
|
||||
|
||||
- Alpine Linux version in `Dockerfile` has been updated to 3.21 ([#7588]).
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Node 20 support, Node 22 will be required in future releases.
|
||||
|
||||
**NOTE:** `npm` may be replaced with a different tool, such as `pnpm` or `yarn`, in a future release.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Filtering for DHCP clients ([#7734]).
|
||||
|
||||
- Incorrect label on login page ([#7729]).
|
||||
|
||||
- Validation process for the HTTPS port on the *Encryption Settings* page.
|
||||
|
||||
### Removed
|
||||
|
||||
- Node 18 support.
|
||||
|
||||
[#7588]: https://github.com/AdguardTeam/AdGuardHome/issues/7588
|
||||
[#7729]: https://github.com/AdguardTeam/AdGuardHome/issues/7729
|
||||
[#7734]: https://github.com/AdguardTeam/AdGuardHome/issues/7734
|
||||
|
||||
[go-1.24.2]: https://groups.google.com/g/golang-announce/c/Y2uBTVKjBQk
|
||||
[ms-v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/milestone/95?closed=1
|
||||
|
||||
## [v0.107.59] - 2025-03-21
|
||||
|
||||
See also the [v0.107.59 GitHub milestone][ms-v0.107.59].
|
||||
|
||||
- Rules with the `client` modifier not working ([#7708]).
|
||||
|
||||
- The search form not working in the query log ([#7704]).
|
||||
|
||||
[#7704]: https://github.com/AdguardTeam/AdGuardHome/issues/7704
|
||||
[#7708]: https://github.com/AdguardTeam/AdGuardHome/issues/7708
|
||||
|
||||
[ms-v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/milestone/94?closed=1
|
||||
|
||||
## [v0.107.58] - 2025-03-19
|
||||
|
||||
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].
|
||||
|
||||
### Added
|
||||
|
||||
- The ability to check filtering rules for host names using an optional query type and optional ClientID or client IP address ([#4036]).
|
||||
|
||||
- Optional `client` and `qtype` URL query parameters to the `GET /control/check_host` HTTP API.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Clearing the DNS cache on the *DNS settings* page now includes both global cache and custom client cache.
|
||||
|
||||
- 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]).
|
||||
|
||||
[#4036]: https://github.com/AdguardTeam/AdGuardHome/issues/4036
|
||||
[#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].
|
||||
|
||||
### Security
|
||||
|
||||
- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.23.6][go-1.23.6].
|
||||
@@ -28,21 +148,19 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
||||
|
||||
### Changed
|
||||
|
||||
- The *Fastest IP adddress* upstream mode now collects statistics for the all upstream DNS servers.
|
||||
- The *Fastest IP address* upstream mode now correctly collects statistics for all upstream DNS servers.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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]).
|
||||
|
||||
[#7590]: https://github.com/AdguardTeam/AdGuardHome/issues/7590
|
||||
[#7627]: https://github.com/AdguardTeam/AdGuardHome/issues/7627
|
||||
|
||||
[go-1.23.6]: https://groups.google.com/g/golang-announce/c/xU1ZCHUZw3k
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
-->
|
||||
[go-1.23.6]: https://groups.google.com/g/golang-announce/c/xU1ZCHUZw3k
|
||||
[ms-v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/milestone/92?closed=1
|
||||
|
||||
## [v0.107.56] - 2025-01-23
|
||||
|
||||
@@ -3012,11 +3130,16 @@ 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
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.57...HEAD
|
||||
[v0.107.57]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...v0.107.57
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.62...HEAD
|
||||
[v0.107.62]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...v0.107.62
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.61...HEAD
|
||||
[v0.107.61]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.60...v0.107.61
|
||||
[v0.107.60]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.59...v0.107.60
|
||||
[v0.107.59]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.58...v0.107.59
|
||||
[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.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.54]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.53...v0.107.54
|
||||
|
||||
19
Makefile
19
Makefile
@@ -27,19 +27,17 @@ DIST_DIR = dist
|
||||
GOAMD64 = v1
|
||||
GOPROXY = https://proxy.golang.org|direct
|
||||
GOTELEMETRY = off
|
||||
GOTOOLCHAIN = go1.23.6
|
||||
GOTOOLCHAIN = go1.24.2
|
||||
GPG_KEY = devteam@adguard.com
|
||||
GPG_KEY_PASSPHRASE = not-a-real-password
|
||||
NPM = npm
|
||||
NPM_FLAGS = --prefix $(CLIENT_DIR)
|
||||
NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress --ignore-engines\
|
||||
--ignore-optional --ignore-platform --ignore-scripts
|
||||
NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress
|
||||
RACE = 0
|
||||
REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
|
||||
SIGN = 1
|
||||
SIGNER_API_KEY = not-a-real-key
|
||||
VERSION = v0.0.0
|
||||
YARN = yarn
|
||||
|
||||
NEXTAPI = 0
|
||||
|
||||
@@ -105,10 +103,12 @@ build-docker: ; $(ENV) "$(SHELL)" ./scripts/make/build-docker.sh
|
||||
build-release: $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT))
|
||||
$(ENV) "$(SHELL)" ./scripts/make/build-release.sh
|
||||
|
||||
js-build: ; $(NPM) $(NPM_FLAGS) run build-prod
|
||||
js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
||||
js-build: ; $(NPM) $(NPM_FLAGS) run build-prod
|
||||
js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
||||
js-typecheck: ; $(NPM) $(NPM_FLAGS) run typecheck
|
||||
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-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||
@@ -138,5 +138,4 @@ txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
||||
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
|
||||
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh
|
||||
|
||||
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
||||
openapi-show: ; cd ./openapi/ && $(YARN) start
|
||||
# TODO(a.garipov): Re-add openapi-lint.
|
||||
|
||||
22
README.md
22
README.md
@@ -205,9 +205,9 @@ Run `make init` to prepare the development environment.
|
||||
|
||||
You will need this to build AdGuard Home:
|
||||
|
||||
- [Go](https://golang.org/dl/) v1.23 or later;
|
||||
- [Node.js](https://nodejs.org/en/download/) v18.18 or later;
|
||||
- [npm](https://www.npmjs.com/) v8 or later;
|
||||
- [Go](https://golang.org/dl/) v1.24 or later;
|
||||
- [Node.js](https://nodejs.org/en/download/) v20.19 or later;
|
||||
- [npm](https://www.npmjs.com/) v10.8 or later;
|
||||
|
||||
### <a href="#building" id="building" name="building">Building</a>
|
||||
|
||||
@@ -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-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>
|
||||
|
||||
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.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
|
||||
'stages':
|
||||
- 'Build frontend':
|
||||
@@ -50,7 +50,7 @@
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'key': 'BF'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
@@ -157,6 +157,7 @@
|
||||
|
||||
# Print Docker info.
|
||||
docker info
|
||||
docker buildx version
|
||||
|
||||
# Prepare and push the build.
|
||||
env \
|
||||
@@ -277,8 +278,8 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final
|
||||
# release is built.
|
||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
@@ -293,5 +294,5 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'channel': 'development'
|
||||
|
||||
'stages':
|
||||
@@ -29,11 +29,17 @@
|
||||
jobs:
|
||||
- 'Artifact'
|
||||
|
||||
- 'E2E':
|
||||
manual: false
|
||||
final: false
|
||||
jobs:
|
||||
- 'Test e2e'
|
||||
|
||||
'Test frontend':
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'key': 'JSTEST'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
@@ -48,7 +54,7 @@
|
||||
|
||||
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':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
@@ -97,7 +103,7 @@
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'key': 'BF'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
@@ -165,6 +171,38 @@
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'Test e2e':
|
||||
'artifact-subscriptions':
|
||||
- 'artifact': 'AdGuardHome_linux_amd64'
|
||||
- 'artifact': 'AdGuardHome frontend'
|
||||
'docker':
|
||||
'image': '${bamboo.dockerFrontend}'
|
||||
'volumes':
|
||||
'${system.NPM_DIR}': '${bamboo.cacheNpm}'
|
||||
'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':
|
||||
'create': 'for-pull-request'
|
||||
'delete':
|
||||
@@ -195,6 +233,6 @@
|
||||
# Set the default release channel on the release branch to beta, as we
|
||||
# may need to build a few of these.
|
||||
'variables':
|
||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||
'dockerGo': 'adguard/go-builder:1.23.6--1'
|
||||
'dockerFrontend': 'adguard/home-js-builder:3.1'
|
||||
'dockerGo': 'adguard/go-builder:1.24.2--1'
|
||||
'channel': 'candidate'
|
||||
|
||||
22
client/.eslintrc.json
vendored
22
client/.eslintrc.json
vendored
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"prettier",
|
||||
@@ -21,12 +23,23 @@
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||
"extensions": [
|
||||
".js",
|
||||
".jsx",
|
||||
".ts",
|
||||
".tsx"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
@@ -43,7 +56,10 @@
|
||||
"no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["warn", "error"]
|
||||
"allow": [
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
],
|
||||
"import/no-extraneous-dependencies": [
|
||||
|
||||
6
client/jest.config.mjs
vendored
6
client/jest.config.mjs
vendored
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'babel-jest',
|
||||
},
|
||||
};
|
||||
8829
client/package-lock.json
generated
vendored
8829
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",
|
||||
"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",
|
||||
"lint": "echo 'Lint temporarily disabled'",
|
||||
"lint-new": "eslint './src/**/*.(ts|tsx)'",
|
||||
"lint:fix": "eslint './src/**/*.(ts|tsx)' --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"lint": "eslint --ext .ts,.tsx src",
|
||||
"lint:fix": "eslint --ext .ts,.tsx src --fix",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest --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:watch": "tsc --noEmit --watch"
|
||||
},
|
||||
@@ -20,6 +23,7 @@
|
||||
"@nivo/line": "^0.64.0",
|
||||
"axios": "^0.19.2",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"countries-and-timezones": "^3.6.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"i18next": "^19.6.2",
|
||||
@@ -34,6 +38,7 @@
|
||||
"react": "^16.13.1",
|
||||
"react-click-outside": "^3.0.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-i18next": "^11.7.2",
|
||||
"react-modal": "^3.11.2",
|
||||
"react-popper-tooltip": "^2.11.1",
|
||||
@@ -46,7 +51,6 @@
|
||||
"react-transition-group": "^4.4.5",
|
||||
"redux": "^4.0.5",
|
||||
"redux-actions": "^2.6.5",
|
||||
"redux-form": "^8.3.10",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"ts-migrate": "^0.1.35",
|
||||
"url-polyfill": "^1.1.12"
|
||||
@@ -60,15 +64,15 @@
|
||||
"@babel/plugin-transform-runtime": "^7.24.3",
|
||||
"@babel/preset-env": "^7.24.5",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "^17.0.80",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-redux": "^7.1.33",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/redux-actions": "^2.6.5",
|
||||
"@types/redux-form": "^8.3.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||
"@typescript-eslint/parser": "^7.10.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
@@ -85,8 +89,6 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jscodeshift": "^0.15.2",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"path": "^0.12.7",
|
||||
@@ -97,6 +99,7 @@
|
||||
"stylelint": "^16.5.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"vitest": "^3.1.1",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.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,
|
||||
},
|
||||
});
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"client_settings": "Налады кліентаў",
|
||||
"example_upstream_reserved": "upstream <0>для канкрэтных даменаў</0>;",
|
||||
"example_multiple_upstreams_reserved": "некалькі DNS-сервераў <0>для канкрэтных даменаў</0>;",
|
||||
"example_multiple_upstreams_reserved": "некалькі сервер DNSаў <0>для канкрэтных даменаў</0>;",
|
||||
"example_upstream_comment": "каментар.",
|
||||
"upstream_parallel": "Ужыць адначасныя запыты да ўсіх сервераў для паскарэння апрацоўкі запыту",
|
||||
"parallel_requests": "Паралельныя запыты",
|
||||
"load_balancing": "Размеркаванне нагрузкі",
|
||||
"load_balancing_desc": "Запытвайце па адным серверы за раз. AdGuard Home будзе выкарыстоўваць выпадковы алгарытм для выбару сервера, так што самы хуткі сервер будзе выкарыстоўвацца часцей.",
|
||||
"bootstrap_dns": "Bootstrap DNS-серверы",
|
||||
"bootstrap_dns_desc": "IP-адрасы DNS-сервераў, якія выкарыстоўваюцца для вырашэння IP-адрасоў распознавальнікаў DoH/DoT, якія вы ўказваеце ў якасці перадачы. Каментары не дапускаюцца.",
|
||||
"fallback_dns_title": "Рэзервовыя DNS-серверы",
|
||||
"fallback_dns_desc": "Спіс рэзервовых DNS-сервераў, якія выкарыстоўваюцца, калі вышэйшыя DNS-серверы не адказваюць. Сінтаксіс такі ж, як і ў галоўным полі ўверх.",
|
||||
"bootstrap_dns": "Bootstrap сервер DNSы",
|
||||
"bootstrap_dns_desc": "IP-адрасы сервер DNSаў, якія выкарыстоўваюцца для вырашэння IP-адрасоў распознавальнікаў DoH/DoT, якія вы ўказваеце ў якасці перадачы. Каментары не дапускаюцца.",
|
||||
"fallback_dns_title": "Рэзервовыя сервер DNSы",
|
||||
"fallback_dns_desc": "Спіс рэзервовых сервер DNSаў, якія выкарыстоўваюцца, калі вышэйшыя сервер DNSы не адказваюць. Сінтаксіс такі ж, як і ў галоўным полі ўверх.",
|
||||
"fallback_dns_placeholder": "Увядзіце па адным рэзервовым серверы DNS у радку",
|
||||
"local_ptr_title": "Прыватныя DNS-серверы",
|
||||
"local_ptr_title": "Прыватныя сервер DNSы",
|
||||
"local_ptr_desc": "DNS-серверы, якія AdGuard Home выкарыстоўвае для лакальных PTR-запытаў. Гэтыя серверы выкарыстоўваюцца, каб атрымаць даменавыя імёны кліентаў з прыватнымі IP-адрасамі, напрыклад «192.168.12.34», з дапамогай rDNS. Калі спіс пусты, AdGuard Home выкарыстоўвае прадвызначаныя DNS-серверы вашай АС.",
|
||||
"local_ptr_default_resolver": "Па змаўчанні AdGuard Home выкарыстоўвае наступныя зваротныя DNS-рэзолверы: {{ip}}.",
|
||||
"local_ptr_no_default_resolver": "AdGuard Home не змог вызначыць прыдатныя прыватныя адваротныя DNS-рэзолверы для гэтай сістэмы.",
|
||||
"local_ptr_placeholder": "Увядзіце па адным адрасе на радок",
|
||||
"resolve_clients_title": "Уключыць запытванне даменавых імёнаў для кліентаў",
|
||||
"resolve_clients_desc": "AdGuard Home будзе спрабаваць аўтаматычна вызначыць даменавыя імёны кліентаў праз PTR-запыты да адпаведных сервераў (прыватны DNS-сервер для лакальных кліентаў, upstream-серверы для кліентаў з публічным IP-адрасам).",
|
||||
"resolve_clients_desc": "AdGuard Home будзе спрабаваць аўтаматычна вызначыць даменавыя імёны кліентаў праз PTR-запыты да адпаведных сервераў (прыватны сервер DNS для лакальных кліентаў, upstream-серверы для кліентаў з публічным IP-адрасам).",
|
||||
"use_private_ptr_resolvers_title": "Ужываць прыватныя адваротныя DNS-рэзолверы",
|
||||
"use_private_ptr_resolvers_desc": "Пасылаць адваротныя DNS-запыты для лакальна абслугоўных адрасоў на паказаныя серверы. Калі адключана, AdGuard Home будзе адказваць NXDOMAIN на ўсе падобныя PTR-запыты, апроч запытаў пра кліентаў, ужо вядомых па DHCP, /etc/hosts і гэтак далей.",
|
||||
"check_dhcp_servers": "Праверыць DHCP-серверы",
|
||||
@@ -101,13 +101,13 @@
|
||||
"compact": "Компактный",
|
||||
"nothing_found": "Нічога не знойдзена",
|
||||
"faq": "FAQ",
|
||||
"version": "версія",
|
||||
"version": "Версія",
|
||||
"address": "Адрас",
|
||||
"protocol": "Пратакол",
|
||||
"on": "УКЛ",
|
||||
"off": "Выкл",
|
||||
"copyright": "Усе правы захаваныя",
|
||||
"homepage": "Галоўная",
|
||||
"homepage": "Хатняя старонка",
|
||||
"report_an_issue": "Паведаміць пра праблему",
|
||||
"privacy_policy": "Палітыка прыватнасці",
|
||||
"enable_protection": "Уключыць абарону",
|
||||
@@ -165,8 +165,8 @@
|
||||
"custom_filtering_rules": "Карыстальніцкія правілы фільтрацыі",
|
||||
"encryption_settings": "Налады шыфравання",
|
||||
"dhcp_settings": "Налады DHCP",
|
||||
"upstream_dns": "Upstream DNS-серверы",
|
||||
"upstream_dns_help": "Увядзіце адрасы сервераў па адным у радку. <a>Даведацца больш </a> пра наладжванне DNS-сервераў.",
|
||||
"upstream_dns": "Upstream сервер DNSы",
|
||||
"upstream_dns_help": "Увядзіце адрасы сервераў па адным у радку. <a>Даведацца больш </a> пра наладжванне сервер DNSаў.",
|
||||
"upstream_dns_configured_in_file": "Наладжаны ў {{path}}",
|
||||
"test_upstream_btn": "Тэст upstream сервераў",
|
||||
"upstreams": "Upstreams",
|
||||
@@ -182,7 +182,7 @@
|
||||
"enabled_save_search_toast": "Уключаны бяспечны пошук",
|
||||
"updated_save_search_toast": "Налады бяспечнага пошуку абноўлены",
|
||||
"enabled_table_header": "УКЛ.",
|
||||
"name_table_header": "Імя",
|
||||
"name_table_header": "Назва",
|
||||
"list_url_table_header": "URL-адрас спіса",
|
||||
"rules_count_table_header": "Колькасць правілаў:",
|
||||
"last_time_updated_table_header": "Апошняе абнаўленне",
|
||||
@@ -196,7 +196,7 @@
|
||||
"no_whitelist_added": "Белыя спісы не дададзены",
|
||||
"add_blocklist": "Дадаць чорны спіс",
|
||||
"add_allowlist": "Дадаць белы спіс",
|
||||
"cancel_btn": "Адмена",
|
||||
"cancel_btn": "Скасаваць",
|
||||
"enter_name_hint": "Увядзіце імя",
|
||||
"enter_url_or_path_hint": "Увядзіце URL-адрас ці абсалютны шлях да спіса",
|
||||
"check_updates_btn": "Праверыць абнаўленні",
|
||||
@@ -219,7 +219,7 @@
|
||||
"example_meaning_host_block": "адказаць 127.0.0.1 для example.org (але не для яго паддаменаў);",
|
||||
"example_comment": "! Так можна дадаваць апісанне.",
|
||||
"example_comment_meaning": "каментар;",
|
||||
"example_comment_hash": "# І вось так таксама.",
|
||||
"example_comment_hash": "# Таксама каментарый.",
|
||||
"example_regex_meaning": "блакаваць доступ да даменаў, якія адпавядаюць зададзенаму рэгулярнаму выразу.",
|
||||
"example_upstream_regular": "звычайны DNS (наўзверх UDP);",
|
||||
"example_upstream_regular_port": "звычайны DNS (праз UDP, імя хаста);",
|
||||
@@ -233,13 +233,13 @@
|
||||
"example_upstream_tcp_port": "звычайны DNS (праз TCP, імя хаста);",
|
||||
"example_upstream_tcp_hostname": "звычайны DNS (праз TCP, імя хаста);",
|
||||
"all_lists_up_to_date_toast": "Усе спісы ўжо абноўлены",
|
||||
"updated_upstream_dns_toast": "Upstream DNS-серверы абноўлены",
|
||||
"updated_upstream_dns_toast": "Upstream сервер DNSы абноўлены",
|
||||
"dns_test_ok_toast": "Паказаныя серверы DNS працуюць карэктна",
|
||||
"dns_test_not_ok_toast": "Сервер «{{key}}»: немагчыма выкарыстоўваць, праверце слушнасць напісання",
|
||||
"dns_test_parsing_error_toast": "Раздзел {{section}}: радок {{line}}: немагчыма выкарыстоўваць, праверце слушнасць напісання",
|
||||
"dns_test_warning_toast": "Upstream «{{key}}» не адказвае на тэставыя запыты і можа не працаваць належным чынам",
|
||||
"unblock": "Адблакаваць",
|
||||
"block": "Заблакаваць",
|
||||
"block": "Заблакіраваць",
|
||||
"disallow_this_client": "Забараніць доступ гэтаму кліенту",
|
||||
"allow_this_client": "Дазволіць доступ гэтаму кліенту",
|
||||
"block_for_this_client_only": "Заблакаваць толькі для гэтага кліента",
|
||||
@@ -259,7 +259,7 @@
|
||||
"no_logs_found": "Логі не знойдзены",
|
||||
"refresh_btn": "Абнавіць",
|
||||
"previous_btn": "Назад",
|
||||
"next_btn": "Наперад",
|
||||
"next_btn": "Далей",
|
||||
"loading_table_status": "Загрузка...",
|
||||
"page_table_footer_text": "Старонка",
|
||||
"rows_table_footer_text": "радкоў",
|
||||
@@ -280,7 +280,7 @@
|
||||
"query_log_retention_confirm": "Вы ўпэўнены, што хочаце змяніць тэрмін захоўвання запытаў? Пры памяншэнні інтэрвалу, некаторыя даныя могуць быць страчаны",
|
||||
"anonymize_client_ip": "Ананімізацыя IP-адрасы кліента",
|
||||
"anonymize_client_ip_desc": "Не захоўвайце поўныя IP-адрасы гэтых удзельнікаў у часопісах або статыстыцы",
|
||||
"dns_config": "Налады DNS-сервера",
|
||||
"dns_config": "Налады сервер DNSа",
|
||||
"dns_cache_config": "Налада кэша DNS",
|
||||
"dns_cache_config_desc": "Тут можна наладзіць кэш DNS",
|
||||
"blocking_mode": "Рэжым блакавання",
|
||||
@@ -342,14 +342,14 @@
|
||||
"unknown_filter": "Невядомы фільтр {{filterId}}",
|
||||
"known_tracker": "Вядомы трэкер",
|
||||
"install_welcome_title": "Сардэчна запрашаем у AdGuard Home!",
|
||||
"install_welcome_desc": "AdGuard Home – гэта DNS-сервер, што блакуе рэкламу і трэкінг. Яго мэта – даць вам магчымасць кантраляваць усю ваша сеціва і ўсе падлучаныя прылады. Ён не патрабуе ўсталёўкі кліенцкіх праграм.",
|
||||
"install_welcome_desc": "AdGuard Home – гэта сервер DNS, што блакуе рэкламу і трэкінг. Яго мэта – даць вам магчымасць кантраляваць усю ваша сеціва і ўсе падлучаныя прылады. Ён не патрабуе ўсталёўкі кліенцкіх праграм.",
|
||||
"install_settings_title": "Ўэб-інтэрфейс адміністравання",
|
||||
"install_settings_listen": "Інтэрфейс сеціва",
|
||||
"install_settings_port": "Порт",
|
||||
"install_settings_interface_link": "Ваш ўэб-інтэрфейс адміністравання AdGuard Home будзе даступны па наступных адрасах:",
|
||||
"form_error_port": "Увядзіце карэктны нумар порта",
|
||||
"install_settings_dns": "DNS-сервер",
|
||||
"install_settings_dns_desc": "Вам будзе трэба наладзіць свае прылады ці роўтар на выкарыстанне DNS-сервера на адным з наступных адрасоў:",
|
||||
"install_settings_dns_desc": "Вам будзе трэба наладзіць свае прылады ці роўтар на выкарыстанне сервер DNSа на адным з наступных адрасоў:",
|
||||
"install_settings_all_interfaces": "Усе інтэрфейсы",
|
||||
"install_auth_title": "Аўтарызацыя",
|
||||
"install_auth_desc": "Настойліва рэкамендуецца наладзіць аўтэнтыфікацыю паролем для ўэб-інтэрфейсу AdGuard Home. Нават калі ён даступны толькі ў вашай лакальнай сетцы, важна абараніць яго ад неабмежаванага доступу.",
|
||||
@@ -365,17 +365,17 @@
|
||||
"install_submit_desc": "Працэдура налады завершана і вы гатовы пачаць выкарыстанне AdGuard Home.",
|
||||
"install_devices_router": "Роўтар",
|
||||
"install_devices_router_desc": "Такая наладка аўтаматычна пакрые ўсе прылады, што выкарыстоўваюць ваш хатні роўтар, і вам не трэба будзе наладжваць кожнае з іх у асобнасці.",
|
||||
"install_devices_address": "DNS-сервер AdGuard Home даступны па наступных адрасах",
|
||||
"install_devices_address": "сервер DNS AdGuard Home даступны па наступных адрасах",
|
||||
"install_devices_router_list_1": "Адкрыйце налады вашага роўтара. Звычайна вы можаце адкрыць іх у вашым браўзары, напрыклад, http://192.168.0.1/ ці http://192.168.1.1/. Вас могуць папрасіць увесці пароль. Калі вы не помніце яго, пароль часта можна скінуць, націснуўшы на кнопку на самым роўтары. Некаторыя роўтары патрабуюць адмысловага дадатку, які ў гэтым выпадку павінен быць ужо ўсталявана на ваш кампутар ці тэлефон.",
|
||||
"install_devices_router_list_2": "Знайдзіце налады DHCP ці DNS. Знайдзіце літары «DNS» поруч з тэкставым полем, у якое можна ўвесці два ці тры шэрагі лічбаў, падзеленых на 4 групы ад адной до трох лічбаў.",
|
||||
"install_devices_router_list_3": "Увядзіце туды адрас вашага AdGuard Home.",
|
||||
"install_devices_router_list_4": "Вы не можаце ўсталяваць уласны DNS-сервер на некаторых тыпах маршрутызатараў. У гэтым выпадку можа дапамагчы налада AdGuard Home у якасці <a href='#dhcp'>DHCP-сервера</a>. У адваротным выпадку вам трэба звярнуцца да кіраўніцтва па наладзе DNS-сервераў для вашай пэўнай мадэлі маршрутызатара.",
|
||||
"install_devices_router_list_4": "Вы не можаце ўсталяваць уласны сервер DNS на некаторых тыпах маршрутызатараў. У гэтым выпадку можа дапамагчы налада AdGuard Home у якасці <a href='#dhcp'>DHCP-сервера</a>. У адваротным выпадку вам трэба звярнуцца да кіраўніцтва па наладзе сервер DNSаў для вашай пэўнай мадэлі маршрутызатара.",
|
||||
"install_devices_windows_list_1": "Адкрыйце Панэль кіравання праз меню «Пуск» ці праз пошук Windows.",
|
||||
"install_devices_windows_list_2": "Перайдзіце ў «Сеціва і інтэрнэт», а потым у «Цэнтр кіравання сеціва і агульным доступам».",
|
||||
"install_devices_windows_list_3": "У левым боку экрана клікніце «Змена параметраў адаптара».",
|
||||
"install_devices_windows_list_4": "Пстрыкніце правай кнопкай мышы ваша актыўнае злучэнне і абярыце Уласцівасці.",
|
||||
"install_devices_windows_list_5": "Знайдзіце ў спісе пункт «IP версіі 4 (TCP/IPv4)», вылучыце яго і потым ізноў націсніце «Уласцівасці».",
|
||||
"install_devices_windows_list_6": "Абярыце «Выкарыстаць наступныя адрасы DNS-сервераў» і ўвядзіце адрас AdGuard Home.",
|
||||
"install_devices_windows_list_6": "Абярыце «Выкарыстаць наступныя адрасы сервер DNSаў» і ўвядзіце адрас AdGuard Home.",
|
||||
"install_devices_macos_list_1": "Клікніце па абразку Apple і перайдзіце ў Сістэмныя налады.",
|
||||
"install_devices_macos_list_2": "Клікніце па іконцы Сеціва.",
|
||||
"install_devices_macos_list_3": "Абярыце першае падлучэнне ў спісе і націсніце кнопку «Дадаткова».",
|
||||
@@ -415,7 +415,7 @@
|
||||
"encryption_key": "Прыватны ключ",
|
||||
"encryption_key_input": "Скапіюйце сюды прыватны ключ у PEM-кадоўцы.",
|
||||
"encryption_enable": "Уключыць шыфраванне (HTTPS, DNS-over-HTTPS і DNS-over-TLS)",
|
||||
"encryption_enable_desc": "Калі шыфраванне ўлучана, ўэб-інтэрфейс AdGuard Home будзе працаваць па HTTPS, а DNS-сервер будзе таксама працаваць па DNS-over-HTTPS і DNS-over-TLS.",
|
||||
"encryption_enable_desc": "Калі шыфраванне ўлучана, ўэб-інтэрфейс AdGuard Home будзе працаваць па HTTPS, а сервер DNS будзе таксама працаваць па DNS-over-HTTPS і DNS-over-TLS.",
|
||||
"encryption_chain_valid": "Ланцужок сертыфікатаў валідны",
|
||||
"encryption_chain_invalid": "Ланцужок сертыфікатаў не валідны",
|
||||
"encryption_key_valid": "Валідны {{type}} прыватны ключ",
|
||||
@@ -435,8 +435,8 @@
|
||||
"update_announcement": "AdGuard Home {{version}} ужо даступная! <0>Націсніце сюды</0>, каб даведацца больш.",
|
||||
"setup_guide": "Інструкцыя па наладзе",
|
||||
"dns_addresses": "Адрасы DNS",
|
||||
"dns_start": "DNS-сервер запускаецца",
|
||||
"dns_status_error": "Памылка праверкі стану DNS-сервера",
|
||||
"dns_start": "сервер DNS запускаецца",
|
||||
"dns_status_error": "Памылка праверкі стану сервер DNSа",
|
||||
"down": "Уніз",
|
||||
"fix": "Выправіць",
|
||||
"dns_providers": "<0>Спіс вядомых DNS-правайдараў</0> на выбар.",
|
||||
@@ -449,7 +449,7 @@
|
||||
"settings_global": "Глабальныя",
|
||||
"settings_custom": "Свае",
|
||||
"table_client": "Кліент",
|
||||
"table_name": "Імя",
|
||||
"table_name": "Назва",
|
||||
"save_btn": "Захаваць",
|
||||
"client_add": "Дадаць кліента",
|
||||
"client_new": "Новы кліент",
|
||||
@@ -475,7 +475,7 @@
|
||||
"auto_clients_title": "Кліенты (runtime)",
|
||||
"auto_clients_desc": "Інфармацыя аб IP-адрасах прылад, якія выкарыстоўваюць або могуць выкарыстоўваць AdGuard Home. Гэтая інфармацыя збіраецца з некалькіх крыніц, уключаючы файлы хостаў, зваротны DNS і г.д.",
|
||||
"access_title": "Налады доступу",
|
||||
"access_desc": "Тут вы можаце наладзіць правілы доступу да DNS-серверу AdGuard Home",
|
||||
"access_desc": "Тут вы можаце наладзіць правілы доступу да сервер DNSу AdGuard Home",
|
||||
"access_allowed_title": "Дазволеныя кліенты",
|
||||
"access_allowed_desc": "Спіс CIDR, IP-адрасоў або <a>ClientID</a>. Калі ў гэтым спісе ёсць запісы, AdGuard Home будзе прымаць запыты толькі ад гэтых кліентаў.",
|
||||
"access_disallowed_title": "Забароненыя кліенты",
|
||||
@@ -596,7 +596,7 @@
|
||||
"disable_ipv6_desc": "Ігнараваць усе запыты DNS для адрасоў IPv6 (тып AAAA) і выдаленне дадзеных IPv6 з адказаў тыпу HTTPS.",
|
||||
"fastest_addr": "Найхуткі IP-адрас",
|
||||
"fastest_addr_desc": "Апытайце ўсе DNS-серверы і вярніце самы хуткі IP-адрас сярод усіх адказаў. Гэта замарудзіць выкананне DNS-запытаў, бо нам давядзецца чакаць адказаў ад усіх DNS-сервераў, але палепшыць агульную ўзаемасувязь.",
|
||||
"autofix_warning_text": "Пры націску «Выправіць» AdGuard Home наладзіць вашу сістэму на выкарыстанне DNS-сервера AdGuard Home.",
|
||||
"autofix_warning_text": "Пры націску «Выправіць» AdGuard Home наладзіць вашу сістэму на выкарыстанне сервер DNSа AdGuard Home.",
|
||||
"autofix_warning_list": "Будуць выконвацца наступныя заданні: <0>Дэактываваць сістэмны DNSStubListener</0> <0>Усталяваць адрас сервера DNS на 127.0.0.1</0> <0>Стварыць сімвалічную спасылку /etc/resolv.conf на /run/systemd/resolve/resolv.conf</0> <0>Спыніць DNSStubListener (перазагрузіць сістэмную службу)</0>.",
|
||||
"autofix_warning_result": "У выніку ўсе DNS-запыты ад вашай сістэмы будуць па змаўчанні апрацоўвацца AdGuard Home.\n",
|
||||
"tags_title": "Тэгі",
|
||||
@@ -634,12 +634,12 @@
|
||||
"validated_with_dnssec": "Проверено с помощью DNSSEC",
|
||||
"all_queries": "Усе запыты",
|
||||
"show_blocked_responses": "Заблакавана",
|
||||
"show_whitelisted_responses": "Белы спіс",
|
||||
"show_whitelisted_responses": "У белым спісе",
|
||||
"show_processed_responses": "Апрацавана",
|
||||
"blocked_safebrowsing": "Заблакіравана згодна з базай даных Safe Browsing",
|
||||
"blocked_adult_websites": "Заблакавана Бацькоўскім кантролем",
|
||||
"blocked_threats": "Заблакавана пагроз",
|
||||
"allowed": "Дазволены",
|
||||
"allowed": "У белым спісе",
|
||||
"filtered": "Адфільтраваныя",
|
||||
"rewritten": "Перапісаныя",
|
||||
"safe_search": "Бяспечны пошук",
|
||||
@@ -738,7 +738,7 @@
|
||||
"thursday_short": "Чц.",
|
||||
"friday_short": "Пт.",
|
||||
"saturday_short": "Сб.",
|
||||
"upstream_dns_cache_configuration": "Канфігурацыя кэша upstream DNS-сервераў",
|
||||
"upstream_dns_cache_configuration": "Канфігурацыя кэша upstream сервер DNSаў",
|
||||
"enable_upstream_dns_cache": "Ўключыць кэшаванне для карыстацкай канфігурацыі upstream-сервераў гэтага кліента",
|
||||
"dns_cache_size": "Памер кэша DNS, у байтах"
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"filter": "Филтър",
|
||||
"query_log": "История на заявките",
|
||||
"compact": "Compact",
|
||||
"nothing_found": "Нищо не е намерено",
|
||||
"faq": "ЧЗВ",
|
||||
"version": "версия",
|
||||
"address": "Адрес",
|
||||
@@ -65,14 +66,12 @@
|
||||
"stats_malware_phishing": "вируси/атаки",
|
||||
"stats_adult": "сайтове за възрастни",
|
||||
"stats_query_domain": "Най-отваряни страници",
|
||||
"for_last_24_hours": "за последните 24 часа",
|
||||
"no_domains_found": "Няма намерени резултати",
|
||||
"requests_count": "Сума на заявките",
|
||||
"top_blocked_domains": "Най-блокирани страници",
|
||||
"top_clients": "Най-активни IP адреси",
|
||||
"no_clients_found": "Нямa намерени адреси",
|
||||
"general_statistics": "Обща статисика",
|
||||
"number_of_dns_query_24_hours": "Сума на DNS заявки за последните 24 часа",
|
||||
"number_of_dns_query_blocked_24_hours": "Сума на блокирани DNS заявки от филтрите за реклама и местни",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Сума на блокирани DNS заявки от AdGuard свързани със сигурността",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Сума на блокирани сайтове за възрастни",
|
||||
@@ -156,6 +155,7 @@
|
||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
|
||||
"default": "По подразбиране",
|
||||
"custom_ip": "Персонализиран IP",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-пред-HTTPS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"plain_dns": "Обикновен DNS",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Důvod: {{reason}}",
|
||||
"check_service": "Název služby: {{service}}",
|
||||
"check_hostname": "Název hostitele nebo domény",
|
||||
"check_client_id": "Identifikátor klienta (ClientID nebo IP adresa)",
|
||||
"check_enter_client_id": "Zadejte identifikátor klienta",
|
||||
"check_dns_record": "Vyberte typ DNS záznamu",
|
||||
"service_name": "Název služby",
|
||||
"check_not_found": "Nenalezeno ve Vašich seznamech filtrů",
|
||||
"client_confirm_block": "Opravdu chcete zablokovat klienta „{{ip}}“?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Zakázaný",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Velikost mezipaměti",
|
||||
"cache_size_desc": "Velikost mezipaměti DNS (v bajtech). Chcete-li ukládání do mezipaměti zakázat, ponechte prázdné.",
|
||||
"cache_size_desc": "Velikost mezipaměti DNS (v bajtech). Chcete-li ukládání do mezipaměti zakázat, nastavte 0.",
|
||||
"cache_ttl_min_override": "Přepsat minimální hodnotu TTL",
|
||||
"cache_ttl_max_override": "Přepsat maximální hodnotu TTL",
|
||||
"enter_cache_size": "Zadejte velikost mezipaměti (v bajtech)",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Årsag: {{reason}}",
|
||||
"check_service": "Tjenestenavn: {{service}}",
|
||||
"check_hostname": "Værts- eller domænenavn",
|
||||
"check_client_id": "Klientidentifikator (ClientID eller IP-adresse)",
|
||||
"check_enter_client_id": "Angiv klientidentifikator",
|
||||
"check_dns_record": "Vælg DNS-posttype",
|
||||
"service_name": "Tjenestenavn",
|
||||
"check_not_found": "Ikke fundet i dine filterlister",
|
||||
"client_confirm_block": "Sikker på, at du vil blokere klienten \"{{ip}}\"?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Sortliste",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Cache-størrelse",
|
||||
"cache_size_desc": "DNS cache-størrelse (i bytes). Lad stå tomt for at deaktivere cache.",
|
||||
"cache_size_desc": "DNS cache-størrelse (i bytes). Sæt til 0 for at deaktivere cache.",
|
||||
"cache_ttl_min_override": "Tilsidesæt minimum TTL",
|
||||
"cache_ttl_max_override": "Tilsidesæt maksimal TTL",
|
||||
"enter_cache_size": "Angiv cache-størrelse (bytes)",
|
||||
|
||||
@@ -97,9 +97,9 @@
|
||||
"settings": "Einstellungen",
|
||||
"filters": "Filter",
|
||||
"filter": "Filter",
|
||||
"query_log": "Anfragenprotokoll",
|
||||
"query_log": "Abfrageprotokoll",
|
||||
"compact": "Kompakt",
|
||||
"nothing_found": "Nichts gefunden\n",
|
||||
"nothing_found": "Nichts gefunden",
|
||||
"faq": "FAQ",
|
||||
"version": "Version",
|
||||
"address": "Adresse",
|
||||
@@ -199,7 +199,7 @@
|
||||
"cancel_btn": "Abbrechen",
|
||||
"enter_name_hint": "Name eingeben",
|
||||
"enter_url_or_path_hint": "URL oder absoluten Pfad der Liste eingeben",
|
||||
"check_updates_btn": "Nach Aktualisierungen suchen",
|
||||
"check_updates_btn": "Nach Updates suchen",
|
||||
"new_blocklist": "Neue Sperrliste",
|
||||
"new_allowlist": "Neue Positivliste",
|
||||
"edit_blocklist": "Sperrliste bearbeiten",
|
||||
@@ -566,7 +566,7 @@
|
||||
"ignore_domains": "Ignorierte Domains (durch Zeilenumbruch getrennt)",
|
||||
"ignore_domains_title": "Ignorierte Domains",
|
||||
"ignore_domains_desc_stats": "Abfragen, die diesen Regeln entsprechen, werden nicht in die Statistik aufgenommen",
|
||||
"ignore_domains_desc_query": "Abfragen, die diesen Regeln entsprechen, werden nicht in das Anfragenprotokoll aufgenommen",
|
||||
"ignore_domains_desc_query": "Abfragen, die diesen Regeln entsprechen, werden nicht in das Abfrageprotokoll aufgenommen",
|
||||
"interval_hours": "{{count}} Stunde",
|
||||
"interval_hours_plural": "{{count}} Stunden",
|
||||
"filters_configuration": "Filterkonfiguration",
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Grund: {{reason}}",
|
||||
"check_service": "Dienstname: {{service}}",
|
||||
"check_hostname": "Hostname oder Domainname",
|
||||
"check_client_id": "Client-Kennung (ClientID oder IP-Adresse)",
|
||||
"check_enter_client_id": "Client-Kennung eingeben",
|
||||
"check_dns_record": "DNS-Datensatztyp auswählen",
|
||||
"service_name": "Name des Dienstes",
|
||||
"check_not_found": "Nicht in Ihren Filterlisten enthalten",
|
||||
"client_confirm_block": "Möchten Sie den Client „{{ip}}“ wirklich sperren?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Sperrliste",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Größe des Cache",
|
||||
"cache_size_desc": "Größe des DNS-Zwischenspeichers (in Bytes)",
|
||||
"cache_size_desc": "Größe des DNS-Cache (in Bytes). Um das Caching zu deaktivieren, setzen Sie den Wert auf 0.",
|
||||
"cache_ttl_min_override": "TTL-Minimalwert überschreiben",
|
||||
"cache_ttl_max_override": "TTL-Höchstwert überschreiben",
|
||||
"enter_cache_size": "Größe des Cache (Bytes) eingeben",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Reason: {{reason}}",
|
||||
"check_service": "Service name: {{service}}",
|
||||
"check_hostname": "Hostname or domain name",
|
||||
"check_client_id": "Client identifier (ClientID or IP address)",
|
||||
"check_enter_client_id": "Enter client identifier",
|
||||
"check_dns_record": "Select DNS record type",
|
||||
"service_name": "Service name",
|
||||
"check_not_found": "Not found in your filter lists",
|
||||
"client_confirm_block": "Are you sure you want to block the client \"{{ip}}\"?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Blocklist",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Cache size",
|
||||
"cache_size_desc": "DNS cache size (in bytes). To disable caching, leave empty.",
|
||||
"cache_size_desc": "DNS cache size (in bytes). To disable caching, set to 0.",
|
||||
"cache_ttl_min_override": "Override minimum TTL",
|
||||
"cache_ttl_max_override": "Override maximum TTL",
|
||||
"enter_cache_size": "Enter cache size (bytes)",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"client_settings": "Configuración de clientes",
|
||||
"example_upstream_reserved": "un DNS de subida <0>para un dominio específico</0>.",
|
||||
"example_multiple_upstreams_reserved": "múltiples upstreams <0>para dominios específicos</0>;",
|
||||
"example_upstream_reserved": "un proveedor DNS <0>para un dominio específico</0>.",
|
||||
"example_multiple_upstreams_reserved": "múltiples proveedores DNS <0>para dominios específicos</0>.",
|
||||
"example_upstream_comment": "un comentario.",
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los servidores DNS de subida.",
|
||||
"upstream_parallel": "Usar consultas paralelas para acelerar la resolución al consultar simultáneamente a todos los proveedores DNS.",
|
||||
"parallel_requests": "Consultas paralelas",
|
||||
"load_balancing": "Balanceo de carga",
|
||||
"load_balancing_desc": "Consulta un servidor Dns upstream a la vez.<br/>AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo medio de búsqueda.",
|
||||
"load_balancing_desc": "Consulta un proveedor DNS a la vez.<br/>AdGuard Home utiliza un algoritmo aleatorio ponderado para seleccionar los servidores con el menor número de fallos y el menor tiempo promedio de búsqueda.",
|
||||
"bootstrap_dns": "Servidores DNS de arranque",
|
||||
"bootstrap_dns_desc": "Direcciones IP de servidores DNS utilizadas para resolver direcciones IP de los solucionadores DoH/DoT que especifiques como ascendentes. No se permiten comentarios.",
|
||||
"bootstrap_dns_desc": "Direcciones IP de los servidores DNS utilizados para resolver las direcciones IP de los resolutores DoH/DoT que especifiques como proveedores DNS. No se permiten comentarios.",
|
||||
"fallback_dns_title": "Servidores DNS alternativos",
|
||||
"fallback_dns_desc": "Lista de servidores DNS alternativos utilizados cuando los servidores DNS de subida no responden. La sintaxis es la misma que en el campo de los principales DNS de subida anterior.",
|
||||
"fallback_dns_desc": "Lista de servidores DNS alternativos utilizados cuando los proveedores DNS no responden. La sintaxis es la misma que en el campo de los principales proveedores DNS anterior.",
|
||||
"fallback_dns_placeholder": "Ingresa un servidor DNS alternativo por línea",
|
||||
"local_ptr_title": "Servidores DNS inversos y privados",
|
||||
"local_ptr_desc": "Los servidores DNS que AdGuard Home utiliza para consultas PTR, SOA y NS privadas. La petición se considera privada si solicita un dominio ARPA que contiene una subred dentro de rangos IP privados, por ejemplo \"192.168.12.34\", y procede de un cliente con dirección privada. Si no se configura, AdGuard Home utiliza las direcciones de los resolvedores DNS predeterminados de tu sistema operativo, excepto las direcciones del propio AdGuard Home.",
|
||||
@@ -18,9 +18,9 @@
|
||||
"local_ptr_no_default_resolver": "AdGuard Home no pudo determinar los resolutores DNS inversos y privados adecuados para este sistema.",
|
||||
"local_ptr_placeholder": "Ingresa una dirección IP por línea",
|
||||
"resolve_clients_title": "Habilitar la resolución inversa de las direcciones IP de clientes",
|
||||
"resolve_clients_desc": "Resolve de manera inversa las direcciones IP de los clientes a sus nombres de hosts enviando consultas PTR a los resolutores correspondientes (servidores DNS privados para clientes locales, servidores DNS de subida para clientes con direcciones IP públicas).",
|
||||
"resolve_clients_desc": "Resuelve de manera inversa las direcciones IP de los clientes a sus nombres de hosts enviando consultas PTR a los resolutores correspondientes (servidores DNS privados para clientes locales, proveedores DNS para clientes con direcciones IP públicas).",
|
||||
"use_private_ptr_resolvers_title": "Usar resolutores DNS inversos y privados",
|
||||
"use_private_ptr_resolvers_desc": "Resolver las peticiones PTR, SOA y NS para dominios ARPA que contienen direcciones privadas utilizando servidores upstream privados, DHCP, /etc/hosts, etc. Si se desactiva, AdGuard Home responde a todas estas consultas con NXDOMAIN.",
|
||||
"use_private_ptr_resolvers_desc": "Resuelve peticiones PTR, SOA y NS para dominios ARPA que contienen direcciones IP privadas a través de proveedores DNS privados, DHCP, /etc/hosts, etc. Si se deshabilita, AdGuard Home responderá a todas estas peticiones con NXDOMAIN.",
|
||||
"check_dhcp_servers": "Comprobar si hay servidores DHCP",
|
||||
"save_config": "Guardar configuración",
|
||||
"enabled_dhcp": "Servidor DHCP habilitado",
|
||||
@@ -132,8 +132,8 @@
|
||||
"top_clients": "Clientes más frecuentes",
|
||||
"no_clients_found": "No se han encontrado clientes",
|
||||
"general_statistics": "Estadísticas generales",
|
||||
"top_upstreams": "DNS de subida más frecuentes",
|
||||
"no_upstreams_data_found": "No se han encontrado datos de DNS de subida",
|
||||
"top_upstreams": "Proveedores DNS más frecuentes",
|
||||
"no_upstreams_data_found": "No se han encontrado datos de proveedores DNS",
|
||||
"number_of_dns_query_days": "Número de consultas DNS procesadas durante el último {{count}} día",
|
||||
"number_of_dns_query_days_plural": "Número de consultas DNS procesadas durante los últimos {{count}} días",
|
||||
"number_of_dns_query_hours": "Número de consultas DNS procesadas durante la última {{count}} hora",
|
||||
@@ -144,7 +144,7 @@
|
||||
"enforced_save_search": "Búsquedas seguras forzadas",
|
||||
"number_of_dns_query_to_safe_search": "Número de peticiones DNS a los motores de búsqueda para los que se aplicó la búsqueda segura forzada",
|
||||
"average_processing_time": "Tiempo promedio de procesamiento",
|
||||
"average_upstream_response_time": "Tiempo promedio de respuesta upstream",
|
||||
"average_upstream_response_time": "Tiempo promedio de respuesta del proveedor DNS",
|
||||
"response_time": "Tiempo de respuesta",
|
||||
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una petición DNS",
|
||||
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
|
||||
@@ -157,7 +157,7 @@
|
||||
"enforce_save_search_hint": "AdGuard Home reforzará la búsqueda segura en los siguientes motores de búsqueda: Google, YouTube, Bing, DuckDuckGo, Ecosia, Yandex y Pixabay.",
|
||||
"no_servers_specified": "No hay servidores especificados",
|
||||
"general_settings": "Configuración general",
|
||||
"dns_settings": "Configuración del DNS",
|
||||
"dns_settings": "Configuración DNS",
|
||||
"dns_blocklists": "Listas de bloqueo DNS",
|
||||
"dns_allowlists": "Listas de permitido DNS",
|
||||
"dns_blocklists_desc": "AdGuard Home bloqueará los dominios que coincidan con las listas de bloqueo.",
|
||||
@@ -165,12 +165,12 @@
|
||||
"custom_filtering_rules": "Reglas de filtrado personalizado",
|
||||
"encryption_settings": "Configuración de cifrado",
|
||||
"dhcp_settings": "Configuración DHCP",
|
||||
"upstream_dns": "Servidores DNS de subida",
|
||||
"upstream_dns_help": "Ingresa una dirección de servidor por línea. <a>Más información</a> sobre la configuración de los servidores DNS de subida.",
|
||||
"upstream_dns": "Proveedores DNS",
|
||||
"upstream_dns_help": "Ingresa una dirección de servidor por línea. <a>Más información</a> sobre la configuración de los proveedores DNS.",
|
||||
"upstream_dns_configured_in_file": "Configurado en {{path}}",
|
||||
"test_upstream_btn": "Probar DNS de subida",
|
||||
"upstreams": "DNS de subida",
|
||||
"upstream": "DNS de subida",
|
||||
"test_upstream_btn": "Probar proveedores DNS",
|
||||
"upstreams": "Proveedores DNS",
|
||||
"upstream": "Proveedor DNS",
|
||||
"apply_btn": "Aplicar",
|
||||
"disabled_filtering_toast": "Filtrado deshabilitado",
|
||||
"enabled_filtering_toast": "Filtrado habilitado",
|
||||
@@ -233,11 +233,11 @@
|
||||
"example_upstream_tcp_port": "DNS regular (mediante TCP, con puerto).",
|
||||
"example_upstream_tcp_hostname": "DNS regular (mediante TCP, nombre del host).",
|
||||
"all_lists_up_to_date_toast": "Todas las listas ya están actualizadas",
|
||||
"updated_upstream_dns_toast": "Servidores DNS de subida guardados correctamente",
|
||||
"updated_upstream_dns_toast": "Proveedores DNS guardados correctamente",
|
||||
"dns_test_ok_toast": "Los servidores DNS especificados funcionan correctamente",
|
||||
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no se puede utilizar, por favor revisa si lo has escrito correctamente",
|
||||
"dns_test_parsing_error_toast": "No se pudo utilizar la sección {{section}}: línea {{line}}:, verifica si la escribiste correctamente",
|
||||
"dns_test_warning_toast": "DNS de subida \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
|
||||
"dns_test_warning_toast": "Proveedor DNS \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
|
||||
"unblock": "Desbloquear",
|
||||
"block": "Bloquear",
|
||||
"disallow_this_client": "No permitir a este cliente",
|
||||
@@ -294,9 +294,9 @@
|
||||
"blocked_response_ttl": "Respuesta TTL bloqueada",
|
||||
"blocked_response_ttl_desc": "Especifica durante cuántos segundos los clientes deben almacenar en cache una respuesta filtrada",
|
||||
"form_enter_blocked_response_ttl": "Ingresa el TTL de respuesta bloqueada (segundos)",
|
||||
"upstream_timeout": "Tiempo de espera del upstream",
|
||||
"upstream_timeout_desc": "Especifica el número de segundos que se debe esperar para recibir una respuesta del servidor upstream",
|
||||
"form_enter_upstream_timeout": "Ingresa la duración del tiempo de espera del servidor DNS upstream en segundos",
|
||||
"upstream_timeout": "Tiempo de espera del proveedor DNS",
|
||||
"upstream_timeout_desc": "Especifica el número de segundos que se debe esperar para recibir una respuesta del proveedor DNS",
|
||||
"form_enter_upstream_timeout": "Ingresa la duración de tiempo de espera del proveedor DNS en segundos",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS mediante HTTPS",
|
||||
"dns_over_tls": "DNS mediante TLS",
|
||||
@@ -311,7 +311,7 @@
|
||||
"form_enter_rate_limit": "Ingresa el límite de cantidad",
|
||||
"rate_limit": "Límite de cantidad",
|
||||
"edns_enable": "Habilitar subred de cliente EDNS",
|
||||
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del DNS de subida y registra los valores enviados por los clientes en el registro de consultas.",
|
||||
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del proveedor DNS y registra los valores enviados por los clientes en el registro de consultas.",
|
||||
"edns_use_custom_ip": "Usar IP personalizada para EDNS",
|
||||
"edns_use_custom_ip_desc": "Permitir el uso de IP personalizadas para EDNS",
|
||||
"rate_limit_desc": "Número de peticiones por segundo permitidas por cliente. Establecerlo en 0 significa que no hay límite.",
|
||||
@@ -335,7 +335,7 @@
|
||||
"theme_auto": "Auto",
|
||||
"theme_light": "Claro",
|
||||
"theme_dark": "Oscuro",
|
||||
"upstream_dns_client_desc": "Si se mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración del DNS</0>.",
|
||||
"upstream_dns_client_desc": "Si se mantiene este campo vacío, AdGuard Home utilizará los servidores configurados en la <0>configuración DNS</0>.",
|
||||
"tracker_source": "Fuente del rastreador",
|
||||
"source_label": "Fuente",
|
||||
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
|
||||
@@ -596,12 +596,12 @@
|
||||
"example_rewrite_wildcard": "reescribe las respuestas para todos los subdominios de <0>ejemplo.org</0>.",
|
||||
"rewrite_ip_address": "Dirección IP: utiliza esta IP en una respuesta A o AAAA",
|
||||
"rewrite_domain_name": "Nombre de dominio: añade un registro CNAME",
|
||||
"rewrite_A": "<0>A</0>: valor especial, mantiene registros <0>A</0> del DNS de subida",
|
||||
"rewrite_AAAA": "<0>AAAA</0>: valor especial, mantiene registros <0>AAAA</0> del DNS de subida",
|
||||
"rewrite_A": "<0>A</0>: valor especial, mantiene registros <0>A</0> del proveedor DNS",
|
||||
"rewrite_AAAA": "<0>AAAA</0>: valor especial, mantiene registros <0>AAAA</0> del proveedor DNS",
|
||||
"disable_ipv6": "Deshabilitar resolución de direcciones IPv6",
|
||||
"disable_ipv6_desc": "Descarta todas las consultas de DNS para direcciones IPv6 (tipo AAAA) y elimina las sugerencias de IPv6 de las respuestas HTTPS.",
|
||||
"fastest_addr": "Dirección IP más rápida",
|
||||
"fastest_addr_desc": "Espera a que respondan <b>todos</b> los servidores DNS, mide la velocidad de conexión TCP de cada servidor y devuelve la Dirección IP del servidor con la velocidad de conexión más rápida.<br/>Este modo puede ralentizar significativamente las consultas DNS, si uno o más servidores DNS de upstream no están respondiendo. Asegúrate de que tus servidores DNS upstream sean estables y tu tiempo de espera de upstream sea bajo.",
|
||||
"fastest_addr_desc": "Espera respuestas de <b>todos</b> los servidores DNS, mide la velocidad de conexión TCP de cada servidor y devuelve la dirección IP del servidor con la velocidad de conexión más rápida.<br/>Este modo puede ralentizar significativamente las consultas DNS, si uno o más proveedores DNS no responden. Asegúrate de que tus proveedores DNS sean estables y de que el tiempo de espera tu proveedor DNS sea bajo.",
|
||||
"autofix_warning_text": "Si haces clic en \"Corregir\", AdGuard Home configurará tu sistema para utilizar el servidor DNS de AdGuard Home.",
|
||||
"autofix_warning_list": "Realizará estas tareas: <0>Deshabilitar el sistema DNSStubListener</0> <0>Establecer la dirección del servidor DNS en 127.0.0.1</0> <0>Reemplazar el destino del enlace simbólico de /etc/resolv.conf por /run/systemd/resolve/resolv.conf</0> <0>Detener DNSStubListener (recargar el servicio systemd-resolved)</0>",
|
||||
"autofix_warning_result": "Como resultado, todas las peticiones DNS de tu sistema serán procesadas por AdGuard Home de manera predeterminada.",
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Razón: {{reason}}",
|
||||
"check_service": "Nombre del servicio: {{service}}",
|
||||
"check_hostname": "Nombre de host o nombre de dominio",
|
||||
"check_client_id": "Identificador del cliente (ClientID o dirección IP)",
|
||||
"check_enter_client_id": "Ingresa el identificador del cliente",
|
||||
"check_dns_record": "Selecciona el tipo de registro DNS",
|
||||
"service_name": "Nombre del servicio",
|
||||
"check_not_found": "No se ha encontrado en tus listas de filtros",
|
||||
"client_confirm_block": "¿Estás seguro de que deseas bloquear al cliente \"{{ip}}\"?",
|
||||
@@ -652,13 +656,13 @@
|
||||
"blocklist": "Lista de bloqueo",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Tamaño de la caché",
|
||||
"cache_size_desc": "Tamaño de la caché DNS (en bytes). Para deshabilitar el almacenamiento en caché, déjalo vacío.",
|
||||
"cache_size_desc": "Tamaño de la caché DNS (en bytes). Para desactivar el almacenamiento en caché, configúralo en 0.",
|
||||
"cache_ttl_min_override": "Anular TTL mínimo",
|
||||
"cache_ttl_max_override": "Anular TTL máximo",
|
||||
"enter_cache_size": "Ingresa el tamaño de la caché (bytes)",
|
||||
"enter_cache_ttl_min_override": "Ingresa el TTL mínimo (en segundos)",
|
||||
"enter_cache_ttl_max_override": "Ingresa el TTL máximo (en segundos)",
|
||||
"cache_ttl_min_override_desc": "Amplía el corto tiempo de vida (segundos) de los valores recibidos del servidor DNS de subida al almacenar en caché las respuestas DNS.",
|
||||
"cache_ttl_min_override_desc": "Amplía el corto tiempo de vida (segundos) de los valores recibidos del proveedor DNS al almacenar en caché las respuestas DNS.",
|
||||
"cache_ttl_max_override_desc": "Establece un valor de tiempo de vida (segundos) máximo para las entradas en la caché DNS.",
|
||||
"ttl_cache_validation": "La anulación TTL mínimo de la caché debe ser menor o igual al máximo",
|
||||
"cache_optimistic": "Caché optimista",
|
||||
@@ -744,7 +748,7 @@
|
||||
"thursday_short": "Jue.",
|
||||
"friday_short": "Vie.",
|
||||
"saturday_short": "Sáb.",
|
||||
"upstream_dns_cache_configuration": "Configuración de la caché DNS upstream",
|
||||
"enable_upstream_dns_cache": "Habilitar el almacenamiento en caché de DNS para la configuración personalizada de este cliente",
|
||||
"upstream_dns_cache_configuration": "Configuración de la caché del proveedor DNS",
|
||||
"enable_upstream_dns_cache": "Habilitar el almacenamiento en caché del DNS para la configuración personalizada de este cliente",
|
||||
"dns_cache_size": "Tamaño de la caché DNS, en bytes"
|
||||
}
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME : {{cname}}",
|
||||
"check_reason": "Raison : {{reason}}",
|
||||
"check_service": "Nom du service : {{service}}",
|
||||
"check_hostname": "Nom d'hôte ou nom de domaine",
|
||||
"check_client_id": "Identifiant du client (ClientID ou adresse IP)",
|
||||
"check_enter_client_id": "Saisissez l'identifiant du client",
|
||||
"check_dns_record": "Sélectionnez le type d'enregistrement DNS",
|
||||
"service_name": "Nom du service",
|
||||
"check_not_found": "Introuvable dans vos listes de filtres",
|
||||
"client_confirm_block": "Voulez-vous vraiment bloquer le client « {{ip}} » ?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Liste de blocage",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Taille du cache",
|
||||
"cache_size_desc": "Taille du cache DNS (en octets). Pour désactiver la mise en cache, laissez vide.",
|
||||
"cache_size_desc": "Taille du cache DNS (en octets). Pour désactiver la mise en cache, mettez la valeur sur 0.",
|
||||
"cache_ttl_min_override": "Remplacer le TTL minimum",
|
||||
"cache_ttl_max_override": "Remplacer le TTL maximum",
|
||||
"enter_cache_size": "Entrer la taille du cache (octets)",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Motivo: {{reason}}",
|
||||
"check_service": "Nome servizio: {{service}}",
|
||||
"check_hostname": "Nome host o nome di dominio",
|
||||
"check_client_id": "Identificatore client (ClientID o indirizzo IP)",
|
||||
"check_enter_client_id": "Inserisci identificatore client",
|
||||
"check_dns_record": "Seleziona il tipo di registrazione DNS",
|
||||
"service_name": "Nome servizio",
|
||||
"check_not_found": "Non trovato negli elenchi dei filtri",
|
||||
"client_confirm_block": "Sei sicuro di voler bloccare il client \"{{ip}}\"?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Lista nera",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Dimensioni cache",
|
||||
"cache_size_desc": "Dimensione della cache DNS (in byte). Per disabilitare la memorizzazione nella cache, lascia vuoto.",
|
||||
"cache_size_desc": "Dimensione della cache DNS (in byte). Per disabilitare la cache, impostare su 0.",
|
||||
"cache_ttl_min_override": "Sovrascrivi TTL minimo",
|
||||
"cache_ttl_max_override": "Sovrascrivi TTL massimo",
|
||||
"enter_cache_size": "Immetti dimensioni cache (in byte)",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "理由: {{reason}}",
|
||||
"check_service": "サービス名: {{service}}",
|
||||
"check_hostname": "ホスト名またはドメイン名",
|
||||
"check_client_id": "クライアント識別子 (ClientID または IP アドレス)",
|
||||
"check_enter_client_id": "クライアント識別子を入力してください",
|
||||
"check_dns_record": "DNSレコードタイプ(DNS record type)を選択",
|
||||
"service_name": "サービス名",
|
||||
"check_not_found": "フィルタ一覧には見つかりません",
|
||||
"client_confirm_block": "クライアント\"{{ip}}\"をブロックしてもよろしいですか?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "ブロックリスト",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "キャッシュサイズ",
|
||||
"cache_size_desc": "DNSキャッシュサイズ(バイト単位)。※キャッシュを無効化するには、この欄を空してください。",
|
||||
"cache_size_desc": "DNSキャッシュサイズ(バイト単位)※キャッシュを無効化するには、「0」(ゼロ)にしてください。",
|
||||
"cache_ttl_min_override": "最小TTLの上書き(秒単位)",
|
||||
"cache_ttl_max_override": "最大TTLの上書き(秒単位)",
|
||||
"enter_cache_size": "キャッシュサイズ(バイト単位)を入力してください",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "이유: {{reason}}",
|
||||
"check_service": "서비스 이름: {{service}}",
|
||||
"check_hostname": "호스트 이름 또는 도메인 이름",
|
||||
"check_client_id": "클라이언트 식별자(클라이언트 ID 또는 IP 주소)",
|
||||
"check_enter_client_id": "클라이언트 식별자 입력",
|
||||
"check_dns_record": "DNS 레코드 유형 선택",
|
||||
"service_name": "서비스 이름",
|
||||
"check_not_found": "필터 목록에서 찾을 수 없음",
|
||||
"client_confirm_block": "정말로 클라이언트 '{{ip}}'을(를) 차단하시겠습니까?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "차단 목록",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "캐시 크기",
|
||||
"cache_size_desc": "DNS 캐시 크기(바이트). 캐싱을 비활성화하려면 비워 둡니다.",
|
||||
"cache_size_desc": "DNS 캐시 크기(바이트). 캐싱을 사용하지 않으려면 0으로 설정합니다.",
|
||||
"cache_ttl_min_override": "최소 TTL (초) 무시",
|
||||
"cache_ttl_max_override": "최대 TTL (초) 무시",
|
||||
"enter_cache_size": "캐시 크기를 입력하세요",
|
||||
|
||||
@@ -110,9 +110,9 @@
|
||||
"homepage": "Startpagina",
|
||||
"report_an_issue": "Rapporteer een probleem",
|
||||
"privacy_policy": "Privacybeleid",
|
||||
"enable_protection": "Schakel bescherming in",
|
||||
"enable_protection": "Bescherming inschakelen",
|
||||
"enabled_protection": "Bescherming ingeschakeld",
|
||||
"disable_protection": "Schakel bescherming uit",
|
||||
"disable_protection": "Bescherming uitschakelen",
|
||||
"disabled_protection": "Bescherming uitgeschakeld",
|
||||
"refresh_statics": "Ververs statistieken",
|
||||
"dns_query": "DNS-queries",
|
||||
@@ -327,10 +327,10 @@
|
||||
"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_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_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",
|
||||
"theme_auto": "Automatisch",
|
||||
"theme_light": "Licht",
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Reden: {{reason}}",
|
||||
"check_service": "Servicenaam: {{service}}",
|
||||
"check_hostname": "Hostnaam of domeinnaam",
|
||||
"check_client_id": "Client identificator (ClientID of IP-adres)",
|
||||
"check_enter_client_id": "Voer Client identificator in",
|
||||
"check_dns_record": "Selecteer type DNS-record",
|
||||
"service_name": "Naam service",
|
||||
"check_not_found": "Niet in je lijst met filters gevonden",
|
||||
"client_confirm_block": "Weet je zeker dat je client \"{{ip}}\" wil blokkeren?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Blokkeerlijst",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Cache grootte",
|
||||
"cache_size_desc": "DNS-cachegrootte (in bytes). Leeg laten om caching uit te schakelen.",
|
||||
"cache_size_desc": "DNS-cachegrootte (in bytes). Om caching uit te schakelen, stel deze in op 0.",
|
||||
"cache_ttl_min_override": "Minimale TTL overschrijven",
|
||||
"cache_ttl_max_override": "Maximale TTL overschrijven",
|
||||
"enter_cache_size": "Cache grootte invoeren (bytes)",
|
||||
@@ -698,13 +702,13 @@
|
||||
"disable_for_hours": "Voor {{count}} uur",
|
||||
"disable_for_hours_plural": "Voor {{count}} uren",
|
||||
"disable_until_tomorrow": "Tot morgen",
|
||||
"disable_notify_for_seconds": "Beveiliging uitschakelen voor {{count}} seconde",
|
||||
"disable_notify_for_seconds_plural": "Beveiliging uitschakelen voor {{count}} seconden",
|
||||
"disable_notify_for_minutes": "Beveiliging uitschakelen voor {{count}} minuut",
|
||||
"disable_notify_for_minutes_plural": "Beveiliging uitschakelen voor {{count}} minuten",
|
||||
"disable_notify_for_hours": "Beveiliging uitschakelen voor {{count}} uur",
|
||||
"disable_notify_for_hours_plural": "Beveiliging uitschakelen voor {{count}} uren",
|
||||
"disable_notify_until_tomorrow": "Beveiliging uitschakelen tot morgen",
|
||||
"disable_notify_for_seconds": "Bescherming uitschakelen voor {{count}} seconde",
|
||||
"disable_notify_for_seconds_plural": "Bescherming uitschakelen voor {{count}} seconden",
|
||||
"disable_notify_for_minutes": "Bescherming uitschakelen voor {{count}} minuut",
|
||||
"disable_notify_for_minutes_plural": "Bescherming uitschakelen voor {{count}} minuten",
|
||||
"disable_notify_for_hours": "Bescherming uitschakelen voor {{count}} uur",
|
||||
"disable_notify_for_hours_plural": "Bescherming uitschakelen voor {{count}} uren",
|
||||
"disable_notify_until_tomorrow": "Bescherming uitschakelen tot morgen",
|
||||
"enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}",
|
||||
"custom_retention_input": "Voer retentie in uren in",
|
||||
"custom_rotation_input": "Voer rotatie in uren in",
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"stats_malware_phishing": "Blokkert skadevare/phishing",
|
||||
"stats_adult": "Blokkerte voksennettsteder",
|
||||
"stats_query_domain": "Mest forespurte domener",
|
||||
"for_last_24_hours": "de siste 24 timene",
|
||||
"for_last_days": "for den siste {{count}} dagen",
|
||||
"for_last_days_plural": "de siste {{count}} dagene",
|
||||
"stats_disabled": "Statistikkene har blitt skrudd av. Du kan skru den på fra <0>innstillingssiden</0>.",
|
||||
@@ -121,7 +120,6 @@
|
||||
"no_upstreams_data_found": "Ingen oppstrøms servere data funnet",
|
||||
"number_of_dns_query_days": "Antall DNS-spørringer behandlet for de siste {{count}} dagene",
|
||||
"number_of_dns_query_days_plural": "Antall DNS-forespørsler som ble behandlet de siste {{count}} dagene",
|
||||
"number_of_dns_query_24_hours": "Antall DNS-forespørsler som ble behandlet de siste 24 timene",
|
||||
"number_of_dns_query_blocked_24_hours": "Antall DNS-forespørsler som ble blokkert av adblock-filtre, hosts-lister, og domene-lister",
|
||||
"number_of_dns_query_blocked_24_hours_by_sec": "Antall DNS-forespørsler som ble blokkert av AdGuard sin nettlesersikkerhetsmodul",
|
||||
"number_of_dns_query_blocked_24_hours_adult": "Antall voksennettsteder som ble blokkert",
|
||||
@@ -266,6 +264,7 @@
|
||||
"custom_ip": "Tilpasset IP",
|
||||
"blocking_ipv4": "IPv4-blokkering",
|
||||
"blocking_ipv6": "IPv6-blokkering",
|
||||
"blocked_response_ttl": "Blokkerte svars TTL",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
@@ -627,7 +626,6 @@
|
||||
"use_saved_key": "Bruk den tidligere lagrede nøkkelen",
|
||||
"parental_control": "Foreldrekontroll",
|
||||
"safe_browsing": "Sikker surfing",
|
||||
"served_from_cache": "{{value}} <i>(formidlet fra mellomlageret)</i>",
|
||||
"theme_dark_desc": "Mørkt tema",
|
||||
"theme_light_desc": "Lyst tema",
|
||||
"disable_notify_until_tomorrow": "Deaktiver beskyttelsen til i morgen",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Motivo: {{reason}}",
|
||||
"check_service": "Nome do serviço: {{service}}",
|
||||
"check_hostname": "Nome do anfitrião ou nome de domínio",
|
||||
"check_client_id": "Identificador do cliente (ClienteID ou endereço de IP)",
|
||||
"check_enter_client_id": "Insira o identificador do cliente",
|
||||
"check_dns_record": "Selecione o tipo de registro DNS",
|
||||
"service_name": "Nome do serviço",
|
||||
"check_not_found": "Não encontrado em suas listas de filtros",
|
||||
"client_confirm_block": "Você tem certeza de que deseja bloquear o cliente \"{{ip}}\"?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Lista de bloqueio",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Tamanho do cache",
|
||||
"cache_size_desc": "Tamanho do cache do DNS (em bytes). Para desativar o cache, deixe em branco.",
|
||||
"cache_size_desc": "Tamanho do cache do DNS (em bytes). Para desativar o cache, defina como 0.",
|
||||
"cache_ttl_min_override": "Sobrepor o TTL mínimo",
|
||||
"cache_ttl_max_override": "Sobrepor o TTL máximo",
|
||||
"enter_cache_size": "Digite o tamanho do cache (bytes)",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Motivo: {{reason}}",
|
||||
"check_service": "Nome do serviço: {{service}}",
|
||||
"check_hostname": "Nome do hospedeiro ou nome de domínio",
|
||||
"check_client_id": "Identificador do cliente (ClientID ou endereço IP)",
|
||||
"check_enter_client_id": "Insira o identificador do cliente",
|
||||
"check_dns_record": "Selecione o tipo de registro DNS",
|
||||
"service_name": "Nome do serviço",
|
||||
"check_not_found": "Não encontrado nas tuas listas de filtros",
|
||||
"client_confirm_block": "Você tem certeza de que deseja bloquear o cliente \"{{ip}}\"?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Lista de bloqueio",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Tamanho do cache",
|
||||
"cache_size_desc": "Tamanho do cache DNS (em bytes). Para desativar o cache, deixar o campo vazio.",
|
||||
"cache_size_desc": "Tamanho do cache DNS (em bytes). Para desativar o cache, defina como 0.",
|
||||
"cache_ttl_min_override": "Sobrepor o TTL mínimo",
|
||||
"cache_ttl_max_override": "Sobrepor o TTL máximo",
|
||||
"enter_cache_size": "Digite o tamanho do cache (bytes)",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Причина: {{reason}}",
|
||||
"check_service": "Название сервиса: {{service}}",
|
||||
"check_hostname": "Имя хоста или домена",
|
||||
"check_client_id": "Идентификатор клиента (ClientID или IP-адрес)",
|
||||
"check_enter_client_id": "Введите идентификатор клиента",
|
||||
"check_dns_record": "Выберите тип DNS-записи",
|
||||
"service_name": "Имя сервиса",
|
||||
"check_not_found": "Не найдено в вашем списке фильтров",
|
||||
"client_confirm_block": "Вы уверены, что хотите заблокировать клиента «{{ip}}»?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Чёрный список",
|
||||
"milliseconds_abbreviation": "мс",
|
||||
"cache_size": "Размер кеша",
|
||||
"cache_size_desc": "Размера кеша DNS (в байтах). Чтобы отключить кэширование, оставьте поле пустым.",
|
||||
"cache_size_desc": "Размер кеша DNS (в байтах). Чтобы отключить кеширование, установите значение 0.",
|
||||
"cache_ttl_min_override": "Переопределить минимальный TTL",
|
||||
"cache_ttl_max_override": "Переопределить максимальный TTL",
|
||||
"enter_cache_size": "Введите размер кеша (в байтах)",
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Dôvod: {{reason}}",
|
||||
"check_service": "Meno služby: {{service}}",
|
||||
"check_hostname": "Názov hostiteľa alebo názov domény",
|
||||
"check_client_id": "Identifikátor klienta (ClientID alebo IP adresa)",
|
||||
"check_enter_client_id": "Zadajte identifikátor klienta",
|
||||
"check_dns_record": "Vyberte typ DNS záznamu",
|
||||
"service_name": "Názov služby",
|
||||
"check_not_found": "Nenašlo sa vo Vašom zozname filtrov",
|
||||
"client_confirm_block": "Naozaj chcete zablokovať klienta \"{{ip}}\"?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "Zoznam blokovaní",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Veľkosť cache",
|
||||
"cache_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete zakázať ukladanie do vyrovnávacej pamäte, ponechajte pole prázdne.",
|
||||
"cache_size_desc": "Veľkosť vyrovnávacej pamäte DNS (v bajtoch). Ak chcete vypnúť ukladanie do vyrovnávacej pamäte, nastavte hodnotu 0.",
|
||||
"cache_ttl_min_override": "Prepísať minimálne TTL",
|
||||
"cache_ttl_max_override": "Prepísať maximálne TTL",
|
||||
"enter_cache_size": "Zadať veľkosť cache (v bajtoch)",
|
||||
|
||||
@@ -40,11 +40,11 @@
|
||||
"dhcp_ipv4_settings": "DHCP IPv4 Ayarları",
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
|
||||
"form_error_required": "Gerekli alan",
|
||||
"form_error_ip4_format": "Geçersiz IPv4 adresi",
|
||||
"form_error_ip4_gateway_format": "Geçersiz ağ geçidi IPv4 adresi",
|
||||
"form_error_ip6_format": "Geçersiz IPv6 adresi",
|
||||
"form_error_ip_format": "Geçersiz IP adresi",
|
||||
"form_error_mac_format": "Geçersiz MAC adresi",
|
||||
"form_error_ip4_format": "IPv4 adresi geçersiz",
|
||||
"form_error_ip4_gateway_format": "Ağ geçidi IPv4 adresi geçersiz",
|
||||
"form_error_ip6_format": "IPv6 adresi geçersiz",
|
||||
"form_error_ip_format": "IP adresi geçersiz",
|
||||
"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_server_name": "Sunucu adı geçersiz",
|
||||
"form_error_subnet": "\"{{cidr}}\" alt ağı, \"{{ip}}\" IP adresini içermiyor",
|
||||
@@ -68,7 +68,7 @@
|
||||
"ip": "IP",
|
||||
"dhcp_table_hostname": "Ana makine Adı",
|
||||
"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_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.",
|
||||
@@ -147,13 +147,13 @@
|
||||
"average_upstream_response_time": "Ortalama üst kaynak 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",
|
||||
"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.",
|
||||
"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_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.",
|
||||
"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.",
|
||||
"no_servers_specified": "Sunucu belirtilmedi",
|
||||
"general_settings": "Genel ayarlar",
|
||||
@@ -294,6 +294,9 @@
|
||||
"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",
|
||||
"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",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
@@ -308,7 +311,7 @@
|
||||
"form_enter_rate_limit": "Sıklık limitini girin",
|
||||
"rate_limit": "Sıklık limiti",
|
||||
"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_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.",
|
||||
@@ -342,17 +345,17 @@
|
||||
"unknown_filter": "Bilinmeyen filtre {{filterId}}",
|
||||
"known_tracker": "Bilinen izleyici",
|
||||
"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_listen": "Dinleme arayüzü",
|
||||
"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",
|
||||
"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_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_password": "Parola",
|
||||
"install_auth_confirm": "Parolayı onayla",
|
||||
@@ -366,10 +369,10 @@
|
||||
"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_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_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_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.",
|
||||
@@ -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_3": "O anda aktif olan ağın adına dokunun.",
|
||||
"install_devices_ios_list_4": "DNS alanına AdGuard Home sunucunuzun adreslerini girin.",
|
||||
"get_started": "Başlayın",
|
||||
"get_started": "Başla",
|
||||
"next": "Sonraki",
|
||||
"open_dashboard": "Panoyu Aç",
|
||||
"install_saved": "Başarıyla kaydedildi",
|
||||
@@ -452,14 +455,14 @@
|
||||
"settings_global": "Genel",
|
||||
"settings_custom": "Özel",
|
||||
"table_client": "İstemci",
|
||||
"table_name": "AdAdı",
|
||||
"table_name": "Ad",
|
||||
"save_btn": "Kaydet",
|
||||
"client_add": "İstemci Ekle",
|
||||
"client_new": "Yeni İstemci",
|
||||
"client_edit": "İstemciyi Düzenle",
|
||||
"client_identifier": "Tanımlayıcı",
|
||||
"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_subnet_ip": "\"{{cidr}}\" alt ağına bir IP adresi girin",
|
||||
"form_enter_mac": "MAC adresi girin",
|
||||
@@ -476,7 +479,7 @@
|
||||
"client_confirm_delete": "\"{{key}}\" istemcisini silmek istediğinizden emin misiniz?",
|
||||
"list_confirm_delete": "Bu listeyi silmek istediğinizden emin misiniz?",
|
||||
"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_desc": "AdGuard Home DNS sunucusu için erişim kurallarını buradan yapılandırabilirsiniz",
|
||||
"access_allowed_title": "İzin verilen istemciler",
|
||||
@@ -598,12 +601,12 @@
|
||||
"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.",
|
||||
"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_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.",
|
||||
"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",
|
||||
"check_title": "Filtrelemeyi denetleyin",
|
||||
"check_desc": "Ana makine adının filtreleme durumunu kontrol edin.",
|
||||
@@ -617,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Sebep: {{reason}}",
|
||||
"check_service": "Hizmet adı: {{service}}",
|
||||
"check_hostname": "Ana makine adı veya alan adı",
|
||||
"check_client_id": "İstemci tanımlayıcısı (ClientID veya IP adresi)",
|
||||
"check_enter_client_id": "İstemci tanımlayıcısı girin",
|
||||
"check_dns_record": "DNS kayıt türünü seçin",
|
||||
"service_name": "Hizmet adı",
|
||||
"check_not_found": "Filtre listelerinizde bulunamadı",
|
||||
"client_confirm_block": "\"{{ip}}\" istemcisini engellemek istediğinizden emin misiniz?",
|
||||
@@ -624,11 +631,11 @@
|
||||
"client_blocked": "\"{{ip}}\" istemcisi başarıyla engellendi",
|
||||
"client_unblocked": "\"{{ip}}\" istemcinin engellemesi başarıyla kaldırıldı",
|
||||
"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",
|
||||
"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_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?",
|
||||
"list_updated": "{{count}} liste güncellendi",
|
||||
"list_updated_plural": "{{count}} liste güncellendi",
|
||||
@@ -649,7 +656,7 @@
|
||||
"blocklist": "Engel listesi",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "Önbellek boyutu",
|
||||
"cache_size_desc": "DNS önbellek boyutu (bayt cinsinden). Önbelleğe almayı devre dışı bırakmak için boş bırakın.",
|
||||
"cache_size_desc": "DNS önbellek boyutu (bayt cinsinden). Önbelleği devre dışı bırakmak için 0 olarak ayarlayın.",
|
||||
"cache_ttl_min_override": "Minimum kullanım süresini geçersiz kıl",
|
||||
"cache_ttl_max_override": "Maksimum kullanım süresini geçersiz kıl",
|
||||
"enter_cache_size": "Önbellek boyutunu girin (bayt)",
|
||||
@@ -707,8 +714,8 @@
|
||||
"custom_rotation_input": "Rotasyonu saat cinsinden girin",
|
||||
"protection_section_label": "Koruma",
|
||||
"log_and_stats_section_label": "Sorgu günlüğü ve istatistikler",
|
||||
"ignore_query_log": "Sorgu günlüğünde bu istemciyi yoksay",
|
||||
"ignore_statistics": "İstatistiklerde bu istemciyi yoksay",
|
||||
"ignore_query_log": "Sorgu günlüğünde bu istemciyi gösterme",
|
||||
"ignore_statistics": "İstatistiklerde bu istemciyi gösterme",
|
||||
"schedule_services": "Hizmet engellemeyi duraklat",
|
||||
"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",
|
||||
@@ -742,6 +749,6 @@
|
||||
"friday_short": "Cum",
|
||||
"saturday_short": "Cmt",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "原因:{{reason}}",
|
||||
"check_service": "服务名称:{{service}}",
|
||||
"check_hostname": "主机名或域名",
|
||||
"check_client_id": "客户端标识符(ClientID 或 IP 地址)",
|
||||
"check_enter_client_id": "输入客户端标识符",
|
||||
"check_dns_record": "选择 DNS 记录类型",
|
||||
"service_name": "服务名称",
|
||||
"check_not_found": "未在您的筛选列表中找到",
|
||||
"client_confirm_block": "确定要阻止客户端 \"{{ip}}\"?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "黑名单",
|
||||
"milliseconds_abbreviation": "毫秒",
|
||||
"cache_size": "缓存大小",
|
||||
"cache_size_desc": "DNS 缓存大小(单位:字节)。若要关闭缓存,请留空。",
|
||||
"cache_size_desc": "DNS 缓存大小(单位:字节)。若要禁用缓存,请设置为 0。",
|
||||
"cache_ttl_min_override": "覆盖最小 TTL 值",
|
||||
"cache_ttl_max_override": "覆盖最大 TTL 值",
|
||||
"enter_cache_size": "输入缓存大小(字节)",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"client_settings": "用戶端設定",
|
||||
"example_upstream_reserved": "<0>供特定的網域</0>之上游;",
|
||||
"example_upstream_reserved": "<0>特定網域</0>的上游;",
|
||||
"example_multiple_upstreams_reserved": "<0>特定網域</0>的多個上游伺服器;",
|
||||
"example_upstream_comment": "註解。",
|
||||
"upstream_parallel": "透過同時地查詢所有上游的伺服器,使用並行的查詢以加速解析。",
|
||||
@@ -20,7 +20,7 @@
|
||||
"resolve_clients_title": "啟用用戶端的 IP 位址之反向的解析",
|
||||
"resolve_clients_desc": "透過傳送指標(PTR)查詢到對應的解析器(私人 DNS 伺服器供區域的用戶端,上游的伺服器供有公共 IP 位址的用戶端),反向地解析用戶端的 IP 位址變為它們的主機名稱。",
|
||||
"use_private_ptr_resolvers_title": "使用私人反向的 DNS 解析器",
|
||||
"use_private_ptr_resolvers_desc": "使用私人上游伺服器、DHCP、/etc/hosts 等方式解析包含私人 IP 位址的 ARPA 網域的 PTR、SOA 和 NS 請求。如果停用,AdGuard Home 將對所有此類請求以 NXDOMAIN 回應。",
|
||||
"use_private_ptr_resolvers_desc": "透過私有上游伺服器、DHCP 或 /etc/hosts 等管道,解析含有私有 IP 位址的 ARPA 網域的 PTR、SOA 與 NS 請求。若停用此功能,AdGuard Home 將以 NXDOMAIN 回應所有相關請求。",
|
||||
"check_dhcp_servers": "檢查動態主機設定協定(DHCP)伺服器",
|
||||
"save_config": "儲存配置",
|
||||
"enabled_dhcp": "動態主機設定協定(DHCP)伺服器被啟用",
|
||||
@@ -112,8 +112,8 @@
|
||||
"privacy_policy": "隱私政策",
|
||||
"enable_protection": "啟用防護",
|
||||
"enabled_protection": "已啟用防護",
|
||||
"disable_protection": "禁用防護",
|
||||
"disabled_protection": "已禁用防護",
|
||||
"disable_protection": "停用防護",
|
||||
"disabled_protection": "已停用防護",
|
||||
"refresh_statics": "重新整理統計資料",
|
||||
"dns_query": "DNS 查詢",
|
||||
"blocked_by": "<0>被過濾器封鎖</0>",
|
||||
@@ -124,8 +124,8 @@
|
||||
"for_last_hours_plural": "在過去的 {{count}} 小時內",
|
||||
"for_last_days": "在最近的 {{count}} 日內",
|
||||
"for_last_days_plural": "在最近的 {{count}} 日內",
|
||||
"stats_disabled": "該統計資料已被禁用。您可從<0>設定頁面</0>中打開它。",
|
||||
"stats_disabled_short": "該統計資料已被禁用",
|
||||
"stats_disabled": "統計功能目前停用中,請至<0>設定頁面</0>重新開啟。",
|
||||
"stats_disabled_short": "該統計資料已停用",
|
||||
"no_domains_found": "無已發現之網域",
|
||||
"requests_count": "請求總數",
|
||||
"top_blocked_domains": "熱門已封鎖的網域",
|
||||
@@ -172,13 +172,13 @@
|
||||
"upstreams": "上游",
|
||||
"upstream": "上游伺服器",
|
||||
"apply_btn": "套用",
|
||||
"disabled_filtering_toast": "已禁用過濾",
|
||||
"disabled_filtering_toast": "已停用過濾",
|
||||
"enabled_filtering_toast": "已啟用過濾",
|
||||
"disabled_safe_browsing_toast": "已禁用安全瀏覽",
|
||||
"disabled_safe_browsing_toast": "已停用安全瀏覽",
|
||||
"enabled_safe_browsing_toast": "已啟用安全瀏覽",
|
||||
"disabled_parental_toast": "已禁用家長控制",
|
||||
"disabled_parental_toast": "已停用家長控制",
|
||||
"enabled_parental_toast": "已啟用家長控制",
|
||||
"disabled_safe_search_toast": "已禁用安全搜尋",
|
||||
"disabled_safe_search_toast": "已停用安全搜尋",
|
||||
"enabled_save_search_toast": "已啟用安全搜尋",
|
||||
"updated_save_search_toast": "安全搜尋設定更新成功",
|
||||
"enabled_table_header": "已啟用",
|
||||
@@ -275,7 +275,7 @@
|
||||
"query_log_retention": "查詢記錄保留時間",
|
||||
"query_log_enable": "啟用記錄",
|
||||
"query_log_configuration": "記錄配置",
|
||||
"query_log_disabled": "查詢記錄被禁用並可在<0>設定</0>中被配置",
|
||||
"query_log_disabled": "查詢記錄功能已停用,請至「<0>設定</0>」調整",
|
||||
"query_log_strict_search": "使用雙引號於嚴謹的搜尋",
|
||||
"query_log_retention_confirm": "您確定要更改記錄檔保存期限嗎?如果您縮短期限部分資料可能將會遺失",
|
||||
"anonymize_client_ip": "將用戶端 IP 匿名",
|
||||
@@ -401,7 +401,7 @@
|
||||
"encryption_config_saved": "加密配置被儲存",
|
||||
"encryption_server": "伺服器名稱",
|
||||
"encryption_server_enter": "輸入您的域名",
|
||||
"encryption_server_desc": "如果被設定,AdGuard Home 檢測用戶端 IDs,回覆 DDR 查詢,並執行額外的連線驗證。如果未被設定,這些功能被禁用。必須與在該憑證裡的 DNS 名稱其中之一相符。",
|
||||
"encryption_server_desc": "如果設定,AdGuard Home 會偵測 ClientID、回應 DDR 查詢,並執行其他連線驗證。如果未設定,則會停用這些功能。必須符合憑證中的一個 DNS 名稱。",
|
||||
"encryption_redirect": "自動地重新導向到 HTTPS",
|
||||
"encryption_redirect_desc": "如果被勾選,AdGuard Home 將自動地重新導向您從 HTTP 到 HTTPS 位址。",
|
||||
"encryption_https": "HTTPS 連接埠",
|
||||
@@ -429,8 +429,8 @@
|
||||
"encryption_reset": "您確定您想要重置加密設定嗎?",
|
||||
"encryption_warning": "警告",
|
||||
"encryption_plain_dns_enable": "啟用一般的 DNS",
|
||||
"encryption_plain_dns_desc": "預設情況下啟用一般的 DNS。使用者可以禁用它,強制所有裝置使用一般的 DNS。為此,必須至少啟用一個一般的 DNS 協定。",
|
||||
"encryption_plain_dns_error": "要禁用一般的 DNS,請至少啟用一個一般的 DNS 協定",
|
||||
"encryption_plain_dns_desc": "預設啟用一般 DNS。您可以停用它以強制所有裝置使用加密 DNS。若要這樣做,您必須啟用至少一個加密 DNS 通訊協定",
|
||||
"encryption_plain_dns_error": "若要停用一般 DNS,請啟用至少一個加密 DNS 通訊協定",
|
||||
"topline_expiring_certificate": "您的安全通訊端層(SSL)憑證即將到期。更新<0>加密設定</0>。",
|
||||
"topline_expired_certificate": "您的安全通訊端層(SSL)憑證為已到期的。更新<0>加密設定</0>。",
|
||||
"form_error_port_range": "輸入在 80-65535 之範圍內的連接埠號碼",
|
||||
@@ -572,7 +572,7 @@
|
||||
"filters_configuration": "過濾器配置",
|
||||
"filters_enable": "啟用過濾器",
|
||||
"filters_interval": "過濾器更新間隔",
|
||||
"disabled": "已禁用",
|
||||
"disabled": "已停用",
|
||||
"username_label": "使用者名稱",
|
||||
"username_placeholder": "輸入使用者名稱",
|
||||
"password_label": "密碼",
|
||||
@@ -598,7 +598,7 @@
|
||||
"rewrite_domain_name": "域名:新增一筆正規名稱(CNAME)記錄",
|
||||
"rewrite_A": "<0>A</0>:特殊的數值,阻止 <0>A</0> 記錄免於該上游",
|
||||
"rewrite_AAAA": "<0>AAAA</0>:特殊的數值,阻止 <0>AAAA</0> 記錄免於該上游",
|
||||
"disable_ipv6": "禁用 IPv6 位址之解析",
|
||||
"disable_ipv6": "停用 IPv6 位址解析",
|
||||
"disable_ipv6_desc": "停止所有對於 IPv6 位址(類型 AAAA)的 DNS 查詢,並從 HTTPS 回應中移除 IPv6 的提示。",
|
||||
"fastest_addr": "最快的 IP 位址",
|
||||
"fastest_addr_desc": "等待<b>所有</b> DNS 伺服器的回應,測量每個伺服器的 TCP 連線速度,並返回連線速度最快的伺服器的 IP 位址。<br/>如果一個或多個上游伺服器沒有回應,此模式會顯著減慢 DNS 查詢速度。確保您的上游伺服器穩定且上游超時時間短。",
|
||||
@@ -620,6 +620,10 @@
|
||||
"check_cname": "正規名稱(CNAME):{{cname}}",
|
||||
"check_reason": "原因:{{reason}}",
|
||||
"check_service": "服務名稱:{{service}}",
|
||||
"check_hostname": "主機名稱或域名",
|
||||
"check_client_id": "用戶端識別碼(ClientID 或 IP 位址)",
|
||||
"check_enter_client_id": "輸入用戶識別碼",
|
||||
"check_dns_record": "選擇 DNS 記錄類型",
|
||||
"service_name": "服務名稱",
|
||||
"check_not_found": "未在您的過濾器清單中被找到",
|
||||
"client_confirm_block": "您確定您想要封鎖該用戶端 \"{{ip}}\" 嗎?",
|
||||
@@ -652,7 +656,7 @@
|
||||
"blocklist": "封鎖清單",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"cache_size": "快取大小",
|
||||
"cache_size_desc": "DNS 快取大小(以位元組)。要禁用快取,留空。",
|
||||
"cache_size_desc": "DNS 快取大小(位元組)。若要停用快取,請設為 0。",
|
||||
"cache_ttl_min_override": "覆寫最小的存活時間(TTL)",
|
||||
"cache_ttl_max_override": "覆寫最大的存活時間(TTL)",
|
||||
"enter_cache_size": "輸入快取大小(位元組)",
|
||||
@@ -677,13 +681,13 @@
|
||||
"port_53_faq_link": "連接埠 53 常被 \"DNSStubListener\" 或 \"systemd-resolved\" 服務佔用。請閱讀有關如何解決這個的<0>用法說明</0>。",
|
||||
"adg_will_drop_dns_queries": "AdGuard Home 將持續排除來自此用戶端之所有的 DNS 查詢。",
|
||||
"filter_allowlist": "警告:此動作也將把 \"{{disallowed_rule}}\" 規則排除在被允許的用戶端的清單之外。",
|
||||
"last_rule_in_allowlist": "因為排除 \"{{disallowed_rule}}\" 規則將禁用\"被允許的用戶端\"清單,無法不允許此用戶端。",
|
||||
"last_rule_in_allowlist": "無法禁止此用戶端,因為排除規則 \"{{disallowed_rule}}\" 會停用「允許的用戶端」清單。",
|
||||
"use_saved_key": "使用該先前已儲存的金鑰",
|
||||
"parental_control": "家長控制",
|
||||
"safe_browsing": "安全瀏覽",
|
||||
"served_from_cache_label": "從快取中",
|
||||
"form_error_password_length": "密碼長度必須為 {{min}} 到 {{max}} 個字符",
|
||||
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。",
|
||||
"anonymizer_notification": "<0>注意:</0>IP 匿名功能已開啟。您可在<1>一般設定</1>中關閉。",
|
||||
"confirm_dns_cache_clear": "您確定您想要清除 DNS 快取嗎?",
|
||||
"cache_cleared": "DNS 快取被成功地清除",
|
||||
"clear_cache": "清除快取",
|
||||
@@ -698,14 +702,14 @@
|
||||
"disable_for_hours": "{{count}} 小時",
|
||||
"disable_for_hours_plural": "{{count}} 小時",
|
||||
"disable_until_tomorrow": "直到明天",
|
||||
"disable_notify_for_seconds": "計 {{count}} 秒禁用防護",
|
||||
"disable_notify_for_seconds_plural": "計 {{count}} 秒禁用防護",
|
||||
"disable_notify_for_minutes": "計 {{count}} 分鐘禁用防護",
|
||||
"disable_notify_for_minutes_plural": "計 {{count}} 分鐘禁用防護",
|
||||
"disable_notify_for_hours": "計 {{count}} 小時禁用防護",
|
||||
"disable_notify_for_hours_plural": "計 {{count}} 小時禁用防護",
|
||||
"disable_notify_until_tomorrow": "禁用防護直到明天",
|
||||
"enable_protection_timer": "防護將於 {{time}} 被啟用",
|
||||
"disable_notify_for_seconds": "計 {{count}} 秒停用防護",
|
||||
"disable_notify_for_seconds_plural": "計 {{count}} 秒停用防護",
|
||||
"disable_notify_for_minutes": "計 {{count}} 分鐘停用防護",
|
||||
"disable_notify_for_minutes_plural": "計 {{count}} 分鐘停用防護",
|
||||
"disable_notify_for_hours": "計 {{count}} 小時停用防護",
|
||||
"disable_notify_for_hours_plural": "計 {{count}} 小時停用防護",
|
||||
"disable_notify_until_tomorrow": "停用防護直到明天",
|
||||
"enable_protection_timer": "防護將於 {{time}} 啟用",
|
||||
"custom_retention_input": "輸入保留時間(小時)",
|
||||
"custom_rotation_input": "輸入旋轉時間(小時)",
|
||||
"protection_section_label": "防護",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { describe, expect, test, afterEach, vi, beforeEach, it } from 'vitest';
|
||||
|
||||
import { sortIp, countClientsStatistics, findAddressType, subnetMaskToBitMask } from '../helpers/helpers';
|
||||
import { ADDRESS_TYPES } from '../helpers/constants';
|
||||
|
||||
@@ -259,7 +261,7 @@ describe('sortIp', () => {
|
||||
const originalWarn = console.warn;
|
||||
|
||||
beforeEach(() => {
|
||||
console.warn = jest.fn();
|
||||
console.warn = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -347,15 +349,15 @@ describe('sortIp', () => {
|
||||
});
|
||||
|
||||
describe('findAddressType', () => {
|
||||
describe('ip', () => {
|
||||
it('should return IP type for IP addresses', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
describe('mac', () => {
|
||||
it('should return UNKNOWN type for MAC addresses', () => {
|
||||
expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
CHECK_TIMEOUT,
|
||||
STATUS_RESPONSE,
|
||||
SETTINGS_NAMES,
|
||||
FORM_NAME,
|
||||
MANUAL_UPDATE_LINK,
|
||||
DISABLE_PROTECTION_TIMINGS,
|
||||
} 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 { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } =
|
||||
getState().form[FORM_NAME.UPSTREAM].values;
|
||||
const { bootstrap_dns, upstream_dns, local_ptr_upstreams, fallback_dns } = formValues;
|
||||
|
||||
return dispatch(
|
||||
testUpstream(
|
||||
@@ -512,16 +510,15 @@ export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST');
|
||||
export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');
|
||||
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());
|
||||
try {
|
||||
const req = {
|
||||
interface: name,
|
||||
interface: selectedInterface,
|
||||
};
|
||||
const activeDhcp = await apiClient.findActiveDhcp(req);
|
||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
||||
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 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) => {
|
||||
dispatch(setAllSettingsRequest());
|
||||
try {
|
||||
const { confirm_password, ...config } = values;
|
||||
const config = { ...values };
|
||||
delete config.confirm_password;
|
||||
|
||||
await apiClient.setAllSettings(config);
|
||||
dispatch(setAllSettingsSuccess());
|
||||
@@ -48,7 +49,11 @@ export const checkConfig = (values: any) => async (dispatch: any) => {
|
||||
dispatch(checkConfigRequest());
|
||||
try {
|
||||
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) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(checkConfigFailure());
|
||||
|
||||
@@ -3,8 +3,9 @@ import { createAction } from 'redux-actions';
|
||||
import apiClient from '../api/Api';
|
||||
|
||||
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 { SearchFormValues } from '../components/Logs';
|
||||
|
||||
const getLogsWithParams = async (config: any) => {
|
||||
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 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 totalData = total || { logs };
|
||||
|
||||
const queryForm = getState().form[FORM_NAME.LOGS_FILTER];
|
||||
const currentQuery = queryForm && queryForm.values.search;
|
||||
const previousQuery = filter?.search;
|
||||
const isQueryTheSame =
|
||||
typeof previousQuery === 'string' && typeof currentQuery === 'string' && previousQuery === currentQuery;
|
||||
@@ -51,7 +50,7 @@ const shortPollQueryLogs = async (data: any, filter: any, dispatch: any, getStat
|
||||
filter,
|
||||
});
|
||||
if (additionalLogs.oldest.length > 0) {
|
||||
return await shortPollQueryLogs(additionalLogs, filter, dispatch, getState, {
|
||||
return await shortPollQueryLogs(additionalLogs, filter, dispatch, currentQuery, {
|
||||
logs: [...totalData.logs, ...additionalLogs.logs],
|
||||
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());
|
||||
try {
|
||||
const { isFiltered, filter, oldest } = getState().queryLogs;
|
||||
|
||||
const data = await getLogsWithParams({
|
||||
older_than: oldest,
|
||||
filter,
|
||||
});
|
||||
|
||||
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;
|
||||
dispatch(getLogsSuccess(updatedData));
|
||||
} else {
|
||||
@@ -122,13 +122,13 @@ export const setLogsFilterRequest = createAction('SET_LOGS_FILTER_REQUEST');
|
||||
* @param {string} filter.response_status 'QUERY' field of RESPONSE_FILTER object
|
||||
* @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 setFilteredLogsFailure = createAction('SET_FILTERED_LOGS_FAILURE');
|
||||
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());
|
||||
try {
|
||||
const data = await getLogsWithParams({
|
||||
@@ -136,7 +136,9 @@ export const setFilteredLogs = (filter?: any) => async (dispatch: any, getState:
|
||||
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;
|
||||
|
||||
dispatch(
|
||||
|
||||
@@ -1,62 +1,112 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
import { renderInputField } from '../../../helpers/form';
|
||||
|
||||
import Info from './Info';
|
||||
import { FORM_NAME } from '../../../helpers/constants';
|
||||
import { RootState } from '../../../initialState';
|
||||
|
||||
interface CheckProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
pristine: boolean;
|
||||
invalid: boolean;
|
||||
import { RootState } from '../../../initialState';
|
||||
import { validateRequiredValue } from '../../../helpers/validators';
|
||||
import { Input } from '../../ui/Controls/Input';
|
||||
import { DNS_RECORD_TYPES } from '../../../helpers/constants';
|
||||
import { Select } from '../../ui/Controls/Select';
|
||||
|
||||
export type FilteringCheckFormValues = {
|
||||
name: string;
|
||||
client?: string;
|
||||
qtype?: string;
|
||||
}
|
||||
|
||||
const Check = (props: CheckProps) => {
|
||||
const { pristine, invalid, handleSubmit } = props;
|
||||
type Props = {
|
||||
onSubmit?: (data: FilteringCheckFormValues) => void;
|
||||
};
|
||||
|
||||
const Check = ({ onSubmit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const processingCheck = useSelector((state: RootState) => state.filtering.processingCheck);
|
||||
|
||||
const hostname = useSelector((state: RootState) => state.filtering.check.hostname);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isValid },
|
||||
} = useForm<FilteringCheckFormValues>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
name: '',
|
||||
client: '',
|
||||
qtype: DNS_RECORD_TYPES[0],
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card title={t('check_title')} subtitle={t('check_desc')}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-group">
|
||||
<Field
|
||||
id="name"
|
||||
name="name"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_host')}
|
||||
/>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ validate: validateRequiredValue }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
label={t('check_hostname')}
|
||||
data-testid="check_domain_name"
|
||||
placeholder="example.com"
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<span className="input-group-append">
|
||||
<button
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
disabled={pristine || invalid || processingCheck}>
|
||||
{t('check')}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<Controller
|
||||
name="client"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
data-testid="check_client_id"
|
||||
label={t('check_client_id')}
|
||||
placeholder={t('check_enter_client_id')}
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="qtype"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
label={t('check_dns_record')}
|
||||
data-testid="check_dns_record_type"
|
||||
>
|
||||
{DNS_RECORD_TYPES.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
type="submit"
|
||||
data-testid="check_domain_submit"
|
||||
disabled={!isValid || processingCheck}
|
||||
>
|
||||
{t('check')}
|
||||
</button>
|
||||
|
||||
{hostname && (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
<Info />
|
||||
</>
|
||||
)}
|
||||
@@ -67,4 +117,4 @@ const Check = (props: CheckProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm({ form: FORM_NAME.DOMAIN_CHECK })(Check);
|
||||
export default Check;
|
||||
|
||||
@@ -7,7 +7,7 @@ import PageTitle from '../ui/PageTitle';
|
||||
|
||||
import Examples from './Examples';
|
||||
|
||||
import Check from './Check';
|
||||
import Check, { FilteringCheckFormValues } from './Check';
|
||||
|
||||
import { getTextareaCommentsHighlight, syncScroll } from '../../helpers/highlightTextareaComments';
|
||||
import { COMMENT_LINE_DEFAULT_TOKEN } from '../../helpers/constants';
|
||||
@@ -48,8 +48,18 @@ class CustomRules extends Component<CustomRulesProps> {
|
||||
this.props.setRules(this.props.filtering.userRules);
|
||||
};
|
||||
|
||||
handleCheck = (values: any) => {
|
||||
this.props.checkHost(values);
|
||||
handleCheck = (values: FilteringCheckFormValues) => {
|
||||
const params: FilteringCheckFormValues = { name: values.name };
|
||||
|
||||
if (values.client) {
|
||||
params.client = values.client;
|
||||
}
|
||||
|
||||
if (values.qtype) {
|
||||
params.qtype = values.qtype;
|
||||
}
|
||||
|
||||
this.props.checkHost(params);
|
||||
};
|
||||
|
||||
onScroll = (e: any) => syncScroll(e, this.ref);
|
||||
@@ -68,6 +78,7 @@ class CustomRules extends Component<CustomRulesProps> {
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="text-edit-container mb-4">
|
||||
<textarea
|
||||
data-testid="custom_rule_textarea"
|
||||
className="form-control font-monospace text-input"
|
||||
value={userRules}
|
||||
onChange={this.handleChange}
|
||||
@@ -81,6 +92,7 @@ class CustomRules extends Component<CustomRulesProps> {
|
||||
|
||||
<div className="card-actions">
|
||||
<button
|
||||
data-testid="apply_custom_rule"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}>
|
||||
|
||||
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 { Field, reduxForm } from 'redux-form';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import classNames from 'classnames';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { validatePath, validateRequiredValue } from '../../helpers/validators';
|
||||
|
||||
import { CheckboxField, renderInputField } from '../../helpers/form';
|
||||
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants';
|
||||
import { MODAL_OPEN_TIMEOUT, MODAL_TYPE } from '../../helpers/constants';
|
||||
import filtersCatalog from '../../helpers/filters/filters';
|
||||
import { FiltersList } from './FiltersList';
|
||||
import { Input } from '../ui/Controls/Input';
|
||||
|
||||
const getIconsData = (homepage: any, source: any) => [
|
||||
{
|
||||
iconName: 'dashboard',
|
||||
href: homepage,
|
||||
className: 'ml-1',
|
||||
},
|
||||
{
|
||||
iconName: 'info',
|
||||
href: source,
|
||||
},
|
||||
];
|
||||
type FormValues = {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const renderIcons = (iconsData: any) =>
|
||||
iconsData.map(({ iconName, href, className = '' }: any) => (
|
||||
<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>
|
||||
));
|
||||
const defaultValues: FormValues = {
|
||||
enabled: true,
|
||||
name: '',
|
||||
url: '',
|
||||
};
|
||||
|
||||
interface renderCheckboxFieldProps {
|
||||
// https://redux-form.com/8.3.0/docs/api/field.md/#props
|
||||
input: {
|
||||
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;
|
||||
type Props = {
|
||||
closeModal: () => void;
|
||||
onSubmit: (values: FormValues) => void;
|
||||
processingAddFilter: boolean;
|
||||
processingConfigFilter: boolean;
|
||||
whitelist?: boolean;
|
||||
modalType: string;
|
||||
toggleFilteringModal: (...args: unknown[]) => unknown;
|
||||
selectedSources?: object;
|
||||
}
|
||||
toggleFilteringModal: ({ type }: { type?: keyof typeof MODAL_TYPE }) => void;
|
||||
selectedSources?: Record<string, boolean>;
|
||||
initialValues?: FormValues;
|
||||
};
|
||||
|
||||
const Form = (props: FormProps) => {
|
||||
const {
|
||||
t,
|
||||
closeModal,
|
||||
handleSubmit,
|
||||
processingAddFilter,
|
||||
processingConfigFilter,
|
||||
whitelist,
|
||||
modalType,
|
||||
toggleFilteringModal,
|
||||
selectedSources,
|
||||
} = props;
|
||||
export const Form = ({
|
||||
closeModal,
|
||||
processingAddFilter,
|
||||
processingConfigFilter,
|
||||
whitelist,
|
||||
modalType,
|
||||
toggleFilteringModal,
|
||||
selectedSources,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const openModal = (modalType: any, timeout = MODAL_OPEN_TIMEOUT) => {
|
||||
toggleFilteringModal();
|
||||
const methods = useForm({
|
||||
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);
|
||||
};
|
||||
|
||||
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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body modal-body--filters">
|
||||
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<div className="d-flex justify-content-around">
|
||||
<button
|
||||
onClick={openFilteringListModal}
|
||||
className="btn btn-success btn-standard mr-2 btn-large">
|
||||
{t('choose_from_list')}
|
||||
</button>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="modal-body modal-body--filters">
|
||||
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<div className="d-flex justify-content-around">
|
||||
<button
|
||||
onClick={openFilteringListModal}
|
||||
className="btn btn-success btn-standard mr-2 btn-large">
|
||||
{t('choose_from_list')}
|
||||
</button>
|
||||
|
||||
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
||||
{t('add_custom_list')}
|
||||
</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()}
|
||||
/>
|
||||
<button onClick={openAddFiltersModal} className="btn btn-primary btn-standard">
|
||||
{t('add_custom_list')}
|
||||
</button>
|
||||
</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">
|
||||
<Field
|
||||
id="url"
|
||||
name="url"
|
||||
type="text"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('enter_url_or_path_hint')}
|
||||
validate={[validateRequiredValue, validatePath]}
|
||||
normalizeOnBlur={(data: any) => data.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group">
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
rules={{ validate: { validateRequiredValue, validatePath } }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
data-testid="filters_url"
|
||||
placeholder={t('enter_url_or_path_hint')}
|
||||
error={fieldState.error?.message}
|
||||
trimOnBlur
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__description">
|
||||
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="form__description">
|
||||
{whitelist ? t('enter_valid_allowlist') : t('enter_valid_blocklist')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
||||
{t('cancel_btn')}
|
||||
</button>
|
||||
|
||||
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success"
|
||||
disabled={processingAddFilter || processingConfigFilter}>
|
||||
{t('save_btn')}
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={closeModal}>
|
||||
{t('cancel_btn')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{modalType !== MODAL_TYPE.SELECT_MODAL_TYPE && (
|
||||
<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 Form from './Form';
|
||||
import { Form } from './Form';
|
||||
import '../ui/Modal.css';
|
||||
|
||||
import { getMap } from '../../helpers/helpers';
|
||||
@@ -75,25 +75,15 @@ class Modal extends Component<ModalProps> {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
|
||||
processingAddFilter,
|
||||
|
||||
processingConfigFilter,
|
||||
|
||||
handleSubmit,
|
||||
|
||||
modalType,
|
||||
|
||||
currentFilterData,
|
||||
|
||||
whitelist,
|
||||
|
||||
toggleFilteringModal,
|
||||
|
||||
filters,
|
||||
|
||||
t,
|
||||
|
||||
filtersCatalog,
|
||||
} = this.props;
|
||||
|
||||
|
||||
@@ -1,42 +1,69 @@
|
||||
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 { FORM_NAME } from '../../../helpers/constants';
|
||||
import { Input } from '../../ui/Controls/Input';
|
||||
|
||||
interface FormProps {
|
||||
pristine: boolean;
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
reset: (...args: unknown[]) => string;
|
||||
toggleRewritesModal: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
processingAdd: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
initialValues?: object;
|
||||
interface RewriteFormValues {
|
||||
domain: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
const Form = (props: FormProps) => {
|
||||
const { t, handleSubmit, reset, pristine, submitting, toggleRewritesModal, processingAdd } = props;
|
||||
type 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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="modal-body">
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>domain_desc</Trans>
|
||||
</div>
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="domain"
|
||||
<Controller
|
||||
name="domain"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_domain')}
|
||||
validate={[validateRequiredValue, validateDomain]}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
validate: validateDomain,
|
||||
required: validateRequiredValue,
|
||||
},
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
data-testid="rewrites_domain"
|
||||
placeholder={t('form_domain')}
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Trans>examples_title</Trans>:
|
||||
@@ -44,7 +71,6 @@ const Form = (props: FormProps) => {
|
||||
<li>
|
||||
<code>example.org</code> – <Trans>example_rewrite_domain</Trans>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>*.example.org</code> –
|
||||
<span>
|
||||
@@ -53,14 +79,24 @@ const Form = (props: FormProps) => {
|
||||
</li>
|
||||
</ol>
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="answer"
|
||||
<Controller
|
||||
name="answer"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_answer')}
|
||||
validate={[validateRequiredValue, validateAnswer]}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
validate: validateAnswer,
|
||||
required: validateRequiredValue,
|
||||
},
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
data-testid="rewrites_answer"
|
||||
placeholder={t('form_answer')}
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,8 +113,9 @@ const Form = (props: FormProps) => {
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="rewrites_cancel"
|
||||
className="btn btn-secondary btn-standard"
|
||||
disabled={submitting || processingAdd}
|
||||
disabled={isSubmitting || processingAdd}
|
||||
onClick={() => {
|
||||
reset();
|
||||
toggleRewritesModal();
|
||||
@@ -88,8 +125,9 @@ const Form = (props: FormProps) => {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="rewrites_save"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || pristine || processingAdd}>
|
||||
disabled={isSubmitting || !isDirty || processingAdd}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
@@ -98,10 +136,4 @@ const Form = (props: FormProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default flow([
|
||||
withTranslation(),
|
||||
reduxForm({
|
||||
form: FORM_NAME.REWRITES,
|
||||
enableReinitialize: true,
|
||||
}),
|
||||
])(Form);
|
||||
export default Form;
|
||||
|
||||
@@ -14,7 +14,7 @@ interface ModalProps {
|
||||
processingAdd: boolean;
|
||||
processingDelete: boolean;
|
||||
modalType: string;
|
||||
currentRewrite?: object;
|
||||
currentRewrite?: { answer: string, domain: string; };
|
||||
}
|
||||
|
||||
const Modal = (props: ModalProps) => {
|
||||
@@ -23,7 +23,6 @@ const Modal = (props: ModalProps) => {
|
||||
handleSubmit,
|
||||
toggleRewritesModal,
|
||||
processingAdd,
|
||||
processingDelete,
|
||||
modalType,
|
||||
currentRewrite,
|
||||
} = props;
|
||||
@@ -50,11 +49,10 @@ const Modal = (props: ModalProps) => {
|
||||
</div>
|
||||
|
||||
<Form
|
||||
initialValues={{ ...currentRewrite }}
|
||||
onSubmit={handleSubmit}
|
||||
toggleRewritesModal={toggleRewritesModal}
|
||||
processingAdd={processingAdd}
|
||||
processingDelete={processingDelete}
|
||||
currentRewrite={currentRewrite}
|
||||
/>
|
||||
</div>
|
||||
</ReactModal>
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { toggleAllServices } from '../../../helpers/helpers';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { renderServiceField } from '../../../helpers/form';
|
||||
import { FORM_NAME } from '../../../helpers/constants';
|
||||
import { ServiceField } from './ServiceField';
|
||||
|
||||
export type BlockedService = {
|
||||
id: string;
|
||||
name: string;
|
||||
icon_svg: string;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
blocked_services: Record<string, boolean>;
|
||||
};
|
||||
|
||||
interface FormProps {
|
||||
blockedServices: unknown[];
|
||||
pristine: boolean;
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
initialValues: Record<string, boolean>;
|
||||
blockedServices: BlockedService[];
|
||||
onSubmit: (values: FormValues) => void;
|
||||
processing: boolean;
|
||||
processingSet: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
}
|
||||
|
||||
const Form = (props: FormProps) => {
|
||||
const { blockedServices, handleSubmit, change, pristine, submitting, processing, processingSet } = props;
|
||||
export const Form = ({ initialValues, blockedServices, processing, processingSet, onSubmit }: FormProps) => {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="form__group">
|
||||
<div className="row mb-4">
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="blocked_services_block_all"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={processing || processingSet}
|
||||
onClick={() => toggleAllServices(blockedServices, change, true)}>
|
||||
onClick={() => handleToggleAllServices(true)}>
|
||||
<Trans>block_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
@@ -40,24 +57,30 @@ const Form = (props: FormProps) => {
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="blocked_services_unblock_all"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={processing || processingSet}
|
||||
onClick={() => toggleAllServices(blockedServices, change, false)}>
|
||||
onClick={() => handleToggleAllServices(false)}>
|
||||
<Trans>unblock_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="services">
|
||||
{blockedServices.map((service: any) => (
|
||||
<Field
|
||||
{blockedServices.map((service: BlockedService) => (
|
||||
<Controller
|
||||
key={service.id}
|
||||
icon={service.icon_svg}
|
||||
name={`blocked_services.${service.id}`}
|
||||
type="checkbox"
|
||||
component={renderServiceField}
|
||||
placeholder={service.name}
|
||||
disabled={processing || processingSet}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<ServiceField
|
||||
{...field}
|
||||
data-testid={`blocked_services_${service.id}`}
|
||||
placeholder={service.name}
|
||||
disabled={processing || processingSet}
|
||||
icon={service.icon_svg}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -66,19 +89,12 @@ const Form = (props: FormProps) => {
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="blocked_services_save"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={submitting || pristine || processing || processingSet}>
|
||||
disabled={isSubmitting || processing || processingSet}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</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 Form from './Form';
|
||||
import { Form } from './Form';
|
||||
|
||||
import Card from '../../ui/Card';
|
||||
import { getBlockedServices, getAllBlockedServices, updateBlockedServices } from '../../../actions/services';
|
||||
@@ -86,7 +86,8 @@ const Services = () => {
|
||||
<Card
|
||||
title={t('schedule_services')}
|
||||
subtitle={t('schedule_services_desc')}
|
||||
bodyType="card-body box-body--settings">
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<ScheduleForm schedule={services.list.schedule} onScheduleSubmit={handleScheduleSubmit} />
|
||||
</Card>
|
||||
</>
|
||||
|
||||
@@ -59,7 +59,7 @@ const Header = () => {
|
||||
<div className="header__column">
|
||||
<div className="header__right">
|
||||
{!processingProfile && name && (
|
||||
<a href="control/logout" className="btn btn-sm btn-outline-secondary">
|
||||
<a href="control/logout" className="btn btn-sm btn-outline-secondary" data-testid="sign_out">
|
||||
{t('sign_out')}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -288,7 +288,7 @@ const Row = memo(
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={style} className={className} onClick={onClick} role="row">
|
||||
<div style={style} className={className} onClick={onClick} role="row" data-testid="querylog_cell">
|
||||
<DateCell {...rowProps} />
|
||||
|
||||
<DomainCell {...rowProps} />
|
||||
|
||||
@@ -1,158 +1,81 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { Field, type InjectedFormProps, reduxForm } from 'redux-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
DEBOUNCE_FILTER_TIMEOUT,
|
||||
DEFAULT_LOGS_FILTER,
|
||||
FORM_NAME,
|
||||
RESPONSE_FILTER,
|
||||
RESPONSE_FILTER_QUERIES,
|
||||
} from '../../../helpers/constants';
|
||||
import { setLogsFilter } from '../../../actions/queryLogs';
|
||||
import useDebounce from '../../../helpers/useDebounce';
|
||||
|
||||
import { createOnBlurHandler, getLogsUrlParams } from '../../../helpers/helpers';
|
||||
import { getLogsUrlParams } from '../../../helpers/helpers';
|
||||
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import { RootState } from '../../../initialState';
|
||||
import { SearchField } from './SearchField';
|
||||
import { SearchFormValues } from '..';
|
||||
|
||||
interface renderFilterFieldProps {
|
||||
input: {
|
||||
value: string;
|
||||
};
|
||||
id: string;
|
||||
onClearInputClick: (...args: unknown[]) => unknown;
|
||||
type Props = {
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
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>}
|
||||
</>
|
||||
);
|
||||
setIsLoading: (value: boolean) => void;
|
||||
};
|
||||
|
||||
const FORM_NAMES = {
|
||||
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;
|
||||
|
||||
export const Form = ({ className, setIsLoading }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const { response_status, search } = useSelector(
|
||||
(state: RootState) => state?.form[FORM_NAME.LOGS_FILTER].values,
|
||||
shallowEqual,
|
||||
);
|
||||
const { register, watch, setValue } = useFormContext<SearchFormValues>();
|
||||
|
||||
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(() => {
|
||||
dispatch(
|
||||
setLogsFilter({
|
||||
response_status,
|
||||
response_status: responseStatusValue,
|
||||
search: debouncedSearch,
|
||||
}),
|
||||
);
|
||||
|
||||
history.replace(`${getLogsUrlParams(debouncedSearch, response_status)}`);
|
||||
}, [response_status, debouncedSearch]);
|
||||
history.replace(`${getLogsUrlParams(debouncedSearch, responseStatusValue)}`);
|
||||
}, [responseStatusValue, debouncedSearch]);
|
||||
|
||||
if (response_status && !(response_status in RESPONSE_FILTER_QUERIES)) {
|
||||
change(FORM_NAMES.response_status, DEFAULT_LOGS_FILTER[FORM_NAMES.response_status]);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (responseStatusValue && !(responseStatusValue in RESPONSE_FILTER_QUERIES)) {
|
||||
setValue('response_status', DEFAULT_LOGS_FILTER.response_status);
|
||||
}
|
||||
}, [responseStatusValue, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const { search: searchUrlParam } = queryString.parse(history.location.search);
|
||||
|
||||
if (searchUrlParam !== searchValue) {
|
||||
setValue('search', searchUrlParam ? searchUrlParam.toString() : '');
|
||||
}
|
||||
}, [history.location.search]);
|
||||
|
||||
const onInputClear = async () => {
|
||||
setIsLoading(true);
|
||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
history.push(getLogsUrlParams(DEFAULT_LOGS_FILTER.search, responseStatusValue));
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onEnterPress = (e: any) => {
|
||||
const onEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
setDebouncedSearch(search);
|
||||
setDebouncedSearch(searchValue);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeOnBlur = (data: any) => data.trim();
|
||||
|
||||
return (
|
||||
<form
|
||||
className="d-flex flex-wrap form-control--container"
|
||||
@@ -160,40 +83,29 @@ const Form = (props: FiltersFormProps & InjectedFormProps) => {
|
||||
e.preventDefault();
|
||||
}}>
|
||||
<div className="field__search">
|
||||
<Field
|
||||
id={FORM_NAMES.search}
|
||||
name={FORM_NAMES.search}
|
||||
component={renderFilterField}
|
||||
type="text"
|
||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||
<SearchField
|
||||
data-testid="querylog_search"
|
||||
value={searchValue}
|
||||
handleChange={(val) => setValue('search', val)}
|
||||
onKeyDown={onEnterPress}
|
||||
onClear={onInputClear}
|
||||
placeholder={t('domain_or_client')}
|
||||
tooltip={t('query_log_strict_search')}
|
||||
onClearInputClick={onInputClear}
|
||||
onKeyDown={onEnterPress}
|
||||
normalizeOnBlur={normalizeOnBlur}
|
||||
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field__select">
|
||||
<Field
|
||||
name={FORM_NAMES.response_status}
|
||||
component="select"
|
||||
className={classNames(
|
||||
'form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent',
|
||||
responseStatusClass,
|
||||
)}>
|
||||
<select
|
||||
{...register('response_status')}
|
||||
className="form-control custom-select custom-select--logs custom-select__arrow--left form-control--transparent d-sm-block">
|
||||
{Object.values(RESPONSE_FILTER).map(({ QUERY, LABEL, disabled }: any) => (
|
||||
<option key={LABEL} value={QUERY} disabled={disabled}>
|
||||
{t(LABEL)}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
</select>
|
||||
</div>
|
||||
</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 { useDispatch } from 'react-redux';
|
||||
|
||||
import { FiltersForm } from './Form';
|
||||
import { Form } from './Form';
|
||||
import { refreshFilteredLogs } from '../../../actions/queryLogs';
|
||||
import { addSuccessToast } from '../../../actions/toasts';
|
||||
|
||||
interface FiltersProps {
|
||||
filter: object;
|
||||
processingGetLogs: boolean;
|
||||
setIsLoading: (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
||||
const Filters = ({ setIsLoading }: FiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -38,7 +37,9 @@ const Filters = ({ filter, setIsLoading }: FiltersProps) => {
|
||||
</svg>
|
||||
</button>
|
||||
</h1>
|
||||
<FiltersForm responseStatusClass="d-sm-block" setIsLoading={setIsLoading} initialValues={filter} />
|
||||
<Form
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ interface InfiniteTableProps {
|
||||
isLoading: boolean;
|
||||
items: unknown[];
|
||||
isSmallScreen: boolean;
|
||||
currentQuery: string;
|
||||
setDetailedDataCurrent: Dispatch<SetStateAction<any>>;
|
||||
setButtonType: (...args: unknown[]) => unknown;
|
||||
setModalOpened: (...args: unknown[]) => unknown;
|
||||
@@ -27,6 +28,7 @@ const InfiniteTable = ({
|
||||
isLoading,
|
||||
items,
|
||||
isSmallScreen,
|
||||
currentQuery,
|
||||
setDetailedDataCurrent,
|
||||
setButtonType,
|
||||
setModalOpened,
|
||||
@@ -43,7 +45,7 @@ const InfiniteTable = ({
|
||||
|
||||
const listener = useCallback(() => {
|
||||
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 queryString from 'query-string';
|
||||
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';
|
||||
|
||||
@@ -29,7 +30,12 @@ import { BUTTON_PREFIX } from './Cells/helpers';
|
||||
import AnonymizerNotification from './AnonymizerNotification';
|
||||
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]) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -76,7 +82,6 @@ const Logs = () => {
|
||||
const {
|
||||
enabled,
|
||||
processingGetConfig,
|
||||
// processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
anonymize_client_ip: anonymizeClientIp,
|
||||
} = useSelector((state: RootState) => state.queryLogs, shallowEqual);
|
||||
@@ -88,6 +93,17 @@ const Logs = () => {
|
||||
const search = search_url_param || filter?.search || '';
|
||||
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 [detailedDataCurrent, setDetailedDataCurrent] = useState({});
|
||||
const [buttonType, setButtonType] = useState(BLOCK_ACTIONS.BLOCK);
|
||||
@@ -174,15 +190,12 @@ const Logs = () => {
|
||||
|
||||
const renderPage = () => (
|
||||
<>
|
||||
<Filters
|
||||
filter={{
|
||||
response_status,
|
||||
search,
|
||||
}}
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
// processingAdditionalLogs={processingAdditionalLogs}
|
||||
/>
|
||||
<FormProvider {...formMethods}>
|
||||
<Filters
|
||||
setIsLoading={setIsLoading}
|
||||
processingGetLogs={processingGetLogs}
|
||||
/>
|
||||
</FormProvider>
|
||||
|
||||
<InfiniteTable
|
||||
isLoading={isLoading}
|
||||
@@ -191,6 +204,7 @@ const Logs = () => {
|
||||
setDetailedDataCurrent={setDetailedDataCurrent}
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
currentQuery={currentQuery}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -12,7 +12,7 @@ import whoisCell from './whoisCell';
|
||||
|
||||
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 { TABLES_MIN_ROWS } from '../../../helpers/constants';
|
||||
|
||||
@@ -66,7 +66,7 @@ class AutoClients extends Component<AutoClientsProps> {
|
||||
return (
|
||||
<div className="logs__row">
|
||||
<div className="logs__text" title={clientStats}>
|
||||
<LogsSearchLink search={row.original.ip}>{clientStats}</LogsSearchLink>
|
||||
<LogsSearchLink search={row.original.ip}>{formatNumber(clientStats)}</LogsSearchLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import ReactTable from 'react-table';
|
||||
import { getAllBlockedServices, getBlockedServices } from '../../../../actions/services';
|
||||
|
||||
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 Card from '../../../ui/Card';
|
||||
@@ -111,6 +111,12 @@ const ClientsTable = ({
|
||||
config.tags = [];
|
||||
}
|
||||
|
||||
if (values.ids) {
|
||||
config.ids = values.ids.map((id) => id.name);
|
||||
} else {
|
||||
config.ids = [];
|
||||
}
|
||||
|
||||
if (typeof values.upstreams_cache_size === 'string') {
|
||||
config.upstreams_cache_size = 0;
|
||||
}
|
||||
@@ -300,12 +306,15 @@ const ClientsTable = ({
|
||||
sortMethod: (a: any, b: any) => b - a,
|
||||
minWidth: 120,
|
||||
Cell: (row: any) => {
|
||||
const content = CellWrap(row);
|
||||
|
||||
if (!row.value) {
|
||||
let content = row.value;
|
||||
if (typeof content === "number") {
|
||||
content = formatNumber(content);
|
||||
} else {
|
||||
content = CellWrap(row);
|
||||
}
|
||||
if (!content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return <LogsSearchLink search={row.original.name}>{content}</LogsSearchLink>;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,514 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { Field, FieldArray, reduxForm, formValueSelector, FormErrors } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import Select from 'react-select';
|
||||
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
import Tabs from '../../ui/Tabs';
|
||||
|
||||
import Examples from '../Dns/Upstream/Examples';
|
||||
|
||||
import { ScheduleForm } from '../../Filters/Services/ScheduleForm';
|
||||
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
|
||||
import {
|
||||
toNumber,
|
||||
renderInputField,
|
||||
renderGroupField,
|
||||
CheckboxField,
|
||||
renderServiceField,
|
||||
renderTextareaField,
|
||||
} from '../../../helpers/form';
|
||||
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
|
||||
import { CLIENT_ID_LINK, FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
|
||||
import './Service.css';
|
||||
import { RootState } from '../../../initialState';
|
||||
|
||||
const settingsCheckboxes = [
|
||||
{
|
||||
name: 'use_global_settings',
|
||||
placeholder: 'client_global_settings',
|
||||
},
|
||||
{
|
||||
name: 'filtering_enabled',
|
||||
placeholder: 'block_domain_use_filters_and_hosts',
|
||||
},
|
||||
{
|
||||
name: 'safebrowsing_enabled',
|
||||
placeholder: 'use_adguard_browsing_sec',
|
||||
},
|
||||
{
|
||||
name: 'parental_enabled',
|
||||
placeholder: 'use_adguard_parental',
|
||||
},
|
||||
];
|
||||
|
||||
const logAndStatsCheckboxes = [
|
||||
{
|
||||
name: 'ignore_querylog',
|
||||
placeholder: 'ignore_query_log',
|
||||
},
|
||||
{
|
||||
name: 'ignore_statistics',
|
||||
placeholder: 'ignore_statistics',
|
||||
},
|
||||
];
|
||||
const validate = (values: any): FormErrors<any, string> => {
|
||||
const errors: {
|
||||
name?: string;
|
||||
ids?: string[];
|
||||
} = {};
|
||||
const { name, ids } = values;
|
||||
|
||||
errors.name = validateRequiredValue(name);
|
||||
|
||||
if (ids && ids.length) {
|
||||
const idArrayErrors: any = [];
|
||||
ids.forEach((id: any, idx: any) => {
|
||||
idArrayErrors[idx] = validateRequiredValue(id) || validateClientId(id);
|
||||
});
|
||||
|
||||
if (idArrayErrors.length) {
|
||||
errors.ids = idArrayErrors;
|
||||
}
|
||||
}
|
||||
// @ts-expect-error FIXME: ts migration
|
||||
return errors;
|
||||
};
|
||||
|
||||
const renderFieldsWrapper = (placeholder: any, buttonTitle: any) =>
|
||||
function cell(row: any) {
|
||||
const { fields } = row;
|
||||
return (
|
||||
<div className="form__group">
|
||||
{fields.map((ip: any, index: any) => (
|
||||
<div key={index} className="mb-1">
|
||||
<Field
|
||||
name={ip}
|
||||
component={renderGroupField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={placeholder}
|
||||
isActionAvailable={index !== 0}
|
||||
removeField={() => fields.remove(index)}
|
||||
normalizeOnBlur={(data: any) => data.trim()}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link btn-block btn-sm"
|
||||
onClick={() => fields.push()}
|
||||
title={buttonTitle}>
|
||||
<svg className="icon icon--24">
|
||||
<use xlinkHref="#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Should create function outside of component to prevent component re-renders
|
||||
const renderFields = renderFieldsWrapper(i18n.t('form_enter_id'), i18n.t('form_add_id'));
|
||||
|
||||
interface renderMultiselectProps {
|
||||
input: {
|
||||
name: string;
|
||||
value: string;
|
||||
checked: boolean;
|
||||
onChange: (...args: unknown[]) => unknown;
|
||||
onBlur: (...args: unknown[]) => unknown;
|
||||
};
|
||||
placeholder?: string;
|
||||
options?: unknown[];
|
||||
}
|
||||
|
||||
const renderMultiselect = (props: renderMultiselectProps) => {
|
||||
const { input, placeholder, options } = props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...input}
|
||||
options={options}
|
||||
className="basic-multi-select"
|
||||
classNamePrefix="select"
|
||||
onChange={(value: any) => input.onChange(value)}
|
||||
onBlur={() => input.onBlur(input.value)}
|
||||
placeholder={placeholder}
|
||||
blurInputOnSelect={false}
|
||||
isMulti
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormProps {
|
||||
pristine: boolean;
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
reset: (...args: unknown[]) => string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
handleClose: (...args: unknown[]) => unknown;
|
||||
useGlobalSettings?: boolean;
|
||||
useGlobalServices?: boolean;
|
||||
blockedServicesSchedule?: {
|
||||
time_zone: string;
|
||||
};
|
||||
t: (...args: unknown[]) => string;
|
||||
processingAdding: boolean;
|
||||
processingUpdating: boolean;
|
||||
invalid: boolean;
|
||||
tagsOptions: unknown[];
|
||||
initialValues?: {
|
||||
safe_search: any;
|
||||
};
|
||||
}
|
||||
|
||||
let Form = (props: FormProps) => {
|
||||
const {
|
||||
t,
|
||||
handleSubmit,
|
||||
reset,
|
||||
change,
|
||||
submitting,
|
||||
useGlobalSettings,
|
||||
useGlobalServices,
|
||||
blockedServicesSchedule,
|
||||
handleClose,
|
||||
processingAdding,
|
||||
processingUpdating,
|
||||
invalid,
|
||||
tagsOptions,
|
||||
initialValues,
|
||||
} = props;
|
||||
|
||||
const services = useSelector((store: RootState) => store?.services);
|
||||
const { safe_search } = initialValues;
|
||||
const safeSearchServices = { ...safe_search };
|
||||
delete safeSearchServices.enabled;
|
||||
|
||||
const [activeTabLabel, setActiveTabLabel] = useState('settings');
|
||||
|
||||
const handleScheduleSubmit = (values: any) => {
|
||||
change('blocked_services_schedule', { ...values });
|
||||
};
|
||||
|
||||
const tabs = {
|
||||
settings: {
|
||||
title: 'settings',
|
||||
|
||||
component: (
|
||||
<div title={props.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}>
|
||||
<Field
|
||||
name={setting.name}
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t(setting.placeholder)}
|
||||
disabled={setting.name !== 'use_global_settings' ? useGlobalSettings : false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="safe_search.enabled"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('enforce_safe_search')}
|
||||
disabled={useGlobalSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group--inner">
|
||||
{Object.keys(safeSearchServices).map((searchKey) => (
|
||||
<div key={searchKey}>
|
||||
<Field
|
||||
name={`safe_search.${searchKey}`}
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={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}>
|
||||
<Field
|
||||
name={setting.name}
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t(setting.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
block_services: {
|
||||
title: 'block_services',
|
||||
|
||||
component: (
|
||||
<div title={props.t('block_services')}>
|
||||
<div className="form__group">
|
||||
<Field
|
||||
name="use_global_blocked_services"
|
||||
type="checkbox"
|
||||
component={renderServiceField}
|
||||
placeholder={t('blocked_services_global')}
|
||||
modifier="service--global"
|
||||
/>
|
||||
|
||||
<div className="row mb-4">
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={useGlobalServices}
|
||||
onClick={() => toggleAllServices(services.allServices, change, true)}>
|
||||
<Trans>block_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="col-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-block"
|
||||
disabled={useGlobalServices}
|
||||
onClick={() => toggleAllServices(services.allServices, change, false)}>
|
||||
<Trans>unblock_all</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{services.allServices.length > 0 && (
|
||||
<div className="services">
|
||||
{services.allServices.map((service: any) => (
|
||||
<Field
|
||||
key={service.id}
|
||||
icon={service.icon_svg}
|
||||
name={`blocked_services.${service.id}`}
|
||||
type="checkbox"
|
||||
component={renderServiceField}
|
||||
placeholder={service.name}
|
||||
disabled={useGlobalServices}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
schedule_services: {
|
||||
title: 'schedule_services',
|
||||
component: (
|
||||
<>
|
||||
<div className="form__desc mb-4">
|
||||
<Trans>schedule_services_desc_client</Trans>
|
||||
</div>
|
||||
|
||||
<ScheduleForm
|
||||
schedule={blockedServicesSchedule}
|
||||
onScheduleSubmit={handleScheduleSubmit}
|
||||
clientForm
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
upstream_dns: {
|
||||
title: 'upstream_dns',
|
||||
|
||||
component: (
|
||||
<div title={props.t('upstream_dns')}>
|
||||
<div className="form__desc mb-3">
|
||||
<Trans
|
||||
components={[
|
||||
<a href="#dns" key="0">
|
||||
link
|
||||
</a>,
|
||||
]}>
|
||||
upstream_dns_client_desc
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
id="upstreams"
|
||||
name="upstreams"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control form-control--textarea mb-5"
|
||||
placeholder={t('upstream_dns')}
|
||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||
/>
|
||||
|
||||
<Examples />
|
||||
|
||||
<div className="form__label--bold mt-5 mb-3">{t('upstream_dns_cache_configuration')}</div>
|
||||
|
||||
<div className="form__group mb-2">
|
||||
<Field
|
||||
name="upstreams_cache_enabled"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={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>
|
||||
|
||||
<Field
|
||||
name="upstreams_cache_size"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
placeholder={t('enter_cache_size')}
|
||||
className="form-control"
|
||||
normalize={toNumber}
|
||||
min={0}
|
||||
max={UINT32_RANGE.MAX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const activeTab = tabs[activeTabLabel].component;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="form__group mb-0">
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="name"
|
||||
name="name"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_client_name')}
|
||||
normalizeOnBlur={(data: any) => data.trim()}
|
||||
/>
|
||||
</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">
|
||||
link
|
||||
</a>,
|
||||
]}>
|
||||
tags_desc
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
name="tags"
|
||||
component={renderMultiselect}
|
||||
placeholder={t('form_select_tags')}
|
||||
options={tagsOptions}
|
||||
/>
|
||||
</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">
|
||||
text
|
||||
</a>,
|
||||
]}>
|
||||
client_identifier_desc
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form__group">
|
||||
<FieldArray name="ids" component={renderFields} />
|
||||
</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={submitting}
|
||||
onClick={() => {
|
||||
reset();
|
||||
handleClose();
|
||||
}}>
|
||||
<Trans>cancel_btn</Trans>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || invalid || processingAdding || processingUpdating}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const selector = formValueSelector(FORM_NAME.CLIENT);
|
||||
|
||||
Form = connect((state) => {
|
||||
const useGlobalSettings = selector(state, 'use_global_settings');
|
||||
const useGlobalServices = selector(state, 'use_global_blocked_services');
|
||||
const blockedServicesSchedule = selector(state, 'blocked_services_schedule');
|
||||
return {
|
||||
useGlobalSettings,
|
||||
useGlobalServices,
|
||||
blockedServicesSchedule,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
export default flow([
|
||||
withTranslation(),
|
||||
reduxForm({
|
||||
form: FORM_NAME.CLIENT,
|
||||
enableReinitialize: true,
|
||||
validate,
|
||||
}),
|
||||
])(Form);
|
||||
@@ -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 { 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) => {
|
||||
if (initial && initial.blocked_services) {
|
||||
@@ -19,6 +26,7 @@ const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
|
||||
return {
|
||||
...initial,
|
||||
blocked_services: blocked,
|
||||
ids: normalizeIds(initial.ids),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,11 +34,14 @@ const getInitialData = ({ initial, modalType, clientId, clientName }: any) => {
|
||||
return {
|
||||
...initial,
|
||||
name: clientName,
|
||||
ids: [clientId],
|
||||
ids: [{ name: clientId }],
|
||||
};
|
||||
}
|
||||
|
||||
return initial;
|
||||
return {
|
||||
...initial,
|
||||
ids: normalizeIds(initial.ids),
|
||||
};
|
||||
};
|
||||
|
||||
interface ModalProps {
|
||||
@@ -41,7 +52,7 @@ interface ModalProps {
|
||||
handleClose: (...args: unknown[]) => unknown;
|
||||
processingAdding: boolean;
|
||||
processingUpdating: boolean;
|
||||
tagsOptions: unknown[];
|
||||
tagsOptions: { label: string; value: string }[];
|
||||
t: (...args: unknown[]) => string;
|
||||
clientId?: string;
|
||||
}
|
||||
@@ -85,7 +96,7 @@ const Modal = ({
|
||||
<Form
|
||||
initialValues={{ ...initialData }}
|
||||
onSubmit={handleSubmit}
|
||||
handleClose={handleClose}
|
||||
onClose={handleClose}
|
||||
processingAdding={processingAdding}
|
||||
processingUpdating={processingUpdating}
|
||||
tagsOptions={tagsOptions}
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { renderInputField, toNumber } from '../../../helpers/form';
|
||||
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
|
||||
import { UINT32_RANGE } from '../../../helpers/constants';
|
||||
import {
|
||||
validateIpv4,
|
||||
validateRequiredValue,
|
||||
validateIpv4RangeEnd,
|
||||
validateGatewaySubnetMask,
|
||||
validateIpForGatewaySubnetMask,
|
||||
validateIpv4,
|
||||
validateIpv4RangeEnd,
|
||||
validateNotInRange,
|
||||
validateRequiredValue,
|
||||
} from '../../../helpers/validators';
|
||||
import { RootState } from '../../../initialState';
|
||||
import { DhcpFormValues } from '.';
|
||||
import { Input } from '../../ui/Controls/Input';
|
||||
import { toNumber } from '../../../helpers/form';
|
||||
|
||||
interface FormDHCPv4Props {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
submitting: boolean;
|
||||
initialValues: { v4?: any };
|
||||
type FormDHCPv4Props = {
|
||||
processingConfig?: boolean;
|
||||
change: (field: string, value: any) => void;
|
||||
reset: () => void;
|
||||
ipv4placeholders?: {
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
@@ -30,127 +24,179 @@ interface FormDHCPv4Props {
|
||||
range_end: 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 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 interface_name = interfaces?.values?.interface_name;
|
||||
const interfaceName = watch('interface_name');
|
||||
const isInterfaceIncludesIpv4 = interfaces?.[interfaceName]?.ipv4_addresses;
|
||||
|
||||
const isInterfaceIncludesIpv4 = useSelector(
|
||||
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses,
|
||||
);
|
||||
const formValues = watch('v4');
|
||||
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 invalid =
|
||||
dhcp?.syncErrors ||
|
||||
interfaces?.syncErrors ||
|
||||
!isInterfaceIncludesIpv4 ||
|
||||
isEmptyConfig ||
|
||||
submitting ||
|
||||
processingConfig;
|
||||
|
||||
const validateRequired = useCallback(
|
||||
(value) => {
|
||||
if (isEmptyConfig) {
|
||||
return undefined;
|
||||
}
|
||||
return validateRequiredValue(value);
|
||||
},
|
||||
[isEmptyConfig],
|
||||
);
|
||||
const isDisabled = useMemo(() => {
|
||||
return isSubmitting || hasV4Errors || processingConfig || !isInterfaceIncludesIpv4 || isEmptyConfig;
|
||||
}, [isSubmitting, hasV4Errors, processingConfig, isInterfaceIncludesIpv4, isEmptyConfig]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row">
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<label>{t('dhcp_form_gateway_input')}</label>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="v4.gateway_ip"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.gateway_ip)}
|
||||
validate={[validateIpv4, validateRequired, validateNotInRange]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
ipv4: validateIpv4,
|
||||
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
|
||||
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 className="form__group form__group--settings">
|
||||
<label>{t('dhcp_form_subnet_input')}</label>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="v4.subnet_mask"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.subnet_mask)}
|
||||
validate={[validateRequired, validateGatewaySubnetMask]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
|
||||
subnet: validateGatewaySubnetMask,
|
||||
},
|
||||
}}
|
||||
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 className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<div className="form__group mb-0">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<label>{t('dhcp_form_range_title')}</label>
|
||||
</div>
|
||||
|
||||
<div className="col">
|
||||
<Field
|
||||
<Controller
|
||||
name="v4.range_start"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.range_start)}
|
||||
validate={[validateIpv4, validateIpForGatewaySubnetMask]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
ipv4: validateIpv4,
|
||||
gateway: validateIpForGatewaySubnetMask,
|
||||
},
|
||||
}}
|
||||
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 className="col">
|
||||
<Field
|
||||
<Controller
|
||||
name="v4.range_end"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.range_end)}
|
||||
validate={[validateIpv4, validateIpv4RangeEnd, validateIpForGatewaySubnetMask]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
ipv4: validateIpv4,
|
||||
rangeEnd: validateIpv4RangeEnd,
|
||||
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 className="form__group form__group--settings">
|
||||
<label>{t('dhcp_form_lease_title')}</label>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="v4.lease_duration"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.lease_duration)}
|
||||
validate={validateRequired}
|
||||
normalize={toNumber}
|
||||
min={1}
|
||||
max={UINT32_RANGE.MAX}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
required: (value) => (isEmptyConfig ? undefined : validateRequiredValue(value)),
|
||||
},
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...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 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')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -158,9 +204,4 @@ const FormDHCPv4 = ({ handleSubmit, submitting, processingConfig, ipv4placeholde
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm<
|
||||
Record<string, any>,
|
||||
Omit<FormDHCPv4Props, 'submitting' | 'handleSubmit' | 'reset' | 'change'>
|
||||
>({
|
||||
form: FORM_NAME.DHCPv4,
|
||||
})(FormDHCPv4);
|
||||
export default FormDHCPv4;
|
||||
|
||||
@@ -1,93 +1,92 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { renderInputField, toNumber } from '../../../helpers/form';
|
||||
import { FORM_NAME, UINT32_RANGE } from '../../../helpers/constants';
|
||||
import { UINT32_RANGE } from '../../../helpers/constants';
|
||||
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 {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
submitting: boolean;
|
||||
initialValues: {
|
||||
v6?: any;
|
||||
};
|
||||
change: (field: string, value: any) => void;
|
||||
reset: () => void;
|
||||
type FormDHCPv6Props = {
|
||||
processingConfig?: boolean;
|
||||
ipv6placeholders?: {
|
||||
range_start: string;
|
||||
range_end: 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 {
|
||||
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 interface_name = interfaces?.values?.interface_name;
|
||||
const formValues = watch('v6');
|
||||
const isEmptyConfig = !Object.values(formValues || {}).some(Boolean);
|
||||
|
||||
const isInterfaceIncludesIpv6 = useSelector(
|
||||
(state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses,
|
||||
);
|
||||
|
||||
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],
|
||||
);
|
||||
const isDisabled = useMemo(() => {
|
||||
return isSubmitting || !isValid || processingConfig || !isInterfaceIncludesIpv6 || isEmptyConfig;
|
||||
}, [isSubmitting, isValid, processingConfig, isInterfaceIncludesIpv6, isEmptyConfig]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row">
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<div className="form__group mb-0">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<label>{t('dhcp_form_range_title')}</label>
|
||||
</div>
|
||||
|
||||
<div className="col">
|
||||
<Field
|
||||
<Controller
|
||||
name="v6.range_start"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv6placeholders.range_start)}
|
||||
validate={[validateIpv6, validateRequired]}
|
||||
disabled={!isInterfaceIncludesIpv6}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: isInterfaceIncludesIpv6
|
||||
? {
|
||||
ipv6: validateIpv6,
|
||||
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 className="col">
|
||||
<Field
|
||||
<Controller
|
||||
name="v6.range_end"
|
||||
component="input"
|
||||
type="text"
|
||||
className="form-control disabled cursor--not-allowed"
|
||||
placeholder={t(ipv6placeholders.range_end)}
|
||||
value={t(ipv6placeholders.range_end)}
|
||||
disabled
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
data-testid="v6_range_end"
|
||||
placeholder={t(ipv6placeholders.range_end)}
|
||||
error={fieldState.error?.message}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,25 +96,43 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
|
||||
|
||||
<div className="row">
|
||||
<div className="col-lg-6 form__group form__group--settings">
|
||||
<label>{t('dhcp_form_lease_title')}</label>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="v6.lease_duration"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t(ipv6placeholders.lease_duration)}
|
||||
validate={validateRequired}
|
||||
normalizeOnBlur={toNumber}
|
||||
min={1}
|
||||
max={UINT32_RANGE.MAX}
|
||||
disabled={!isInterfaceIncludesIpv6}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: isInterfaceIncludesIpv6
|
||||
? {
|
||||
required: validateRequiredValue,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
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 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')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -123,9 +140,4 @@ const FormDHCPv6 = ({ handleSubmit, submitting, processingConfig, ipv6placeholde
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm<
|
||||
Record<string, any>,
|
||||
Omit<FormDHCPv6Props, 'handleSubmit' | 'change' | 'submitting' | 'reset'>
|
||||
>({
|
||||
form: FORM_NAME.DHCPv6,
|
||||
})(FormDHCPv6);
|
||||
export default FormDHCPv6;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { renderSelectField } from '../../../helpers/form';
|
||||
import { validateRequiredValue } from '../../../helpers/validators';
|
||||
import { FORM_NAME } from '../../../helpers/constants';
|
||||
import { RootState } from '../../../initialState';
|
||||
import { DhcpFormValues } from '.';
|
||||
|
||||
const renderInterfaces = (interfaces: any) =>
|
||||
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;
|
||||
hardware_address: 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">
|
||||
<ul className="list-unstyled m-0">
|
||||
{getInterfaceValues({
|
||||
@@ -77,11 +75,15 @@ const renderInterfaceValues = ({ gateway_ip, hardware_address, ip_addresses }: r
|
||||
|
||||
const Interfaces = () => {
|
||||
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 =
|
||||
useSelector((store: RootState) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
|
||||
const interface_name = watch('interface_name');
|
||||
|
||||
if (processingInterfaces || !interfaces) {
|
||||
return null;
|
||||
@@ -92,27 +94,34 @@ const Interfaces = () => {
|
||||
return (
|
||||
<div className="row dhcp__interfaces">
|
||||
<div className="col col__dhcp">
|
||||
<Field
|
||||
name="interface_name"
|
||||
component={renderSelectField}
|
||||
<label htmlFor="interface_name" className="form__label">
|
||||
{t('dhcp_interface_select')}
|
||||
</label>
|
||||
<select
|
||||
id="interface_name"
|
||||
data-testid="interface_name"
|
||||
className="form-control custom-select pl-4 col-md"
|
||||
validate={[validateRequiredValue]}
|
||||
label="dhcp_interface_select">
|
||||
disabled={enabled}
|
||||
{...register('interface_name', {
|
||||
validate: validateRequiredValue,
|
||||
})}>
|
||||
<option value="" disabled={enabled}>
|
||||
{t('dhcp_interface_select')}
|
||||
</option>
|
||||
{renderInterfaces(interfaces)}
|
||||
</Field>
|
||||
</select>
|
||||
{errors.interface_name && (
|
||||
<div className="form__message form__message--error">{t(errors.interface_name.message)}</div>
|
||||
)}
|
||||
</div>
|
||||
{interfaceValue && renderInterfaceValues({
|
||||
gateway_ip: interfaceValue.gateway_ip,
|
||||
hardware_address: interfaceValue.hardware_address,
|
||||
ip_addresses: interfaceValue.ip_addresses
|
||||
})}
|
||||
{interfaceValue &&
|
||||
renderInterfaceValues({
|
||||
gateway_ip: interfaceValue.gateway_ip,
|
||||
hardware_address: interfaceValue.hardware_address,
|
||||
ip_addresses: interfaceValue.ip_addresses,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm({
|
||||
form: FORM_NAME.DHCP_INTERFACES,
|
||||
})(Interfaces);
|
||||
export default Interfaces;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||
|
||||
import { renderInputField, normalizeMac } from '../../../../helpers/form';
|
||||
import { normalizeMac } from '../../../../helpers/form';
|
||||
import {
|
||||
validateIpv4,
|
||||
validateMac,
|
||||
@@ -12,12 +11,12 @@ import {
|
||||
validateIpv4InCidr,
|
||||
validateIpGateway,
|
||||
} from '../../../../helpers/validators';
|
||||
import { FORM_NAME } from '../../../../helpers/constants';
|
||||
|
||||
import { toggleLeaseModal } from '../../../../actions';
|
||||
import { RootState } from '../../../../initialState';
|
||||
import { Input } from '../../../ui/Controls/Input';
|
||||
|
||||
interface FormStaticLeaseProps {
|
||||
type Props = {
|
||||
initialValues?: {
|
||||
mac?: string;
|
||||
ip?: string;
|
||||
@@ -25,63 +24,91 @@ interface FormStaticLeaseProps {
|
||||
cidr?: string;
|
||||
gatewayIp?: string;
|
||||
};
|
||||
pristine: boolean;
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
reset: () => void;
|
||||
submitting: boolean;
|
||||
processingAdding?: boolean;
|
||||
cidr?: string;
|
||||
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 dispatch = useDispatch();
|
||||
|
||||
const dynamicLease = useSelector((store: RootState) => store.dhcp.leaseModalConfig, shallowEqual);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitting, isDirty },
|
||||
} = useForm({
|
||||
defaultValues: initialValues,
|
||||
mode: 'onBlur',
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
reset();
|
||||
dispatch(toggleLeaseModal());
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="modal-body">
|
||||
<div className="form__group">
|
||||
<Field
|
||||
id="mac"
|
||||
<Controller
|
||||
name="mac"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_mac')}
|
||||
normalize={normalizeMac}
|
||||
validate={[validateRequiredValue, validateMac]}
|
||||
disabled={isEdit}
|
||||
control={control}
|
||||
rules={{ validate: { required: validateRequiredValue, mac: validateMac } }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
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 className="form__group">
|
||||
<Field
|
||||
id="ip"
|
||||
<Controller
|
||||
name="ip"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_subnet_ip', { cidr })}
|
||||
validate={[validateRequiredValue, validateIpv4, validateIpv4InCidr, validateIpGateway]}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
required: validateRequiredValue,
|
||||
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 className="form__group">
|
||||
<Field
|
||||
id="hostname"
|
||||
<Controller
|
||||
name="hostname"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_hostname')}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
data-testid="static_lease_hostname"
|
||||
error={fieldState.error?.message}
|
||||
placeholder={t('form_enter_hostname')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,16 +117,18 @@ const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cid
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="static_lease_cancel"
|
||||
className="btn btn-secondary btn-standard"
|
||||
disabled={submitting}
|
||||
disabled={isSubmitting}
|
||||
onClick={onClick}>
|
||||
<Trans>cancel_btn</Trans>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="static_lease_save"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || processingAdding || (pristine && !dynamicLease)}>
|
||||
disabled={isSubmitting || processingAdding || (!isDirty && !dynamicLease)}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
</div>
|
||||
@@ -107,8 +136,3 @@ const Form = ({ handleSubmit, reset, pristine, submitting, processingAdding, cid
|
||||
</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 { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Form from './Form';
|
||||
import { Form } from './Form';
|
||||
|
||||
import { toggleLeaseModal } from '../../../../actions';
|
||||
import { MODAL_TYPE } from '../../../../helpers/constants';
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { destroy } from 'redux-form';
|
||||
import { DHCP_DESCRIPTION_PLACEHOLDERS, DHCP_FORM_NAMES, STATUS_RESPONSE, FORM_NAME } from '../../../helpers/constants';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { DHCP_DESCRIPTION_PLACEHOLDERS, STATUS_RESPONSE } from '../../../helpers/constants';
|
||||
|
||||
import Leases from './Leases';
|
||||
|
||||
@@ -40,6 +40,55 @@ import {
|
||||
import './index.css';
|
||||
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 { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
@@ -65,12 +114,21 @@ const Dhcp = () => {
|
||||
modalType,
|
||||
} = useSelector((state: RootState) => state.dhcp, shallowEqual);
|
||||
|
||||
const interface_name =
|
||||
useSelector((state: RootState) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name);
|
||||
const isInterfaceIncludesIpv4 =
|
||||
useSelector((state: RootState) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses);
|
||||
const methods = useForm<DhcpFormValues>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
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 [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6);
|
||||
@@ -85,6 +143,22 @@ const Dhcp = () => {
|
||||
}
|
||||
}, [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(() => {
|
||||
const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? [];
|
||||
const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? [];
|
||||
@@ -103,13 +177,17 @@ const Dhcp = () => {
|
||||
const clear = () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
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(getDhcpStatus());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
const handleSubmit = (values: DhcpFormValues) => {
|
||||
dispatch(
|
||||
setDhcpConfig({
|
||||
interface_name,
|
||||
@@ -130,12 +208,7 @@ const Dhcp = () => {
|
||||
const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName;
|
||||
|
||||
const getToggleDhcpButton = () => {
|
||||
const filledConfig =
|
||||
interface_name &&
|
||||
(Object.values(v4)
|
||||
|
||||
.every(Boolean) ||
|
||||
Object.values(v6).every(Boolean));
|
||||
const filledConfig = interface_name && (Object.values(v4).every(Boolean) || Object.values(v6).every(Boolean));
|
||||
|
||||
const className = classNames('btn btn-sm', {
|
||||
'btn-gray': enabled,
|
||||
@@ -173,9 +246,6 @@ const Dhcp = () => {
|
||||
|
||||
const toggleModal = () => dispatch(toggleLeaseModal());
|
||||
|
||||
const initialV4 = enteredSomeV4Value ? v4 : {};
|
||||
const initialV6 = enteredSomeV6Value ? v6 : {};
|
||||
|
||||
if (processing || processingInterfaces) {
|
||||
return <Loading />;
|
||||
}
|
||||
@@ -196,19 +266,13 @@ const Dhcp = () => {
|
||||
|
||||
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(
|
||||
dhcp?.syncErrors ||
|
||||
!isInterfaceIncludesIpv4 ||
|
||||
isEmptyConfig ||
|
||||
processingConfig ||
|
||||
!inputtedIPv4values,
|
||||
!isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values,
|
||||
);
|
||||
const cidr = inputtedIPv4values
|
||||
? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}`
|
||||
: '';
|
||||
const cidr = inputtedIPv4values ? `${ipv4Config.gateway_ip}/${subnetMaskToBitMask(ipv4Config.subnet_mask)}` : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -246,29 +310,30 @@ const Dhcp = () => {
|
||||
</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 && (
|
||||
<Card title={t('dhcp_leases')} bodyType="card-body box-body--settings">
|
||||
<div className="row">
|
||||
@@ -290,7 +355,7 @@ const Dhcp = () => {
|
||||
processingDeleting={processingDeleting}
|
||||
processingUpdating={processingUpdating}
|
||||
cidr={cidr}
|
||||
gatewayIp={dhcp?.values?.v4?.gateway_ip}
|
||||
gatewayIp={ipv4Config.gateway_ip}
|
||||
/>
|
||||
|
||||
<div className="btn-list mt-2">
|
||||
|
||||
@@ -1,118 +1,140 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import i18next from 'i18next';
|
||||
import { CLIENT_ID_LINK } from '../../../../helpers/constants';
|
||||
import { removeEmptyLines, trimMultilineString } from '../../../../helpers/helpers';
|
||||
import { Textarea } from '../../../ui/Controls/Textarea';
|
||||
|
||||
import { renderTextareaField } from '../../../../helpers/form';
|
||||
import { trimMultilineString, removeEmptyLines } from '../../../../helpers/helpers';
|
||||
import { CLIENT_ID_LINK, FORM_NAME } from '../../../../helpers/constants';
|
||||
type FormData = {
|
||||
allowed_clients: string;
|
||||
disallowed_clients: string;
|
||||
blocked_hosts: string;
|
||||
};
|
||||
|
||||
const fields = [
|
||||
const fields: {
|
||||
id: keyof FormData;
|
||||
title: string;
|
||||
subtitle: ReactNode;
|
||||
normalizeOnBlur: (value: string) => string;
|
||||
}[] = [
|
||||
{
|
||||
id: 'allowed_clients',
|
||||
title: 'access_allowed_title',
|
||||
subtitle: 'access_allowed_desc',
|
||||
title: i18next.t('access_allowed_title'),
|
||||
subtitle: (
|
||||
<Trans
|
||||
components={{
|
||||
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||
}}>
|
||||
access_allowed_desc
|
||||
</Trans>
|
||||
),
|
||||
normalizeOnBlur: removeEmptyLines,
|
||||
},
|
||||
{
|
||||
id: 'disallowed_clients',
|
||||
title: 'access_disallowed_title',
|
||||
subtitle: 'access_disallowed_desc',
|
||||
title: i18next.t('access_disallowed_title'),
|
||||
subtitle: (
|
||||
<Trans
|
||||
components={{
|
||||
a: <a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||
}}>
|
||||
access_disallowed_desc
|
||||
</Trans>
|
||||
),
|
||||
normalizeOnBlur: trimMultilineString,
|
||||
},
|
||||
{
|
||||
id: 'blocked_hosts',
|
||||
title: 'access_blocked_title',
|
||||
subtitle: 'access_blocked_desc',
|
||||
title: i18next.t('access_blocked_title'),
|
||||
subtitle: i18next.t('access_blocked_desc'),
|
||||
normalizeOnBlur: removeEmptyLines,
|
||||
},
|
||||
];
|
||||
|
||||
interface FormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
initialValues: object;
|
||||
type FormProps = {
|
||||
initialValues?: {
|
||||
allowed_clients?: string;
|
||||
disallowed_clients?: string;
|
||||
blocked_hosts?: string;
|
||||
};
|
||||
onSubmit: (data: FormData) => void;
|
||||
processingSet: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
textarea?: boolean;
|
||||
allowedClients?: string;
|
||||
}
|
||||
};
|
||||
|
||||
interface renderFieldProps {
|
||||
id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
disabled?: boolean;
|
||||
processingSet?: boolean;
|
||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
||||
}
|
||||
const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let Form = (props: FormProps) => {
|
||||
const { allowedClients, handleSubmit, submitting, invalid, processingSet } = props;
|
||||
const {
|
||||
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 = ({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
disabled = false,
|
||||
processingSet,
|
||||
normalizeOnBlur,
|
||||
}: renderFieldProps) => (
|
||||
<div key={id} className="form__group mb-5">
|
||||
<label className="form__label form__label--with-desc" htmlFor={id}>
|
||||
<Trans>{title}</Trans>
|
||||
}: {
|
||||
id: keyof FormData;
|
||||
title: string;
|
||||
subtitle: ReactNode;
|
||||
normalizeOnBlur: (value: string) => string;
|
||||
}) => {
|
||||
const disabled = allowedClients && id === 'disallowed_clients';
|
||||
|
||||
{disabled && (
|
||||
<>
|
||||
<span> </span>(<Trans>disabled</Trans>)
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
return (
|
||||
<div key={id} className="form__group mb-5">
|
||||
<label className="form__label form__label--with-desc" htmlFor={id}>
|
||||
{title}
|
||||
{disabled && <> ({t('disabled')})</>}
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans
|
||||
components={{
|
||||
a: (
|
||||
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
|
||||
text
|
||||
</a>
|
||||
),
|
||||
}}>
|
||||
{subtitle}
|
||||
</Trans>
|
||||
<div className="form__desc form__desc--top">{subtitle}</div>
|
||||
|
||||
<Controller
|
||||
name={id}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
id={id}
|
||||
data-testid={id}
|
||||
disabled={disabled || processingSet}
|
||||
onBlur={(e) => {
|
||||
field.onChange(normalizeOnBlur(e.target.value));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{fields.map((f) => {
|
||||
return renderField({
|
||||
...f,
|
||||
disabled: allowedClients && f.id === 'disallowed_clients' || false
|
||||
});
|
||||
})}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{fields.map((f) => renderField(f))}
|
||||
|
||||
<div className="card-actions">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="access_save"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || invalid || processingSet}>
|
||||
<Trans>save_config</Trans>
|
||||
disabled={isSubmitting || !isDirty || processingSet}>
|
||||
{t('save_config')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,18 +142,4 @@ let Form = (props: FormProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const selector = formValueSelector(FORM_NAME.ACCESS);
|
||||
|
||||
Form = connect((state) => {
|
||||
const allowedClients = selector(state, 'allowed_clients');
|
||||
return {
|
||||
allowedClients,
|
||||
};
|
||||
})(Form);
|
||||
|
||||
export default flow([
|
||||
withTranslation(),
|
||||
reduxForm({
|
||||
form: FORM_NAME.ACCESS,
|
||||
}),
|
||||
])(Form);
|
||||
export default Form;
|
||||
|
||||
@@ -1,52 +1,72 @@
|
||||
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 { 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 i18next from 'i18next';
|
||||
import { clearDnsCache } from '../../../../actions/dnsConfig';
|
||||
import { CACHE_CONFIG_FIELDS, UINT32_RANGE } from '../../../../helpers/constants';
|
||||
import { replaceZeroWithEmptyString } from '../../../../helpers/helpers';
|
||||
import { RootState } from '../../../../initialState';
|
||||
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||
|
||||
const INPUTS_FIELDS = [
|
||||
{
|
||||
name: CACHE_CONFIG_FIELDS.cache_size,
|
||||
title: 'cache_size',
|
||||
description: 'cache_size_desc',
|
||||
placeholder: 'enter_cache_size',
|
||||
title: i18next.t('cache_size'),
|
||||
description: i18next.t('cache_size_desc'),
|
||||
placeholder: i18next.t('enter_cache_size'),
|
||||
},
|
||||
{
|
||||
name: CACHE_CONFIG_FIELDS.cache_ttl_min,
|
||||
title: 'cache_ttl_min_override',
|
||||
description: 'cache_ttl_min_override_desc',
|
||||
placeholder: 'enter_cache_ttl_min_override',
|
||||
title: i18next.t('cache_ttl_min_override'),
|
||||
description: i18next.t('cache_ttl_min_override_desc'),
|
||||
placeholder: i18next.t('enter_cache_ttl_min_override'),
|
||||
},
|
||||
{
|
||||
name: CACHE_CONFIG_FIELDS.cache_ttl_max,
|
||||
title: 'cache_ttl_max_override',
|
||||
description: 'cache_ttl_max_override_desc',
|
||||
placeholder: 'enter_cache_ttl_max_override',
|
||||
title: i18next.t('cache_ttl_max_override'),
|
||||
description: i18next.t('cache_ttl_max_override_desc'),
|
||||
placeholder: i18next.t('enter_cache_ttl_max_override'),
|
||||
},
|
||||
];
|
||||
|
||||
interface CacheFormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
}
|
||||
type FormData = {
|
||||
cache_size: number;
|
||||
cache_ttl_min: number;
|
||||
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 dispatch = useDispatch();
|
||||
|
||||
const { processingSetConfig } = useSelector((state: RootState) => state.dnsConfig, shallowEqual);
|
||||
const { cache_ttl_max, cache_ttl_min } = useSelector(
|
||||
(state: RootState) => state.form[FORM_NAME.CACHE].values,
|
||||
shallowEqual,
|
||||
);
|
||||
const { processingSetConfig } = useSelector((state: RootState) => state.dnsConfig);
|
||||
|
||||
const {
|
||||
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;
|
||||
|
||||
@@ -57,29 +77,30 @@ const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row">
|
||||
{INPUTS_FIELDS.map(({ name, title, description, placeholder }) => (
|
||||
<div className="col-12" key={name}>
|
||||
<div className="col-12 col-md-7 p-0">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor={name} className="form__label form__label--with-desc">
|
||||
{t(title)}
|
||||
{title}
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">{t(description)}</div>
|
||||
<div className="form__desc form__desc--top">{description}</div>
|
||||
|
||||
<Field
|
||||
name={name}
|
||||
<input
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
placeholder={t(placeholder)}
|
||||
disabled={processingSetConfig}
|
||||
data-testid={`dns_${name}`}
|
||||
className="form-control"
|
||||
normalizeOnBlur={replaceZeroWithEmptyString}
|
||||
normalize={toNumber}
|
||||
placeholder={placeholder}
|
||||
disabled={processingSetConfig}
|
||||
min={0}
|
||||
max={UINT32_RANGE.MAX}
|
||||
{...register(name as keyof FormData, {
|
||||
valueAsNumber: true,
|
||||
setValueAs: (value) => replaceZeroWithEmptyString(value),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,13 +112,18 @@ const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="cache_optimistic"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('cache_optimistic')}
|
||||
disabled={processingSetConfig}
|
||||
subtitle={t('cache_optimistic_desc')}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid="dns_cache_optimistic"
|
||||
title={t('cache_optimistic')}
|
||||
subtitle={t('cache_optimistic_desc')}
|
||||
disabled={processingSetConfig}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,19 +131,21 @@ const Form = ({ handleSubmit, submitting, invalid }: CacheFormProps) => {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="dns_save"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={submitting || invalid || processingSetConfig || minExceedsMax}>
|
||||
<Trans>save_btn</Trans>
|
||||
disabled={isSubmitting || !isDirty || processingSetConfig || minExceedsMax}>
|
||||
{t('save_btn')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-testid="dns_clear"
|
||||
className="btn btn-outline-secondary btn-standard form__button"
|
||||
onClick={handleClearCache}>
|
||||
<Trans>clear_cache</Trans>
|
||||
{t('clear_cache')}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm({ form: FORM_NAME.CACHE })(Form);
|
||||
export default Form;
|
||||
|
||||
@@ -1,211 +1,279 @@
|
||||
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 { Trans, useTranslation } from 'react-i18next';
|
||||
import {
|
||||
renderInputField,
|
||||
renderRadioField,
|
||||
renderTextareaField,
|
||||
CheckboxField,
|
||||
toNumber,
|
||||
} from '../../../../helpers/form';
|
||||
import {
|
||||
validateIpv4,
|
||||
validateIpv6,
|
||||
validateRequiredValue,
|
||||
validateIp,
|
||||
validateIPv4Subnet,
|
||||
validateIPv6Subnet,
|
||||
} from '../../../../helpers/validators';
|
||||
import i18next from 'i18next';
|
||||
import { validateIp, validateIpv4, validateIpv6, validateRequiredValue } from '../../../../helpers/validators';
|
||||
|
||||
import { removeEmptyLines } from '../../../../helpers/helpers';
|
||||
import { BLOCKING_MODES, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
|
||||
import { RootState } from '../../../../initialState';
|
||||
import { BLOCKING_MODES, UINT32_RANGE } from '../../../../helpers/constants';
|
||||
import { Checkbox } from '../../../ui/Controls/Checkbox';
|
||||
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',
|
||||
placeholder: 'dnssec_enable',
|
||||
subtitle: 'dnssec_enable_desc',
|
||||
placeholder: i18next.t('dnssec_enable'),
|
||||
subtitle: i18next.t('dnssec_enable_desc'),
|
||||
},
|
||||
{
|
||||
name: 'disable_ipv6',
|
||||
placeholder: 'disable_ipv6',
|
||||
subtitle: 'disable_ipv6_desc',
|
||||
placeholder: i18next.t('disable_ipv6'),
|
||||
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',
|
||||
label: i18next.t('blocking_ipv4'),
|
||||
description: i18next.t('blocking_ipv4_desc'),
|
||||
validateIp: validateIpv4,
|
||||
},
|
||||
{
|
||||
description: 'blocking_ipv6_desc',
|
||||
name: 'blocking_ipv6',
|
||||
label: i18next.t('blocking_ipv6'),
|
||||
description: i18next.t('blocking_ipv6_desc'),
|
||||
validateIp: validateIpv6,
|
||||
},
|
||||
];
|
||||
|
||||
const getFields = (processing: any, t: any) =>
|
||||
Object.values(BLOCKING_MODES)
|
||||
const blockingModeOptions = [
|
||||
{
|
||||
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) => (
|
||||
<Field
|
||||
key={mode}
|
||||
name="blocking_mode"
|
||||
type="radio"
|
||||
component={renderRadioField}
|
||||
value={mode}
|
||||
placeholder={t(mode)}
|
||||
disabled={processing}
|
||||
/>
|
||||
));
|
||||
const blockingModeDescriptions = [
|
||||
i18next.t(`blocking_mode_default`),
|
||||
i18next.t(`blocking_mode_refused`),
|
||||
i18next.t(`blocking_mode_nxdomain`),
|
||||
i18next.t(`blocking_mode_null_ip`),
|
||||
i18next.t(`blocking_mode_custom_ip`),
|
||||
];
|
||||
|
||||
interface ConfigFormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
type FormData = {
|
||||
ratelimit: number;
|
||||
ratelimit_subnet_len_ipv4: number;
|
||||
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;
|
||||
}
|
||||
initialValues?: Partial<FormData>;
|
||||
onSubmit: (data: FormData) => void;
|
||||
};
|
||||
|
||||
const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps) => {
|
||||
const Form = ({ processing, initialValues, onSubmit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { blocking_mode, edns_cs_enabled, edns_cs_use_custom } = useSelector(
|
||||
(state: RootState) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {},
|
||||
shallowEqual,
|
||||
);
|
||||
|
||||
const {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit" className="form__label form__label--with-desc">
|
||||
<Trans>rate_limit</Trans>
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>rate_limit_desc</Trans>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="ratelimit"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_rate_limit')}
|
||||
normalize={toNumber}
|
||||
validate={validateRequiredValue}
|
||||
min={UINT32_RANGE.MIN}
|
||||
max={UINT32_RANGE.MAX}
|
||||
control={control}
|
||||
rules={{ validate: validateRequiredValue }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="dns_config_ratelimit"
|
||||
type="number"
|
||||
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 className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit_subnet_len_ipv4" className="form__label form__label--with-desc">
|
||||
<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
|
||||
<Controller
|
||||
name="ratelimit_subnet_len_ipv4"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_rate_limit_subnet_len')}
|
||||
normalize={toNumber}
|
||||
validate={[validateRequiredValue, validateIPv4Subnet]}
|
||||
min={0}
|
||||
max={32}
|
||||
control={control}
|
||||
rules={{ validate: validateRequiredValue }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="dns_config_subnet_ipv4"
|
||||
type="number"
|
||||
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 className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit_subnet_len_ipv6" className="form__label form__label--with-desc">
|
||||
<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
|
||||
<Controller
|
||||
name="ratelimit_subnet_len_ipv6"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_rate_limit_subnet_len')}
|
||||
normalize={toNumber}
|
||||
validate={[validateRequiredValue, validateIPv6Subnet]}
|
||||
min={0}
|
||||
max={128}
|
||||
control={control}
|
||||
rules={{ validate: validateRequiredValue }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="dns_config_subnet_ipv6"
|
||||
type="number"
|
||||
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 className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="ratelimit_whitelist" className="form__label form__label--with-desc">
|
||||
<Trans>rate_limit_whitelist</Trans>
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>rate_limit_whitelist_desc</Trans>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="ratelimit_whitelist"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('rate_limit_whitelist_placeholder')}
|
||||
normalizeOnBlur={removeEmptyLines}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
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 className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="edns_cs_enabled"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('edns_enable')}
|
||||
disabled={processing}
|
||||
subtitle={t('edns_cs_desc')}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid="dns_config_edns_cs_enabled"
|
||||
title={t('edns_enable')}
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 form__group form__group--inner">
|
||||
<div className="form__group ">
|
||||
<Field
|
||||
<div className="form__group">
|
||||
<Controller
|
||||
name="edns_cs_use_custom"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('edns_use_custom_ip')}
|
||||
disabled={processing || !edns_cs_enabled}
|
||||
subtitle={t('edns_use_custom_ip_desc')}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid="dns_config_edns_use_custom_ip"
|
||||
title={t('edns_use_custom_ip')}
|
||||
disabled={processing || !edns_cs_enabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{edns_cs_use_custom && (
|
||||
<Field
|
||||
<Controller
|
||||
name="edns_cs_custom_ip"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[validateIp, validateRequiredValue]}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
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>
|
||||
@@ -213,13 +281,18 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
||||
{checkboxes.map(({ name, placeholder, subtitle }) => (
|
||||
<div className="col-12" key={name}>
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name={name}
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t(placeholder)}
|
||||
disabled={processing}
|
||||
subtitle={t(subtitle)}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid={`dns_config_${name}`}
|
||||
title={placeholder}
|
||||
subtitle={subtitle}
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,42 +300,50 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings mb-4">
|
||||
<label className="form__label form__label--with-desc">
|
||||
<Trans>blocking_mode</Trans>
|
||||
</label>
|
||||
<label className="form__label form__label--with-desc">{t('blocking_mode')}</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
{Object.values(BLOCKING_MODES)
|
||||
|
||||
.map((mode: any) => (
|
||||
<li key={mode}>
|
||||
<Trans>{`blocking_mode_${mode}`}</Trans>
|
||||
</li>
|
||||
))}
|
||||
{blockingModeDescriptions.map((desc: string) => (
|
||||
<li key={desc}>{desc}</li>
|
||||
))}
|
||||
</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>
|
||||
{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="form__group form__group--settings">
|
||||
<label className="form__label form__label--with-desc" htmlFor={name}>
|
||||
<Trans>{name}</Trans>
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>{description}</Trans>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name={name}
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_ip')}
|
||||
validate={[validateIp, validateRequiredValue]}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
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>
|
||||
@@ -272,24 +353,27 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
||||
|
||||
<div className="col-12 col-md-7">
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="blocked_response_ttl" className="form__label form__label--with-desc">
|
||||
<Trans>blocked_response_ttl</Trans>
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>blocked_response_ttl_desc</Trans>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="blocked_response_ttl"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_blocked_response_ttl')}
|
||||
normalize={toNumber}
|
||||
validate={validateRequiredValue}
|
||||
min={UINT32_RANGE.MIN}
|
||||
max={UINT32_RANGE.MAX}
|
||||
control={control}
|
||||
rules={{ validate: validateRequiredValue }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="dns_config_blocked_response_ttl"
|
||||
type="number"
|
||||
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>
|
||||
@@ -297,14 +381,13 @@ const Form = ({ handleSubmit, submitting, invalid, processing }: ConfigFormProps
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="dns_config_save"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={submitting || invalid || processing}>
|
||||
<Trans>save_btn</Trans>
|
||||
disabled={isSubmitting || !isDirty || processing}>
|
||||
{t('save_btn')}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm<Record<string, any>, Omit<ConfigFormProps, 'invalid' | 'submitting' | 'handleSubmit'>>({
|
||||
form: FORM_NAME.BLOCKING_MODE,
|
||||
})(Form);
|
||||
export default Form;
|
||||
|
||||
@@ -1,185 +1,110 @@
|
||||
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 { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
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 i18next from 'i18next';
|
||||
import clsx from 'clsx';
|
||||
import { testUpstreamWithFormValues } from '../../../../actions';
|
||||
|
||||
import { removeEmptyLines, trimLinesAndRemoveEmpty } from '../../../../helpers/helpers';
|
||||
|
||||
import { DNS_REQUEST_OPTIONS, UINT32_RANGE, UPSTREAM_CONFIGURATION_WIKI_LINK } from '../../../../helpers/constants';
|
||||
import { removeEmptyLines } from '../../../../helpers/helpers';
|
||||
import { getTextareaCommentsHighlight, syncScroll } from '../../../../helpers/highlightTextareaComments';
|
||||
import '../../../ui/texareaCommentsHighlight.css';
|
||||
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 { toNumber } from '../../../../helpers/form';
|
||||
|
||||
const UPSTREAM_DNS_NAME = 'upstream_dns';
|
||||
const UPSTREAM_MODE_NAME = 'upstream_mode';
|
||||
|
||||
interface renderFieldProps {
|
||||
name: string;
|
||||
component: any;
|
||||
type: string;
|
||||
className?: string;
|
||||
placeholder: string;
|
||||
subtitle?: string;
|
||||
value?: string;
|
||||
normalizeOnBlur?: (...args: unknown[]) => unknown;
|
||||
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>
|
||||
);
|
||||
type FormData = {
|
||||
upstream_dns: string;
|
||||
upstream_mode: string;
|
||||
fallback_dns: string;
|
||||
bootstrap_dns: string;
|
||||
local_ptr_upstreams: string;
|
||||
use_private_ptr_resolvers: boolean;
|
||||
resolve_clients: boolean;
|
||||
upstream_timeout: number;
|
||||
};
|
||||
|
||||
interface renderTextareaWithHighlightFieldProps {
|
||||
className: string;
|
||||
disabled?: boolean;
|
||||
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)}
|
||||
</>
|
||||
);
|
||||
type FormProps = {
|
||||
initialValues?: Partial<FormData>;
|
||||
onSubmit: (data: FormData) => void;
|
||||
};
|
||||
|
||||
const INPUT_FIELDS = [
|
||||
const upstreamModeOptions = [
|
||||
{
|
||||
name: UPSTREAM_MODE_NAME,
|
||||
type: 'radio',
|
||||
label: i18next.t('load_balancing'),
|
||||
desc: <Trans components={{ br: <br />, b: <b /> }}>load_balancing_desc</Trans>,
|
||||
value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
||||
component: renderRadioField,
|
||||
subtitle: 'load_balancing_desc',
|
||||
placeholder: 'load_balancing',
|
||||
},
|
||||
{
|
||||
name: UPSTREAM_MODE_NAME,
|
||||
type: 'radio',
|
||||
label: i18next.t('parallel_requests'),
|
||||
desc: <Trans components={{ br: <br />, b: <b /> }}>upstream_parallel</Trans>,
|
||||
value: DNS_REQUEST_OPTIONS.PARALLEL,
|
||||
component: renderRadioField,
|
||||
subtitle: 'upstream_parallel',
|
||||
placeholder: 'parallel_requests',
|
||||
},
|
||||
{
|
||||
name: UPSTREAM_MODE_NAME,
|
||||
type: 'radio',
|
||||
label: i18next.t('fastest_addr'),
|
||||
desc: <Trans components={{ br: <br />, b: <b /> }}>fastest_addr_desc</Trans>,
|
||||
value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
|
||||
component: renderRadioField,
|
||||
subtitle: 'fastest_addr_desc',
|
||||
placeholder: 'fastest_addr',
|
||||
},
|
||||
];
|
||||
|
||||
interface 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 Form = ({ initialValues, onSubmit }: FormProps) => {
|
||||
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 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 handleUpstreamTest = () => dispatch(testUpstreamWithFormValues());
|
||||
|
||||
const testButtonClass = classnames('btn btn-primary btn-standard mr-2', {
|
||||
'btn-loading': processingTestUpstream,
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting, isDirty },
|
||||
} = useForm<FormData>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
upstream_dns: initialValues?.upstream_dns || '',
|
||||
upstream_mode: initialValues?.upstream_mode || DNS_REQUEST_OPTIONS.LOAD_BALANCING,
|
||||
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 = {
|
||||
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||
const upstream_dns = watch('upstream_dns');
|
||||
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 (
|
||||
<form onSubmit={handleSubmit} className="form--upstream">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="form--upstream">
|
||||
<div className="row">
|
||||
<label className="col form__label" htmlFor={UPSTREAM_DNS_NAME}>
|
||||
<Trans components={components}>upstream_dns_help</Trans>{' '}
|
||||
<label className="col form__label" htmlFor="upstream_dns">
|
||||
<Trans
|
||||
components={{
|
||||
a: <a href={UPSTREAM_CONFIGURATION_WIKI_LINK} target="_blank" rel="noopener noreferrer" />,
|
||||
}}>
|
||||
upstream_dns_help
|
||||
</Trans>{' '}
|
||||
<Trans
|
||||
components={[
|
||||
<a
|
||||
@@ -196,44 +121,69 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
||||
|
||||
<div className="col-12 mb-4">
|
||||
<div className="text-edit-container">
|
||||
<Field
|
||||
id={UPSTREAM_DNS_NAME}
|
||||
name={UPSTREAM_DNS_NAME}
|
||||
component={renderTextareaWithHighlightField}
|
||||
type="text"
|
||||
className="form-control form-control--textarea font-monospace text-input"
|
||||
placeholder={t('upstream_dns')}
|
||||
disabled={processingSetConfig || processingTestUpstream}
|
||||
normalizeOnBlur={removeEmptyLines}
|
||||
<Controller
|
||||
name="upstream_dns"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<Textarea
|
||||
{...field}
|
||||
id={UPSTREAM_DNS_NAME}
|
||||
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>
|
||||
{INPUT_FIELDS.map(renderField)}
|
||||
|
||||
<div className="col-12">
|
||||
<Examples />
|
||||
|
||||
<hr />
|
||||
</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">
|
||||
<label className="form__label form__label--with-desc" htmlFor="fallback_dns">
|
||||
<Trans>fallback_dns_title</Trans>
|
||||
{t('fallback_dns_title')}
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>fallback_dns_desc</Trans>
|
||||
</div>
|
||||
<div className="form__desc form__desc--top">{t('fallback_dns_desc')}</div>
|
||||
|
||||
<Field
|
||||
id="fallback_dns"
|
||||
<Controller
|
||||
name="fallback_dns"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
||||
placeholder={t('fallback_dns_placeholder')}
|
||||
disabled={processingSetConfig}
|
||||
normalizeOnBlur={removeEmptyLines}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
id="fallback_dns"
|
||||
data-testid="fallback_dns"
|
||||
wrapperClassName="mb-0"
|
||||
placeholder={t('fallback_dns_placeholder')}
|
||||
disabled={processingSetConfig}
|
||||
trimOnBlur
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -241,24 +191,30 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div className="col-12 mb-2">
|
||||
<div className="col-12">
|
||||
<label className="form__label form__label--with-desc" htmlFor="bootstrap_dns">
|
||||
<Trans>bootstrap_dns</Trans>
|
||||
{t('bootstrap_dns')}
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>bootstrap_dns_desc</Trans>
|
||||
</div>
|
||||
<div className="form__desc form__desc--top">{t('bootstrap_dns_desc')}</div>
|
||||
|
||||
<Field
|
||||
id="bootstrap_dns"
|
||||
<Controller
|
||||
name="bootstrap_dns"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
||||
placeholder={t('bootstrap_dns')}
|
||||
disabled={processingSetConfig}
|
||||
normalizeOnBlur={removeEmptyLines}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
id="bootstrap_dns"
|
||||
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>
|
||||
|
||||
@@ -268,43 +224,47 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form__label form__label--with-desc" htmlFor="local_ptr">
|
||||
<Trans>local_ptr_title</Trans>
|
||||
{t('local_ptr_title')}
|
||||
</label>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>local_ptr_desc</Trans>
|
||||
</div>
|
||||
<div className="form__desc form__desc--top">{t('local_ptr_desc')}</div>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
{/** TODO: Add internazionalization for "" */}
|
||||
{defaultLocalPtrUpstreams?.length > 0 ? (
|
||||
<Trans values={{ ip: defaultLocalPtrUpstreams.map((s: any) => `"${s}"`).join(', ') }}>
|
||||
local_ptr_default_resolver
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>local_ptr_no_default_resolver</Trans>
|
||||
)}
|
||||
{defaultLocalPtrUpstreams?.length > 0
|
||||
? t('local_ptr_default_resolver', {
|
||||
ip: defaultLocalPtrUpstreams.map((s: any) => `"${s}"`).join(', '),
|
||||
})
|
||||
: t('local_ptr_no_default_resolver')}
|
||||
</div>
|
||||
|
||||
<Field
|
||||
id="local_ptr_upstreams"
|
||||
<Controller
|
||||
name="local_ptr_upstreams"
|
||||
component={renderTextareaField}
|
||||
type="text"
|
||||
className="form-control form-control--textarea form-control--textarea-small font-monospace"
|
||||
placeholder={t('local_ptr_placeholder')}
|
||||
disabled={processingSetConfig}
|
||||
normalizeOnBlur={removeEmptyLines}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
id="local_ptr_upstreams"
|
||||
data-testid="local_ptr_upstreams"
|
||||
placeholder={t('local_ptr_placeholder')}
|
||||
disabled={processingSetConfig}
|
||||
trimOnBlur
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<Field
|
||||
<Controller
|
||||
name="use_private_ptr_resolvers"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('use_private_ptr_resolvers_title')}
|
||||
subtitle={t('use_private_ptr_resolvers_desc')}
|
||||
disabled={processingSetConfig}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
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>
|
||||
@@ -313,14 +273,19 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<Field
|
||||
<div className="col-12 mb-4">
|
||||
<Controller
|
||||
name="resolve_clients"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('resolve_clients_title')}
|
||||
subtitle={t('resolve_clients_desc')}
|
||||
disabled={processingSetConfig}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid="dns_resolve_clients"
|
||||
title={t('resolve_clients_title')}
|
||||
subtitle={t('resolve_clients_desc')}
|
||||
disabled={processingSetConfig}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -338,16 +303,26 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
||||
<Trans>upstream_timeout_desc</Trans>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="upstream_timeout"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_upstream_timeout')}
|
||||
normalize={toNumber}
|
||||
validate={validateRequiredValue}
|
||||
min={1}
|
||||
max={UINT32_RANGE.MAX}
|
||||
control={control}
|
||||
rules={{ validate: validateRequiredValue }}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
id="upstream_timeout"
|
||||
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>
|
||||
@@ -357,17 +332,21 @@ const Form = ({ submitting, invalid, handleSubmit }: FormProps) => {
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="button"
|
||||
className={testButtonClass}
|
||||
data-testid="dns_upstream_test"
|
||||
className={clsx('btn btn-primary btn-standard mr-2', {
|
||||
'btn-loading': processingTestUpstream,
|
||||
})}
|
||||
onClick={handleUpstreamTest}
|
||||
disabled={!upstream_dns || processingTestUpstream}>
|
||||
<Trans>test_upstream_btn</Trans>
|
||||
{t('test_upstream_btn')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="dns_upstream_save"
|
||||
className="btn btn-success btn-standard"
|
||||
disabled={submitting || invalid || processingSetConfig || processingTestUpstream}>
|
||||
<Trans>apply_btn</Trans>
|
||||
disabled={isSubmitting || !isDirty || processingSetConfig || processingTestUpstream}>
|
||||
{t('apply_btn')}
|
||||
</button>
|
||||
</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 { connect } from 'react-redux';
|
||||
|
||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { renderInputField, CheckboxField, renderRadioField, toNumber } from '../../../helpers/form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import i18next from 'i18next';
|
||||
import {
|
||||
validateServerName,
|
||||
validateIsSafePort,
|
||||
@@ -14,7 +12,6 @@ import {
|
||||
validatePortTLS,
|
||||
validatePlainDns,
|
||||
} from '../../../helpers/validators';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
import KeyStatus from './KeyStatus';
|
||||
|
||||
@@ -22,51 +19,39 @@ import CertificateStatus from './CertificateStatus';
|
||||
import {
|
||||
DNS_OVER_QUIC_PORT,
|
||||
DNS_OVER_TLS_PORT,
|
||||
FORM_NAME,
|
||||
STANDARD_HTTPS_PORT,
|
||||
ENCRYPTION_SOURCE,
|
||||
} 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 errors: { port_dns_over_tls?: string; port_https?: string } = {};
|
||||
const certificateSourceOptions = [
|
||||
{
|
||||
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) {
|
||||
if (values.port_dns_over_tls === values.port_https) {
|
||||
errors.port_dns_over_tls = i18n.t('form_error_equal');
|
||||
const keySourceOptions = [
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
const validationMessage = (warningValidation: string, isWarning: boolean) => {
|
||||
if (!warningValidation) {
|
||||
return null;
|
||||
}
|
||||
@@ -88,56 +73,60 @@ const validationMessage = (warningValidation: any, isWarning: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface FormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
handleChange?: (...args: unknown[]) => unknown;
|
||||
isEnabled: boolean;
|
||||
servePlainDns: boolean;
|
||||
certificateChain: string;
|
||||
privateKey: string;
|
||||
certificatePath: string;
|
||||
privateKeyPath: string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
initialValues: object;
|
||||
processingConfig: boolean;
|
||||
processingValidate: boolean;
|
||||
status_key?: string;
|
||||
not_after?: string;
|
||||
warning_validation?: string;
|
||||
valid_chain?: boolean;
|
||||
valid_key?: boolean;
|
||||
valid_cert?: boolean;
|
||||
valid_pair?: boolean;
|
||||
dns_names?: string[];
|
||||
key_type?: string;
|
||||
issuer?: string;
|
||||
subject?: string;
|
||||
t: (...args: unknown[]) => string;
|
||||
setTlsConfig: (...args: unknown[]) => unknown;
|
||||
validateTlsConfig: (...args: unknown[]) => unknown;
|
||||
certificateSource?: string;
|
||||
privateKeySource?: string;
|
||||
privateKeySaved?: boolean;
|
||||
}
|
||||
export type EncryptionFormValues = {
|
||||
enabled?: boolean;
|
||||
serve_plain_dns?: boolean;
|
||||
server_name?: string;
|
||||
force_https?: boolean;
|
||||
port_https?: number;
|
||||
port_dns_over_tls?: number;
|
||||
port_dns_over_quic?: number;
|
||||
certificate_chain?: string;
|
||||
private_key?: string;
|
||||
certificate_path?: string;
|
||||
private_key_path?: string;
|
||||
certificate_source?: string;
|
||||
key_source?: string;
|
||||
private_key_saved?: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialValues: EncryptionFormValues;
|
||||
encryption: EncryptionData;
|
||||
onSubmit: (values: EncryptionFormValues) => void;
|
||||
debouncedConfigValidation: (values: EncryptionFormValues) => void;
|
||||
setTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||
validateTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||
};
|
||||
|
||||
const defaultValues = {
|
||||
enabled: false,
|
||||
serve_plain_dns: true,
|
||||
server_name: '',
|
||||
force_https: false,
|
||||
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 {
|
||||
t,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
isEnabled,
|
||||
servePlainDns,
|
||||
certificateChain,
|
||||
privateKey,
|
||||
certificatePath,
|
||||
privateKeyPath,
|
||||
change,
|
||||
invalid,
|
||||
submitting,
|
||||
processingConfig,
|
||||
processingValidate,
|
||||
not_after,
|
||||
valid_chain,
|
||||
valid_key,
|
||||
@@ -148,37 +137,100 @@ let Form = (props: FormProps) => {
|
||||
issuer,
|
||||
subject,
|
||||
warning_validation,
|
||||
setTlsConfig,
|
||||
validateTlsConfig,
|
||||
certificateSource,
|
||||
privateKeySource,
|
||||
privateKeySaved,
|
||||
} = props;
|
||||
processingConfig,
|
||||
processingValidate,
|
||||
} = encryption;
|
||||
|
||||
const {
|
||||
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 processing = submitting || processingConfig || processingValidate;
|
||||
const processing = isSubmitting || processingConfig || processingValidate;
|
||||
|
||||
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 isWarning = valid_key && valid_cert && valid_pair;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings mb-3">
|
||||
<Field
|
||||
<Controller
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('encryption_enable')}
|
||||
onChange={handleChange}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox {...field} title={t('encryption_enable')} onBlur={handleBlur} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -187,13 +239,13 @@ let Form = (props: FormProps) => {
|
||||
</div>
|
||||
|
||||
<div className="form__group mb-3 mt-5">
|
||||
<Field
|
||||
<Controller
|
||||
name="serve_plain_dns"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('encryption_plain_dns_enable')}
|
||||
onChange={handleChange}
|
||||
validate={validatePlainDns}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => validatePlainDns(value, getValues()),
|
||||
}}
|
||||
render={({ field }) => <Checkbox {...field} title={t('encryption_plain_dns_enable')} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -212,16 +264,20 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
id="server_name"
|
||||
<Controller
|
||||
name="server_name"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_server_enter')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
validate={validateServerName}
|
||||
control={control}
|
||||
rules={{ validate: validateServerName }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={t('encryption_server_enter')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -232,13 +288,12 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="col-lg-6">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="force_https"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('encryption_redirect')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox {...field} title={t('encryption_redirect')} disabled={!isEnabled} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -255,17 +310,24 @@ let Form = (props: FormProps) => {
|
||||
<Trans>encryption_https</Trans>
|
||||
</label>
|
||||
|
||||
<Field
|
||||
id="port_https"
|
||||
<Controller
|
||||
name="port_https"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_https')}
|
||||
validate={[validatePort, validateIsSafePort]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
rules={{ validate: { validatePort, validateIsSafePort } }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder={t('encryption_https')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -280,17 +342,24 @@ let Form = (props: FormProps) => {
|
||||
<Trans>encryption_dot</Trans>
|
||||
</label>
|
||||
|
||||
<Field
|
||||
id="port_dns_over_tls"
|
||||
<Controller
|
||||
name="port_dns_over_tls"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_dot')}
|
||||
validate={[validatePortTLS]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
rules={{ validate: validatePortTLS }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder={t('encryption_dot')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -305,17 +374,24 @@ let Form = (props: FormProps) => {
|
||||
<Trans>encryption_doq</Trans>
|
||||
</label>
|
||||
|
||||
<Field
|
||||
id="port_dns_over_quic"
|
||||
<Controller
|
||||
name="port_dns_over_quic"
|
||||
component={renderInputField}
|
||||
type="number"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_doq')}
|
||||
validate={[validatePortQuic]}
|
||||
normalize={toNumber}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
rules={{ validate: validatePortQuic }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder={t('encryption_doq')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(toNumber(value));
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="form__desc">
|
||||
@@ -352,50 +428,44 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="form__inline mb-2">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
<Controller
|
||||
name="certificate_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
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}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio {...field} options={certificateSourceOptions} disabled={!isEnabled} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{certificateSource === ENCRYPTION_SOURCE.CONTENT && (
|
||||
<Field
|
||||
id="certificate_chain"
|
||||
{certificateSource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<Controller
|
||||
name="certificate_chain"
|
||||
component="textarea"
|
||||
type="text"
|
||||
className="form-control form-control--textarea"
|
||||
placeholder={t('encryption_certificates_input')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder={t('encryption_certificates_input')}
|
||||
disabled={!isEnabled}
|
||||
error={fieldState.error?.message}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{certificateSource === ENCRYPTION_SOURCE.PATH && (
|
||||
<Field
|
||||
id="certificate_path"
|
||||
) : (
|
||||
<Controller
|
||||
name="certificate_path"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_certificate_path')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
placeholder={t('encryption_certificate_path')}
|
||||
error={fieldState.error?.message}
|
||||
disabled={!isEnabled}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -424,70 +494,67 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="form__inline mb-2">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
<Controller
|
||||
name="key_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
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}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Radio {...field} options={keySourceOptions} disabled={!isEnabled} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{privateKeySource === ENCRYPTION_SOURCE.PATH && (
|
||||
<Field
|
||||
{privateKeySource === ENCRYPTION_SOURCE.CONTENT ? (
|
||||
<>
|
||||
<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"
|
||||
component={renderInputField}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('encryption_private_key_path')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
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 className="form__status">
|
||||
@@ -505,44 +572,11 @@ let Form = (props: FormProps) => {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-standart"
|
||||
disabled={submitting || processingConfig}
|
||||
onClick={() => clearFields(change, setTlsConfig, validateTlsConfig, t)}>
|
||||
disabled={isSubmitting || processingConfig}
|
||||
onClick={clearFields}>
|
||||
<Trans>reset_settings</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</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 { withTranslation } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { debounce } from 'lodash';
|
||||
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
|
||||
|
||||
import Form from './Form';
|
||||
|
||||
import { EncryptionFormValues, Form } from './Form';
|
||||
import Card from '../../ui/Card';
|
||||
|
||||
import PageTitle from '../../ui/PageTitle';
|
||||
|
||||
import Loading from '../../ui/Loading';
|
||||
import { EncryptionData } from '../../../initialState';
|
||||
|
||||
interface EncryptionProps {
|
||||
setTlsConfig: (...args: unknown[]) => unknown;
|
||||
validateTlsConfig: (...args: unknown[]) => unknown;
|
||||
type Props = {
|
||||
encryption: EncryptionData;
|
||||
t: (...args: unknown[]) => string;
|
||||
}
|
||||
setTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||
validateTlsConfig: (values: Partial<EncryptionData>) => void;
|
||||
};
|
||||
|
||||
class Encryption extends Component<EncryptionProps> {
|
||||
componentDidMount() {
|
||||
const { validateTlsConfig, encryption } = this.props;
|
||||
export const Encryption = ({ encryption, setTlsConfig, validateTlsConfig }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (encryption.enabled) {
|
||||
validateTlsConfig(encryption);
|
||||
}
|
||||
}
|
||||
|
||||
handleFormSubmit = (values: any) => {
|
||||
const submitValues = this.getSubmitValues(values);
|
||||
|
||||
this.props.setTlsConfig(submitValues);
|
||||
};
|
||||
|
||||
handleFormChange = debounce((values) => {
|
||||
const submitValues = this.getSubmitValues(values);
|
||||
|
||||
if (submitValues.enabled) {
|
||||
this.props.validateTlsConfig(submitValues);
|
||||
}
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
getInitialValues = (data: any) => {
|
||||
const { certificate_chain, private_key, private_key_saved } = data;
|
||||
const initialValues = useMemo((): EncryptionFormValues => {
|
||||
const {
|
||||
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,
|
||||
} = encryption;
|
||||
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;
|
||||
|
||||
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,
|
||||
key_source,
|
||||
};
|
||||
};
|
||||
}, [encryption]);
|
||||
|
||||
getSubmitValues = (values: any) => {
|
||||
const getSubmitValues = useCallback((values: any) => {
|
||||
const { certificate_source, key_source, private_key_saved, ...config } = values;
|
||||
|
||||
if (certificate_source === ENCRYPTION_SOURCE.PATH) {
|
||||
@@ -76,63 +75,47 @@ class Encryption extends Component<EncryptionProps> {
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
const { encryption, t } = this.props;
|
||||
const {
|
||||
enabled,
|
||||
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,
|
||||
} = encryption;
|
||||
const handleFormSubmit = useCallback(
|
||||
(values: any) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
setTlsConfig(submitValues);
|
||||
},
|
||||
[getSubmitValues, setTlsConfig],
|
||||
);
|
||||
|
||||
const initialValues = this.getInitialValues({
|
||||
enabled,
|
||||
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,
|
||||
});
|
||||
const validateConfig = useCallback((values) => {
|
||||
const submitValues = getSubmitValues(values);
|
||||
|
||||
return (
|
||||
<div className="encryption">
|
||||
<PageTitle title={t('encryption_settings')} />
|
||||
if (submitValues.enabled) {
|
||||
validateTlsConfig(submitValues);
|
||||
}
|
||||
}, []);
|
||||
|
||||
{encryption.processing && <Loading />}
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
}
|
||||
const debouncedConfigValidation = useMemo(() => debounce(validateConfig, DEBOUNCE_TIMEOUT), [validateConfig]);
|
||||
|
||||
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,85 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import { CheckboxField, toNumber } from '../../../helpers/form';
|
||||
import { FILTERS_INTERVALS_HOURS, FILTERS_RELATIVE_LINK, FORM_NAME } from '../../../helpers/constants';
|
||||
|
||||
const getTitleForInterval = (interval: any, t: any) => {
|
||||
if (interval === 0) {
|
||||
return t('disabled');
|
||||
}
|
||||
if (interval === 72 || interval === 168) {
|
||||
return t('interval_days', { count: interval / 24 });
|
||||
}
|
||||
|
||||
return t('interval_hours', { count: interval });
|
||||
};
|
||||
|
||||
const getIntervalSelect = (processing: any, t: any, handleChange: any, toNumber: any) => (
|
||||
<Field
|
||||
name="interval"
|
||||
className="custom-select"
|
||||
component="select"
|
||||
onChange={handleChange}
|
||||
normalize={toNumber}
|
||||
disabled={processing}>
|
||||
{FILTERS_INTERVALS_HOURS.map((interval) => (
|
||||
<option value={interval} key={interval}>
|
||||
{getTitleForInterval(interval, t)}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
);
|
||||
|
||||
interface FormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
handleChange?: (...args: unknown[]) => unknown;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
processing: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
}
|
||||
|
||||
const Form = (props: FormProps) => {
|
||||
const { handleSubmit, handleChange, processing, t } = props;
|
||||
|
||||
const components = {
|
||||
a: <a href={FILTERS_RELATIVE_LINK} rel="noopener noreferrer" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
modifier="checkbox--settings"
|
||||
component={CheckboxField}
|
||||
placeholder={t('block_domain_use_filters_and_hosts')}
|
||||
subtitle={<Trans components={components}>filters_block_toggle_hint</Trans>}
|
||||
onChange={handleChange}
|
||||
disabled={processing}
|
||||
/>
|
||||
</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>
|
||||
{getIntervalSelect(processing, t, handleChange, toNumber)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default flow([withTranslation(), reduxForm({ form: FORM_NAME.FILTER_CONFIG })])(Form);
|
||||
@@ -1,39 +1,115 @@
|
||||
import React from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
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 {
|
||||
initialValues: object;
|
||||
processing: boolean;
|
||||
setFiltersConfig: (...args: unknown[]) => unknown;
|
||||
t: (...args: unknown[]) => string;
|
||||
}
|
||||
if (interval === THREE_DAYS_INTERVAL || interval === SEVEN_DAYS_INTERVAL) {
|
||||
return i18next.t('interval_days', { count: interval / DAY });
|
||||
}
|
||||
|
||||
const FiltersConfig = (props: FiltersConfigProps) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
return i18next.t('interval_hours', { count: interval });
|
||||
};
|
||||
|
||||
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 { change, Field, formValueSelector, reduxForm } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
|
||||
import {
|
||||
CheckboxField,
|
||||
toFloatNumber,
|
||||
renderTextareaField,
|
||||
renderInputField,
|
||||
renderRadioField,
|
||||
} from '../../../helpers/form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import i18next from 'i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||
import {
|
||||
FORM_NAME,
|
||||
QUERY_LOG_INTERVALS_DAYS,
|
||||
HOUR,
|
||||
DAY,
|
||||
RETENTION_CUSTOM,
|
||||
RETENTION_CUSTOM_INPUT,
|
||||
RETENTION_RANGE,
|
||||
CUSTOM_INTERVAL,
|
||||
} from '../../../helpers/constants';
|
||||
import { QUERY_LOG_INTERVALS_DAYS, HOUR, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
|
||||
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) {
|
||||
case RETENTION_CUSTOM:
|
||||
return t('settings_custom');
|
||||
return i18next.t('settings_custom');
|
||||
case 6 * HOUR:
|
||||
return t('interval_6_hour');
|
||||
return i18next.t('interval_6_hour');
|
||||
case DAY:
|
||||
return t('interval_24_hour');
|
||||
return i18next.t('interval_24_hour');
|
||||
default:
|
||||
return t('interval_days', { count: interval / DAY });
|
||||
return i18next.t('interval_days', { count: interval / DAY });
|
||||
}
|
||||
};
|
||||
|
||||
const getIntervalFields = (processing: any, t: any, toNumber: any) =>
|
||||
QUERY_LOG_INTERVALS_DAYS.map((interval) => (
|
||||
<Field
|
||||
key={interval}
|
||||
name="interval"
|
||||
type="radio"
|
||||
component={renderRadioField}
|
||||
value={interval}
|
||||
placeholder={getIntervalTitle(interval, t)}
|
||||
normalize={toNumber}
|
||||
disabled={processing}
|
||||
/>
|
||||
));
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
anonymize_client_ip: boolean;
|
||||
interval: number;
|
||||
customInterval?: number | null;
|
||||
ignored: string;
|
||||
};
|
||||
|
||||
interface FormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
handleClear: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
type Props = {
|
||||
initialValues: Partial<FormValues>;
|
||||
processing: boolean;
|
||||
processingClear: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
interval?: number;
|
||||
customInterval?: number;
|
||||
dispatch: (...args: unknown[]) => unknown;
|
||||
}
|
||||
processingReset: boolean;
|
||||
onSubmit: (values: FormValues) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
export const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let Form = (props: FormProps) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
submitting,
|
||||
invalid,
|
||||
processing,
|
||||
processingClear,
|
||||
handleClear,
|
||||
t,
|
||||
interval,
|
||||
customInterval,
|
||||
dispatch,
|
||||
} = props;
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<FormValues>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
enabled: initialValues.enabled || false,
|
||||
anonymize_client_ip: initialValues.anonymize_client_ip || false,
|
||||
interval: initialValues.interval || DAY,
|
||||
customInterval: initialValues.customInterval || null,
|
||||
ignored: initialValues.ignored || '',
|
||||
},
|
||||
});
|
||||
|
||||
const intervalValue = watch('interval');
|
||||
const customIntervalValue = watch('customInterval');
|
||||
|
||||
useEffect(() => {
|
||||
if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) {
|
||||
dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null));
|
||||
if (QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)) {
|
||||
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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('query_log_enable')}
|
||||
disabled={processing}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid="logs_enabled"
|
||||
title={t('query_log_enable')}
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="anonymize_client_ip"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('anonymize_client_ip')}
|
||||
subtitle={t('anonymize_client_ip_desc')}
|
||||
disabled={processing}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid="logs_anonymize_client_ip"
|
||||
title={t('anonymize_client_ip')}
|
||||
subtitle={t('anonymize_client_ip_desc')}
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="form__label">
|
||||
<div className="form__label">
|
||||
<Trans>query_log_retention</Trans>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form__group form__group--settings">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
key={RETENTION_CUSTOM}
|
||||
name="interval"
|
||||
type="radio"
|
||||
component={renderRadioField}
|
||||
value={QUERY_LOG_INTERVALS_DAYS.includes(interval) ? RETENTION_CUSTOM : interval}
|
||||
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
|
||||
normalize={toFloatNumber}
|
||||
disabled={processing}
|
||||
/>
|
||||
{!QUERY_LOG_INTERVALS_DAYS.includes(interval) && (
|
||||
<label className="custom-control custom-radio">
|
||||
<input
|
||||
type="radio"
|
||||
data-testid="logs_config_interval"
|
||||
className="custom-control-input"
|
||||
disabled={processing}
|
||||
checked={!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue)}
|
||||
value={RETENTION_CUSTOM}
|
||||
onChange={(e) => {
|
||||
setValue('interval', parseInt(e.target.value, 10));
|
||||
}}
|
||||
/>
|
||||
|
||||
<span className="custom-control-label">{getIntervalTitle(RETENTION_CUSTOM)}</span>
|
||||
</label>
|
||||
|
||||
{!QUERY_LOG_INTERVALS_DAYS.includes(intervalValue) && (
|
||||
<div className="form__group--input">
|
||||
<div className="form__desc form__desc--top">{t('custom_rotation_input')}</div>
|
||||
|
||||
<Field
|
||||
key={RETENTION_CUSTOM_INPUT}
|
||||
name={CUSTOM_INTERVAL}
|
||||
type="number"
|
||||
className="form-control"
|
||||
component={renderInputField}
|
||||
disabled={processing}
|
||||
normalize={toFloatNumber}
|
||||
min={RETENTION_RANGE.MIN}
|
||||
max={RETENTION_RANGE.MAX}
|
||||
<Controller
|
||||
name="customInterval"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
data-testid="logs_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>
|
||||
)}
|
||||
{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>
|
||||
|
||||
@@ -154,51 +189,41 @@ let Form = (props: FormProps) => {
|
||||
</div>
|
||||
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="ignored"
|
||||
type="textarea"
|
||||
className="form-control form-control--textarea font-monospace text-input"
|
||||
component={renderTextareaField}
|
||||
placeholder={t('ignore_domains')}
|
||||
disabled={processing}
|
||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
data-testid="logs_config_ingored"
|
||||
placeholder={t('ignore_domains')}
|
||||
className="text-input"
|
||||
disabled={processing}
|
||||
error={fieldState.error?.message}
|
||||
onBlur={handleIgnoredBlur}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="logs_config_save"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={
|
||||
submitting ||
|
||||
invalid ||
|
||||
processing ||
|
||||
(!QUERY_LOG_INTERVALS_DAYS.includes(interval) && !customInterval)
|
||||
}>
|
||||
disabled={disableSubmit}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-testid="logs_config_clear"
|
||||
className="btn btn-outline-secondary btn-standard form__button"
|
||||
onClick={() => handleClear()}
|
||||
disabled={processingClear}>
|
||||
onClick={onReset}
|
||||
disabled={processingReset}>
|
||||
<Trans>query_log_clear</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</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 Form from './Form';
|
||||
import { Form, FormValues } from './Form';
|
||||
import { HOUR } from '../../../helpers/constants';
|
||||
|
||||
interface LogsConfigProps {
|
||||
@@ -20,7 +20,7 @@ interface LogsConfigProps {
|
||||
}
|
||||
|
||||
class LogsConfig extends Component<LogsConfigProps> {
|
||||
handleFormSubmit = (values: any) => {
|
||||
handleFormSubmit = (values: FormValues) => {
|
||||
const { t, interval: prevInterval } = this.props;
|
||||
const { interval, customInterval, ...rest } = values;
|
||||
|
||||
@@ -53,19 +53,12 @@ class LogsConfig extends Component<LogsConfigProps> {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
|
||||
enabled,
|
||||
|
||||
interval,
|
||||
|
||||
processing,
|
||||
|
||||
processingClear,
|
||||
|
||||
anonymize_client_ip,
|
||||
|
||||
ignored,
|
||||
|
||||
customInterval,
|
||||
} = this.props;
|
||||
|
||||
@@ -80,10 +73,10 @@ class LogsConfig extends Component<LogsConfigProps> {
|
||||
anonymize_client_ip,
|
||||
ignored: ignored?.join('\n'),
|
||||
}}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
processing={processing}
|
||||
processingClear={processingClear}
|
||||
handleClear={this.handleClear}
|
||||
processingReset={processingClear}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
onReset={this.handleClear}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
}
|
||||
|
||||
.form__message {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -97,6 +98,10 @@
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.form__label {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form__label--bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -1,90 +1,101 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { change, Field, formValueSelector, reduxForm } from 'redux-form';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import flow from 'lodash/flow';
|
||||
import { connect } from 'react-redux';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import {
|
||||
renderRadioField,
|
||||
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 { Controller, useForm } from 'react-hook-form';
|
||||
import { STATS_INTERVALS_DAYS, DAY, RETENTION_CUSTOM, RETENTION_RANGE } from '../../../helpers/constants';
|
||||
|
||||
import { trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||
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) => {
|
||||
switch (intervalMs) {
|
||||
const getIntervalTitle = (interval: any) => {
|
||||
switch (interval) {
|
||||
case RETENTION_CUSTOM:
|
||||
return t('settings_custom');
|
||||
return i18next.t('settings_custom');
|
||||
case DAY:
|
||||
return t('interval_24_hour');
|
||||
return i18next.t('interval_24_hour');
|
||||
default:
|
||||
return t('interval_days', { count: intervalMs / DAY });
|
||||
return i18next.t('interval_days', { count: interval / DAY });
|
||||
}
|
||||
};
|
||||
|
||||
interface FormProps {
|
||||
handleSubmit: (...args: unknown[]) => string;
|
||||
handleReset: (...args: unknown[]) => string;
|
||||
change: (...args: unknown[]) => unknown;
|
||||
submitting: boolean;
|
||||
invalid: boolean;
|
||||
export type FormValues = {
|
||||
enabled: boolean;
|
||||
interval: number;
|
||||
customInterval?: number | null;
|
||||
ignored: string;
|
||||
};
|
||||
|
||||
const defaultFormValues = {
|
||||
enabled: false,
|
||||
interval: DAY,
|
||||
customInterval: null,
|
||||
ignored: '',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
initialValues: FormValues;
|
||||
processing: boolean;
|
||||
processingReset: boolean;
|
||||
t: (...args: unknown[]) => string;
|
||||
interval?: number;
|
||||
customInterval?: number;
|
||||
dispatch: (...args: unknown[]) => unknown;
|
||||
}
|
||||
onSubmit: (values: FormValues) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
export const Form = ({ initialValues, processing, processingReset, onSubmit, onReset }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let Form = (props: FormProps) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
processing,
|
||||
submitting,
|
||||
invalid,
|
||||
handleReset,
|
||||
processingReset,
|
||||
t,
|
||||
interval,
|
||||
customInterval,
|
||||
dispatch,
|
||||
} = props;
|
||||
watch,
|
||||
setValue,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<FormValues>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: {
|
||||
...defaultFormValues,
|
||||
...initialValues,
|
||||
},
|
||||
});
|
||||
|
||||
const intervalValue = watch('interval');
|
||||
const customIntervalValue = watch('customInterval');
|
||||
|
||||
useEffect(() => {
|
||||
if (STATS_INTERVALS_DAYS.includes(interval)) {
|
||||
dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null));
|
||||
if (STATS_INTERVALS_DAYS.includes(intervalValue)) {
|
||||
setValue('customInterval', null);
|
||||
}
|
||||
}, [interval]);
|
||||
}, [intervalValue]);
|
||||
|
||||
const onSubmitForm = (data: FormValues) => {
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
const disableSubmit = isSubmitting || processing || (intervalValue === RETENTION_CUSTOM && !customIntervalValue);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit(onSubmitForm)}>
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
component={CheckboxField}
|
||||
placeholder={t('statistics_enable')}
|
||||
disabled={processing}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
data-testid="stats_config_enabled"
|
||||
title={t('statistics_enable')}
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="form__label form__label--with-desc">
|
||||
<div className="form__label form__label--with-desc">
|
||||
<Trans>statistics_retention</Trans>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>statistics_retention_desc</Trans>
|
||||
@@ -92,85 +103,105 @@ let Form = (props: FormProps) => {
|
||||
|
||||
<div className="form__group form__group--settings mt-2">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
key={RETENTION_CUSTOM}
|
||||
name="interval"
|
||||
type="radio"
|
||||
component={renderRadioField}
|
||||
value={STATS_INTERVALS_DAYS.includes(interval) ? RETENTION_CUSTOM : interval}
|
||||
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
|
||||
normalize={toFloatNumber}
|
||||
disabled={processing}
|
||||
/>
|
||||
{!STATS_INTERVALS_DAYS.includes(interval) && (
|
||||
<div className="form__group--input">
|
||||
<div className="form__desc form__desc--top">{t('custom_retention_input')}</div>
|
||||
<label className="custom-control custom-radio">
|
||||
<input
|
||||
type="radio"
|
||||
data-testid="stats_config_interval"
|
||||
className="custom-control-input"
|
||||
disabled={processing}
|
||||
checked={!STATS_INTERVALS_DAYS.includes(intervalValue)}
|
||||
value={RETENTION_CUSTOM}
|
||||
onChange={(e) => {
|
||||
setValue('interval', parseInt(e.target.value, 10));
|
||||
}}
|
||||
/>
|
||||
|
||||
<Field
|
||||
key={RETENTION_CUSTOM_INPUT}
|
||||
name={CUSTOM_INTERVAL}
|
||||
type="number"
|
||||
className="form-control"
|
||||
component={renderInputField}
|
||||
disabled={processing}
|
||||
normalize={toFloatNumber}
|
||||
min={RETENTION_RANGE.MIN}
|
||||
max={RETENTION_RANGE.MAX}
|
||||
<span className="custom-control-label">{getIntervalTitle(RETENTION_CUSTOM)}</span>
|
||||
</label>
|
||||
|
||||
{!STATS_INTERVALS_DAYS.includes(intervalValue) && (
|
||||
<div className="form__group--input">
|
||||
<div className="form__desc form__desc--top">{i18next.t('custom_retention_input')}</div>
|
||||
|
||||
<Controller
|
||||
name="customInterval"
|
||||
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>
|
||||
)}
|
||||
{STATS_INTERVALS_DAYS.map((interval) => (
|
||||
<Field
|
||||
key={interval}
|
||||
name="interval"
|
||||
type="radio"
|
||||
component={renderRadioField}
|
||||
value={interval}
|
||||
placeholder={getIntervalTitle(interval, t)}
|
||||
normalize={toNumber}
|
||||
disabled={processing}
|
||||
/>
|
||||
<label key={interval} className="custom-control custom-radio">
|
||||
<input
|
||||
type="radio"
|
||||
className="custom-control-input"
|
||||
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>
|
||||
|
||||
<label className="form__label form__label--with-desc">
|
||||
<div className="form__label form__label--with-desc">
|
||||
<Trans>ignore_domains_title</Trans>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans>ignore_domains_desc_stats</Trans>
|
||||
</div>
|
||||
|
||||
<div className="form__group form__group--settings">
|
||||
<Field
|
||||
<Controller
|
||||
name="ignored"
|
||||
type="textarea"
|
||||
className="form-control form-control--textarea font-monospace text-input"
|
||||
component={renderTextareaField}
|
||||
placeholder={t('ignore_domains')}
|
||||
disabled={processing}
|
||||
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
data-testid="stats_config_ignored"
|
||||
placeholder={t('ignore_domains')}
|
||||
className="text-input"
|
||||
disabled={processing}
|
||||
error={fieldState.error?.message}
|
||||
trimOnBlur
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<button
|
||||
type="submit"
|
||||
data-testid="stats_config_save"
|
||||
className="btn btn-success btn-standard btn-large"
|
||||
disabled={
|
||||
submitting ||
|
||||
invalid ||
|
||||
processing ||
|
||||
(!STATS_INTERVALS_DAYS.includes(interval) && !customInterval)
|
||||
}>
|
||||
disabled={disableSubmit}>
|
||||
<Trans>save_btn</Trans>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-testid="stats_config_clear"
|
||||
className="btn btn-outline-secondary btn-standard form__button"
|
||||
onClick={() => handleReset()}
|
||||
onClick={onReset}
|
||||
disabled={processingReset}>
|
||||
<Trans>statistics_clear</Trans>
|
||||
</button>
|
||||
@@ -178,16 +209,3 @@ let Form = (props: FormProps) => {
|
||||
</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 Form from './Form';
|
||||
import { Form, FormValues } from './Form';
|
||||
import { HOUR } from '../../../helpers/constants';
|
||||
|
||||
interface StatsConfigProps {
|
||||
@@ -19,7 +19,7 @@ interface 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 newInterval = customInterval ? customInterval * HOUR : interval;
|
||||
|
||||
@@ -49,17 +49,11 @@ class StatsConfig extends Component<StatsConfigProps> {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
|
||||
interval,
|
||||
|
||||
customInterval,
|
||||
|
||||
processing,
|
||||
|
||||
processingReset,
|
||||
|
||||
ignored,
|
||||
|
||||
enabled,
|
||||
} = this.props;
|
||||
|
||||
@@ -73,10 +67,10 @@ class StatsConfig extends Component<StatsConfigProps> {
|
||||
enabled,
|
||||
ignored: ignored.join('\n'),
|
||||
}}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
processing={processing}
|
||||
processingReset={processingReset}
|
||||
handleReset={this.handleReset}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
onReset={this.handleReset}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import i18next from 'i18next';
|
||||
import StatsConfig from './StatsConfig';
|
||||
|
||||
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';
|
||||
|
||||
@@ -24,14 +25,16 @@ const ORDER_KEY = 'order';
|
||||
const SETTINGS = {
|
||||
safebrowsing: {
|
||||
enabled: false,
|
||||
title: 'use_adguard_browsing_sec',
|
||||
subtitle: 'use_adguard_browsing_sec_hint',
|
||||
title: i18next.t('use_adguard_browsing_sec'),
|
||||
subtitle: i18next.t('use_adguard_browsing_sec_hint'),
|
||||
testId: 'safebrowsing',
|
||||
[ORDER_KEY]: 0,
|
||||
},
|
||||
parental: {
|
||||
enabled: false,
|
||||
title: 'use_adguard_parental',
|
||||
subtitle: 'use_adguard_parental_hint',
|
||||
title: i18next.t('use_adguard_parental'),
|
||||
subtitle: i18next.t('use_adguard_parental_hint'),
|
||||
testId: 'parental',
|
||||
[ORDER_KEY]: 1,
|
||||
},
|
||||
};
|
||||
@@ -89,9 +92,19 @@ class Settings extends Component<SettingsProps> {
|
||||
renderSettings = (settings: any) =>
|
||||
getObjectKeysSorted(SETTINGS, ORDER_KEY).map((key: any) => {
|
||||
const setting = settings[key];
|
||||
const { enabled } = setting;
|
||||
const { enabled, title, subtitle, testId } = setting;
|
||||
|
||||
return <Checkbox {...setting} key={key} handleChange={() => this.props.toggleSetting(key, enabled)} />;
|
||||
return (
|
||||
<div key={key} className="form__group form__group--checkbox">
|
||||
<Checkbox
|
||||
data-testid={testId}
|
||||
value={enabled}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onChange={(checked) => this.props.toggleSetting(key, !checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
renderSafeSearch = () => {
|
||||
@@ -106,27 +119,30 @@ class Settings extends Component<SettingsProps> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
enabled={enabled}
|
||||
title="enforce_safe_search"
|
||||
subtitle="enforce_save_search_hint"
|
||||
handleChange={({ target: { checked: enabled } }) =>
|
||||
this.props.toggleSetting('safesearch', { ...safesearch, enabled })
|
||||
}
|
||||
/>
|
||||
<div className="form__group form__group--checkbox">
|
||||
<Checkbox
|
||||
data-testid="safesearch"
|
||||
value={enabled}
|
||||
title={i18next.t('enforce_safe_search')}
|
||||
subtitle={i18next.t('enforce_save_search_hint')}
|
||||
onChange={(checked) =>
|
||||
this.props.toggleSetting('safesearch', { ...safesearch, enabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form__group--inner">
|
||||
{Object.keys(searches).map((searchKey) => (
|
||||
<Checkbox
|
||||
key={searchKey}
|
||||
enabled={searches[searchKey]}
|
||||
title={captitalizeWords(searchKey)}
|
||||
subtitle=""
|
||||
disabled={!safesearch.enabled}
|
||||
handleChange={({ target: { checked } }: any) =>
|
||||
this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })
|
||||
}
|
||||
/>
|
||||
<div key={searchKey} className="form__group form__group--checkbox">
|
||||
<Checkbox
|
||||
value={searches[searchKey]}
|
||||
title={captitalizeWords(searchKey)}
|
||||
disabled={!safesearch.enabled}
|
||||
onChange={(checked) =>
|
||||
this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
@@ -136,23 +152,14 @@ class Settings extends Component<SettingsProps> {
|
||||
render() {
|
||||
const {
|
||||
settings,
|
||||
|
||||
setStatsConfig,
|
||||
|
||||
resetStats,
|
||||
|
||||
stats,
|
||||
|
||||
queryLogs,
|
||||
|
||||
setLogsConfig,
|
||||
|
||||
clearLogs,
|
||||
|
||||
filtering,
|
||||
|
||||
setFiltersConfig,
|
||||
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
@@ -163,6 +170,7 @@ class Settings extends Component<SettingsProps> {
|
||||
<PageTitle title={t('general_settings')} />
|
||||
|
||||
{!isDataReady && <Loading />}
|
||||
|
||||
{isDataReady && (
|
||||
<div className="content">
|
||||
<div className="row">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
|
||||
import Guide from '../ui/Guide';
|
||||
import { Guide } from '../ui/Guide';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
|
||||
@@ -14,10 +14,7 @@ interface SetupGuideProps {
|
||||
t: (id: string) => string;
|
||||
}
|
||||
|
||||
const SetupGuide = ({
|
||||
t,
|
||||
dashboard: { dnsAddresses },
|
||||
}: SetupGuideProps) => (
|
||||
const SetupGuide = ({ t, dashboard: { dnsAddresses } }: SetupGuideProps) => (
|
||||
<div className="guide">
|
||||
<PageTitle title={t('setup_guide')} />
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import './Checkbox.css';
|
||||
|
||||
interface CheckboxProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
enabled: boolean;
|
||||
handleChange: (...args: unknown[]) => unknown;
|
||||
disabled?: boolean;
|
||||
t?: (...args: unknown[]) => string;
|
||||
}
|
||||
|
||||
class Checkbox extends Component<CheckboxProps> {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
|
||||
subtitle,
|
||||
|
||||
enabled,
|
||||
|
||||
handleChange,
|
||||
|
||||
disabled,
|
||||
|
||||
t,
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="form__group form__group--checkbox">
|
||||
<label className="checkbox checkbox--settings">
|
||||
<span className="checkbox__marker" />
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox__input"
|
||||
onChange={handleChange}
|
||||
checked={enabled}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<span className="checkbox__label">
|
||||
<span className="checkbox__label-text">
|
||||
<span className="checkbox__label-title">{t(title)}</span>
|
||||
|
||||
<span
|
||||
className="checkbox__label-subtitle"
|
||||
dangerouslySetInnerHTML={{ __html: t(subtitle) }}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(Checkbox);
|
||||
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';
|
||||
@@ -94,14 +94,17 @@ const Footer = () => {
|
||||
auto: {
|
||||
desc: t('theme_auto_desc'),
|
||||
icon: '#auto',
|
||||
testId: 'theme_auto',
|
||||
},
|
||||
dark: {
|
||||
desc: t('theme_dark_desc'),
|
||||
icon: '#dark',
|
||||
testId: 'theme_dark',
|
||||
},
|
||||
light: {
|
||||
desc: t('theme_light_desc'),
|
||||
icon: '#light',
|
||||
testId: 'theme_light',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -113,7 +116,9 @@ const Footer = () => {
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary footer__theme-button"
|
||||
onClick={() => onThemeChange(theme)}
|
||||
title={content[theme].desc}>
|
||||
title={content[theme].desc}
|
||||
data-testid={content[theme].testId}
|
||||
>
|
||||
<svg className={cn('footer__theme-icon', { 'footer__theme-icon--active': currentValue === theme })}>
|
||||
<use xlinkHref={content[theme].icon} />
|
||||
</svg>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
|
||||
|
||||
import Tabs from '../Tabs';
|
||||
|
||||
import MobileConfigForm from './MobileConfigForm';
|
||||
import { MobileConfigForm } from './MobileConfigForm';
|
||||
import { RootState } from '../../../initialState';
|
||||
|
||||
interface renderLiProps {
|
||||
@@ -346,7 +346,7 @@ interface GuideProps {
|
||||
dnsAddresses?: unknown[];
|
||||
}
|
||||
|
||||
const Guide = ({ dnsAddresses }: GuideProps) => {
|
||||
export const Guide = ({ dnsAddresses }: GuideProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const serverName = useSelector((state: RootState) => state.encryption?.server_name);
|
||||
@@ -381,5 +381,3 @@ const Guide = ({ dnsAddresses }: GuideProps) => {
|
||||
Guide.defaultProps = {
|
||||
dnsAddresses: [],
|
||||
};
|
||||
|
||||
export default Guide;
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import i18next from 'i18next';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { getPathWithQueryString } from '../../../helpers/helpers';
|
||||
import { CLIENT_ID_LINK, FORM_NAME, MOBILE_CONFIG_LINKS, STANDARD_HTTPS_PORT } from '../../../helpers/constants';
|
||||
import { renderInputField, renderSelectField, toNumber } from '../../../helpers/form';
|
||||
import { CLIENT_ID_LINK, MOBILE_CONFIG_LINKS, STANDARD_HTTPS_PORT } from '../../../helpers/constants';
|
||||
import { toNumber } from '../../../helpers/form';
|
||||
import {
|
||||
validateConfigClientId,
|
||||
validateServerName,
|
||||
validatePort,
|
||||
validateIsSafePort,
|
||||
} 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) {
|
||||
return (
|
||||
<button type="button" className="btn btn-success btn-standard btn-large disabled">
|
||||
<Trans>download_mobileconfig</Trans>
|
||||
{i18next.t('download_mobileconfig')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const linkParams: { host: string, client_id?: string } = { host };
|
||||
const linkParams: { host: string; client_id?: string } = { host };
|
||||
|
||||
if (clientId) {
|
||||
linkParams.client_id = clientId;
|
||||
@@ -37,29 +36,48 @@ const getDownloadLink = (host: any, clientId: any, protocol: any, invalid: any)
|
||||
href={getPathWithQueryString(protocol, linkParams)}
|
||||
className={cn('btn btn-success btn-standard btn-large')}
|
||||
download>
|
||||
<Trans>download_mobileconfig</Trans>
|
||||
{i18next.t('download_mobileconfig')}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
interface MobileConfigFormProps {
|
||||
invalid: boolean;
|
||||
}
|
||||
type FormValues = {
|
||||
host: string;
|
||||
clientId: string;
|
||||
protocol: string;
|
||||
port?: number;
|
||||
};
|
||||
|
||||
const MobileConfigForm = ({ invalid }: MobileConfigFormProps) => {
|
||||
const formValues = useSelector((state: RootState) => state.form[FORM_NAME.MOBILE_CONFIG]?.values);
|
||||
type Props = {
|
||||
initialValues?: FormValues;
|
||||
};
|
||||
|
||||
if (!formValues) {
|
||||
return null;
|
||||
}
|
||||
const defaultFormValues = {
|
||||
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 = (
|
||||
<a href={CLIENT_ID_LINK} target="_blank" rel="noopener noreferrer">
|
||||
text
|
||||
</a>
|
||||
);
|
||||
const {
|
||||
watch,
|
||||
control,
|
||||
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 = () => {
|
||||
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="row">
|
||||
<div className="col">
|
||||
<label htmlFor="host" className="form__label">
|
||||
{i18next.t('dhcp_table_hostname')}
|
||||
</label>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="host"
|
||||
type="text"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={i18next.t('form_enter_hostname')}
|
||||
validate={validateServerName}
|
||||
control={control}
|
||||
rules={{ validate: validateServerName }}
|
||||
render={({ field, fieldState }) => (
|
||||
<Input
|
||||
{...field}
|
||||
type="text"
|
||||
data-testid="mobile_config_host"
|
||||
label={t('dhcp_table_hostname')}
|
||||
placeholder={t('form_enter_hostname')}
|
||||
error={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{protocol === MOBILE_CONFIG_LINKS.DOH && (
|
||||
<div className="col">
|
||||
<label htmlFor="port" className="form__label">
|
||||
{i18next.t('encryption_https')}
|
||||
</label>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="port"
|
||||
type="number"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={i18next.t('encryption_https')}
|
||||
validate={[validatePort, validateIsSafePort]}
|
||||
normalize={toNumber}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
range: (value) => validatePort(value) || true,
|
||||
safety: (value) => validateIsSafePort(value) || true,
|
||||
},
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
@@ -110,39 +142,49 @@ const MobileConfigForm = ({ invalid }: MobileConfigFormProps) => {
|
||||
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="clientId" className="form__label form__label--with-desc">
|
||||
{i18next.t('client_id')}
|
||||
{t('client_id')}
|
||||
</label>
|
||||
|
||||
<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>
|
||||
|
||||
<Field
|
||||
<Controller
|
||||
name="clientId"
|
||||
type="text"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={i18next.t('client_id_placeholder')}
|
||||
validate={validateConfigClientId}
|
||||
control={control}
|
||||
rules={{
|
||||
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 className="form__group form__group--settings">
|
||||
<label htmlFor="protocol" className="form__label">
|
||||
{i18next.t('protocol')}
|
||||
</label>
|
||||
|
||||
<Field name="protocol" type="text" component={renderSelectField} className="form-control">
|
||||
<option value={MOBILE_CONFIG_LINKS.DOT}>{i18next.t('dns_over_tls')}</option>
|
||||
|
||||
<option value={MOBILE_CONFIG_LINKS.DOH}>{i18next.t('dns_over_https')}</option>
|
||||
</Field>
|
||||
<Controller
|
||||
name="protocol"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select {...field} label={t('protocol')} data-testid="mobile_config_protocol">
|
||||
<option value={MOBILE_CONFIG_LINKS.DOT}>{t('dns_over_tls')}</option>
|
||||
<option value={MOBILE_CONFIG_LINKS.DOH}>{t('dns_over_https')}</option>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getDownloadLink(getHostName(), clientId, protocol, invalid)}
|
||||
{getDownloadLink(getHostName(), clientId, protocol, !isValid)}
|
||||
</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 { toggleProtection, getClients } from '../actions';
|
||||
import { getStats, getStatsConfig, setStatsConfig } from '../actions/stats';
|
||||
import { getStats, getStatsConfig } from '../actions/stats';
|
||||
import { getAccessList } from '../actions/access';
|
||||
|
||||
import Dashboard from '../components/Dashboard';
|
||||
@@ -19,7 +19,7 @@ type DispatchProps = {
|
||||
getStats: (...args: unknown[]) => unknown;
|
||||
getStatsConfig: (...args: unknown[]) => unknown;
|
||||
getAccessList: () => (dispatch: any) => void;
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps: DispatchProps = {
|
||||
toggleProtection,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getTlsStatus, setTlsConfig, validateTlsConfig } from '../actions/encryption';
|
||||
|
||||
import Encryption from '../components/Settings/Encryption';
|
||||
import { Encryption } from '../components/Settings/Encryption';
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const { encryption } = state;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user