Compare commits
114 Commits
1383-devic
...
v0.105.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fe227646b | ||
|
|
28b9abf1dd | ||
|
|
f5c47b6193 | ||
|
|
39f6689b39 | ||
|
|
8fdd021ed7 | ||
|
|
3e0238aa99 | ||
|
|
adb76aa9b8 | ||
|
|
6f155f78b6 | ||
|
|
a83c3cf2ea | ||
|
|
3f050a750d | ||
|
|
a32f8118b1 | ||
|
|
7042811ee6 | ||
|
|
cd88137333 | ||
|
|
4aa6f77a61 | ||
|
|
510573a904 | ||
|
|
0d0a419bd3 | ||
|
|
eeeb03839a | ||
|
|
3af079a81b | ||
|
|
154c9c1c26 | ||
|
|
a76fb2cd52 | ||
|
|
fc9ddcf941 | ||
|
|
3e9edd9eac | ||
|
|
1c1a7683ef | ||
|
|
c215b82004 | ||
|
|
44c7221ae9 | ||
|
|
abf8f65f05 | ||
|
|
e71019a1f3 | ||
|
|
fb9acf4cbd | ||
|
|
684017a75f | ||
|
|
5dffef53cd | ||
|
|
ecd9ef47b0 | ||
|
|
d9482b7588 | ||
|
|
7fab31beae | ||
|
|
5a50efadb2 | ||
|
|
715df4cd92 | ||
|
|
9f75725dfa | ||
|
|
679bbcdc26 | ||
|
|
56cb8a4dde | ||
|
|
f2c6e1c682 | ||
|
|
4474e9fcf9 | ||
|
|
e8c1f5c8d3 | ||
|
|
0d67aa251d | ||
|
|
bba74859e2 | ||
|
|
18097edee1 | ||
|
|
18be0ad80b | ||
|
|
89915e35bd | ||
|
|
ce55625d70 | ||
|
|
d1434408e5 | ||
|
|
76d9560292 | ||
|
|
2298a9ed09 | ||
|
|
933ca2af2a | ||
|
|
cfd492cf7c | ||
|
|
3706f559c1 | ||
|
|
e20e94ddd4 | ||
|
|
0e84962fde | ||
|
|
5e20ac7ed5 | ||
|
|
aef4659e93 | ||
|
|
455efd60f4 | ||
|
|
fa96d49dcb | ||
|
|
483f02c92a | ||
|
|
8a1d86aa7d | ||
|
|
93ffed7809 | ||
|
|
338209f32b | ||
|
|
15d8f979b1 | ||
|
|
67ad07d69a | ||
|
|
ace7c1c892 | ||
|
|
e5780fa308 | ||
|
|
644a9b5565 | ||
|
|
955b735c8b | ||
|
|
1191a9acb7 | ||
|
|
cbdf80727f | ||
|
|
0801c89234 | ||
|
|
4bc1337cc9 | ||
|
|
664ef85c6c | ||
|
|
39268c754a | ||
|
|
18b5f6e5e4 | ||
|
|
9fb6bf82c7 | ||
|
|
8e793261e6 | ||
|
|
6f50a4c964 | ||
|
|
6a5bd99eed | ||
|
|
ab1cefca4b | ||
|
|
5b3dfe3274 | ||
|
|
fa1056b424 | ||
|
|
a560dafa37 | ||
|
|
bc9be8d9ee | ||
|
|
334b3fc636 | ||
|
|
1f649b9799 | ||
|
|
e829e7a064 | ||
|
|
fc79e2e8f8 | ||
|
|
1c754788f9 | ||
|
|
925c5df801 | ||
|
|
4cc6834186 | ||
|
|
07497beb78 | ||
|
|
7eb3e00b35 | ||
|
|
c62dd03921 | ||
|
|
ab63b6e131 | ||
|
|
a8677c082b | ||
|
|
026cc2ecbf | ||
|
|
bdff46ec1d | ||
|
|
e393acf5eb | ||
|
|
e7f7799b3e | ||
|
|
da9e92ad6d | ||
|
|
61691fdedb | ||
|
|
2f265f249e | ||
|
|
f165fd91c0 | ||
|
|
49c55e356f | ||
|
|
5f84cb1afe | ||
|
|
66d593bb0c | ||
|
|
fd7b061dd6 | ||
|
|
2e8352d31c | ||
|
|
2c56a68597 | ||
|
|
a2d39c810a | ||
|
|
0cddf4c11d | ||
|
|
2c2a359d0a |
@@ -1,40 +1,3 @@
|
||||
.DS_Store
|
||||
/.git
|
||||
/.github
|
||||
/.vscode
|
||||
.idea
|
||||
/AdGuardHome
|
||||
/AdGuardHome.exe
|
||||
/AdGuardHome.yaml
|
||||
/AdGuardHome.log
|
||||
/data
|
||||
/build
|
||||
/dist
|
||||
/client/node_modules
|
||||
/.gitattributes
|
||||
/.gitignore
|
||||
/.goreleaser.yml
|
||||
/changelog.config.js
|
||||
/coverage.txt
|
||||
/Dockerfile
|
||||
/LICENSE.txt
|
||||
/Makefile
|
||||
/querylog.json
|
||||
/querylog.json.1
|
||||
/*.md
|
||||
|
||||
# Test output
|
||||
dnsfilter/tests/top-1m.csv
|
||||
dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof
|
||||
|
||||
# Snapcraft build temporary files
|
||||
*.snap
|
||||
launchpad_credentials
|
||||
snapcraft_login
|
||||
snapcraft.yaml.bak
|
||||
|
||||
# IntelliJ IDEA project files
|
||||
*.iml
|
||||
|
||||
# Packr
|
||||
*-packr.go
|
||||
# Ignore everything except for explicitly allowed stuff.
|
||||
*
|
||||
!dist/docker
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e;
|
||||
|
||||
found=0
|
||||
git diff --cached --name-only | grep -q '.js$' && found=1
|
||||
if [ $found == 1 ]; then
|
||||
npm --prefix client run lint || exit 1
|
||||
npm run test --prefix client || exit 1
|
||||
fi
|
||||
|
||||
found=0
|
||||
git diff --cached --name-only | grep -q '.go$' && found=1
|
||||
if [ $found == 1 ]; then
|
||||
make go-lint || exit 1
|
||||
go test ./... || exit 1
|
||||
fi
|
||||
|
||||
exit 0;
|
||||
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -21,6 +21,8 @@ Please answer the following questions for yourself before submitting an issue. *
|
||||
|
||||
* **Version of AdGuard Home server:**
|
||||
* <!-- (e.g. v1.0) -->
|
||||
* **How did you install AdGuard Home:**
|
||||
* <!-- (e.g. Snapcraft, Docker, Github releases) -->
|
||||
* **How did you setup DNS configuration:**
|
||||
* <!-- (System/Router/IoT) -->
|
||||
* **If it's a router or IoT, please write device model:**
|
||||
|
||||
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -8,6 +8,7 @@
|
||||
- 'enhancement'
|
||||
- 'feature request'
|
||||
- 'localization'
|
||||
- 'recurrent'
|
||||
# Label to use when marking an issue as stale.
|
||||
'staleLabel': 'wontfix'
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable.
|
||||
|
||||
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.14'
|
||||
'NODE_VERSION': '13'
|
||||
'NODE_VERSION': '14'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
@@ -55,14 +55,14 @@
|
||||
'restore-keys': '${{ runner.os }}-node-'
|
||||
- 'name': 'Run make ci'
|
||||
'shell': 'bash'
|
||||
'run': 'make ci'
|
||||
'run': 'make VERBOSE=1 ci'
|
||||
- 'name': 'Upload coverage'
|
||||
'uses': 'codecov/codecov-action@v1'
|
||||
'if': "success() && matrix.os == 'ubuntu-latest'"
|
||||
'with':
|
||||
'token': '${{ secrets.CODECOV_TOKEN }}'
|
||||
'file': './coverage.txt'
|
||||
'app':
|
||||
'build-release':
|
||||
'runs-on': 'ubuntu-latest'
|
||||
'needs': 'test'
|
||||
'steps':
|
||||
@@ -95,30 +95,16 @@
|
||||
'restore-keys': '${{ runner.os }}-node-'
|
||||
- 'name': 'Set up Snapcraft'
|
||||
'run': 'sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft'
|
||||
- 'name': 'Set up GoReleaser'
|
||||
'run': 'curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | BINDIR="$(go env GOPATH)/bin" sh'
|
||||
- 'name': 'Run snapshot build'
|
||||
'run': 'make release'
|
||||
|
||||
'docker':
|
||||
'runs-on': 'ubuntu-latest'
|
||||
'needs': 'test'
|
||||
'steps':
|
||||
- 'name': 'Checkout'
|
||||
'uses': 'actions/checkout@v2'
|
||||
'with':
|
||||
'fetch-depth': 0
|
||||
- 'name': 'Set up QEMU'
|
||||
'uses': 'docker/setup-qemu-action@v1'
|
||||
- 'name': 'Set up Docker Buildx'
|
||||
'uses': 'docker/setup-buildx-action@v1'
|
||||
- 'name': 'Docker Buildx (build)'
|
||||
'run': 'make docker-multi-arch'
|
||||
- 'name': 'Run snapshot build'
|
||||
'run': 'make SIGN=0 VERBOSE=1 js-deps js-build build-release build-docker'
|
||||
|
||||
'notify':
|
||||
'needs':
|
||||
- 'app'
|
||||
- 'docker'
|
||||
- 'build-release'
|
||||
# Secrets are not passed to workflows that are triggered by a pull request
|
||||
# from a fork.
|
||||
#
|
||||
@@ -139,7 +125,7 @@
|
||||
'uses': '8398a7/action-slack@v3'
|
||||
'with':
|
||||
'status': '${{ env.WORKFLOW_CONCLUSION }}'
|
||||
'fields': 'repo, message, commit, author, job'
|
||||
'fields': 'repo, message, commit, author, workflow'
|
||||
'env':
|
||||
'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}'
|
||||
'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}'
|
||||
|
||||
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -13,15 +13,15 @@
|
||||
- 'uses': 'actions/checkout@v2'
|
||||
- 'name': 'run-lint'
|
||||
'run': >
|
||||
make go-install-tools go-lint
|
||||
make go-deps go-tools go-lint
|
||||
'eslint':
|
||||
'runs-on': 'ubuntu-latest'
|
||||
'steps':
|
||||
- 'uses': 'actions/checkout@v2'
|
||||
- 'name': 'Install modules'
|
||||
'run': 'npm --prefix client ci'
|
||||
'run': 'npm --prefix="./client" ci'
|
||||
- 'name': 'Run ESLint'
|
||||
'run': 'npm --prefix client run lint'
|
||||
'run': 'npm --prefix="./client" run lint'
|
||||
'notify':
|
||||
'needs':
|
||||
- 'go-lint'
|
||||
@@ -46,7 +46,7 @@
|
||||
'uses': '8398a7/action-slack@v3'
|
||||
'with':
|
||||
'status': '${{ env.WORKFLOW_CONCLUSION }}'
|
||||
'fields': 'repo, message, commit, author, job'
|
||||
'fields': 'repo, message, commit, author, workflow'
|
||||
'env':
|
||||
'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}'
|
||||
'SLACK_WEBHOOK_URL': '${{ secrets.SLACK_WEBHOOK_URL }}'
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,9 +7,11 @@
|
||||
# Only build, run, and test outputs here. Sorted.
|
||||
*-packr.go
|
||||
*.db
|
||||
*.log
|
||||
*.snap
|
||||
/bin/
|
||||
/build/
|
||||
/build2/
|
||||
/data/
|
||||
/dist/
|
||||
/dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof
|
||||
@@ -19,4 +21,5 @@
|
||||
/snapcraft_login
|
||||
AdGuardHome*
|
||||
coverage.txt
|
||||
leases.db
|
||||
node_modules/
|
||||
|
||||
115
.goreleaser.yml
115
.goreleaser.yml
@@ -1,115 +0,0 @@
|
||||
'project_name': 'AdGuardHome'
|
||||
|
||||
'env':
|
||||
- 'GO111MODULE=on'
|
||||
- 'GOPROXY=https://goproxy.io'
|
||||
|
||||
'before':
|
||||
'hooks':
|
||||
- 'go mod download'
|
||||
- 'go generate ./...'
|
||||
|
||||
'builds':
|
||||
- 'main': './main.go'
|
||||
'ldflags':
|
||||
- '-s -w -X main.version={{.Version}} -X main.channel={{.Env.CHANNEL}} -X main.goarm={{.Env.GOARM}}'
|
||||
'env':
|
||||
- 'CGO_ENABLED=0'
|
||||
'goos':
|
||||
- 'darwin'
|
||||
- 'linux'
|
||||
- 'freebsd'
|
||||
- 'windows'
|
||||
'goarch':
|
||||
- '386'
|
||||
- 'amd64'
|
||||
- 'arm'
|
||||
- 'arm64'
|
||||
- 'mips'
|
||||
- 'mipsle'
|
||||
- 'mips64'
|
||||
- 'mips64le'
|
||||
'goarm':
|
||||
- '5'
|
||||
- '6'
|
||||
- '7'
|
||||
'gomips':
|
||||
- 'softfloat'
|
||||
'ignore':
|
||||
- 'goos': 'freebsd'
|
||||
'goarch': 'mips'
|
||||
- 'goos': 'freebsd'
|
||||
'goarch': 'mipsle'
|
||||
|
||||
'archives':
|
||||
- # Archive name template.
|
||||
# Defaults:
|
||||
# - if format is `tar.gz`, `tar.xz`, `gz` or `zip`:
|
||||
# - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}`
|
||||
# - if format is `binary`:
|
||||
# - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}`
|
||||
'name_template': '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}'
|
||||
'wrap_in_directory': 'AdGuardHome'
|
||||
'format_overrides':
|
||||
- 'goos': 'windows'
|
||||
'format': 'zip'
|
||||
- 'goos': 'darwin'
|
||||
'format': 'zip'
|
||||
'files':
|
||||
- 'LICENSE.txt'
|
||||
- 'README.md'
|
||||
|
||||
'snapcrafts':
|
||||
- 'name': 'adguard-home'
|
||||
'base': 'core20'
|
||||
'name_template': '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||
'summary': 'Network-wide ads & trackers blocking DNS server'
|
||||
'description': |
|
||||
AdGuard Home is a network-wide software for blocking ads & tracking. After
|
||||
you set it up, it'll cover ALL your home devices, and you don't need any
|
||||
client-side software for that.
|
||||
|
||||
It operates as a DNS server that re-routes tracking domains to a "black hole,"
|
||||
thus preventing your devices from connecting to those servers. It's based
|
||||
on software we use for our public AdGuard DNS servers -- both share a lot
|
||||
of common code.
|
||||
'grade': 'stable'
|
||||
'confinement': 'strict'
|
||||
'publish': false
|
||||
'license': 'GPL-3.0'
|
||||
'extra_files':
|
||||
- 'source': 'scripts/snap/local/adguard-home-web.sh'
|
||||
'destination': 'adguard-home-web.sh'
|
||||
'mode': 0755
|
||||
- 'source': 'scripts/snap/gui/adguard-home-web.desktop'
|
||||
'destination': 'meta/gui/adguard-home-web.desktop'
|
||||
'mode': 0644
|
||||
- 'source': 'scripts/snap/gui/adguard-home-web.png'
|
||||
'destination': 'meta/gui/adguard-home-web.png'
|
||||
'mode': 0644
|
||||
'apps':
|
||||
'adguard-home':
|
||||
'command': 'AdGuardHome -w $SNAP_DATA --no-check-update'
|
||||
'plugs':
|
||||
# Add the "netrwork-bind" plug to bind to interfaces.
|
||||
- 'network-bind'
|
||||
# Add the "netrwork-observe" plug to be able to bind to ports below 1024
|
||||
# (cap_net_bind_service) and also to bind to a particular interface using
|
||||
# SO_BINDTODEVICE (cap_net_raw).
|
||||
- 'network-observe'
|
||||
'daemon': 'simple'
|
||||
'adguard-home-web':
|
||||
'command': 'adguard-home-web.sh'
|
||||
'plugs':
|
||||
- 'desktop'
|
||||
|
||||
'checksum':
|
||||
'name_template': 'checksums.txt'
|
||||
|
||||
'snapshot':
|
||||
# TODO(a.garipov): A temporary solution to trim the prerelease versions.
|
||||
# A real solution would consist of making a better versioning scheme that also
|
||||
# doesn't break SemVer or Snapcraft.
|
||||
#
|
||||
# See https://github.com/AdguardTeam/AdGuardHome/issues/2412.
|
||||
'name_template': '{{ slice .Tag 0 8 }}-SNAPSHOT-{{ .ShortCommit }}'
|
||||
@@ -1833,16 +1833,22 @@ Response:
|
||||
200 OK
|
||||
|
||||
{
|
||||
"reason":"FilteredBlackList",
|
||||
"filter_id":1,
|
||||
"rule":"||doubleclick.net^",
|
||||
"service_name": "...", // set if reason=FilteredBlockedService
|
||||
|
||||
// if reason=ReasonRewrite:
|
||||
"cname": "...",
|
||||
"ip_addrs": ["1.2.3.4", ...],
|
||||
"reason":"FilteredBlackList",
|
||||
"rules":{
|
||||
"filter_list_id":42,
|
||||
"text":"||doubleclick.net^",
|
||||
},
|
||||
// If we have "reason":"FilteredBlockedService".
|
||||
"service_name": "...",
|
||||
// If we have "reason":"Rewrite".
|
||||
"cname": "...",
|
||||
"ip_addrs": ["1.2.3.4", ...]
|
||||
}
|
||||
|
||||
There are also deprecated properties `filter_id` and `rule` on the top level of
|
||||
the response object. Their usage should be replaced with
|
||||
`rules[*].filter_list_id` and `rules[*].text` correspondingly. See the
|
||||
_OpenAPI_ documentation and the `./openapi/CHANGELOG.md` file.
|
||||
|
||||
## Log-in page
|
||||
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -10,20 +10,29 @@ and this project adheres to
|
||||
## [Unreleased]
|
||||
|
||||
<!--
|
||||
## [v0.105.0] - 2020-12-28
|
||||
## [v0.105.0] - 2021-02-03
|
||||
-->
|
||||
|
||||
### Added
|
||||
|
||||
- Detecting of network interface configurated to have static IP address via
|
||||
- `ipset` subdomain matching, just like `dnsmasq` does ([#2179]).
|
||||
- Client ID support for DNS-over-HTTPS, DNS-over-QUIC, and DNS-over-TLS
|
||||
([#1383]).
|
||||
- `$dnsrewrite` modifier for filters ([#2102]).
|
||||
- The host checking API and the query logs API can now return multiple matched
|
||||
rules ([#2102]).
|
||||
- Detecting of network interface configured to have static IP address via
|
||||
`/etc/network/interfaces` ([#2302]).
|
||||
- DNSCrypt protocol support [#1361].
|
||||
- DNSCrypt protocol support ([#1361]).
|
||||
- A 5 second wait period until a DHCP server's network interface gets an IP
|
||||
address ([#2304]).
|
||||
- `$dnstype` modifier for filters ([#2337]).
|
||||
- HTTP API request body size limit ([#2305]).
|
||||
|
||||
[#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361
|
||||
[#1383]: https://github.com/AdguardTeam/AdGuardHome/issues/1383
|
||||
[#2102]: https://github.com/AdguardTeam/AdGuardHome/issues/2102
|
||||
[#2179]: https://github.com/AdguardTeam/AdGuardHome/issues/2179
|
||||
[#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302
|
||||
[#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304
|
||||
[#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305
|
||||
@@ -31,36 +40,67 @@ and this project adheres to
|
||||
|
||||
### Changed
|
||||
|
||||
- `workDir` now supports symlinks.
|
||||
- Stopped mounting together the directories `/opt/adguardhome/conf` and
|
||||
`/opt/adguardhome/work` in our Docker images ([#2589]).
|
||||
- When `dns.bogus_nxdomain` option is used, the server will now transform
|
||||
responses if there is at least one bogus address instead of all of them
|
||||
([#2394]). The new behavior is the same as in `dnsmasq`.
|
||||
- Post-updating relaunch possibility is now determined OS-dependently ([#2231],
|
||||
[#2391]).
|
||||
- Made the mobileconfig HTTP API more robust and predictable, add parameters and
|
||||
improve error response ([#2358]).
|
||||
- Improved HTTP requests handling and timeouts ([#2343]).
|
||||
- Our snap package now uses the `core20` image as its base ([#2306]).
|
||||
- Various internal improvements ([#2267], [#2271], [#2297]).
|
||||
- New build system and various internal improvements ([#2271], [#2276], [#2297],
|
||||
[#2509], [#2552]).
|
||||
|
||||
[#2231]: https://github.com/AdguardTeam/AdGuardHome/issues/2231
|
||||
[#2267]: https://github.com/AdguardTeam/AdGuardHome/issues/2267
|
||||
[#2271]: https://github.com/AdguardTeam/AdGuardHome/issues/2271
|
||||
[#2276]: https://github.com/AdguardTeam/AdGuardHome/issues/2276
|
||||
[#2297]: https://github.com/AdguardTeam/AdGuardHome/issues/2297
|
||||
[#2306]: https://github.com/AdguardTeam/AdGuardHome/issues/2306
|
||||
[#2343]: https://github.com/AdguardTeam/AdGuardHome/issues/2343
|
||||
[#2358]: https://github.com/AdguardTeam/AdGuardHome/issues/2358
|
||||
[#2391]: https://github.com/AdguardTeam/AdGuardHome/issues/2391
|
||||
[#2394]: https://github.com/AdguardTeam/AdGuardHome/issues/2394
|
||||
[#2509]: https://github.com/AdguardTeam/AdGuardHome/issues/2509
|
||||
[#2552]: https://github.com/AdguardTeam/AdGuardHome/issues/2552
|
||||
[#2589]: https://github.com/AdguardTeam/AdGuardHome/issues/2589
|
||||
|
||||
### Deprecated
|
||||
|
||||
- _Go_ 1.14 support. v0.106.0 will require at least _Go_ 1.15 to build.
|
||||
- The `darwin/386` port. It will be removed in v0.106.0.
|
||||
- The `"rule"` and `"filter_id"` fields in `GET /filtering/check_host` and
|
||||
`GET /querylog` responses. They will be removed in v0.106.0 ([#2102]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Unnecessary conversions from `string` to `net.IP`, and vice versa ([#2508]).
|
||||
- Inability to set DNS cache TTL limits ([#2459]).
|
||||
- Possible freezes on slower machines ([#2225]).
|
||||
- A mitigation against records being shown in the wrong order on the query log
|
||||
page ([#2293]).
|
||||
- A JSON parsing error in query log ([#2345]).
|
||||
- Incorrect detection of the IPv6 address of an interface as well as another
|
||||
infinite loop in the `/dhcp/find_active_dhcp` HTTP API ([#2355]).
|
||||
|
||||
[#2225]: https://github.com/AdguardTeam/AdGuardHome/issues/2225
|
||||
[#2293]: https://github.com/AdguardTeam/AdGuardHome/issues/2293
|
||||
[#2345]: https://github.com/AdguardTeam/AdGuardHome/issues/2345
|
||||
[#2355]: https://github.com/AdguardTeam/AdGuardHome/issues/2355
|
||||
[#2459]: https://github.com/AdguardTeam/AdGuardHome/issues/2459
|
||||
[#2508]: https://github.com/AdguardTeam/AdGuardHome/issues/2508
|
||||
|
||||
### Removed
|
||||
|
||||
- The undocumented ability to use hostnames as any of `bind_host` values in
|
||||
configuration. Documentation requires them to be valid IP addresses, and now
|
||||
the implementation makes sure that that is the case ([#2508]).
|
||||
- `Dockerfile` ([#2276]). Replaced with the script
|
||||
`scripts/make/build-docker.sh` which uses `scripts/make/Dockerfile`.
|
||||
- Support for pre-v0.99.3 format of query logs ([#2102]).
|
||||
|
||||
## [v0.104.3] - 2020-11-19
|
||||
|
||||
|
||||
77
Dockerfile
77
Dockerfile
@@ -1,77 +0,0 @@
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} tonistiigi/xx:golang AS xgo
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.14-alpine as builder
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION=dev
|
||||
ARG CHANNEL=release
|
||||
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GO111MODULE on
|
||||
ENV GOPROXY https://goproxy.io
|
||||
|
||||
COPY --from=xgo / /
|
||||
RUN go env
|
||||
|
||||
RUN apk --update --no-cache add \
|
||||
build-base \
|
||||
gcc \
|
||||
git \
|
||||
npm \
|
||||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
|
||||
# Prepare the client code
|
||||
RUN npm --prefix client ci && npm --prefix client run build-prod
|
||||
|
||||
# Download go dependencies
|
||||
RUN go mod download
|
||||
RUN go generate ./...
|
||||
|
||||
# It's important to place TARGET* arguments here to avoid running npm and go mod download for every platform
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
RUN go build -ldflags="-s -w -X main.version=${VERSION} -X main.channel=${CHANNEL} -X main.goarm=${GOARM}"
|
||||
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:latest
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION
|
||||
ARG CHANNEL
|
||||
|
||||
LABEL maintainer="AdGuard Team <devteam@adguard.com>" \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.url="https://adguard.com/adguard-home.html" \
|
||||
org.opencontainers.image.source="https://github.com/AdguardTeam/AdGuardHome" \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.revision=$VCS_REF \
|
||||
org.opencontainers.image.vendor="AdGuard" \
|
||||
org.opencontainers.image.title="AdGuard Home" \
|
||||
org.opencontainers.image.description="Network-wide ads & trackers blocking DNS server" \
|
||||
org.opencontainers.image.licenses="GPL-3.0"
|
||||
|
||||
RUN apk --update --no-cache add \
|
||||
ca-certificates \
|
||||
libcap \
|
||||
libressl \
|
||||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
COPY --from=builder --chown=nobody:nogroup /app/AdGuardHome /opt/adguardhome/AdGuardHome
|
||||
COPY --from=builder --chown=nobody:nogroup /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
|
||||
|
||||
RUN /opt/adguardhome/AdGuardHome --version \
|
||||
&& mkdir -p /opt/adguardhome/conf /opt/adguardhome/work \
|
||||
&& chown -R nobody: /opt/adguardhome \
|
||||
&& setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
|
||||
|
||||
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 853/tcp 3000/tcp
|
||||
WORKDIR /opt/adguardhome/work
|
||||
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
|
||||
|
||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
||||
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"]
|
||||
84
HACKING.md
84
HACKING.md
@@ -1,4 +1,4 @@
|
||||
# *AdGuardHome* Developer Guidelines
|
||||
# AdGuard Home Developer Guidelines
|
||||
|
||||
As of **December 2020**, this document is partially a work-in-progress, but
|
||||
should still be followed. Some of the rules aren't enforced as thoroughly or
|
||||
@@ -19,8 +19,9 @@ The rules are mostly sorted in the alphabetical order.
|
||||
pkg: fix the network error logging issue
|
||||
```
|
||||
|
||||
Where `pkg` is the package where most changes took place. If there are
|
||||
several such packages, or the change is top-level only, write `all`.
|
||||
Where `pkg` is the directory or Go package (without the `internal/` part)
|
||||
where most changes took place. If there are several such packages, or the
|
||||
change is top-level only, write `all`.
|
||||
|
||||
* Keep your commit messages, including headers, to eighty (**80**) columns.
|
||||
|
||||
@@ -45,6 +46,14 @@ The rules are mostly sorted in the alphabetical order.
|
||||
|
||||
* Avoid `new`, especially with structs.
|
||||
|
||||
* Check against empty strings like this:
|
||||
|
||||
```go
|
||||
if s == "" {
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
* Constructors should validate their arguments and return meaningful errors.
|
||||
As a corollary, avoid lazy initialization.
|
||||
|
||||
@@ -53,9 +62,12 @@ The rules are mostly sorted in the alphabetical order.
|
||||
* Don't use underscores in file and package names, unless they're build tags
|
||||
or for tests. This is to prevent accidental build errors with weird tags.
|
||||
|
||||
* Don't write code with more than four (**4**) levels of indentation. Just
|
||||
like [Linus said], plus an additional level for an occasional error check or
|
||||
struct initialization.
|
||||
* Don't write non-test code with more than four (**4**) levels of indentation.
|
||||
Just like [Linus said], plus an additional level for an occasional error
|
||||
check or struct initialization.
|
||||
|
||||
The exception proving the rule is the table-driven test code, where an
|
||||
additional level of indentation is allowed.
|
||||
|
||||
* Eschew external dependencies, including transitive, unless
|
||||
absolutely necessary.
|
||||
@@ -70,6 +82,14 @@ The rules are mostly sorted in the alphabetical order.
|
||||
func TestType_Method_suffix(t *testing.T) { /* … */ }
|
||||
```
|
||||
|
||||
* Name parameters in interface definitions:
|
||||
|
||||
```go
|
||||
type Frobulator interface {
|
||||
Frobulate(f Foo, b Bar) (r Result, err error)
|
||||
}
|
||||
```
|
||||
|
||||
* Name the deferred errors (e.g. when closing something) `cerr`.
|
||||
|
||||
* No shadowing, since it can often lead to subtle bugs, especially with
|
||||
@@ -78,6 +98,17 @@ The rules are mostly sorted in the alphabetical order.
|
||||
* Prefer constants to variables where possible. Reduce global variables. Use
|
||||
[constant errors] instead of `errors.New`.
|
||||
|
||||
* Program code lines should not be longer than one hundred (**100**) columns.
|
||||
For comments, see the text section below.
|
||||
|
||||
* Unused arguments in anonymous functions must be called `_`:
|
||||
|
||||
```go
|
||||
v.onSuccess = func(_ int, msg string) {
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
* Use linters.
|
||||
|
||||
* Use named returns to improve readability of function signatures.
|
||||
@@ -106,7 +137,16 @@ The rules are mostly sorted in the alphabetical order.
|
||||
```go
|
||||
// Foo implements the Fooer interface for *foo.
|
||||
func (f *foo) Foo() {
|
||||
// …
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
When the implemented interface is unexported:
|
||||
|
||||
```go
|
||||
// Unwrap implements the hidden wrapper interface for *fooError.
|
||||
func (err *fooError) Unwrap() (unwrapped error) {
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
@@ -146,19 +186,45 @@ The rules are mostly sorted in the alphabetical order.
|
||||
|
||||
## Shell Scripting
|
||||
|
||||
* Avoid bashisms, prefer *POSIX* features only.
|
||||
* Avoid bashisms and GNUisms, prefer *POSIX* features only.
|
||||
|
||||
* Prefer `'raw strings'` to `"double quoted strings"` whenever possible.
|
||||
|
||||
* Put spaces within `$( cmd )`, `$(( expr ))`, and `{ cmd; }`.
|
||||
|
||||
* `snake_case`, not `camelCase`.
|
||||
* Put utility flags in the ASCII order and **don't** group them together. For
|
||||
example, `ls -1 -A -q`.
|
||||
|
||||
* `snake_case`, not `camelCase` for variables. `kebab-case` for filenames.
|
||||
|
||||
* UPPERCASE names for external exported variables, lowercase for local,
|
||||
unexported ones.
|
||||
|
||||
* Use `set -e -f -u` and also `set -x` in verbose mode.
|
||||
|
||||
* Use `readonly` liberally.
|
||||
|
||||
* Use the `"$var"` form instead of the `$var` form, unless word splitting is
|
||||
required.
|
||||
|
||||
* When concatenating, always use the form with curly braces to prevent
|
||||
accidental bad variable names. That is, `"${var}_tmp.txt"` and **not**
|
||||
`"$var_tmp.txt"`. The latter will try to lookup variable `var_tmp`.
|
||||
|
||||
* When concatenating, surround the whole string with quotes. That is, use
|
||||
this:
|
||||
|
||||
```sh
|
||||
dir="${TOP_DIR}/sub"
|
||||
```
|
||||
|
||||
And **not** this:
|
||||
|
||||
```sh
|
||||
# Bad!
|
||||
dir="${TOP_DIR}"/sub
|
||||
```
|
||||
|
||||
## Text, Including Comments
|
||||
|
||||
* End sentences with appropriate punctuation.
|
||||
|
||||
416
Makefile
416
Makefile
@@ -1,345 +1,103 @@
|
||||
# Keep the Makefile POSIX-compliant. We currently allow hyphens in
|
||||
# target names, but that may change in the future.
|
||||
#
|
||||
# Available targets
|
||||
# See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html.
|
||||
.POSIX:
|
||||
|
||||
CHANNEL = development
|
||||
CLIENT_BETA_DIR = client2
|
||||
CLIENT_DIR = client
|
||||
COMMIT = $$(git rev-parse --short HEAD)
|
||||
DIST_DIR = dist
|
||||
GO = go
|
||||
# TODO(a.garipov): Add more default proxies using pipes after update to
|
||||
# Go 1.15.
|
||||
#
|
||||
# * build -- builds AdGuardHome for the current platform
|
||||
# * client -- builds client-side code of AdGuard Home
|
||||
# * client-watch -- builds client-side code of AdGuard Home and watches for changes there
|
||||
# * docker -- builds a docker image for the current platform
|
||||
# * clean -- clean everything created by previous builds
|
||||
# * lint -- run all linters
|
||||
# * test -- run all unit-tests
|
||||
# * dependencies -- installs dependencies (go and npm modules)
|
||||
# * ci -- installs dependencies, runs linters and tests, intended to be used by CI/CD
|
||||
#
|
||||
# Building releases:
|
||||
#
|
||||
# * release -- builds AdGuard Home distros. CHANNEL must be specified (edge, release or beta).
|
||||
# * release_and_sign -- builds AdGuard Home distros and signs the binary files.
|
||||
# CHANNEL must be specified (edge, release or beta).
|
||||
# * sign -- Repacks all release archive files and signs the binary files inside them.
|
||||
# For signing to work, the public+private key pair for $(GPG_KEY) must be imported:
|
||||
# gpg --import public.txt
|
||||
# gpg --import private.txt
|
||||
# GPG_KEY_PASSPHRASE must contain the GPG key passphrase
|
||||
# * docker-multi-arch -- builds a multi-arch image. If you want it to be pushed to docker hub,
|
||||
# you must specify:
|
||||
# * DOCKER_IMAGE_NAME - adguard/adguard-home
|
||||
# * DOCKER_OUTPUT - type=image,name=adguard/adguard-home,push=true
|
||||
# GOPROXY = https://goproxy.io|https://goproxy.cn|direct
|
||||
GOPROXY = https://goproxy.cn,https://goproxy.io,direct
|
||||
GPG_KEY = devteam@adguard.com
|
||||
GPG_KEY_PASSPHRASE = not-a-real-password
|
||||
NPM = npm
|
||||
NPM_FLAGS = --prefix $(CLIENT_DIR)
|
||||
SIGN = 1
|
||||
VERBOSE = 0
|
||||
VERSION = v0.0.0
|
||||
YARN = yarn
|
||||
YARN_FLAGS = --cwd $(CLIENT_BETA_DIR)
|
||||
|
||||
GO := go
|
||||
GOPATH := $(shell $(GO) env GOPATH)
|
||||
PWD := $(shell pwd)
|
||||
TARGET=AdGuardHome
|
||||
BASE_URL="https://static.adguard.com/adguardhome/$(CHANNEL)"
|
||||
GPG_KEY := devteam@adguard.com
|
||||
GPG_KEY_PASSPHRASE :=
|
||||
GPG_CMD := gpg --detach-sig --default-key $(GPG_KEY) --pinentry-mode loopback --passphrase $(GPG_KEY_PASSPHRASE)
|
||||
VERBOSE := -v
|
||||
ENV = env\
|
||||
COMMIT='$(COMMIT)'\
|
||||
CHANNEL='$(CHANNEL)'\
|
||||
GPG_KEY='$(GPG_KEY)'\
|
||||
GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\
|
||||
DIST_DIR='$(DIST_DIR)'\
|
||||
GO='$(GO)'\
|
||||
GOPROXY='$(GOPROXY)'\
|
||||
PATH="$${PWD}/bin:$$($(GO) env GOPATH)/bin:$${PATH}"\
|
||||
SIGN='$(SIGN)'\
|
||||
VERBOSE='$(VERBOSE)'\
|
||||
VERSION='$(VERSION)'\
|
||||
|
||||
# See release target
|
||||
DIST_DIR=dist
|
||||
# Keep the line above blank.
|
||||
|
||||
# Update channel. Can be release, beta or edge. Uses edge by default.
|
||||
CHANNEL ?= edge
|
||||
# Keep this target first, so that a naked make invocation triggers
|
||||
# a full build.
|
||||
build: deps quick-build
|
||||
|
||||
# Validate channel
|
||||
ifneq ($(CHANNEL),release)
|
||||
ifneq ($(CHANNEL),beta)
|
||||
ifneq ($(CHANNEL),edge)
|
||||
$(error CHANNEL value is not valid. Valid values are release,beta or edge)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
quick-build: js-build go-build
|
||||
|
||||
# Version history URL (see
|
||||
VERSION_HISTORY_URL="https://github.com/AdguardTeam/AdGuardHome/releases"
|
||||
ifeq ($(CHANNEL),edge)
|
||||
VERSION_HISTORY_URL="https://github.com/AdguardTeam/AdGuardHome/commits/master"
|
||||
endif
|
||||
|
||||
# goreleaser command depends on the $CHANNEL
|
||||
GORELEASER_COMMAND=goreleaser release --rm-dist --skip-publish --snapshot --parallelism 1
|
||||
ifneq ($(CHANNEL),edge)
|
||||
# If this is not an "edge" build, use normal release command
|
||||
GORELEASER_COMMAND=goreleaser release --rm-dist --skip-publish --parallelism 1
|
||||
endif
|
||||
|
||||
# Version properties
|
||||
COMMIT=$(shell git rev-parse --short HEAD)
|
||||
# TODO(a.garipov): The cut call is a temporary solution to trim
|
||||
# prerelease versions. See the comment in .goreleaser.yml.
|
||||
TAG_NAME=$(shell git describe --abbrev=0 | cut -c 1-8)
|
||||
RELEASE_VERSION=$(TAG_NAME)
|
||||
SNAPSHOT_VERSION=$(RELEASE_VERSION)-SNAPSHOT-$(COMMIT)
|
||||
|
||||
# Set proper version
|
||||
VERSION=
|
||||
ifeq ($(TAG_NAME),$(shell git describe --abbrev=4))
|
||||
ifeq ($(CHANNEL),edge)
|
||||
VERSION=$(SNAPSHOT_VERSION)
|
||||
else
|
||||
VERSION=$(RELEASE_VERSION)
|
||||
endif
|
||||
else
|
||||
VERSION=$(SNAPSHOT_VERSION)
|
||||
endif
|
||||
|
||||
# Docker target parameters
|
||||
DOCKER_IMAGE_NAME ?= adguardhome-dev
|
||||
DOCKER_IMAGE_FULL_NAME = $(DOCKER_IMAGE_NAME):$(VERSION)
|
||||
DOCKER_PLATFORMS=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le
|
||||
DOCKER_OUTPUT ?= type=image,name=$(DOCKER_IMAGE_NAME),push=false
|
||||
BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
# Docker tags (can be redefined)
|
||||
DOCKER_TAGS ?=
|
||||
ifndef DOCKER_TAGS
|
||||
ifeq ($(CHANNEL),release)
|
||||
DOCKER_TAGS := --tag $(DOCKER_IMAGE_NAME):latest
|
||||
endif
|
||||
ifeq ($(CHANNEL),beta)
|
||||
DOCKER_TAGS := --tag $(DOCKER_IMAGE_NAME):beta
|
||||
endif
|
||||
ifeq ($(CHANNEL),edge)
|
||||
# Don't set the version tag when pushing to "edge"
|
||||
DOCKER_IMAGE_FULL_NAME := $(DOCKER_IMAGE_NAME):edge
|
||||
# DOCKER_TAGS := --tag $(DOCKER_IMAGE_NAME):edge
|
||||
endif
|
||||
endif
|
||||
|
||||
# Validate docker build arguments
|
||||
ifndef DOCKER_IMAGE_NAME
|
||||
$(error DOCKER_IMAGE_NAME value is not set)
|
||||
endif
|
||||
|
||||
# OS-specific flags
|
||||
TEST_FLAGS := --race $(VERBOSE)
|
||||
ifeq ($(OS),Windows_NT)
|
||||
TEST_FLAGS :=
|
||||
endif
|
||||
|
||||
.PHONY: all build client client-watch docker lint lint-js lint-go test dependencies clean release docker-multi-arch
|
||||
all: build
|
||||
|
||||
init:
|
||||
git config core.hooksPath .githooks
|
||||
|
||||
build: client_with_deps
|
||||
$(GO) mod download
|
||||
PATH=$(GOPATH)/bin:$(PATH) $(GO) generate ./...
|
||||
CGO_ENABLED=0 $(GO) build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)"
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
client:
|
||||
npm --prefix client run build-prod
|
||||
|
||||
client_with_deps:
|
||||
npm --prefix client ci
|
||||
npm --prefix client run build-prod
|
||||
|
||||
client-watch:
|
||||
npm --prefix client run watch
|
||||
|
||||
docker:
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled \
|
||||
docker buildx build \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg CHANNEL=$(CHANNEL) \
|
||||
--build-arg VCS_REF=$(COMMIT) \
|
||||
--build-arg BUILD_DATE=$(BUILD_DATE) \
|
||||
$(DOCKER_TAGS) \
|
||||
--load \
|
||||
-t "$(DOCKER_IMAGE_NAME)" -f ./Dockerfile .
|
||||
|
||||
@echo Now you can run the docker image:
|
||||
@echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME)
|
||||
ci: deps test
|
||||
|
||||
deps: js-deps go-deps
|
||||
lint: js-lint go-lint
|
||||
|
||||
js-lint: dependencies
|
||||
npm --prefix client run lint
|
||||
|
||||
go-install-tools:
|
||||
env GO=$(GO) sh ./scripts/go-install-tools.sh
|
||||
|
||||
go-lint:
|
||||
env GO=$(GO) PATH="$$PWD/bin:$$PATH" sh ./scripts/go-lint.sh
|
||||
|
||||
test: js-test go-test
|
||||
|
||||
js-test:
|
||||
npm run test --prefix client
|
||||
# Here and below, keep $(SHELL) in quotes, because on Windows this will
|
||||
# expand to something like "C:/Program Files/Git/usr/bin/sh.exe".
|
||||
build-docker: ; $(ENV) "$(SHELL)" ./scripts/make/build-docker.sh
|
||||
|
||||
go-test:
|
||||
$(GO) test $(TEST_FLAGS) --coverprofile coverage.txt ./...
|
||||
build-release: deps js-build
|
||||
$(ENV) "$(SHELL)" ./scripts/make/build-release.sh
|
||||
|
||||
ci: client_with_deps
|
||||
$(GO) mod download
|
||||
$(MAKE) test
|
||||
clean: ; $(ENV) "$(SHELL)" ./scripts/make/clean.sh
|
||||
init: ; git config core.hooksPath ./scripts/hooks
|
||||
|
||||
js-build:
|
||||
$(NPM) $(NPM_FLAGS) run build-prod
|
||||
$(YARN) $(YARN_FLAGS) build
|
||||
js-deps:
|
||||
$(NPM) $(NPM_FLAGS) ci
|
||||
$(YARN) $(YARN_FLAGS) install
|
||||
|
||||
# TODO(a.garipov): Remove the legacy client tasks support once the new
|
||||
# client is done and the old one is removed.
|
||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||
js-test: ; $(NPM) $(NPM_FLAGS) run test
|
||||
js-beta-lint: ; $(YARN) $(YARN_FLAGS) lint
|
||||
js-beta-test: ; # TODO(v.abdulmyanov): Add tests for the new client.
|
||||
|
||||
go-build: ; $(ENV) "$(SHELL)" ./scripts/make/go-build.sh
|
||||
go-deps: ; $(ENV) "$(SHELL)" ./scripts/make/go-deps.sh
|
||||
go-lint: ; $(ENV) "$(SHELL)" ./scripts/make/go-lint.sh
|
||||
go-test: ; $(ENV) "$(SHELL)" ./scripts/make/go-test.sh
|
||||
go-tools: ; $(ENV) "$(SHELL)" ./scripts/make/go-tools.sh
|
||||
|
||||
go-check: go-tools go-lint go-test
|
||||
|
||||
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
||||
openapi-show: ; cd ./openapi/ && $(YARN) start
|
||||
|
||||
# TODO(a.garipov): Remove the legacy targets once the build
|
||||
# infrastructure stops using them.
|
||||
dependencies:
|
||||
npm --prefix client ci
|
||||
$(GO) mod download
|
||||
|
||||
clean:
|
||||
rm -f ./AdGuardHome ./AdGuardHome.exe ./coverage.txt
|
||||
rm -f -r ./build/ ./client/node_modules/ ./data/ ./$(DIST_DIR)/
|
||||
# Set the GOPATH explicitly in case make clean is called from under sudo
|
||||
# after a Docker build.
|
||||
env PATH="$(GOPATH)/bin:$$PATH" packr clean
|
||||
rm -f -r ./bin/
|
||||
|
||||
@ echo "use make deps instead"
|
||||
@ $(MAKE) deps
|
||||
docker-multi-arch:
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled \
|
||||
docker buildx build \
|
||||
--platform $(DOCKER_PLATFORMS) \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg CHANNEL=$(CHANNEL) \
|
||||
--build-arg VCS_REF=$(COMMIT) \
|
||||
--build-arg BUILD_DATE=$(BUILD_DATE) \
|
||||
$(DOCKER_TAGS) \
|
||||
--output "$(DOCKER_OUTPUT)" \
|
||||
-t "$(DOCKER_IMAGE_FULL_NAME)" -f ./Dockerfile .
|
||||
|
||||
@echo If the image was pushed to the registry, you can now run it:
|
||||
@echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME)
|
||||
|
||||
release: client_with_deps
|
||||
$(GO) mod download
|
||||
@echo Starting release build: version $(VERSION), channel $(CHANNEL)
|
||||
CHANNEL=$(CHANNEL) $(GORELEASER_COMMAND)
|
||||
$(call write_version_file,$(VERSION))
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
release_and_sign: client_with_deps
|
||||
$(MAKE) release
|
||||
$(call repack_dist)
|
||||
|
||||
sign:
|
||||
$(call repack_dist)
|
||||
|
||||
define write_version_file
|
||||
$(eval version := $(1))
|
||||
|
||||
@echo Writing version file: $(version)
|
||||
|
||||
# Variables for CI
|
||||
rm -f $(DIST_DIR)/version.txt
|
||||
echo "version=$(version)" > $(DIST_DIR)/version.txt
|
||||
|
||||
# Prepare the version.json file
|
||||
rm -f $(DIST_DIR)/version.json
|
||||
echo "{" >> $(DIST_DIR)/version.json
|
||||
echo " \"version\": \"$(version)\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"announcement\": \"AdGuard Home $(version) is now available!\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"announcement_url\": \"$(VERSION_HISTORY_URL)\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"selfupdate_min_version\": \"0.0\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Windows builds
|
||||
echo " \"download_windows_amd64\": \"$(BASE_URL)/AdGuardHome_windows_amd64.zip\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_windows_386\": \"$(BASE_URL)/AdGuardHome_windows_386.zip\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# MacOS builds
|
||||
echo " \"download_darwin_amd64\": \"$(BASE_URL)/AdGuardHome_darwin_amd64.zip\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_darwin_386\": \"$(BASE_URL)/AdGuardHome_darwin_386.zip\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Linux
|
||||
echo " \"download_linux_amd64\": \"$(BASE_URL)/AdGuardHome_linux_amd64.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_386\": \"$(BASE_URL)/AdGuardHome_linux_386.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Linux, all kinds of ARM
|
||||
echo " \"download_linux_arm\": \"$(BASE_URL)/AdGuardHome_linux_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_armv5\": \"$(BASE_URL)/AdGuardHome_linux_armv5.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_armv6\": \"$(BASE_URL)/AdGuardHome_linux_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_armv7\": \"$(BASE_URL)/AdGuardHome_linux_armv7.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_arm64\": \"$(BASE_URL)/AdGuardHome_linux_arm64.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Linux, MIPS
|
||||
echo " \"download_linux_mips\": \"$(BASE_URL)/AdGuardHome_linux_mips_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_mipsle\": \"$(BASE_URL)/AdGuardHome_linux_mipsle_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_mips64\": \"$(BASE_URL)/AdGuardHome_linux_mips64_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_mips64le\": \"$(BASE_URL)/AdGuardHome_linux_mips64le_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# FreeBSD
|
||||
echo " \"download_freebsd_386\": \"$(BASE_URL)/AdGuardHome_freebsd_386.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_amd64\": \"$(BASE_URL)/AdGuardHome_freebsd_amd64.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# FreeBSD, all kinds of ARM
|
||||
echo " \"download_freebsd_arm\": \"$(BASE_URL)/AdGuardHome_freebsd_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_armv5\": \"$(BASE_URL)/AdGuardHome_freebsd_armv5.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_armv6\": \"$(BASE_URL)/AdGuardHome_freebsd_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_armv7\": \"$(BASE_URL)/AdGuardHome_freebsd_armv7.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_arm64\": \"$(BASE_URL)/AdGuardHome_freebsd_arm64.tar.gz\"" >> $(DIST_DIR)/version.json
|
||||
|
||||
# Finish
|
||||
echo "}" >> $(DIST_DIR)/version.json
|
||||
endef
|
||||
|
||||
define repack_dist
|
||||
# Repack archive files
|
||||
# A temporary solution for our auto-update code to be able to unpack these archive files
|
||||
# The problem is that goreleaser doesn't add directory AdGuardHome/ to the archive file
|
||||
# and we can't create it
|
||||
rm -rf $(DIST_DIR)/AdGuardHome
|
||||
|
||||
# Windows builds
|
||||
$(call zip_repack_windows,AdGuardHome_windows_amd64.zip)
|
||||
$(call zip_repack_windows,AdGuardHome_windows_386.zip)
|
||||
|
||||
# MacOS builds
|
||||
$(call zip_repack,AdGuardHome_darwin_amd64.zip)
|
||||
$(call zip_repack,AdGuardHome_darwin_386.zip)
|
||||
|
||||
# Linux
|
||||
$(call tar_repack,AdGuardHome_linux_amd64.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_386.tar.gz)
|
||||
|
||||
# Linux, all kinds of ARM
|
||||
$(call tar_repack,AdGuardHome_linux_armv5.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_armv6.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_armv7.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_arm64.tar.gz)
|
||||
|
||||
# Linux, MIPS
|
||||
$(call tar_repack,AdGuardHome_linux_mips_softfloat.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_mipsle_softfloat.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_mips64_softfloat.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
||||
|
||||
# FreeBSD
|
||||
$(call tar_repack,AdGuardHome_freebsd_386.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_freebsd_amd64.tar.gz)
|
||||
|
||||
# FreeBSD, all kinds of ARM
|
||||
$(call tar_repack,AdGuardHome_freebsd_armv5.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_freebsd_armv6.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_freebsd_armv7.tar.gz)
|
||||
$(call tar_repack,AdGuardHome_freebsd_arm64.tar.gz)
|
||||
endef
|
||||
|
||||
define zip_repack_windows
|
||||
$(eval ARC := $(1))
|
||||
cd $(DIST_DIR) && \
|
||||
unzip $(ARC) && \
|
||||
$(GPG_CMD) AdGuardHome/AdGuardHome.exe && \
|
||||
zip -r $(ARC) AdGuardHome/ && \
|
||||
rm -rf AdGuardHome
|
||||
endef
|
||||
|
||||
define zip_repack
|
||||
$(eval ARC := $(1))
|
||||
cd $(DIST_DIR) && \
|
||||
unzip $(ARC) && \
|
||||
$(GPG_CMD) AdGuardHome/AdGuardHome && \
|
||||
zip -r $(ARC) AdGuardHome/ && \
|
||||
rm -rf AdGuardHome
|
||||
endef
|
||||
|
||||
define tar_repack
|
||||
$(eval ARC := $(1))
|
||||
cd $(DIST_DIR) && \
|
||||
tar xzf $(ARC) && \
|
||||
$(GPG_CMD) AdGuardHome/AdGuardHome && \
|
||||
tar czf $(ARC) AdGuardHome/ && \
|
||||
rm -rf AdGuardHome
|
||||
endef
|
||||
@ echo "use make build-docker instead"
|
||||
@ $(MAKE) build-docker
|
||||
go-install-tools:
|
||||
@ echo "use make go-tools instead"
|
||||
@ $(MAKE) go-tools
|
||||
release:
|
||||
@ echo "use make build-release instead"
|
||||
@ $(MAKE) build-release
|
||||
|
||||
85
README.md
85
README.md
@@ -45,7 +45,7 @@
|
||||
|
||||
AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.
|
||||
|
||||
It operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
|
||||
It operates as a DNS server that re-routes tracking domains to a "black hole", thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
|
||||
|
||||
* [Getting Started](#getting-started)
|
||||
* [Comparing AdGuard Home to other solutions](#comparison)
|
||||
@@ -58,7 +58,7 @@ It operates as a DNS server that re-routes tracking domains to a "black hole," t
|
||||
* [Reporting issues](#reporting-issues)
|
||||
* [Help with translations](#translate)
|
||||
* [Other](#help-other)
|
||||
* [Projects that use AdGuardHome](#uses)
|
||||
* [Projects that use AdGuard Home](#uses)
|
||||
* [Acknowledgments](#acknowledgments)
|
||||
* [Privacy](#privacy)
|
||||
|
||||
@@ -87,12 +87,21 @@ If you're running **Linux**, there's a secure and easy way to install AdGuard Ho
|
||||
|
||||
### Guides
|
||||
|
||||
* [FAQ](https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ)
|
||||
* [Configuration](https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration)
|
||||
* [AdGuard Home as a DNS-over-HTTPS or DNS-over-TLS server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption)
|
||||
* [How to install and run AdGuard Home on Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi)
|
||||
* [How to install and run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS)
|
||||
* [How to write your own hosts blocklists properly](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists)
|
||||
* [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started)
|
||||
* [FAQ](https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ)
|
||||
* [How to Write Hosts Blocklists](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists)
|
||||
* [Comparing AdGuard Home to Other Solutions](https://github.com/AdguardTeam/AdGuardHome/wiki/Comparison)
|
||||
* Configuring AdGuard
|
||||
* [Configuration](https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration)
|
||||
* [Configuring AdGuard Home Clients](https://github.com/AdguardTeam/AdGuardHome/wiki/Clients)
|
||||
* [AdGuard Home as a DoH, DoT, or DoQ Server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption)
|
||||
* [AdGuard Home as a DNSCrypt Server](https://github.com/AdguardTeam/AdGuardHome/wiki/DNSCrypt)
|
||||
* [AdGuard Home as a DHCP Server](https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP)
|
||||
* Installing AdGuard Home
|
||||
* [Docker](https://github.com/AdguardTeam/AdGuardHome/wiki/Docker)
|
||||
* [How to Install and Run AdGuard Home on a Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi)
|
||||
* [How to Install and Run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS)
|
||||
* [Verifying Releases](https://github.com/AdguardTeam/AdGuardHome/wiki/Verify-Releases)
|
||||
|
||||
### API
|
||||
|
||||
@@ -123,20 +132,21 @@ AdGuard Home provides a lot of features out-of-the-box with no need to install a
|
||||
|
||||
> Disclaimer: some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature.
|
||||
|
||||
| Feature | AdGuard Home | Pi-Hole |
|
||||
|-------------------------------------------------------------------------|--------------|--------------------------------------------------------|
|
||||
| Blocking ads and trackers | ✅ | ✅ |
|
||||
| Customizing blocklists | ✅ | ✅ |
|
||||
| Built-in DHCP server | ✅ | ✅ |
|
||||
| HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttpd |
|
||||
| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) |
|
||||
| Cross-platform | ✅ | ❌ (not natively, only via Docker) |
|
||||
| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) |
|
||||
| Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) |
|
||||
| Parental control (blocking adult domains) | ✅ | ❌ |
|
||||
| Force Safe search on search engines | ✅ | ❌ |
|
||||
| Per-client (device) configuration | ✅ | ✅ |
|
||||
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
|
||||
| Feature | AdGuard Home | Pi-Hole |
|
||||
|-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------|
|
||||
| Blocking ads and trackers | ✅ | ✅ |
|
||||
| Customizing blocklists | ✅ | ✅ |
|
||||
| Built-in DHCP server | ✅ | ✅ |
|
||||
| HTTPS for the Admin interface | ✅ | Kind of, but you'll need to manually configure lighthttpd |
|
||||
| Encrypted DNS upstream servers (DNS-over-HTTPS, DNS-over-TLS, DNSCrypt) | ✅ | ❌ (requires additional software) |
|
||||
| Cross-platform | ✅ | ❌ (not natively, only via Docker) |
|
||||
| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) |
|
||||
| Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) |
|
||||
| Parental control (blocking adult domains) | ✅ | ❌ |
|
||||
| Force Safe search on search engines | ✅ | ❌ |
|
||||
| Per-client (device) configuration | ✅ | ✅ |
|
||||
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
|
||||
| Running [without root privileges](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser) | ✅ | ❌ |
|
||||
|
||||
<a id="comparison-adblock"></a>
|
||||
### How does AdGuard Home compare to traditional ad blockers
|
||||
@@ -169,7 +179,8 @@ You will need this to build AdGuard Home:
|
||||
|
||||
* [go](https://golang.org/dl/) v1.14 or later.
|
||||
* [node.js](https://nodejs.org/en/download/) v10.16.2 or later.
|
||||
* [npm](https://www.npmjs.com/) v6.14 or later.
|
||||
* [npm](https://www.npmjs.com/) v6.14 or later (temporary requirement, TODO: remove when redesign is finished).
|
||||
* [yarn](https://yarnpkg.com/) v1.22.5 or later.
|
||||
|
||||
### Building
|
||||
|
||||
@@ -188,26 +199,28 @@ In order to do this, specify `GOOS` and `GOARCH` env variables before running ma
|
||||
|
||||
For example:
|
||||
```
|
||||
GOOS=linux GOARCH=arm64 make
|
||||
env GOOS='linux' GOARCH='arm64' make
|
||||
```
|
||||
Or:
|
||||
```
|
||||
make GOOS='linux' GOARCH='arm64'
|
||||
```
|
||||
|
||||
#### Preparing release
|
||||
|
||||
You'll need this to prepare a release build:
|
||||
|
||||
* [goreleaser](https://goreleaser.com/)
|
||||
* [snapcraft](https://snapcraft.io/)
|
||||
|
||||
Commands:
|
||||
|
||||
* `make release` - builds a snapshot build (CHANNEL=edge)
|
||||
* `CHANNEL=beta make release` - builds beta version, tag is mandatory.
|
||||
* `CHANNEL=release make release` - builds release version, tag is mandatory.
|
||||
```
|
||||
make build-release CHANNEL='...' VERSION='...'
|
||||
```
|
||||
|
||||
#### Docker image
|
||||
|
||||
* Run `make docker` to build the Docker image locally.
|
||||
* Run `make docker-multi-arch` to build the multi-arch Docker image (the one that we publish to Docker Hub).
|
||||
* Run `make build-docker` to build the Docker image locally (the one that we publish to DockerHub).
|
||||
|
||||
Please note, that we're using [Docker Buildx](https://docs.docker.com/buildx/working-with-buildx/) to build our official image.
|
||||
|
||||
@@ -255,7 +268,7 @@ curl -sSL https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scrip
|
||||
|
||||
* Beta channel builds
|
||||
* Linux: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz) (recommended for Rapsberry Pi), [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi), [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
||||
* Windows: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip)
|
||||
* MacOS: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip)
|
||||
@@ -264,7 +277,7 @@ curl -sSL https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scrip
|
||||
|
||||
* Edge channel builds
|
||||
* Linux: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_386.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv6.tar.gz) (recommended for Rapsberry Pi), [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi), [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
||||
* Windows: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_386.zip)
|
||||
* MacOS: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_386.zip)
|
||||
@@ -295,12 +308,12 @@ Here's what you can also do to contribute:
|
||||
4. Actualize the list of vetted *blocklists*. It it can be found in [client/src/helpers/filters/filters.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/src/helpers/filters/filters.json).
|
||||
|
||||
<a id="uses"></a>
|
||||
## Projects that use AdGuardHome
|
||||
## Projects that use AdGuard Home
|
||||
|
||||
* Python library (https://github.com/frenck/python-adguardhome)
|
||||
* Hass.io add-on (https://github.com/hassio-addons/addon-adguard-home)
|
||||
* OpenWrt LUCI app (https://github.com/rufengsuixing/luci-app-adguardhome)
|
||||
|
||||
* OpenWrt LUCI app (https://github.com/kongfl888/luci-app-adguardhome)
|
||||
* Prometheus exporter for AdGuard Home (https://github.com/ebrianne/adguard-exporter)
|
||||
|
||||
<a id="acknowledgments"></a>
|
||||
## Acknowledgments
|
||||
@@ -321,7 +334,7 @@ This software wouldn't have been possible without:
|
||||
* And many more node.js packages.
|
||||
* [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
|
||||
|
||||
You might have seen that [CoreDNS](https://coredns.io) was mentioned here before — we've stopped using it in AdGuardHome. While we still use it on our servers for [AdGuard DNS](https://adguard.com/adguard-dns/overview.html) service, it seemed like an overkill for Home as it impeded with Home features that we plan to implement.
|
||||
You might have seen that [CoreDNS](https://coredns.io) was mentioned here before — we've stopped using it in AdGuard Home. While we still use it on our servers for [AdGuard DNS](https://adguard.com/adguard-dns/overview.html) service, it seemed like an overkill for Home as it impeded with Home features that we plan to implement.
|
||||
|
||||
For a full list of all node.js packages in use, please take a look at [client/package.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json) file.
|
||||
|
||||
|
||||
12
client/package-lock.json
generated
vendored
12
client/package-lock.json
generated
vendored
@@ -3066,12 +3066,6 @@
|
||||
"pkg-up": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001062",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
|
||||
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "7.0.30",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
|
||||
@@ -3928,9 +3922,9 @@
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001059",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001059.tgz",
|
||||
"integrity": "sha512-oOrc+jPJWooKIA0IrNZ5sYlsXc7NP7KLhNWrSGEJhnfSzDvDJ0zd3i6HXsslExY9bbu+x0FQ5C61LcqmPt7bOQ==",
|
||||
"version": "1.0.30001165",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz",
|
||||
"integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==",
|
||||
"dev": true
|
||||
},
|
||||
"capture-exit": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon-180x180.png" />
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"form_error_ip_format": "Invalid IP format",
|
||||
"form_error_mac_format": "Invalid MAC format",
|
||||
"form_error_client_id_format": "Invalid client ID format",
|
||||
"form_error_server_name": "Invalid server name",
|
||||
"form_error_positive": "Must be greater than 0",
|
||||
"form_error_negative": "Must be equal to 0 or greater",
|
||||
"range_end_error": "Must be greater than range start",
|
||||
@@ -250,8 +251,12 @@
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"client_id": "Client ID",
|
||||
"client_id_placeholder": "Enter client ID",
|
||||
"client_id_desc": "Different clients can be identified by a special client ID. <a>Here</a> you can learn more about how to identify clients.",
|
||||
"download_mobileconfig_doh": "Download .mobileconfig for DNS-over-HTTPS",
|
||||
"download_mobileconfig_dot": "Download .mobileconfig for DNS-over-TLS",
|
||||
"download_mobileconfig": "Download configuration file",
|
||||
"plain_dns": "Plain DNS",
|
||||
"form_enter_rate_limit": "Enter rate limit",
|
||||
"rate_limit": "Rate limit",
|
||||
@@ -270,7 +275,7 @@
|
||||
"source_label": "Source",
|
||||
"found_in_known_domain_db": "Found in the known domains database.",
|
||||
"category_label": "Category",
|
||||
"rule_label": "Rule",
|
||||
"rule_label": "Rule(s)",
|
||||
"list_label": "List",
|
||||
"unknown_filter": "Unknown filter {{filterId}}",
|
||||
"known_tracker": "Known tracker",
|
||||
@@ -331,7 +336,7 @@
|
||||
"encryption_config_saved": "Encryption config saved",
|
||||
"encryption_server": "Server name",
|
||||
"encryption_server_enter": "Enter your domain name",
|
||||
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate.",
|
||||
"encryption_server_desc": "In order to use HTTPS, you need to enter the server name that matches your SSL certificate or wildcard certificate. If the field is not set, it will accept TLS connections for any domain.",
|
||||
"encryption_redirect": "Redirect to HTTPS automatically",
|
||||
"encryption_redirect_desc": "If checked, AdGuard Home will automatically redirect you from HTTP to HTTPS addresses.",
|
||||
"encryption_https": "HTTPS port",
|
||||
@@ -387,7 +392,7 @@
|
||||
"client_edit": "Edit Client",
|
||||
"client_identifier": "Identifier",
|
||||
"ip_address": "IP address",
|
||||
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address. Please note that using MAC as identifier is possible only if AdGuard Home is also a <0>DHCP server</0>",
|
||||
"client_identifier_desc": "Clients can be identified by the IP address, CIDR, MAC address or a special client ID (can be used for DoT/DoH/DoQ). <0>Here</0> you can learn more about how to identify clients.",
|
||||
"form_enter_ip": "Enter IP",
|
||||
"form_enter_mac": "Enter MAC",
|
||||
"form_enter_id": "Enter identifier",
|
||||
@@ -431,6 +436,7 @@
|
||||
"setup_dns_privacy_other_3": "<0>dnscrypt-proxy</0> supports <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_4": "<0>Mozilla Firefox</0> supports <1>DNS-over-HTTPS</1>.",
|
||||
"setup_dns_privacy_other_5": "You will find more implementations <0>here</0> and <1>here</1>.",
|
||||
"setup_dns_privacy_ioc_mac": "iOS and macOS configuration",
|
||||
"setup_dns_notice": "In order to use <1>DNS-over-HTTPS</1> or <1>DNS-over-TLS</1>, you need to <0>configure Encryption</0> in AdGuard Home settings.",
|
||||
"rewrite_added": "DNS rewrite for \"{{key}}\" successfully added",
|
||||
"rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted",
|
||||
@@ -530,7 +536,6 @@
|
||||
"check_ip": "IP addresses: {{ip}}",
|
||||
"check_cname": "CNAME: {{cname}}",
|
||||
"check_reason": "Reason: {{reason}}",
|
||||
"check_rule": "Rule: {{rule}}",
|
||||
"check_service": "Service name: {{service}}",
|
||||
"service_name": "Service name",
|
||||
"check_not_found": "Not found in your filter lists",
|
||||
|
||||
@@ -287,7 +287,7 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||
try {
|
||||
checkStatus(handleRequestSuccess, handleRequestError);
|
||||
} catch (error) {
|
||||
handleRequestError(error);
|
||||
handleRequestError();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
|
||||
const enrichWithClientInfo = async (logs) => {
|
||||
const clientsParams = getParamsForClientsSearch(logs, 'client');
|
||||
const clientsParams = getParamsForClientsSearch(logs, 'client', 'client_id');
|
||||
|
||||
if (Object.keys(clientsParams).length > 0) {
|
||||
const clients = await apiClient.findClients(clientsParams);
|
||||
return addClientInfo(logs, clients, 'client');
|
||||
return addClientInfo(logs, clients, 'client_id', 'client');
|
||||
}
|
||||
|
||||
return logs;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
:root {
|
||||
--yellow-pale: rgba(247, 181, 0, 0.1);
|
||||
--green79: #67B279;
|
||||
--green79: #67b279;
|
||||
--gray-a5: #a5a5a5;
|
||||
--gray-d8: #d8d8d8;
|
||||
--gray-f3: #F3F3F3;
|
||||
--gray-f3: #f3f3f3;
|
||||
--font-family-monospace: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
input, select, textarea {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 30px;
|
||||
}
|
||||
@@ -71,3 +77,11 @@ body {
|
||||
.button-action--active {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.ReactModal__Body--open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a.btn-success.disabled {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Card from '../ui/Card';
|
||||
import Cell from '../ui/Cell';
|
||||
|
||||
import { getPercent, sortIp } from '../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants';
|
||||
import { BLOCK_ACTIONS, R_CLIENT_ID, STATUS_COLORS } from '../../helpers/constants';
|
||||
import { toggleClientBlock } from '../../actions/access';
|
||||
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
|
||||
import { getStats } from '../../actions/stats';
|
||||
@@ -35,6 +35,10 @@ const CountCell = (row) => {
|
||||
};
|
||||
|
||||
const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
if (R_CLIENT_ID.test(ip)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const processingSet = useSelector((state) => state.access.processingSet);
|
||||
@@ -59,17 +63,19 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
|
||||
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
|
||||
|
||||
const isNotInAllowedList = disallowed && disallowed_rule === '';
|
||||
return <div className="table__action pl-4">
|
||||
<button
|
||||
return (
|
||||
<div className="table__action pl-4">
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={isNotInAllowedList ? undefined : onClick}
|
||||
disabled={isNotInAllowedList || processingSet}
|
||||
title={t(isNotInAllowedList ? 'client_not_in_allowed_clients' : text)}
|
||||
>
|
||||
<Trans>{text}</Trans>
|
||||
</button>
|
||||
</div>;
|
||||
>
|
||||
<Trans>{text}</Trans>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ClientCell = (row) => {
|
||||
@@ -90,13 +96,14 @@ const Clients = ({
|
||||
const { t } = useTranslation();
|
||||
const topClients = useSelector((state) => state.stats.topClients, shallowEqual);
|
||||
|
||||
return <Card
|
||||
return (
|
||||
<Card
|
||||
title={t('top_clients')}
|
||||
subtitle={subtitle}
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<ReactTable
|
||||
>
|
||||
<ReactTable
|
||||
data={topClients.map(({
|
||||
name: ip, count, info, blocked,
|
||||
}) => ({
|
||||
@@ -107,7 +114,7 @@ const Clients = ({
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
Header: 'IP',
|
||||
Header: <Trans>client_table_header</Trans>,
|
||||
accessor: 'ip',
|
||||
sortMethod: sortIp,
|
||||
Cell: ClientCell,
|
||||
@@ -134,8 +141,9 @@ const Clients = ({
|
||||
|
||||
return disallowed ? { className: 'logs__row--red' } : {};
|
||||
}}
|
||||
/>
|
||||
</Card>;
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Clients.propTypes = {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-title__button{
|
||||
.dashboard-title__button {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dashboard-title__button{
|
||||
.dashboard-title__button {
|
||||
margin: 0.5rem 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const Dashboard = ({
|
||||
const refreshButton = <button
|
||||
type="button"
|
||||
className="btn btn-icon btn-outline-primary btn-sm"
|
||||
title={t('refresh_btn')}
|
||||
onClick={() => getAllStats()}
|
||||
>
|
||||
<svg className="icons">
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
checkSafeSearch,
|
||||
checkSafeBrowsing,
|
||||
checkParental,
|
||||
getFilterName,
|
||||
getRulesToFilterList,
|
||||
} from '../../../helpers/helpers';
|
||||
import { BLOCK_ACTIONS, FILTERED, FILTERED_STATUS } from '../../../helpers/constants';
|
||||
import { toggleBlocking } from '../../../actions';
|
||||
@@ -41,32 +41,27 @@ const renderBlockingButton = (isFiltered, domain) => {
|
||||
</button>;
|
||||
};
|
||||
|
||||
const getTitle = (reason) => {
|
||||
const getTitle = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
|
||||
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
|
||||
const filter_id = useSelector((state) => state.filtering.check.filter_id);
|
||||
|
||||
const filterName = getFilterName(
|
||||
filters,
|
||||
whitelistFilters,
|
||||
filter_id,
|
||||
'filtered_custom_rules',
|
||||
(filter) => (filter?.name ? t('query_log_filtered', { filter: filter.name }) : ''),
|
||||
);
|
||||
const rules = useSelector((state) => state.filtering.check.rules, shallowEqual);
|
||||
const reason = useSelector((state) => state.filtering.check.reason);
|
||||
|
||||
const getReasonFiltered = (reason) => {
|
||||
const filterKey = reason.replace(FILTERED, '');
|
||||
return i18next.t('query_log_filtered', { filter: filterKey });
|
||||
};
|
||||
|
||||
const ruleAndFilterNames = getRulesToFilterList(rules, filters, whitelistFilters);
|
||||
|
||||
const REASON_TO_TITLE_MAP = {
|
||||
[FILTERED_STATUS.NOT_FILTERED_NOT_FOUND]: t('check_not_found'),
|
||||
[FILTERED_STATUS.REWRITE]: t('rewrite_applied'),
|
||||
[FILTERED_STATUS.REWRITE_HOSTS]: t('rewrite_hosts_applied'),
|
||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: filterName,
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: filterName,
|
||||
[FILTERED_STATUS.FILTERED_BLACK_LIST]: ruleAndFilterNames,
|
||||
[FILTERED_STATUS.NOT_FILTERED_WHITE_LIST]: ruleAndFilterNames,
|
||||
[FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason),
|
||||
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason),
|
||||
[FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason),
|
||||
@@ -78,7 +73,11 @@ const getTitle = (reason) => {
|
||||
|
||||
return <>
|
||||
<div>{t('check_reason', { reason })}</div>
|
||||
<div>{filterName}</div>
|
||||
<div>
|
||||
{t('rule_label')}:
|
||||
|
||||
{ruleAndFilterNames}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
@@ -86,14 +85,13 @@ const Info = () => {
|
||||
const {
|
||||
hostname,
|
||||
reason,
|
||||
rule,
|
||||
service_name,
|
||||
cname,
|
||||
ip_addrs,
|
||||
} = useSelector((state) => state.filtering.check, shallowEqual);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title = getTitle(reason);
|
||||
const title = getTitle();
|
||||
|
||||
const className = classNames('card mb-0 p-3', {
|
||||
'logs__row--red': checkFiltered(reason),
|
||||
@@ -112,7 +110,6 @@ const Info = () => {
|
||||
<div>{title}</div>
|
||||
{!onlyFiltered
|
||||
&& <>
|
||||
{rule && <div>{t('check_rule', { rule })}</div>}
|
||||
{service_name && <div>{t('check_service', { service: service_name })}</div>}
|
||||
{cname && <div>{t('check_cname', { cname })}</div>}
|
||||
{ip_addrs && <div>{t('check_ip', { ip: ip_addrs.join(', ') })}</div>}
|
||||
|
||||
@@ -46,7 +46,7 @@ const Header = () => {
|
||||
<div className="header__column">
|
||||
<div className="d-flex align-items-center">
|
||||
<Link to="/" className="nav-link pl-0 pr-1">
|
||||
<img src={logo} alt="" className="header-brand-img" />
|
||||
<img src={logo} alt="AdGuard Home logo" className="header-brand-img" />
|
||||
</Link>
|
||||
{!processing && isCoreRunning
|
||||
&& <span className={badgeClass}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { updateLogs } from '../../../actions/queryLogs';
|
||||
|
||||
const ClientCell = ({
|
||||
client,
|
||||
client_id,
|
||||
domain,
|
||||
info,
|
||||
info: {
|
||||
@@ -33,12 +34,14 @@ const ClientCell = ({
|
||||
const autoClient = autoClients.find((autoClient) => autoClient.name === client);
|
||||
const source = autoClient?.source;
|
||||
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
|
||||
const clientName = name || client_id;
|
||||
const clientInfo = { ...info, name: clientName };
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const data = {
|
||||
address: client,
|
||||
name,
|
||||
name: clientName,
|
||||
country: whois_info?.country,
|
||||
city: whois_info?.city,
|
||||
network: whois_info?.orgname,
|
||||
@@ -99,13 +102,20 @@ const ClientCell = ({
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return <>{options.map(({ name, onClick, disabled }) => <button
|
||||
key={name}
|
||||
className="button-action--arrow-option px-4 py-2"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>{t(name)}
|
||||
</button>)}</>;
|
||||
return (
|
||||
<>
|
||||
{options.map(({ name, onClick, disabled }) => (
|
||||
<button
|
||||
key={name}
|
||||
className="button-action--arrow-option px-4 py-2"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t(name)}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const content = getOptions(BUTTON_OPTIONS);
|
||||
@@ -125,45 +135,70 @@ const ClientCell = ({
|
||||
'button-action__container--detailed': isDetailed,
|
||||
});
|
||||
|
||||
return <div className={containerClass}>
|
||||
<button type="button"
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClass}
|
||||
onClick={onClick}
|
||||
disabled={processingRules}
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>
|
||||
{content && <button className={buttonArrowClass} disabled={processingRules}>
|
||||
<IconTooltip
|
||||
className='h-100'
|
||||
tooltipClass='button-action--arrow-option-container'
|
||||
xlinkHref='chevron-down'
|
||||
triggerClass='button-action--icon'
|
||||
content={content} placement="bottom-end" trigger="click"
|
||||
onVisibilityChange={setOptionsOpened}
|
||||
/>
|
||||
</button>}
|
||||
</div>;
|
||||
>
|
||||
{t(buttonType)}
|
||||
</button>
|
||||
{content && (
|
||||
<button className={buttonArrowClass} disabled={processingRules}>
|
||||
<IconTooltip
|
||||
className="h-100"
|
||||
tooltipClass="button-action--arrow-option-container"
|
||||
xlinkHref="chevron-down"
|
||||
triggerClass="button-action--icon"
|
||||
content={content}
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
onVisibilityChange={setOptionsOpened}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <div className="o-hidden h-100 logs__cell logs__cell--client" role="gridcell">
|
||||
<IconTooltip className={hintClass} columnClass='grid grid--limited' tooltipClass='px-5 pb-5 pt-4 mw-75'
|
||||
xlinkHref='question' contentItemClass="contentItemClass" title="client_details"
|
||||
content={processedData} placement="bottom" />
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{renderFormattedClientCell(client, info, isDetailed, true)}
|
||||
return (
|
||||
<div
|
||||
className="o-hidden h-100 logs__cell logs__cell--client"
|
||||
role="gridcell"
|
||||
>
|
||||
<IconTooltip
|
||||
className={hintClass}
|
||||
columnClass="grid grid--limited"
|
||||
tooltipClass="px-5 pb-5 pt-4"
|
||||
xlinkHref="question"
|
||||
contentItemClass="text-truncate key-colon o-hidden"
|
||||
title="client_details"
|
||||
content={processedData}
|
||||
placement="bottom"
|
||||
/>
|
||||
<div className={nameClass}>
|
||||
<div data-tip={true} data-for={id}>
|
||||
{renderFormattedClientCell(client, clientInfo, isDetailed, true)}
|
||||
</div>
|
||||
{isDetailed && clientName && !whoisAvailable && (
|
||||
<div
|
||||
className="detailed-info d-none d-sm-block logs__text"
|
||||
title={clientName}
|
||||
>
|
||||
{clientName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDetailed && name && !whoisAvailable
|
||||
&& <div className="detailed-info d-none d-sm-block logs__text"
|
||||
title={name}>{name}</div>}
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>
|
||||
{renderBlockingButton(isFiltered, domain)}
|
||||
</div>;
|
||||
);
|
||||
};
|
||||
|
||||
ClientCell.propTypes = {
|
||||
client: propTypes.string.isRequired,
|
||||
client_id: propTypes.string,
|
||||
domain: propTypes.string.isRequired,
|
||||
info: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.grid--title {
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.grid--title:not(:first-child) {
|
||||
@@ -65,12 +65,12 @@
|
||||
}
|
||||
|
||||
.grid .key-colon, .grid .title--border {
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.grid .key-colon:nth-child(odd)::after {
|
||||
content: ':';
|
||||
content: ":";
|
||||
}
|
||||
|
||||
.grid__one-row {
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
|
||||
.title--border:before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
border-top: 0.5px solid var(--gray-d8) !important;
|
||||
|
||||
@@ -4,8 +4,9 @@ import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import {
|
||||
getRulesToFilterList,
|
||||
formatElapsedMs,
|
||||
getFilterName,
|
||||
getFilterNames,
|
||||
getServiceName,
|
||||
} from '../../../helpers/helpers';
|
||||
import { FILTERED_STATUS, FILTERED_STATUS_TO_META_MAP } from '../../../helpers/constants';
|
||||
@@ -18,8 +19,7 @@ const ResponseCell = ({
|
||||
response,
|
||||
status,
|
||||
upstream,
|
||||
rule,
|
||||
filterId,
|
||||
rules,
|
||||
service_name,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -36,7 +36,6 @@ const ResponseCell = ({
|
||||
|
||||
const statusLabel = t(isBlockedByResponse ? 'blocked_by_cname_or_ip' : FILTERED_STATUS_TO_META_MAP[reason]?.LABEL || reason);
|
||||
const boldStatusLabel = <span className="font-weight-bold">{statusLabel}</span>;
|
||||
const filter = getFilterName(filters, whitelistFilters, filterId);
|
||||
|
||||
const renderResponses = (responseArr) => {
|
||||
if (!responseArr || responseArr.length === 0) {
|
||||
@@ -57,13 +56,17 @@ const ResponseCell = ({
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
response_code: status,
|
||||
...(service_name ? { service_name: getServiceName(service_name) } : { filter }),
|
||||
rule_label: rule,
|
||||
...(service_name
|
||||
&& { service_name: getServiceName(service_name) }
|
||||
),
|
||||
...(rules.length > 0
|
||||
&& { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }
|
||||
),
|
||||
response_table_header: renderResponses(response),
|
||||
original_response: renderResponses(originalResponse),
|
||||
};
|
||||
|
||||
const content = rule
|
||||
const content = rules.length > 0
|
||||
? Object.entries(COMMON_CONTENT)
|
||||
: Object.entries({
|
||||
...COMMON_CONTENT,
|
||||
@@ -78,7 +81,8 @@ const ResponseCell = ({
|
||||
}
|
||||
return getServiceName(service_name);
|
||||
case FILTERED_STATUS.FILTERED_BLACK_LIST:
|
||||
return filter;
|
||||
case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
|
||||
return getFilterNames(rules, filters, whitelistFilters).join(', ');
|
||||
default:
|
||||
return formattedElapsedMs;
|
||||
}
|
||||
@@ -113,8 +117,10 @@ ResponseCell.propTypes = {
|
||||
response: propTypes.array.isRequired,
|
||||
status: propTypes.string.isRequired,
|
||||
upstream: propTypes.string.isRequired,
|
||||
rule: propTypes.string,
|
||||
filterId: propTypes.number,
|
||||
rules: propTypes.arrayOf(propTypes.shape({
|
||||
text: propTypes.string.isRequired,
|
||||
filter_list_id: propTypes.number.isRequired,
|
||||
})),
|
||||
service_name: propTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import propTypes from 'prop-types';
|
||||
import {
|
||||
captitalizeWords,
|
||||
checkFiltered,
|
||||
getRulesToFilterList,
|
||||
formatDateTime,
|
||||
formatElapsedMs,
|
||||
formatTime,
|
||||
getBlockingClientName,
|
||||
getFilterName,
|
||||
getServiceName,
|
||||
processContent,
|
||||
} from '../../../helpers/helpers';
|
||||
@@ -70,8 +70,8 @@ const Row = memo(({
|
||||
upstream,
|
||||
type,
|
||||
client_proto,
|
||||
filterId,
|
||||
rule,
|
||||
client_id,
|
||||
rules,
|
||||
originalResponse,
|
||||
status,
|
||||
service_name,
|
||||
@@ -107,8 +107,6 @@ const Row = memo(({
|
||||
|
||||
const sourceData = getSourceData(tracker);
|
||||
|
||||
const filter = getFilterName(filters, whitelistFilters, filterId);
|
||||
|
||||
const {
|
||||
confirmMessage,
|
||||
buttonKey: blockingClientKey,
|
||||
@@ -172,13 +170,14 @@ const Row = memo(({
|
||||
response_details: 'title',
|
||||
install_settings_dns: upstream,
|
||||
elapsed: formattedElapsedMs,
|
||||
filter: rule ? filter : null,
|
||||
rule_label: rule,
|
||||
...(rules.length > 0
|
||||
&& { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }
|
||||
),
|
||||
response_table_header: response?.join('\n'),
|
||||
response_code: status,
|
||||
client_details: 'title',
|
||||
ip_address: client,
|
||||
name: info?.name,
|
||||
name: info?.name || client_id,
|
||||
country,
|
||||
city,
|
||||
network,
|
||||
@@ -235,8 +234,11 @@ Row.propTypes = {
|
||||
upstream: propTypes.string.isRequired,
|
||||
type: propTypes.string.isRequired,
|
||||
client_proto: propTypes.string.isRequired,
|
||||
filterId: propTypes.number,
|
||||
rule: propTypes.string,
|
||||
client_id: propTypes.string,
|
||||
rules: propTypes.arrayOf(propTypes.shape({
|
||||
text: propTypes.string.isRequired,
|
||||
filter_list_id: propTypes.number.isRequired,
|
||||
})),
|
||||
originalResponse: propTypes.array,
|
||||
status: propTypes.string.isRequired,
|
||||
service_name: propTypes.string,
|
||||
|
||||
@@ -9,21 +9,18 @@
|
||||
--size-response: 150;
|
||||
--size-client: 123;
|
||||
--gray-216: rgba(216, 216, 216, 0.23);
|
||||
--gray-4d: #4D4D4D;
|
||||
--gray-f3: #F3F3F3;
|
||||
--gray-4d: #4d4d4d;
|
||||
--gray-f3: #f3f3f3;
|
||||
--gray-8: #888;
|
||||
--gray-3: #333;
|
||||
--danger: #DF3812;
|
||||
--danger: #df3812;
|
||||
--white80: rgba(255, 255, 255, 0.8);
|
||||
|
||||
--btn-block: #C23814;
|
||||
--btn-block-disabled: #E3B3A6;
|
||||
--btn-block-active: #A62200;
|
||||
|
||||
--btn-block: #c23814;
|
||||
--btn-block-disabled: #e3b3a6;
|
||||
--btn-block-active: #a62200;
|
||||
--btn-unblock: #888888;
|
||||
--btn-unblock-disabled: #D8D8D8;
|
||||
--btn-unblock-active: #4D4D4D;
|
||||
|
||||
--btn-unblock-disabled: #d8d8d8;
|
||||
--btn-unblock-active: #4d4d4d;
|
||||
--option-border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -40,7 +37,7 @@
|
||||
}
|
||||
|
||||
.logs__text--bold {
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logs__time {
|
||||
@@ -87,7 +84,7 @@
|
||||
}
|
||||
|
||||
.custom-select__arrow--left {
|
||||
background: var(--white) url('../ui/svg/chevron-down.svg') no-repeat;
|
||||
background: var(--white) url("../ui/svg/chevron-down.svg") no-repeat;
|
||||
background-position: 5px 9px;
|
||||
background-size: 22px;
|
||||
}
|
||||
@@ -167,12 +164,13 @@
|
||||
}
|
||||
|
||||
.logs__refresh {
|
||||
--size: 2.5rem;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
--size: 2.5rem;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
padding: 0;
|
||||
@@ -360,7 +358,7 @@
|
||||
color: var(--gray-4d);
|
||||
background-color: var(--white80);
|
||||
pointer-events: none;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding-top: 21rem;
|
||||
display: block;
|
||||
@@ -431,3 +429,13 @@
|
||||
margin-right: 1px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.filteringRules__rule {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filteringRules__filter {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ let Form = (props) => {
|
||||
<div className="form__desc mt-0">
|
||||
<Trans
|
||||
components={[
|
||||
<a href="#dhcp" key="0">
|
||||
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#idclient" key="0" target="_blank" rel="noopener noreferrer">
|
||||
link
|
||||
</a>,
|
||||
]}
|
||||
|
||||
@@ -50,7 +50,7 @@ const CertificateStatus = ({
|
||||
{dnsNames && (
|
||||
<li>
|
||||
<Trans>encryption_hostnames</Trans>:
|
||||
{dnsNames}
|
||||
{dnsNames.join(', ')}
|
||||
</li>
|
||||
)}
|
||||
</Fragment>
|
||||
@@ -65,7 +65,7 @@ CertificateStatus.propTypes = {
|
||||
subject: PropTypes.string,
|
||||
issuer: PropTypes.string,
|
||||
notAfter: PropTypes.string,
|
||||
dnsNames: PropTypes.string,
|
||||
dnsNames: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
export default withTranslation()(CertificateStatus);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
toNumber,
|
||||
} from '../../../helpers/form';
|
||||
import {
|
||||
validateIsSafePort, validatePort, validatePortQuic, validatePortTLS,
|
||||
validateServerName, validateIsSafePort, validatePort, validatePortQuic, validatePortTLS,
|
||||
} from '../../../helpers/validators';
|
||||
import i18n from '../../../i18n';
|
||||
import KeyStatus from './KeyStatus';
|
||||
@@ -127,6 +127,7 @@ let Form = (props) => {
|
||||
placeholder={t('encryption_server_enter')}
|
||||
onChange={handleChange}
|
||||
disabled={!isEnabled}
|
||||
validate={validateServerName}
|
||||
/>
|
||||
<div className="form__desc">
|
||||
<Trans>encryption_server_desc</Trans>
|
||||
@@ -413,7 +414,7 @@ Form.propTypes = {
|
||||
valid_key: PropTypes.bool,
|
||||
valid_cert: PropTypes.bool,
|
||||
valid_pair: PropTypes.bool,
|
||||
dns_names: PropTypes.string,
|
||||
dns_names: PropTypes.arrayOf(PropTypes.string),
|
||||
key_type: PropTypes.string,
|
||||
issuer: PropTypes.string,
|
||||
subject: PropTypes.string,
|
||||
|
||||
@@ -3,27 +3,12 @@ import PropTypes from 'prop-types';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import i18next from 'i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Tabs from './Tabs';
|
||||
import Icons from './Icons';
|
||||
import { getPathWithQueryString } from '../../helpers/helpers';
|
||||
|
||||
const MOBILE_CONFIG_LINKS = {
|
||||
DOT: '/apple/dot.mobileconfig',
|
||||
DOH: '/apple/doh.mobileconfig',
|
||||
};
|
||||
const renderMobileconfigInfo = ({ label, components, server_name }) => <li key={label}>
|
||||
<Trans components={components}>{label}</Trans>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={getPathWithQueryString(MOBILE_CONFIG_LINKS.DOT, { host: server_name })}
|
||||
download>{i18next.t('download_mobileconfig_dot')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={getPathWithQueryString(MOBILE_CONFIG_LINKS.DOH, { host: server_name })}
|
||||
download>{i18next.t('download_mobileconfig_doh')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>;
|
||||
import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
|
||||
|
||||
import Tabs from '../Tabs';
|
||||
import Icons from '../Icons';
|
||||
import MobileConfigForm from './MobileConfigForm';
|
||||
|
||||
const renderLi = ({ label, components }) => <li key={label}>
|
||||
<Trans components={components?.map((props) => {
|
||||
@@ -41,49 +26,8 @@ const renderLi = ({ label, components }) => <li key={label}>
|
||||
</Trans>
|
||||
</li>;
|
||||
|
||||
const getDnsPrivacyList = (server_name) => {
|
||||
const iosList = [
|
||||
{
|
||||
label: 'setup_dns_privacy_ios_2',
|
||||
components: [
|
||||
{
|
||||
key: 0,
|
||||
href: 'https://adguard.com/adguard-ios/overview.html',
|
||||
},
|
||||
<code key="1">text</code>,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'setup_dns_privacy_ios_1',
|
||||
components: [
|
||||
{
|
||||
key: 0,
|
||||
href: 'https://itunes.apple.com/app/id1452162351',
|
||||
},
|
||||
<code key="1">text</code>,
|
||||
{
|
||||
key: 2,
|
||||
href: 'https://dnscrypt.info/stamps',
|
||||
},
|
||||
|
||||
],
|
||||
}];
|
||||
/* Insert second element if can generate .mobileconfig links */
|
||||
if (server_name) {
|
||||
iosList.splice(1, 0, {
|
||||
label: 'setup_dns_privacy_4',
|
||||
components: {
|
||||
highlight: <code />,
|
||||
},
|
||||
renderComponent: ({ label, components }) => renderMobileconfigInfo({
|
||||
label,
|
||||
components,
|
||||
server_name,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return [{
|
||||
const getDnsPrivacyList = () => [
|
||||
{
|
||||
title: 'Android',
|
||||
list: [
|
||||
{
|
||||
@@ -113,7 +57,32 @@ const getDnsPrivacyList = (server_name) => {
|
||||
},
|
||||
{
|
||||
title: 'iOS',
|
||||
list: iosList,
|
||||
list: [
|
||||
{
|
||||
label: 'setup_dns_privacy_ios_2',
|
||||
components: [
|
||||
{
|
||||
key: 0,
|
||||
href: 'https://adguard.com/adguard-ios/overview.html',
|
||||
},
|
||||
<code key="1">text</code>,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'setup_dns_privacy_ios_1',
|
||||
components: [
|
||||
{
|
||||
key: 0,
|
||||
href: 'https://itunes.apple.com/app/id1452162351',
|
||||
},
|
||||
<code key="1">text</code>,
|
||||
{
|
||||
key: 2,
|
||||
href: 'https://dnscrypt.info/stamps',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'setup_dns_privacy_other_title',
|
||||
@@ -166,20 +135,20 @@ const getDnsPrivacyList = (server_name) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
];
|
||||
|
||||
const renderDnsPrivacyList = ({ title, list }) => <div className="tab__paragraph" key={title}>
|
||||
<strong><Trans>{title}</Trans></strong>
|
||||
<ul>{list.map(
|
||||
({
|
||||
label,
|
||||
components,
|
||||
renderComponent = renderLi,
|
||||
}) => renderComponent({ label, components }),
|
||||
)}
|
||||
</ul>
|
||||
</div>;
|
||||
const renderDnsPrivacyList = ({ title, list }) => (
|
||||
<div className="tab__paragraph" key={title}>
|
||||
<strong>
|
||||
<Trans>{title}</Trans>
|
||||
</strong>
|
||||
<ul>
|
||||
{list.map(({ label, components, renderComponent = renderLi }) => (
|
||||
renderComponent({ label, components })
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
const getTabs = ({
|
||||
tlsAddress,
|
||||
@@ -267,8 +236,8 @@ const getTabs = ({
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
{showDnsPrivacyNotice
|
||||
? <div className="tab__paragraph">
|
||||
{showDnsPrivacyNotice ? (
|
||||
<div className="tab__paragraph">
|
||||
<Trans
|
||||
components={[
|
||||
<a
|
||||
@@ -285,35 +254,64 @@ const getTabs = ({
|
||||
setup_dns_notice
|
||||
</Trans>
|
||||
</div>
|
||||
: <>
|
||||
) : (
|
||||
<>
|
||||
<div className="tab__paragraph">
|
||||
<Trans components={[<p key="0">text</p>]}>
|
||||
setup_dns_privacy_3
|
||||
</Trans>
|
||||
</div>
|
||||
{getDnsPrivacyList(server_name).map(renderDnsPrivacyList)}
|
||||
</>}
|
||||
{getDnsPrivacyList().map(renderDnsPrivacyList)}
|
||||
<div>
|
||||
<strong>
|
||||
<Trans>
|
||||
setup_dns_privacy_ioc_mac
|
||||
</Trans>
|
||||
</strong>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Trans components={{ highlight: <code /> }}>
|
||||
setup_dns_privacy_4
|
||||
</Trans>
|
||||
</div>
|
||||
<MobileConfigForm
|
||||
initialValues={{
|
||||
host: server_name,
|
||||
clientId: '',
|
||||
protocol: MOBILE_CONFIG_LINKS.DOH,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const renderContent = ({ title, list, getTitle }) => <div key={title} label={i18next.t(title)}>
|
||||
<div className="tab__title">{i18next.t(title)}</div>
|
||||
<div className="tab__text">
|
||||
{getTitle?.()}
|
||||
{list
|
||||
&& <ol>{list.map((item) => <li key={item}>
|
||||
<Trans>{item}</Trans>
|
||||
</li>)}
|
||||
</ol>}
|
||||
const renderContent = ({ title, list, getTitle }) => (
|
||||
<div key={title} label={i18next.t(title)}>
|
||||
<div className="tab__title">
|
||||
{i18next.t(title)}
|
||||
</div>
|
||||
<div className="tab__text">
|
||||
{getTitle?.()}
|
||||
{list && (
|
||||
<ol>
|
||||
{list.map((item) => (
|
||||
<li key={item}>
|
||||
<Trans>{item}</Trans>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
|
||||
const Guide = ({ dnsAddresses }) => {
|
||||
const { t } = useTranslation();
|
||||
const server_name = useSelector((state) => state.encryption.server_name);
|
||||
const server_name = useSelector((state) => state.encryption?.server_name);
|
||||
const tlsAddress = dnsAddresses?.filter((item) => item.includes('tls://')) ?? '';
|
||||
const httpsAddress = dnsAddresses?.filter((item) => item.includes('https://')) ?? '';
|
||||
const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1;
|
||||
@@ -332,9 +330,14 @@ const Guide = ({ dnsAddresses }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs
|
||||
tabs={tabs}
|
||||
activeTabLabel={activeTabLabel}
|
||||
setActiveTabLabel={setActiveTabLabel}
|
||||
>
|
||||
{activeTab}
|
||||
</Tabs>
|
||||
<Icons />
|
||||
<Tabs tabs={tabs} activeTabLabel={activeTabLabel}
|
||||
setActiveTabLabel={setActiveTabLabel}>{activeTab}</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -364,6 +367,4 @@ renderLi.propTypes = {
|
||||
components: PropTypes.string,
|
||||
};
|
||||
|
||||
renderMobileconfigInfo.propTypes = renderLi.propTypes;
|
||||
|
||||
export default Guide;
|
||||
131
client/src/components/ui/Guide/MobileConfigForm.js
Normal file
131
client/src/components/ui/Guide/MobileConfigForm.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import i18next from 'i18next';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { getPathWithQueryString } from '../../../helpers/helpers';
|
||||
import { FORM_NAME, MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
|
||||
import {
|
||||
renderInputField,
|
||||
renderSelectField,
|
||||
} from '../../../helpers/form';
|
||||
import {
|
||||
validateClientId,
|
||||
validateServerName,
|
||||
} from '../../../helpers/validators';
|
||||
|
||||
const getDownloadLink = (host, clientId, protocol, invalid) => {
|
||||
if (!host || invalid) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success btn-standard btn-large disabled"
|
||||
>
|
||||
<Trans>download_mobileconfig</Trans>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const linkParams = { host };
|
||||
|
||||
if (clientId) {
|
||||
linkParams.client_id = clientId;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={getPathWithQueryString(protocol, linkParams)}
|
||||
className={cn('btn btn-success btn-standard btn-large')}
|
||||
download
|
||||
>
|
||||
<Trans>download_mobileconfig</Trans>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileConfigForm = ({ invalid }) => {
|
||||
const formValues = useSelector((state) => state.form[FORM_NAME.MOBILE_CONFIG]?.values);
|
||||
|
||||
if (!formValues) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { host, clientId, protocol } = formValues;
|
||||
|
||||
const githubLink = (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#idclient"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
text
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="host" className="form__label">
|
||||
{i18next.t('dhcp_table_hostname')}
|
||||
</label>
|
||||
<Field
|
||||
name="host"
|
||||
type="text"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={i18next.t('form_enter_hostname')}
|
||||
validate={validateServerName}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group form__group--settings">
|
||||
<label htmlFor="clientId" className="form__label form__label--with-desc">
|
||||
{i18next.t('client_id')}
|
||||
</label>
|
||||
<div className="form__desc form__desc--top">
|
||||
<Trans components={{ a: githubLink }}>
|
||||
client_id_desc
|
||||
</Trans>
|
||||
</div>
|
||||
<Field
|
||||
name="clientId"
|
||||
type="text"
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={i18next.t('client_id_placeholder')}
|
||||
validate={validateClientId}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getDownloadLink(host, clientId, protocol, invalid)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
MobileConfigForm.propTypes = {
|
||||
invalid: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default reduxForm({ form: FORM_NAME.MOBILE_CONFIG })(MobileConfigForm);
|
||||
1
client/src/components/ui/Guide/index.js
Normal file
1
client/src/components/ui/Guide/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Guide';
|
||||
@@ -6,18 +6,21 @@
|
||||
|
||||
.icon--24 {
|
||||
--size: 1.5rem;
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.icon--20 {
|
||||
--size: 1.25rem;
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.icon--18 {
|
||||
--size: 1.125rem;
|
||||
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export const R_MAC = /^((([a-fA-F0-9][a-fA-F0-9]+[-]){5}|([a-fA-F0-9][a-fA-F0-9]
|
||||
|
||||
export const R_CIDR_IPV6 = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))$/;
|
||||
|
||||
export const R_DOMAIN = /^([a-zA-Z0-9][a-zA-Z0-9-_]*\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$/;
|
||||
|
||||
export const R_PATH_LAST_PART = /\/[^/]*$/;
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
@@ -21,6 +23,8 @@ export const R_UNIX_ABSOLUTE_PATH = /^(\/[^/\x00]+)+$/;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
export const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\|\/)(?:[^\\/:*?"<>|\x00]+\\)*[^\\/:*?"<>|\x00]*$/;
|
||||
|
||||
export const R_CLIENT_ID = /^[a-z0-9-]{1,64}$/;
|
||||
|
||||
export const HTML_PAGES = {
|
||||
INSTALL: '/install.html',
|
||||
LOGIN: '/login.html',
|
||||
@@ -339,6 +343,7 @@ export const FILTERED_STATUS = {
|
||||
FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',
|
||||
REWRITE: 'Rewrite',
|
||||
REWRITE_HOSTS: 'RewriteEtcHosts',
|
||||
REWRITE_RULE: 'RewriteRule',
|
||||
FILTERED_SAFE_SEARCH: 'FilteredSafeSearch',
|
||||
FILTERED_SAFE_BROWSING: 'FilteredSafeBrowsing',
|
||||
FILTERED_PARENTAL: 'FilteredParental',
|
||||
@@ -430,6 +435,10 @@ export const FILTERED_STATUS_TO_META_MAP = {
|
||||
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||
},
|
||||
[FILTERED_STATUS.REWRITE_RULE]: {
|
||||
LABEL: RESPONSE_FILTER.REWRITTEN.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.BLUE,
|
||||
},
|
||||
[FILTERED_STATUS.FILTERED_SAFE_BROWSING]: {
|
||||
LABEL: RESPONSE_FILTER.BLOCKED_THREATS.LABEL,
|
||||
COLOR: QUERY_STATUS_COLORS.YELLOW,
|
||||
@@ -509,6 +518,7 @@ export const FORM_NAME = {
|
||||
INSTALL: 'install',
|
||||
LOGIN: 'login',
|
||||
CACHE: 'cache',
|
||||
MOBILE_CONFIG: 'mobileConfig',
|
||||
...DHCP_FORM_NAMES,
|
||||
};
|
||||
|
||||
@@ -569,6 +579,7 @@ export const TOAST_TIMEOUTS = {
|
||||
export const ADDRESS_TYPES = {
|
||||
IP: 'IP',
|
||||
CIDR: 'CIDR',
|
||||
CLIENT_ID: 'CLIENT_ID',
|
||||
UNKNOWN: 'UNKNOWN',
|
||||
};
|
||||
|
||||
@@ -580,3 +591,8 @@ export const CACHE_CONFIG_FIELDS = {
|
||||
|
||||
export const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
|
||||
export const COMMENT_LINE_DEFAULT_TOKEN = '#';
|
||||
|
||||
export const MOBILE_CONFIG_LINKS = {
|
||||
DOT: '/apple/dot.mobileconfig',
|
||||
DOH: '/apple/doh.mobileconfig',
|
||||
};
|
||||
|
||||
@@ -4,9 +4,9 @@ import dateFormat from 'date-fns/format';
|
||||
import round from 'lodash/round';
|
||||
import axios from 'axios';
|
||||
import i18n from 'i18next';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import queryString from 'query-string';
|
||||
import React from 'react';
|
||||
import { getTrackerData } from './trackers/trackers';
|
||||
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
DHCP_VALUES_PLACEHOLDERS,
|
||||
FILTERED,
|
||||
FILTERED_STATUS,
|
||||
R_CLIENT_ID,
|
||||
SERVICES_ID_NAME_MAP,
|
||||
STANDARD_DNS_PORT,
|
||||
STANDARD_HTTPS_PORT,
|
||||
@@ -61,6 +62,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
|
||||
answer_dnssec,
|
||||
client,
|
||||
client_proto,
|
||||
client_id,
|
||||
elapsedMs,
|
||||
question,
|
||||
reason,
|
||||
@@ -68,6 +70,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
|
||||
time,
|
||||
filterId,
|
||||
rule,
|
||||
rules,
|
||||
service_name,
|
||||
original_answer,
|
||||
upstream,
|
||||
@@ -80,6 +83,15 @@ export const normalizeLogs = (logs) => logs.map((log) => {
|
||||
return `${type}: ${value} (ttl=${ttl})`;
|
||||
}) : []);
|
||||
|
||||
let newRules = rules;
|
||||
/* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */
|
||||
if (rule !== undefined && filterId !== undefined && rules !== undefined && rules.length === 0) {
|
||||
newRules = {
|
||||
filter_list_id: filterId,
|
||||
text: rule,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
time,
|
||||
domain,
|
||||
@@ -88,8 +100,11 @@ export const normalizeLogs = (logs) => logs.map((log) => {
|
||||
reason,
|
||||
client,
|
||||
client_proto,
|
||||
client_id,
|
||||
/* TODO 'filterId' and 'rule' are deprecated, will be removed in 0.106 */
|
||||
filterId,
|
||||
rule,
|
||||
rules: newRules,
|
||||
status,
|
||||
service_name,
|
||||
originalAnswer: original_answer,
|
||||
@@ -113,12 +128,21 @@ export const normalizeTopStats = (stats) => (
|
||||
}))
|
||||
);
|
||||
|
||||
export const addClientInfo = (data, clients, param) => data.map((row) => {
|
||||
const clientIp = row[param];
|
||||
const info = clients.find((item) => item[clientIp]) || '';
|
||||
export const addClientInfo = (data, clients, ...params) => data.map((row) => {
|
||||
let info = '';
|
||||
params.find((param) => {
|
||||
const id = row[param];
|
||||
if (id) {
|
||||
const client = clients.find((item) => item[id]) || '';
|
||||
info = client?.[id] ?? '';
|
||||
}
|
||||
|
||||
return info;
|
||||
});
|
||||
|
||||
return {
|
||||
...row,
|
||||
info: info?.[clientIp] ?? '',
|
||||
info,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -190,7 +214,12 @@ export const getIpList = (interfaces) => Object.values(interfaces)
|
||||
.reduce((acc, curr) => acc.concat(curr.ip_addresses), [])
|
||||
.sort();
|
||||
|
||||
export const getDnsAddress = (ip, port = '') => {
|
||||
/**
|
||||
* @param {string} ip
|
||||
* @param {number} [port]
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getDnsAddress = (ip, port = 0) => {
|
||||
const isStandardDnsPort = port === STANDARD_DNS_PORT;
|
||||
let address = ip;
|
||||
|
||||
@@ -205,7 +234,12 @@ export const getDnsAddress = (ip, port = '') => {
|
||||
return address;
|
||||
};
|
||||
|
||||
export const getWebAddress = (ip, port = '') => {
|
||||
/**
|
||||
* @param {string} ip
|
||||
* @param {number} [port]
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getWebAddress = (ip, port = 0) => {
|
||||
const isStandardWebPort = port === STANDARD_WEB_PORT;
|
||||
let address = `http://${ip}`;
|
||||
|
||||
@@ -391,14 +425,21 @@ export const getPathWithQueryString = (path, params) => {
|
||||
return `${path}?${searchParams.toString()}`;
|
||||
};
|
||||
|
||||
export const getParamsForClientsSearch = (data, param) => {
|
||||
const uniqueClients = uniqBy(data, param);
|
||||
return uniqueClients
|
||||
.reduce((acc, item, idx) => {
|
||||
const key = `ip${idx}`;
|
||||
acc[key] = item[param];
|
||||
return acc;
|
||||
}, {});
|
||||
export const getParamsForClientsSearch = (data, param, additionalParam) => {
|
||||
const clients = new Set();
|
||||
data.forEach((e) => {
|
||||
clients.add(e[param]);
|
||||
if (e[additionalParam]) {
|
||||
clients.add(e[additionalParam]);
|
||||
}
|
||||
});
|
||||
const params = {};
|
||||
const ids = Array.from(clients.values());
|
||||
ids.forEach((id, i) => {
|
||||
params[`ip${i}`] = id;
|
||||
});
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -511,7 +552,7 @@ export const isIpInCidr = (ip, cidr) => {
|
||||
/**
|
||||
*
|
||||
* @param ipOrCidr
|
||||
* @returns {'IP' | 'CIDR' | 'UNKNOWN'}
|
||||
* @returns {'IP' | 'CIDR' | 'CLIENT_ID' | 'UNKNOWN'}
|
||||
*
|
||||
*/
|
||||
export const findAddressType = (address) => {
|
||||
@@ -524,6 +565,9 @@ export const findAddressType = (address) => {
|
||||
if (cidrMaybe && ipaddr.parseCIDR(address)) {
|
||||
return ADDRESS_TYPES.CIDR;
|
||||
}
|
||||
if (R_CLIENT_ID.test(address)) {
|
||||
return ADDRESS_TYPES.CLIENT_ID;
|
||||
}
|
||||
|
||||
return ADDRESS_TYPES.UNKNOWN;
|
||||
} catch (e) {
|
||||
@@ -544,20 +588,31 @@ export const separateIpsAndCidrs = (ids) => ids.reduce((acc, curr) => {
|
||||
if (addressType === ADDRESS_TYPES.CIDR) {
|
||||
acc.cidrs.push(curr);
|
||||
}
|
||||
if (addressType === ADDRESS_TYPES.CLIENT_ID) {
|
||||
acc.clientIds.push(curr);
|
||||
}
|
||||
return acc;
|
||||
}, { ips: [], cidrs: [] });
|
||||
}, { ips: [], cidrs: [], clientIds: [] });
|
||||
|
||||
export const countClientsStatistics = (ids, autoClients) => {
|
||||
const { ips, cidrs } = separateIpsAndCidrs(ids);
|
||||
const { ips, cidrs, clientIds } = separateIpsAndCidrs(ids);
|
||||
|
||||
const ipsCount = ips.reduce((acc, curr) => {
|
||||
const count = autoClients[curr] || 0;
|
||||
return acc + count;
|
||||
}, 0);
|
||||
|
||||
const clientIdsCount = clientIds.reduce((acc, curr) => {
|
||||
const count = autoClients[curr] || 0;
|
||||
return acc + count;
|
||||
}, 0);
|
||||
|
||||
const cidrsCount = Object.entries(autoClients)
|
||||
.reduce((acc, curr) => {
|
||||
const [id, count] = curr;
|
||||
if (!ipaddr.isValid(id)) {
|
||||
return false;
|
||||
}
|
||||
if (cidrs.some((cidr) => isIpInCidr(id, cidr))) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
acc += count;
|
||||
@@ -565,7 +620,7 @@ export const countClientsStatistics = (ids, autoClients) => {
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return ipsCount + cidrsCount;
|
||||
return ipsCount + cidrsCount + clientIdsCount;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -716,6 +771,75 @@ export const getFilterName = (
|
||||
return resolveFilterName(filter);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {array} rules
|
||||
* @param {array} filters
|
||||
* @param {array} whitelistFilters
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export const getFilterNames = (rules, filters, whitelistFilters) => rules.map(
|
||||
({ filter_list_id }) => getFilterName(filters, whitelistFilters, filter_list_id),
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {array} rules
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export const getRuleNames = (rules) => rules.map(({ text }) => text);
|
||||
|
||||
/**
|
||||
* @param {array} rules
|
||||
* @param {array} filters
|
||||
* @param {array} whitelistFilters
|
||||
* @returns {object}
|
||||
*/
|
||||
export const getFilterNameToRulesMap = (rules, filters, whitelistFilters) => rules.reduce(
|
||||
(acc, { text, filter_list_id }) => {
|
||||
const filterName = getFilterName(filters, whitelistFilters, filter_list_id);
|
||||
|
||||
acc[filterName] = (acc[filterName] || []).concat(text);
|
||||
return acc;
|
||||
}, {},
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {array} rules
|
||||
* @param {array} filters
|
||||
* @param {array} whitelistFilters
|
||||
* @param {object} classes
|
||||
* @returns {JSXElement}
|
||||
*/
|
||||
export const getRulesToFilterList = (rules, filters, whitelistFilters, classes = {
|
||||
list: 'filteringRules',
|
||||
rule: 'filteringRules__rule font-monospace',
|
||||
filter: 'filteringRules__filter',
|
||||
}) => {
|
||||
const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters);
|
||||
|
||||
return <dl className={classes.list}>
|
||||
{Object.entries(filterNameToRulesMap).reduce(
|
||||
(acc, [filterName, rulesArr]) => acc
|
||||
.concat(rulesArr.map((rule, i) => <dd key={i} className={classes.rule}>{rule}</dd>))
|
||||
.concat(<dt className={classes.filter} key={classes.filter}>{filterName}</dt>),
|
||||
[],
|
||||
)}
|
||||
</dl>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {array} rules
|
||||
* @param {array} filters
|
||||
* @param {array} whitelistFilters
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getRulesAndFilterNames = (rules, filters, whitelistFilters) => {
|
||||
const filterNameToRulesMap = getFilterNameToRulesMap(rules, filters, whitelistFilters);
|
||||
|
||||
return Object.entries(filterNameToRulesMap).map(
|
||||
([filterName, filterRules]) => filterRules.concat(filterName).join('\n'),
|
||||
).join('\n\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param ip {string}
|
||||
* @param gateway_ip {string}
|
||||
|
||||
@@ -31,7 +31,7 @@ const getFormattedWhois = (whois) => {
|
||||
* @param {object} info.whois_info
|
||||
* @param {boolean} [isDetailed]
|
||||
* @param {boolean} [isLogs]
|
||||
* @returns {JSX.Element}
|
||||
* @returns {JSXElement}
|
||||
*/
|
||||
export const renderFormattedClientCell = (value, info, isDetailed = false, isLogs = false) => {
|
||||
let whoisContainer = null;
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
R_URL_REQUIRES_PROTOCOL,
|
||||
STANDARD_WEB_PORT,
|
||||
UNSAFE_PORTS,
|
||||
R_CLIENT_ID,
|
||||
R_DOMAIN,
|
||||
} from './constants';
|
||||
import { getLastIpv4Octet, isValidAbsolutePath } from './form';
|
||||
|
||||
@@ -16,7 +18,7 @@ import { getLastIpv4Octet, isValidAbsolutePath } from './form';
|
||||
// https://redux-form.com/8.3.0/examples/fieldlevelvalidation/
|
||||
// If the value is valid, the validation function should return undefined.
|
||||
/**
|
||||
* @param value {string}
|
||||
* @param value {string|number}
|
||||
* @returns {undefined|string}
|
||||
*/
|
||||
export const validateRequiredValue = (value) => {
|
||||
@@ -64,19 +66,35 @@ export const validateClientId = (value) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const formattedValue = value ? value.trim() : value;
|
||||
const formattedValue = value.trim();
|
||||
if (formattedValue && !(
|
||||
R_IPV4.test(formattedValue)
|
||||
|| R_IPV6.test(formattedValue)
|
||||
|| R_MAC.test(formattedValue)
|
||||
|| R_CIDR.test(formattedValue)
|
||||
|| R_CIDR_IPV6.test(formattedValue)
|
||||
|| R_CLIENT_ID.test(formattedValue)
|
||||
)) {
|
||||
return 'form_error_client_id_format';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {undefined|string}
|
||||
*/
|
||||
export const validateServerName = (value) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const formattedValue = value ? value.trim() : value;
|
||||
if (formattedValue && !R_DOMAIN.test(formattedValue)) {
|
||||
return 'form_error_server_name';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {undefined|string}
|
||||
|
||||
@@ -41,16 +41,13 @@ const AddressList = ({
|
||||
AddressList.propTypes = {
|
||||
interfaces: PropTypes.object.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
port: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
port: PropTypes.number.isRequired,
|
||||
isDns: PropTypes.bool,
|
||||
};
|
||||
|
||||
renderItem.propTypes = {
|
||||
ip: PropTypes.string.isRequired,
|
||||
port: PropTypes.string.isRequired,
|
||||
port: PropTypes.number.isRequired,
|
||||
isDns: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
@media screen and (max-width: 767px) {
|
||||
input, select, textarea {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.setup {
|
||||
min-height: calc(100vh - 71px);
|
||||
line-height: 1.48;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
@media screen and (max-width: 767px) {
|
||||
input, select, textarea {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -24,13 +24,7 @@ const access = handleActions(
|
||||
|
||||
[actions.setAccessListRequest]: (state) => ({ ...state, processingSet: true }),
|
||||
[actions.setAccessListFailure]: (state) => ({ ...state, processingSet: false }),
|
||||
[actions.setAccessListSuccess]: (state) => {
|
||||
const newState = {
|
||||
...state,
|
||||
processingSet: false,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
[actions.setAccessListSuccess]: (state) => ({ ...state, processingSet: false }),
|
||||
|
||||
[actions.toggleClientBlockRequest]: (state) => ({ ...state, processingSet: true }),
|
||||
[actions.toggleClientBlockFailure]: (state) => ({ ...state, processingSet: false }),
|
||||
|
||||
6
client2/.eslintignore
Normal file
6
client2/.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
scripts
|
||||
node_modules
|
||||
postcss.config.js
|
||||
src/lib/entities
|
||||
src/lib/apis
|
||||
openApi
|
||||
5
client2/.eslintrc
Normal file
5
client2/.eslintrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"./scripts/lint/dev.js"
|
||||
]
|
||||
}
|
||||
18
client2/declaration.d.ts
vendored
Normal file
18
client2/declaration.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
declare module '*.pcss' {
|
||||
const content: {[className: string]: string};
|
||||
export default content;
|
||||
}
|
||||
declare module '*.css' {
|
||||
const content: {[className: string]: string};
|
||||
export default content;
|
||||
}
|
||||
declare module '*.png'
|
||||
declare module '*.jpg'
|
||||
declare let AUTH_TOKEN: string;
|
||||
declare let MAIN_TOKEN: string | undefined;
|
||||
declare let NO_CAPTCHA: boolean | undefined;
|
||||
declare module 'dygraphs';
|
||||
declare module '@novnc/novnc/core/rfb';
|
||||
// cp - CloudPayments script
|
||||
declare let cp: any;
|
||||
declare const DEV: any;
|
||||
92
client2/package.json
Normal file
92
client2/package.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"author": "Performix",
|
||||
"private": true,
|
||||
"name": "adguard-home",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "rm -rf ../build2 && yarn install && webpack --config ./scripts/webpack/webpack.config.prod.js",
|
||||
"start": "webpack serve --config ./scripts/webpack/webpack.config.dev.js",
|
||||
"generate": "rm -rf ./src/lib/entities ./src/lib/apis && ts-node --compiler-options '{ \"module\": \"CommonJS\" }' ./scripts/generator/index.ts",
|
||||
"translations:check": "ts-node --compiler-options '{ \"module\": \"CommonJS\" }' ./scripts/plugins/checkTranslations.ts",
|
||||
"lint": "eslint -c ./scripts/lint/prod.js --ext .tsx --ext .ts ./",
|
||||
"go:build": "cd .. && make REBUILD_CLIENT=0 build",
|
||||
"go:run": "sudo ../AdguardHome"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "yarn lint"
|
||||
}
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sentry/react": "^5.27.0",
|
||||
"antd": "^4.7.2",
|
||||
"classnames": "^2.2.6",
|
||||
"dayjs": "^1.9.3",
|
||||
"formik": "^2.2.0",
|
||||
"mobx": "^6.0.1",
|
||||
"mobx-react-lite": "^3.0.1",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/qs": "^6.9.5",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^4.5.0",
|
||||
"@typescript-eslint/parser": "^4.5.0",
|
||||
"antd-dayjs-webpack-plugin": "^1.0.1",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"connect-history-api-fallback": "^1.6.0",
|
||||
"copy-webpack-plugin": "^6.2.1",
|
||||
"css-loader": "^5.0.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-airbnb-base": "^14.2.0",
|
||||
"eslint-config-airbnb-typescript": "^12.0.0",
|
||||
"eslint-import-resolver-typescript": "^2.3.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"file-loader": "^6.1.1",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"http-proxy-middleware": "^1.0.6",
|
||||
"husky": "^4.3.0",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^5.0.0",
|
||||
"mini-css-extract-plugin": "^1.1.1",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
||||
"postcss": "^8.1.2",
|
||||
"postcss-calc": "^7.0.5",
|
||||
"postcss-css-variables": "^0.17.0",
|
||||
"postcss-custom-media": "^7.0.8",
|
||||
"postcss-import": "^13.0.0",
|
||||
"postcss-inline-svg": "^4.1.0",
|
||||
"postcss-loader": "^4.0.4",
|
||||
"postcss-mixins": "^7.0.1",
|
||||
"postcss-modules": "^3.2.2",
|
||||
"postcss-nested": "^5.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss-reporter": "^7.0.1",
|
||||
"postcss-variables": "^1.1.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"stylelint": "^13.7.2",
|
||||
"stylelint-webpack-plugin": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.0.0",
|
||||
"ts-loader": "^8.0.6",
|
||||
"ts-morph": "^8.1.2",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.10.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-merge": "^5.2.0",
|
||||
"yaml": "^1.10.0"
|
||||
}
|
||||
}
|
||||
17
client2/postcss.config.js
Normal file
17
client2/postcss.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
['postcss-import', {}],
|
||||
['postcss-nested', {}],
|
||||
['postcss-custom-media', {}],
|
||||
['postcss-variables', {}],
|
||||
['postcss-calc', {}],
|
||||
['postcss-mixins', {}],
|
||||
['postcss-preset-env', { stage: 3, features: { 'nesting-rules': true } }],
|
||||
['postcss-reporter', { clearMessages: true }],
|
||||
['postcss-inline-svg', {
|
||||
paths: ['frontend/icons', 'vendor/adguard/utils-bundle/src/Resources/frontend/icons'],
|
||||
svgo: { plugins: [{ cleanupAttrs: true }] }
|
||||
}],
|
||||
['autoprefixer'],
|
||||
]
|
||||
};
|
||||
BIN
client2/public/assets/apple-touch-icon-180x180.png
Normal file
BIN
client2/public/assets/apple-touch-icon-180x180.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
client2/public/assets/favicon.png
Normal file
BIN
client2/public/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
8
client2/public/assets/safari-pinned-tab.svg
Normal file
8
client2/public/assets/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16pt" height="16pt"
|
||||
viewBox="0 0 16 16" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;"
|
||||
d="M 8 0 C 10.5 0 13.515625 0.574219 16 1.835938 L 15.996094 2.542969 C 15.957031 5.605469 15.410156 11.71875 8 16 C 0.5 11.667969 0.03125 5.460938 0.00390625 2.433594 L 0 1.835938 C 2.484375 0.574219 5.5 0 8 0 Z M 11.769531 4.203125 L 11.761719 4.203125 L 7.890625 8.160156 L 6.433594 6.4375 C 5.738281 5.644531 4.792969 6.25 4.570312 6.40625 L 7.929688 10.285156 L 12.570312 4.136719 C 12.230469 3.867188 11.933594 4.054688 11.769531 4.203125 Z M 11.769531 4.203125 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 801 B |
23
client2/public/index.html
Normal file
23
client2/public/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta http-equiv="x-dns-prefetch-control" content="off">
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon-180x180.png" />
|
||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
||||
<title>AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
22
client2/public/install.html
Normal file
22
client2/public/install.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon-180x180.png" />
|
||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
||||
<title>Setup AdGuard Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
22
client2/public/login.html
Normal file
22
client2/public/login.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="google" content="notranslate">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon-180x180.png" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
||||
<title>Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
12
client2/scripts/consts.ts
Normal file
12
client2/scripts/consts.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const OPEN_API_PATH = '../openapi/openapi.yaml';
|
||||
export const ENT_DIR = './src/lib/entities';
|
||||
export const API_DIR = './src/lib/apis';
|
||||
export const LOCALE_FOLDER_PATH = './src/lib/intl/__locales';
|
||||
export const TRANSLATOR_CLASS_NAME = 'Translator';
|
||||
export const USE_INTL_NAME = 'useIntl';
|
||||
|
||||
export const trimQuotes = (str: string) => {
|
||||
return str.replace(/\'|\"/g, '');
|
||||
};
|
||||
|
||||
export const GENERATOR_ENTITY_ALLIAS = 'Entities/';
|
||||
18
client2/scripts/generator/index.ts
Normal file
18
client2/scripts/generator/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as fs from 'fs';
|
||||
import * as YAML from 'yaml';
|
||||
import { OPEN_API_PATH } from '../consts';
|
||||
|
||||
import EntitiesGenerator from './src/generateEntities';
|
||||
import ApisGenerator from './src/generateApis';
|
||||
|
||||
|
||||
const generateApi = (openApi: Record<string, any>) => {
|
||||
const ent = new EntitiesGenerator(openApi);
|
||||
ent.save();
|
||||
|
||||
const api = new ApisGenerator(openApi);
|
||||
api.save();
|
||||
}
|
||||
|
||||
const openApiFile = fs.readFileSync(OPEN_API_PATH, 'utf8');
|
||||
generateApi(YAML.parse(openApiFile));
|
||||
317
client2/scripts/generator/src/generateApis.ts
Normal file
317
client2/scripts/generator/src/generateApis.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { stringify } from 'qs';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as morph from 'ts-morph';
|
||||
|
||||
import {
|
||||
API_DIR as API_DIR_CONST,
|
||||
GENERATOR_ENTITY_ALLIAS,
|
||||
} from '../../consts';
|
||||
import { toCamel, capitalize, schemaParamParser } from './utils';
|
||||
|
||||
|
||||
const API_DIR = path.resolve(API_DIR_CONST);
|
||||
if (!fs.existsSync(API_DIR)) {
|
||||
fs.mkdirSync(API_DIR);
|
||||
}
|
||||
|
||||
const { Project, QuoteKind } = morph;
|
||||
|
||||
|
||||
class ApiGenerator {
|
||||
project = new Project({
|
||||
tsConfigFilePath: './tsconfig.json',
|
||||
addFilesFromTsConfig: false,
|
||||
manipulationSettings: {
|
||||
quoteKind: QuoteKind.Single,
|
||||
usePrefixAndSuffixTextForRename: false,
|
||||
useTrailingCommas: true,
|
||||
},
|
||||
});
|
||||
|
||||
openapi: Record<string, any>;
|
||||
|
||||
serverUrl: string;
|
||||
|
||||
paths: any;
|
||||
|
||||
/* interface Controllers {
|
||||
[controller: string]: {
|
||||
[operationId: string]: { parameters - from opneApi, responses - from opneApi, method }
|
||||
}
|
||||
} */
|
||||
controllers: Record<string, any> = {};
|
||||
|
||||
apis: morph.SourceFile[] = [];
|
||||
|
||||
constructor(openapi: Record<string, any>) {
|
||||
this.openapi = openapi;
|
||||
this.paths = openapi.paths;
|
||||
this.serverUrl = openapi.servers[0].url;
|
||||
|
||||
Object.keys(this.paths).forEach((pathKey) => {
|
||||
Object.keys(this.paths[pathKey]).forEach((method) => {
|
||||
const {
|
||||
tags, operationId, parameters, responses, requestBody, security,
|
||||
} = this.paths[pathKey][method];
|
||||
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));
|
||||
|
||||
if (this.controllers[controller]) {
|
||||
this.controllers[controller][operationId] = {
|
||||
parameters,
|
||||
responses,
|
||||
method,
|
||||
requestBody,
|
||||
security,
|
||||
pathKey: pathKey.replace(/{/g, '${'),
|
||||
};
|
||||
} else {
|
||||
this.controllers[controller] = { [operationId]: {
|
||||
parameters,
|
||||
responses,
|
||||
method,
|
||||
requestBody,
|
||||
security,
|
||||
pathKey: pathKey.replace(/{/g, '${'),
|
||||
} };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.generateApiFiles();
|
||||
}
|
||||
|
||||
generateApiFiles = () => {
|
||||
Object.keys(this.controllers).forEach(this.generateApiFile);
|
||||
};
|
||||
|
||||
generateApiFile = (cName: string) => {
|
||||
const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`);
|
||||
apiFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
|
||||
// const schemaProperties = schemas[schemaName].properties;
|
||||
const importEntities: any[] = [];
|
||||
|
||||
// add api class to file
|
||||
const apiClass = apiFile.addClass({
|
||||
name: `${capitalize(cName)}Api`,
|
||||
isDefaultExport: true,
|
||||
});
|
||||
|
||||
// get operations of controller
|
||||
const controllerOperations = this.controllers[cName];
|
||||
const operationList = Object.keys(controllerOperations).sort();
|
||||
// for each operation add fetcher
|
||||
operationList.forEach((operation) => {
|
||||
const {
|
||||
requestBody, responses, parameters, method, pathKey, security,
|
||||
} = controllerOperations[operation];
|
||||
|
||||
const queryParams: any[] = []; // { name, type }
|
||||
const bodyParam: any[] = []; // { name, type }
|
||||
|
||||
let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
|
||||
let contentType = '';
|
||||
if (parameters) {
|
||||
parameters.forEach((p: any) => {
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(p.schema, this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
}
|
||||
if (p.in === 'query') {
|
||||
queryParams.push({
|
||||
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (queryParams.length > 0) {
|
||||
const imp = apiFile.getImportDeclaration((i) => {
|
||||
return i.getModuleSpecifierValue() === 'qs';
|
||||
}); if (!imp) {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: 'qs',
|
||||
defaultImport: 'qs',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (requestBody) {
|
||||
let content = requestBody.content;
|
||||
const { $ref }: { $ref: string } = requestBody;
|
||||
|
||||
if (!content && $ref) {
|
||||
const name = $ref.split('/').pop() as string;
|
||||
content = this.openapi.components.requestBodies[name].content;
|
||||
}
|
||||
|
||||
[contentType] = Object.keys(content);
|
||||
const data = content[contentType];
|
||||
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(data.schema, this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
|
||||
} else {
|
||||
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
|
||||
|
||||
}
|
||||
}
|
||||
if (responses['200']) {
|
||||
const { content, headers } = responses['200'];
|
||||
if (content && (content['*/*'] || content['application/json'])) {
|
||||
const { schema, examples } = content['*/*'] || content['application/json'];
|
||||
|
||||
if (!schema) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const propType = schemaParamParser(schema, this.openapi);
|
||||
const [pType, , isClass, isImport] = propType;
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
}
|
||||
hasResponseBodyType = propType;
|
||||
}
|
||||
}
|
||||
let returnType = '';
|
||||
if (hasResponseBodyType) {
|
||||
const [pType, isArray, isClass] = hasResponseBodyType as any;
|
||||
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
||||
returnType = data;
|
||||
} else {
|
||||
returnType = 'Promise<number';
|
||||
}
|
||||
const shouldValidate = bodyParam.filter(b => b.isClass);
|
||||
if (shouldValidate.length > 0) {
|
||||
returnType += ' | string[]';
|
||||
}
|
||||
// append Error to default type return;
|
||||
returnType += ' | Error>';
|
||||
|
||||
const fetcher = apiClass.addMethod({
|
||||
isAsync: true,
|
||||
isStatic: true,
|
||||
name: operation,
|
||||
returnType,
|
||||
});
|
||||
const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken)));
|
||||
fetcher.addParameters(params);
|
||||
|
||||
fetcher.setBodyText((w) => {
|
||||
// Add data to URLSearchParams
|
||||
if (contentType === 'text/plain') {
|
||||
bodyParam.forEach((b) => {
|
||||
w.writeLine(`const params = String(${b.name});`);
|
||||
});
|
||||
} else {
|
||||
if (shouldValidate.length > 0) {
|
||||
w.writeLine(`const haveError: string[] = [];`);
|
||||
shouldValidate.forEach((b) => {
|
||||
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
|
||||
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
|
||||
});
|
||||
w.writeLine(`if (haveError.length > 0) {`);
|
||||
w.writeLine(` return Promise.resolve(haveError);`)
|
||||
w.writeLine(`}`);
|
||||
}
|
||||
}
|
||||
// Switch return of fetch in case on queryParams
|
||||
if (queryParams.length > 0) {
|
||||
w.writeLine('const queryParams = {');
|
||||
queryParams.forEach((q) => {
|
||||
w.writeLine(` ${q.name}: ${q.name},`);
|
||||
});
|
||||
w.writeLine('}');
|
||||
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`);
|
||||
} else {
|
||||
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`);
|
||||
}
|
||||
// Add method
|
||||
w.writeLine(` method: '${method.toUpperCase()}',`);
|
||||
|
||||
// add Fetch options
|
||||
if (contentType && contentType !== 'multipart/form-data') {
|
||||
w.writeLine(' headers: {');
|
||||
w.writeLine(` 'Content-Type': '${contentType}',`);
|
||||
w.writeLine(' },');
|
||||
}
|
||||
if (contentType) {
|
||||
switch (contentType) {
|
||||
case 'text/plain':
|
||||
w.writeLine(' body: params,');
|
||||
break;
|
||||
default:
|
||||
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle response
|
||||
if (hasResponseBodyType) {
|
||||
w.writeLine('}).then(async (res) => {');
|
||||
w.writeLine(' if (res.status === 200) {');
|
||||
w.writeLine(' return res.json();');
|
||||
} else {
|
||||
w.writeLine('}).then(async (res) => {');
|
||||
w.writeLine(' if (res.status === 200) {');
|
||||
w.writeLine(' return res.status;');
|
||||
}
|
||||
|
||||
// Handle Error
|
||||
w.writeLine(' } else {');
|
||||
w.writeLine(' return new Error(String(res.status));');
|
||||
w.writeLine(' }');
|
||||
w.writeLine('})');
|
||||
});
|
||||
});
|
||||
|
||||
const imports: any[] = [];
|
||||
const types: string[] = [];
|
||||
importEntities.forEach((i) => {
|
||||
const { type } = i;
|
||||
if (!types.includes(type)) {
|
||||
imports.push(i);
|
||||
types.push(type);
|
||||
}
|
||||
});
|
||||
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||
const { type: pType, isClass } = ie;
|
||||
if (isClass) {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
||||
defaultImport: pType,
|
||||
namedImports: [`I${pType}`],
|
||||
});
|
||||
} else {
|
||||
apiFile.addImportDeclaration({
|
||||
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
||||
namedImports: [pType],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.apis.push(apiFile);
|
||||
};
|
||||
|
||||
save = () => {
|
||||
this.apis.forEach(async (e) => {
|
||||
await e.saveSync();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default ApiGenerator;
|
||||
519
client2/scripts/generator/src/generateEntities.ts
Normal file
519
client2/scripts/generator/src/generateEntities.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as morph from 'ts-morph';
|
||||
|
||||
import { ENT_DIR } from '../../consts';
|
||||
import { TYPES, toCamel, schemaParamParser } from './utils';
|
||||
|
||||
const { Project, QuoteKind } = morph;
|
||||
|
||||
|
||||
const EntDir = path.resolve(ENT_DIR);
|
||||
if (!fs.existsSync(EntDir)) {
|
||||
fs.mkdirSync(EntDir);
|
||||
}
|
||||
|
||||
class EntitiesGenerator {
|
||||
project = new Project({
|
||||
tsConfigFilePath: './tsconfig.json',
|
||||
addFilesFromTsConfig: false,
|
||||
manipulationSettings: {
|
||||
quoteKind: QuoteKind.Single,
|
||||
usePrefixAndSuffixTextForRename: false,
|
||||
useTrailingCommas: true,
|
||||
},
|
||||
});
|
||||
|
||||
openapi: Record<string, any>;
|
||||
|
||||
schemas: Record<string, any>;
|
||||
|
||||
schemaNames: string[];
|
||||
|
||||
entities: morph.SourceFile[] = [];
|
||||
|
||||
constructor(openapi: Record<string, any>) {
|
||||
this.openapi = openapi;
|
||||
this.schemas = openapi.components.schemas;
|
||||
this.schemaNames = Object.keys(this.schemas);
|
||||
this.generateEntities();
|
||||
}
|
||||
|
||||
generateEntities = () => {
|
||||
this.schemaNames.forEach(this.generateEntity);
|
||||
};
|
||||
|
||||
generateEntity = (sName: string) => {
|
||||
const { properties, type, oneOf } = this.schemas[sName];
|
||||
const notAClass = !properties && TYPES[type as keyof typeof TYPES];
|
||||
|
||||
if (oneOf) {
|
||||
this.generateOneOf(sName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (notAClass) {
|
||||
this.generateEnum(sName);
|
||||
} else {
|
||||
this.generateClass(sName);
|
||||
}
|
||||
};
|
||||
|
||||
generateEnum = (sName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
||||
entityFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
|
||||
const { enum: enumMembers } = this.schemas[sName];
|
||||
entityFile.addEnum({
|
||||
name: sName,
|
||||
members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })),
|
||||
isExported: true,
|
||||
});
|
||||
|
||||
this.entities.push(entityFile);
|
||||
};
|
||||
|
||||
generateOneOf = (sName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
||||
entityFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||
const entities = this.schemas[sName].oneOf.map((elem: any) => {
|
||||
const [
|
||||
pType, isArray, isClass, isImport,
|
||||
] = schemaParamParser(elem, this.openapi);
|
||||
importEntities.push({ type: pType, isClass });
|
||||
return { type: pType, isArray };
|
||||
});
|
||||
entityFile.addTypeAlias({
|
||||
name: sName,
|
||||
isExported: true,
|
||||
type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
|
||||
})
|
||||
|
||||
// add import
|
||||
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||
const { type: pType, isClass } = ie;
|
||||
if (isClass) {
|
||||
entityFile.addImportDeclaration({
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [`I${pType}`],
|
||||
});
|
||||
} else {
|
||||
entityFile.addImportDeclaration({
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [pType],
|
||||
});
|
||||
}
|
||||
});
|
||||
this.entities.push(entityFile);
|
||||
}
|
||||
|
||||
generateClass = (sName: string) => {
|
||||
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
|
||||
entityFile.addStatements([
|
||||
'// This file was autogenerated. Please do not change.',
|
||||
'// All changes will be overwrited on commit.',
|
||||
'',
|
||||
]);
|
||||
|
||||
const { properties: sProps, required } = this.schemas[sName];
|
||||
|
||||
const importEntities: { type: string, isClass: boolean }[] = [];
|
||||
const entityInterface = entityFile.addInterface({
|
||||
name: `I${sName}`,
|
||||
isExported: true,
|
||||
});
|
||||
const sortedSProps = Object.keys(sProps || {}).sort();
|
||||
// add server response interface to entityFile
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const [
|
||||
pType, isArray, isClass, isImport, isAdditional
|
||||
] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
|
||||
if (isImport) {
|
||||
importEntities.push({ type: pType, isClass });
|
||||
}
|
||||
const propertyType = isAdditional
|
||||
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
|
||||
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
||||
entityInterface.addProperty({
|
||||
name: sPropName,
|
||||
type: propertyType,
|
||||
hasQuestionToken: !(
|
||||
(required && required.includes(sPropName)) || sProps[sPropName].required
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
// add import
|
||||
const imports: { type: string, isClass: boolean }[] = [];
|
||||
const types: string[] = [];
|
||||
importEntities.forEach((i) => {
|
||||
const { type } = i;
|
||||
if (!types.includes(type)) {
|
||||
imports.push(i);
|
||||
types.push(type);
|
||||
}
|
||||
});
|
||||
imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
||||
const { type: pType, isClass } = ie;
|
||||
if (isClass) {
|
||||
entityFile.addImportDeclaration({
|
||||
defaultImport: pType,
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [`I${pType}`],
|
||||
});
|
||||
} else {
|
||||
entityFile.addImportDeclaration({
|
||||
moduleSpecifier: `./${pType}`,
|
||||
namedImports: [pType],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const entityClass = entityFile.addClass({
|
||||
name: sName,
|
||||
isDefaultExport: true,
|
||||
});
|
||||
|
||||
// addProperties to class;
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
|
||||
const isRequred = (required && required.includes(sPropName))
|
||||
|| sProps[sPropName].required;
|
||||
|
||||
const propertyType = isAdditional
|
||||
? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }`
|
||||
: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`;
|
||||
|
||||
entityClass.addProperty({
|
||||
name: `_${sPropName}`,
|
||||
isReadonly: true,
|
||||
type: propertyType,
|
||||
});
|
||||
const getter = entityClass.addGetAccessor({
|
||||
name: toCamel(sPropName),
|
||||
returnType: propertyType,
|
||||
statements: [`return this._${sPropName};`],
|
||||
});
|
||||
const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
||||
if (description || example) {
|
||||
getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
|
||||
}
|
||||
if (minItems) {
|
||||
entityClass.addGetAccessor({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MinItems`,
|
||||
statements: [`return ${minItems};`],
|
||||
});
|
||||
}
|
||||
if (maxItems) {
|
||||
entityClass.addGetAccessor({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MaxItems`,
|
||||
statements: [`return ${maxItems};`],
|
||||
});
|
||||
}
|
||||
if (typeof minLength === 'number') {
|
||||
entityClass.addGetAccessor({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MinLength`,
|
||||
statements: [`return ${minLength};`],
|
||||
});
|
||||
}
|
||||
if (maxLength) {
|
||||
entityClass.addGetAccessor({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MaxLength`,
|
||||
statements: [`return ${maxLength};`],
|
||||
});
|
||||
}
|
||||
if (typeof minimum === 'number') {
|
||||
entityClass.addGetAccessor({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MinValue`,
|
||||
statements: [`return ${minimum};`],
|
||||
});
|
||||
}
|
||||
if (maximum) {
|
||||
entityClass.addGetAccessor({
|
||||
isStatic: true,
|
||||
name: `${toCamel(sPropName)}MaxValue`,
|
||||
statements: [`return ${maximum};`],
|
||||
});
|
||||
}
|
||||
|
||||
if (!(isArray && isClass) && !isClass) {
|
||||
const isEnum = !isClass && isImport;
|
||||
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
|
||||
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
||||
const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number';
|
||||
if (isRequired || haveValidationFields) {
|
||||
const prop = toCamel(sPropName);
|
||||
const validateField = entityClass.addMethod({
|
||||
isStatic: true,
|
||||
name: `${prop}Validate`,
|
||||
returnType: `boolean`,
|
||||
parameters: [{
|
||||
name: prop,
|
||||
type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`,
|
||||
}],
|
||||
})
|
||||
|
||||
validateField.setBodyText((w) => {
|
||||
w.write('return ');
|
||||
const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`;
|
||||
if (pType === 'string') {
|
||||
if (isArray) {
|
||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`);
|
||||
} else {
|
||||
if (typeof minLength === 'number' && maxLength) {
|
||||
w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`);
|
||||
}
|
||||
if (typeof minLength !== 'number' || !maxLength) {
|
||||
w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'number') {
|
||||
if (isArray) {
|
||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true)`);
|
||||
} else {
|
||||
if (typeof minimum === 'number' && maximum) {
|
||||
w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`);
|
||||
}
|
||||
if (typeof minimum !== 'number' || !maximum) {
|
||||
w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'boolean') {
|
||||
w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
|
||||
} else if (isEnum) {
|
||||
if (isArray){
|
||||
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`);
|
||||
} else {
|
||||
w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
|
||||
}
|
||||
}
|
||||
|
||||
w.write(';');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add constructor;
|
||||
const ctor = entityClass.addConstructor({
|
||||
parameters: [{
|
||||
name: 'props',
|
||||
type: `I${sName}`,
|
||||
}],
|
||||
});
|
||||
ctor.setBodyText((w) => {
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const [
|
||||
pType, isArray, isClass, , isAdditional
|
||||
] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
const req = (required && required.includes(sPropName))
|
||||
|| sProps[sPropName].required;
|
||||
if (!req) {
|
||||
if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) {
|
||||
w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`);
|
||||
} else {
|
||||
w.writeLine(`if (props.${sPropName}) {`);
|
||||
}
|
||||
}
|
||||
if (isAdditional) {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => {
|
||||
return { ...prev, [key]: new ${pType}(p[key])};
|
||||
},{}))`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
||||
return { ...prev, [key]: new ${pType}(props.${sPropName}[key])};
|
||||
},{})`);
|
||||
} else {
|
||||
if (pType === 'string' && !isArray) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
||||
return { ...prev, [key]: props.${sPropName}[key].trim()};
|
||||
},{})`);
|
||||
} else {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
|
||||
return { ...prev, [key]: props.${sPropName}[key]};
|
||||
},{})`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`);
|
||||
} else {
|
||||
if (pType === 'string' && !isArray) {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.trim();`);
|
||||
} else {
|
||||
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!req) {
|
||||
w.writeLine('}');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// add serialize method;
|
||||
const serialize = entityClass.addMethod({
|
||||
isStatic: false,
|
||||
name: 'serialize',
|
||||
returnType: `I${sName}`,
|
||||
});
|
||||
serialize.setBodyText((w) => {
|
||||
w.writeLine(`const data: I${sName} = {`);
|
||||
const unReqFields: string[] = [];
|
||||
sortedSProps.forEach((sPropName) => {
|
||||
const req = (required && required.includes(sPropName))
|
||||
|| sProps[sPropName].required;
|
||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
if (!req) {
|
||||
unReqFields.push(sPropName);
|
||||
return;
|
||||
}
|
||||
if (isAdditional) {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce<Record<string, any>>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`);
|
||||
} else {
|
||||
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`);
|
||||
}
|
||||
} else {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName}.serialize(),`);
|
||||
} else {
|
||||
w.writeLine(` ${sPropName}: this._${sPropName},`);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
w.writeLine('};');
|
||||
unReqFields.forEach((sPropName) => {
|
||||
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`);
|
||||
if (isAdditional) {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`);
|
||||
} else {
|
||||
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`);
|
||||
}
|
||||
} else {
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`);
|
||||
} else if (isClass) {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName}.serialize();`);
|
||||
} else {
|
||||
w.writeLine(` data.${sPropName} = this._${sPropName};`);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
w.writeLine(`}`);
|
||||
});
|
||||
w.writeLine('return data;');
|
||||
});
|
||||
|
||||
// add validate method
|
||||
const validate = entityClass.addMethod({
|
||||
isStatic: false,
|
||||
name: 'validate',
|
||||
returnType: `string[]`,
|
||||
})
|
||||
validate.setBodyText((w) => {
|
||||
w.writeLine('const validate = {');
|
||||
Object.keys(sProps || {}).forEach((sPropName) => {
|
||||
const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
|
||||
|
||||
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
|
||||
|
||||
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
|
||||
const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`;
|
||||
|
||||
if (isArray && isClass) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`);
|
||||
} else if (isClass && !isAdditional) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`);
|
||||
} else {
|
||||
if (pType === 'string') {
|
||||
if (isArray) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`);
|
||||
} else {
|
||||
if (typeof minLength === 'number' && maxLength) {
|
||||
w.writeLine(` ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`);
|
||||
}
|
||||
if (typeof minLength !== 'number' || !maxLength) {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'number') {
|
||||
if (isArray) {
|
||||
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`);
|
||||
} else {
|
||||
if (typeof minimum === 'number' && maximum) {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`);
|
||||
}
|
||||
if (typeof minimum !== 'number' || !maximum) {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`);
|
||||
}
|
||||
}
|
||||
} else if (pType === 'boolean') {
|
||||
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`);
|
||||
}
|
||||
}
|
||||
});
|
||||
w.writeLine('};');
|
||||
w.writeLine('const isError: string[] = [];')
|
||||
w.writeLine('Object.keys(validate).forEach((key) => {');
|
||||
w.writeLine(' if (!(validate as any)[key]) {');
|
||||
w.writeLine(' isError.push(key);');
|
||||
w.writeLine(' }');
|
||||
w.writeLine('});');
|
||||
w.writeLine('return isError;');
|
||||
|
||||
});
|
||||
|
||||
// add update method;
|
||||
const update = entityClass.addMethod({
|
||||
isStatic: false,
|
||||
name: 'update',
|
||||
returnType: `${sName}`,
|
||||
});
|
||||
update.addParameter({
|
||||
name: 'props',
|
||||
type: `Partial<I${sName}>`,
|
||||
});
|
||||
update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); });
|
||||
|
||||
this.entities.push(entityFile);
|
||||
};
|
||||
|
||||
save = () => {
|
||||
this.entities.forEach(async (e) => {
|
||||
await e.saveSync();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default EntitiesGenerator;
|
||||
74
client2/scripts/generator/src/utils.ts
Normal file
74
client2/scripts/generator/src/utils.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
const toCamel = (s: string) => {
|
||||
return s.replace(/([-_][a-z])/ig, ($1) => {
|
||||
return $1.toUpperCase()
|
||||
.replace('-', '')
|
||||
.replace('_', '');
|
||||
});
|
||||
};
|
||||
const capitalize = (s: string) => {
|
||||
return s[0].toUpperCase() + s.slice(1);
|
||||
};
|
||||
const TYPES = {
|
||||
integer: 'number',
|
||||
float: 'number',
|
||||
number: 'number',
|
||||
string: 'string',
|
||||
boolean: 'boolean',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param schemaProp: valueof shema.properties[key]
|
||||
* @param openApi: openapi object
|
||||
* @returns [propType - basicType or import one, isArray, isClass, isImport]
|
||||
*/
|
||||
const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boolean, boolean, boolean] => {
|
||||
let type = '';
|
||||
let isImport = false;
|
||||
let isClass = false;
|
||||
let isArray = false;
|
||||
let isAdditional = false;
|
||||
|
||||
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
|
||||
const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref).split('/');
|
||||
|
||||
if (schemaProp.additionalProperties) {
|
||||
isAdditional = true;
|
||||
}
|
||||
|
||||
type = `${temp[temp.length - 1]}`;
|
||||
|
||||
const cl = openApi ? openApi.components.schemas[temp[temp.length - 1]] : {};
|
||||
|
||||
if (cl.type === 'string' && cl.enum) {
|
||||
isImport = true;
|
||||
}
|
||||
|
||||
if (cl.type === 'object' && !cl.oneOf) {
|
||||
isClass = true;
|
||||
isImport = true;
|
||||
} else if (cl.type === 'array') {
|
||||
const temp: any = schemaParamParser(cl.items, openApi);
|
||||
type = `${temp[0]}`;
|
||||
isArray = true;
|
||||
isClass = isClass || temp[2];
|
||||
isImport = isImport || temp[3];
|
||||
}
|
||||
} else if (schemaProp.type === 'array') {
|
||||
const temp: any = schemaParamParser(schemaProp.items, openApi);
|
||||
type = `${temp[0]}`;
|
||||
isArray = true;
|
||||
isClass = isClass || temp[2];
|
||||
isImport = isImport || temp[3];
|
||||
} else {
|
||||
type = (TYPES as Record<any, string>)[schemaProp.type];
|
||||
}
|
||||
if (!type) {
|
||||
// TODO: Fix bug with Error fields.
|
||||
type = 'any';
|
||||
// throw new Error('Failed to find entity type');
|
||||
}
|
||||
|
||||
return [type, isArray, isClass, isImport, isAdditional];
|
||||
};
|
||||
|
||||
export { TYPES, toCamel, capitalize, schemaParamParser };
|
||||
226
client2/scripts/helpers/checkTranslations.ts
Normal file
226
client2/scripts/helpers/checkTranslations.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import * as fs from 'fs';
|
||||
import {
|
||||
Project,
|
||||
VariableStatement,
|
||||
SyntaxKind,
|
||||
Node,
|
||||
Statement,
|
||||
ts,
|
||||
Identifier,
|
||||
SourceFile,
|
||||
} from 'ts-morph';
|
||||
import {
|
||||
LOCALE_FOLDER_PATH,
|
||||
TRANSLATOR_CLASS_NAME,
|
||||
USE_INTL_NAME,
|
||||
trimQuotes,
|
||||
} from '../consts';
|
||||
import { checkForms, AvailableLocales } from '../../src/localization/Translator';
|
||||
|
||||
const project = new Project({
|
||||
tsConfigFilePath: './tsconfig.json',
|
||||
});
|
||||
|
||||
let lang = 'ru';
|
||||
let option = '';
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
lang = process.argv[2];
|
||||
option = process.argv[3];
|
||||
}
|
||||
|
||||
const usedTranslations: string[] = [];
|
||||
const usedPluralTranslations: string[] = [];
|
||||
|
||||
const problemFiles: string[] = [];
|
||||
const sourceFiles = project.getSourceFiles();
|
||||
const sourceFilesWithIntl = sourceFiles.filter((sf) => {
|
||||
return !!sf.getImportDeclarations().find((id) => {
|
||||
return !!id.getNamedImports().find((ni) => ni.getName() === USE_INTL_NAME)
|
||||
})
|
||||
});
|
||||
const getFileUsedIntl = (statements: Statement<ts.Statement>[]) => {
|
||||
statements.forEach((s) => {
|
||||
if (s instanceof VariableStatement) {
|
||||
s.forEachDescendant((node) => {
|
||||
let intVariableDeclaration: Identifier = null;
|
||||
switch (node.getKind()) {
|
||||
case SyntaxKind.VariableDeclaration:
|
||||
if (node.getSymbol()) {
|
||||
const name = node.getSymbol().getName();
|
||||
const callExp = node.getChildren().find((n) => n.getKind() === SyntaxKind.CallExpression);
|
||||
if (callExp) {
|
||||
const callExpIden = callExp.getChildren().find(n => n.getKind() === SyntaxKind.Identifier);
|
||||
if (callExpIden && callExpIden.getSymbol().getName() === USE_INTL_NAME) {
|
||||
intVariableDeclaration = node as Identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (intVariableDeclaration) {
|
||||
intVariableDeclaration.findReferencesAsNodes().forEach((fr) => {
|
||||
if (fr instanceof Node) {
|
||||
const parent = fr.getParentIfKind(SyntaxKind.PropertyAccessExpression);
|
||||
if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) {
|
||||
const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList);
|
||||
if (syntaxList) {
|
||||
const id = syntaxList.getChildren()[0];
|
||||
if (id && id.getKind() !== SyntaxKind.StringLiteral) {
|
||||
problemFiles.push(fr.getSourceFile().getFilePath());
|
||||
}
|
||||
if (id) {
|
||||
usedTranslations.push(trimQuotes(id.getText()));
|
||||
if (parent.getName() === 'getPlural') {
|
||||
usedPluralTranslations.push(trimQuotes(id.getText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getFileUsedTranslations = (file: SourceFile) => {
|
||||
const namedImport = file.getImportDeclarations().find((id) => !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME));
|
||||
if (namedImport) {
|
||||
const identifier = namedImport.getImportClause().getNamedImports().find((iden) => iden.getName() === TRANSLATOR_CLASS_NAME);
|
||||
const translateReferences = identifier.getNodeProperty('name').findReferencesAsNodes();
|
||||
if (translateReferences.length > 0) {
|
||||
translateReferences.forEach((identifierNode) => {
|
||||
if (identifierNode.getParentIfKind(SyntaxKind.TypeReference)) {
|
||||
const translatorVariable = identifierNode.getParent().getPreviousSibling().getPreviousSiblingIfKind(SyntaxKind.Identifier);
|
||||
if (translatorVariable) {
|
||||
translatorVariable.findReferencesAsNodes().forEach((node) => {
|
||||
const parent = node.getParentIfKind(SyntaxKind.PropertyAccessExpression);
|
||||
if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) {
|
||||
|
||||
const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList);
|
||||
if (syntaxList) {
|
||||
const id = syntaxList.getChildren()[0];
|
||||
if (id && id.getKind() !== SyntaxKind.StringLiteral) {
|
||||
problemFiles.push(parent.getSourceFile().getFilePath());
|
||||
}
|
||||
if (id) {
|
||||
usedTranslations.push(trimQuotes(id.getText()));
|
||||
if (parent.getName() === 'getPlural') {
|
||||
usedPluralTranslations.push(trimQuotes(id.getText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
sourceFilesWithIntl.forEach((file) => {
|
||||
getFileUsedIntl(file.getStatements());
|
||||
})
|
||||
|
||||
const sourceFilesWithTranslator = project.getSourceFiles().filter((sf) => {
|
||||
return !!sf.getImportDeclarations().find((id) => {
|
||||
return !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME)
|
||||
})
|
||||
});
|
||||
sourceFilesWithTranslator.forEach((file) => {
|
||||
getFileUsedTranslations(file);
|
||||
})
|
||||
const filteredUsedTranslations = Array.from(new Set(usedTranslations));
|
||||
const filteredUsedPluralTranslations = Array.from(new Set(usedPluralTranslations));
|
||||
|
||||
if (problemFiles.length) {
|
||||
console.warn(`\n============== Files where translation id provided not as string ==============\n`);
|
||||
console.log(problemFiles.join('\n'));
|
||||
process.exit(255);
|
||||
}
|
||||
|
||||
const allFiles = fs.readdirSync(LOCALE_FOLDER_PATH);
|
||||
// Use ru or needed language
|
||||
const translationFile = allFiles.find((file) => file.includes(`${lang}.json`));
|
||||
|
||||
if (!translationFile) {
|
||||
console.error('File not found');
|
||||
process.exit(255);
|
||||
}
|
||||
|
||||
const translationsObject = JSON.parse(fs.readFileSync(`./src/lib/intl/__locales/${translationFile}`, { flag: 'r+' }) as unknown as string);
|
||||
const translations = {
|
||||
locale: translationFile,
|
||||
messages: Object.keys(translationsObject),
|
||||
};
|
||||
|
||||
const someMessagesNotFound: string[] = [];
|
||||
const notUsed: string[] = [];
|
||||
const notFound: string[] = [];
|
||||
const checkLocaleMessages = (locale: string, messages: string[]) => {
|
||||
filteredUsedTranslations.forEach(f => {
|
||||
if (!messages.includes(f)) {
|
||||
notFound.push(f);
|
||||
}
|
||||
});
|
||||
messages.forEach(t => {
|
||||
if (!filteredUsedTranslations.includes(t)) {
|
||||
notUsed.push(t);
|
||||
}
|
||||
});
|
||||
if (notFound.length > 0) {
|
||||
someMessagesNotFound.push(locale);
|
||||
}
|
||||
}
|
||||
|
||||
const render = (data: string[], title: string) => {
|
||||
console.log(`============ ${title} ============`);
|
||||
console.table(data);
|
||||
console.log(`============ ${title} ============`);
|
||||
}
|
||||
|
||||
checkLocaleMessages(translations.locale, translations.messages);
|
||||
|
||||
const checkPluralForm = () => {
|
||||
const pluralFormWrong: string[] = [];
|
||||
filteredUsedPluralTranslations.forEach((id) => {
|
||||
const message = translationsObject[id];
|
||||
if (!checkForms(message, lang as AvailableLocales, id)) {
|
||||
pluralFormWrong.push(id)
|
||||
}
|
||||
});
|
||||
return pluralFormWrong;
|
||||
}
|
||||
|
||||
const plural = checkPluralForm();
|
||||
if (!option && (someMessagesNotFound.length || plural.length > 0 )) {
|
||||
someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n`));
|
||||
plural.forEach(id => console.error(`\nTranslation with id: "${id}" - have wrong number of plural forms!\n`));
|
||||
process.exit(255);
|
||||
}
|
||||
if (option) {
|
||||
switch (option) {
|
||||
case '--show-missing': {
|
||||
render(notFound, 'NotFound')
|
||||
break;
|
||||
}
|
||||
case '--show-unused': {
|
||||
render(notUsed, 'notUsed')
|
||||
break;
|
||||
}
|
||||
case '--check-plurals': {
|
||||
render(plural, 'Wrong Plural Form')
|
||||
}
|
||||
default: {
|
||||
if (someMessagesNotFound.length) {
|
||||
someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n\n`));
|
||||
process.exit(255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
client2/scripts/lint/common.js
Normal file
79
client2/scripts/lint/common.js
Normal file
@@ -0,0 +1,79 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
extraFileExtensions: ['mjs', 'tsx', 'ts'],
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['react', '@typescript-eslint', 'import'],
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
es2020: true,
|
||||
jest: true,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
pragma: 'React',
|
||||
version: 'detect',
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true
|
||||
}
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||
'@typescript-eslint/explicit-function-return-type': [0, { allowExpressions: true }],
|
||||
'@typescript-eslint/indent': ['error', 4],
|
||||
'@typescript-eslint/interface-name-prefix': [0, { prefixWithI: 'never' }],
|
||||
'@typescript-eslint/no-explicit-any': [0],
|
||||
'@typescript-eslint/naming-convention': [2, {
|
||||
selector: 'enum', format: ['UPPER_CASE', 'PascalCase'],
|
||||
}],
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
'arrow-body-style': 'off',
|
||||
'consistent-return': 0,
|
||||
curly: [2, 'all'],
|
||||
'default-case': 0,
|
||||
'import/no-cycle': 0,
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/no-named-as-default': 0,
|
||||
indent: [0, 4],
|
||||
'no-alert': 2,
|
||||
'no-console': 2,
|
||||
'no-debugger': 2,
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'object-curly-newline': 'off',
|
||||
'react-hooks/exhaustive-deps': 0,
|
||||
'react/display-name': 0,
|
||||
'react/jsx-indent-props': ['error', 4],
|
||||
'react/jsx-indent': ['error', 4],
|
||||
'react/jsx-one-expression-per-line': 'off',
|
||||
'react/jsx-props-no-spreading': 0,
|
||||
'react/prop-types': 'off',
|
||||
'react/state-in-constructor': 'off',
|
||||
},
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
'airbnb-typescript/base',
|
||||
'airbnb/hooks',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:import/typescript',
|
||||
],
|
||||
globals: {},
|
||||
};
|
||||
10
client2/scripts/lint/dev.js
Normal file
10
client2/scripts/lint/dev.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'no-alert': 0,
|
||||
'no-debugger': 0,
|
||||
'no-console': 0,
|
||||
},
|
||||
extends: [
|
||||
'./common',
|
||||
],
|
||||
};
|
||||
5
client2/scripts/lint/prod.js
Normal file
5
client2/scripts/lint/prod.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'./common.js',
|
||||
],
|
||||
};
|
||||
40
client2/scripts/webpack/helpers.js
Normal file
40
client2/scripts/webpack/helpers.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const yaml = require('yaml');
|
||||
const fs = require('fs');
|
||||
|
||||
const ZERO_HOST = '0.0.0.0';
|
||||
const LOCALHOST = '127.0.0.1';
|
||||
const DEFAULT_PORT = 80;
|
||||
|
||||
const importConfig = () => {
|
||||
try {
|
||||
const doc = yaml.parse(fs.readFileSync('../AdguardHome.yaml', 'utf8'));
|
||||
const { bind_host, bind_port } = doc;
|
||||
return {
|
||||
bind_host,
|
||||
bind_port,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
bind_host: ZERO_HOST,
|
||||
bind_port: DEFAULT_PORT,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getDevServerConfig = () => {
|
||||
const { bind_host: host, bind_port: port } = importConfig();
|
||||
const { DEV_SERVER_PORT } = process.env;
|
||||
|
||||
const devServerHost = host === ZERO_HOST ? LOCALHOST : host;
|
||||
const devServerPort = 3000 || port + 8000;
|
||||
|
||||
return {
|
||||
host: devServerHost,
|
||||
port: devServerPort
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
importConfig,
|
||||
getDevServerConfig
|
||||
};
|
||||
74
client2/scripts/webpack/webpack.config.base.js
Normal file
74
client2/scripts/webpack/webpack.config.base.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const path = require('path');
|
||||
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const tsconfig = require('../../tsconfig.json');
|
||||
|
||||
const RESOURCES_PATH = path.resolve(__dirname, '../../');
|
||||
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
|
||||
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
install: './src/Install.tsx',
|
||||
main: './src/App.tsx'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', '.pcss'],
|
||||
alias: Object.keys(tsconfig.compilerOptions.paths).reduce((aliases, key) => {
|
||||
// Reduce to load aliases from ./tsconfig.json in appropriate for webpack form
|
||||
const paths = tsconfig.compilerOptions.paths[key].map(p => p.replace('/*', ''));
|
||||
aliases[key.replace('/*', '')] = path.resolve(
|
||||
__dirname,
|
||||
'../../',
|
||||
tsconfig.compilerOptions.baseUrl,
|
||||
...paths,
|
||||
);
|
||||
return aliases;
|
||||
}, {}),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2)$/,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options:{
|
||||
outputPath:'./',
|
||||
}
|
||||
}],
|
||||
},
|
||||
{
|
||||
test:/\.(png|jpe?g|gif)$/,
|
||||
exclude: /(node_modules)/,
|
||||
use:[{
|
||||
loader:'file-loader',
|
||||
options:{
|
||||
outputPath:'./images',
|
||||
}
|
||||
}]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// new AntdDayjsWebpackPlugin()
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
cache: false,
|
||||
chunks: ['main'],
|
||||
template: HTML_PATH,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
cache: false,
|
||||
chunks: ['install'],
|
||||
filename: 'install.html',
|
||||
template: HTML_INSTALL_PATH,
|
||||
}),
|
||||
],
|
||||
};
|
||||
113
client2/scripts/webpack/webpack.config.dev.js
Normal file
113
client2/scripts/webpack/webpack.config.dev.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const history = require('connect-history-api-fallback');
|
||||
const { merge } = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const proxy = require('http-proxy-middleware');
|
||||
const Webpack = require('webpack');
|
||||
|
||||
const { getDevServerConfig } = require('./helpers');
|
||||
const baseConfig = require('./webpack.config.base');
|
||||
|
||||
const target = getDevServerConfig();
|
||||
|
||||
const options = {
|
||||
target: `http://${target.host}:${target.port}`, // target host
|
||||
changeOrigin: true, // needed for virtual hosted sites
|
||||
};
|
||||
const apiProxy = proxy.createProxyMiddleware(options);
|
||||
|
||||
module.exports = merge(baseConfig, {
|
||||
mode: 'development',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../../build2'),
|
||||
filename: '[name].bundle.js',
|
||||
},
|
||||
optimization: {
|
||||
noEmitOnErrors: true,
|
||||
},
|
||||
devServer: {
|
||||
port: 4000,
|
||||
historyApiFallback: true,
|
||||
before: (app) => {
|
||||
app.use('/control', apiProxy);
|
||||
app.use(history({
|
||||
rewrites: [
|
||||
{
|
||||
from: /\.(png|jpe?g|gif)$/,
|
||||
to: (context) => {
|
||||
const name = context.parsedUrl.pathname.split('/');
|
||||
return `/images/${name[name.length - 1]}`
|
||||
}
|
||||
}, {
|
||||
from: /\.(woff|woff2)$/,
|
||||
to: (context) => {
|
||||
const name = context.parsedUrl.pathname.split('/');
|
||||
return `/${name[name.length - 1]}`
|
||||
}
|
||||
}, {
|
||||
from: /\.(js|css)$/,
|
||||
to: (context) => {
|
||||
const name = context.parsedUrl.pathname.split('/');
|
||||
return `/${name[name.length - 1]}`
|
||||
}
|
||||
}
|
||||
],
|
||||
}));
|
||||
}
|
||||
},
|
||||
devtool: 'eval-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: 'pre',
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
configFile: path.resolve(__dirname, '../lint/dev.js'),
|
||||
}
|
||||
},
|
||||
{
|
||||
test: (resource) => {
|
||||
return (
|
||||
resource.indexOf('.pcss')+1
|
||||
|| resource.indexOf('.css')+1
|
||||
|| resource.indexOf('.less')+1
|
||||
) && !(resource.indexOf('.module.')+1);
|
||||
},
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader', {
|
||||
loader: 'less-loader',
|
||||
options: {
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
test: /\.module\.p?css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
sourceMap: true,
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: "[name]__[local]___[hash:base64:5]",
|
||||
}
|
||||
},
|
||||
},
|
||||
'postcss-loader',
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new Webpack.DefinePlugin({
|
||||
DEV: true,
|
||||
'process.env.DEV_SERVER_PORT': JSON.stringify(3000),
|
||||
}),
|
||||
new Webpack.HotModuleReplacementPlugin(),
|
||||
new Webpack.ProgressPlugin(),
|
||||
],
|
||||
});
|
||||
91
client2/scripts/webpack/webpack.config.prod.js
Normal file
91
client2/scripts/webpack/webpack.config.prod.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const path = require('path');
|
||||
const { merge } = require('webpack-merge');
|
||||
const baseConfig = require('./webpack.config.base');
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
const Webpack = require('webpack');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = merge(baseConfig, {
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../../../build2/static'),
|
||||
filename: '[name].bundle.[hash:5].js',
|
||||
publicPath: '/'
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [new TerserJSPlugin({terserOptions: {
|
||||
output: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
}), new OptimizeCSSAssetsPlugin({})],
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
styles: {
|
||||
name: 'styles',
|
||||
test: /\.css$/,
|
||||
chunks: 'all',
|
||||
enforce: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: (resource) => {
|
||||
return (
|
||||
resource.indexOf('.pcss')+1
|
||||
|| resource.indexOf('.css')+1
|
||||
|| resource.indexOf('.less')+1
|
||||
) && !(resource.indexOf('.module.')+1);
|
||||
},
|
||||
use: [{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
esModules: true,
|
||||
}
|
||||
}, 'css-loader', 'postcss-loader', {
|
||||
loader: 'less-loader',
|
||||
options: {
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
}],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.module\.p?css$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader,
|
||||
options: {
|
||||
esModules: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
sourceMap: true,
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
'postcss-loader',
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new Webpack.DefinePlugin({
|
||||
DEV: false,
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[hash:5].css',
|
||||
}),
|
||||
]
|
||||
});
|
||||
18
client2/src/App.tsx
Normal file
18
client2/src/App.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import './main.pcss';
|
||||
import './lib/ant/ant.less';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Store, { storeValue } from 'Store';
|
||||
import './lib/ant';
|
||||
|
||||
import App from './components/App';
|
||||
|
||||
const Container = () => {
|
||||
return (
|
||||
<Store.Provider value={storeValue}>
|
||||
<App/>
|
||||
</Store.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Container />, document.getElementById('app'));
|
||||
18
client2/src/Install.tsx
Normal file
18
client2/src/Install.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import './main.pcss';
|
||||
import './lib/ant/ant.less';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Store, { storeValue } from 'Store/installStore';
|
||||
import './lib/ant';
|
||||
|
||||
import Install from './components/Install';
|
||||
|
||||
const Container = () => {
|
||||
return (
|
||||
<Store.Provider value={storeValue}>
|
||||
<Install/>
|
||||
</Store.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Container />, document.getElementById('app'));
|
||||
BIN
client2/src/assets/img/install.png
Normal file
BIN
client2/src/assets/img/install.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
16
client2/src/components/App.tsx
Normal file
16
client2/src/components/App.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { FC, useContext } from 'react';
|
||||
import Store from 'Store';
|
||||
import Icons from 'Lib/theme/Icons';
|
||||
|
||||
const App: FC = observer(() => {
|
||||
const store = useContext(Store);
|
||||
return (
|
||||
<div>
|
||||
{store.ui.currentLang}
|
||||
<Icons/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default App;
|
||||
121
client2/src/components/Install/Install.tsx
Normal file
121
client2/src/components/Install/Install.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Layout } from 'antd';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { IInitialConfigurationBeta } from 'Entities/InitialConfigurationBeta';
|
||||
import Icons from 'Lib/theme/Icons';
|
||||
import {
|
||||
DEFAULT_DNS_ADDRESS,
|
||||
DEFAULT_DNS_PORT,
|
||||
DEFAULT_IP_ADDRESS,
|
||||
DEFAULT_IP_PORT,
|
||||
} from 'Consts/install';
|
||||
import { notifyError } from 'Common/ui';
|
||||
import InstallStore from 'Store/stores/Install';
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
import AdminInterface from './components/AdminInterface';
|
||||
import Auth from './components/Auth';
|
||||
import DnsServer from './components/DnsServer';
|
||||
import Stepper from './components/Stepper';
|
||||
import Welcome from './components/Welcome';
|
||||
import ConfigureDevices from './components/ConfigureDevices';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
export type FormValues = IInitialConfigurationBeta & { step: number };
|
||||
|
||||
const InstallForm: FC = observer(() => {
|
||||
const initialValues: FormValues = {
|
||||
step: 0,
|
||||
web: {
|
||||
ip: [DEFAULT_IP_ADDRESS],
|
||||
port: DEFAULT_IP_PORT,
|
||||
},
|
||||
dns: {
|
||||
ip: [DEFAULT_DNS_ADDRESS],
|
||||
port: DEFAULT_DNS_PORT,
|
||||
},
|
||||
password: '',
|
||||
username: '',
|
||||
};
|
||||
|
||||
const onNext = async (values: FormValues, { setFieldValue }: FormikHelpers<FormValues>) => {
|
||||
const currentStep = values.step;
|
||||
const checker = (condition: boolean, message: string) => {
|
||||
if (condition) {
|
||||
setFieldValue('step', currentStep + 1);
|
||||
} else {
|
||||
notifyError(message);
|
||||
}
|
||||
};
|
||||
switch (currentStep) {
|
||||
case 1: {
|
||||
// web
|
||||
const check = await InstallStore.checkConfig(values);
|
||||
checker(check?.web?.status === '', check?.web?.status || '');
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// dns
|
||||
const check = await InstallStore.checkConfig(values);
|
||||
checker(check?.dns?.status === '', check?.dns?.status || '');
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
// configure
|
||||
const config = await InstallStore.configure(values);
|
||||
if (config) {
|
||||
const { web } = values;
|
||||
window.location.href = `http://${web.ip[0]}:${web.port}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
setFieldValue('step', currentStep + 1);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={onNext}
|
||||
>
|
||||
{({ values, handleSubmit, setFieldValue }) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<Stepper currentStep={values.step} />
|
||||
{values.step === 0 && (
|
||||
<Welcome onNext={() => setFieldValue('step', 1)}/>
|
||||
)}
|
||||
{values.step === 1 && (
|
||||
<AdminInterface values={values} setFieldValue={setFieldValue} />
|
||||
)}
|
||||
{values.step === 2 && (
|
||||
<Auth values={values} setFieldValue={setFieldValue} />
|
||||
)}
|
||||
{values.step === 3 && (
|
||||
<DnsServer values={values} setFieldValue={setFieldValue} />
|
||||
)}
|
||||
{values.step === 4 && (
|
||||
<ConfigureDevices values={values} setFieldValue={setFieldValue} />
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
|
||||
const Install: FC = () => {
|
||||
return (
|
||||
<Layout className={theme.install.layout}>
|
||||
<Content className={theme.install.container}>
|
||||
<InstallForm />
|
||||
</Content>
|
||||
<Icons/>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Install;
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FormikHelpers } from 'formik';
|
||||
|
||||
import { Input, Radio, Switch } from 'Common/controls';
|
||||
import { DEFAULT_IP_ADDRESS } from 'Consts/install';
|
||||
import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers';
|
||||
import theme from 'Lib/theme';
|
||||
import Store from 'Store/installStore';
|
||||
|
||||
import { FormValues } from '../../Install';
|
||||
import StepButtons from '../StepButtons';
|
||||
|
||||
enum NETWORK_OPTIONS {
|
||||
ALL = 'all',
|
||||
CUSTOM = 'custom',
|
||||
}
|
||||
|
||||
interface AdminInterfaceProps {
|
||||
values: FormValues;
|
||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
||||
}
|
||||
|
||||
const AdminInterface: FC<AdminInterfaceProps> = observer(({
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => {
|
||||
const { ui: { intl }, install: { addresses } } = useContext(Store);
|
||||
const { web: { ip } } = values;
|
||||
const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS
|
||||
? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM;
|
||||
|
||||
const onSelectRadio = (v: string | number) => {
|
||||
const value = v === NETWORK_OPTIONS.ALL
|
||||
? [DEFAULT_IP_ADDRESS] : [];
|
||||
setFieldValue('web.ip', value);
|
||||
};
|
||||
|
||||
const getManualBlock = () => (
|
||||
<div className={theme.install.options}>
|
||||
{addresses?.interfaces.map((a) => {
|
||||
let name = '';
|
||||
const type = chechNetworkType(a.name);
|
||||
switch (type) {
|
||||
case NETWORK_TYPE.ETHERNET:
|
||||
name = `${intl.getMessage('ethernet')} (${a.name}) `;
|
||||
break;
|
||||
case NETWORK_TYPE.LOCAL:
|
||||
name = `${intl.getMessage('localhost')} (${a.name}) `;
|
||||
break;
|
||||
default:
|
||||
name = a.name || '';
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div key={a.name}>
|
||||
<div className={theme.install.name}>
|
||||
{name}
|
||||
</div>
|
||||
{a.ipAddresses?.map((addrIp) => (
|
||||
<div key={addrIp} className={theme.install.option}>
|
||||
<div className={theme.install.address}>
|
||||
http://{addrIp}
|
||||
</div>
|
||||
<Switch
|
||||
checked={values.web.ip.includes(addrIp)}
|
||||
onChange={() => {
|
||||
const temp = new Set(ip);
|
||||
if (temp.has(addrIp)) {
|
||||
temp.delete(addrIp);
|
||||
} else {
|
||||
temp.add(addrIp);
|
||||
}
|
||||
setFieldValue('web.ip', Array.from(temp.values()));
|
||||
}}/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={theme.install.title}>
|
||||
{intl.getMessage('install_admin_interface_title')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
||||
{intl.getMessage('install_admin_interface_title_decs')}
|
||||
</div>
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_admin_interface_where_interface')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_admin_interface_where_interface_desc')}
|
||||
</div>
|
||||
<Radio
|
||||
value={radioValue}
|
||||
onSelect={onSelectRadio}
|
||||
options={[
|
||||
{
|
||||
value: NETWORK_OPTIONS.ALL,
|
||||
label: intl.getMessage('install_all_networks'),
|
||||
desc: intl.getMessage('install_all_networks_description'),
|
||||
},
|
||||
{
|
||||
value: NETWORK_OPTIONS.CUSTOM,
|
||||
label: intl.getMessage('install_choose_networks'),
|
||||
desc: intl.getMessage('install_choose_networks_desc'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_admin_interface_port')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_admin_interface_port_desc')}
|
||||
</div>
|
||||
<Input
|
||||
label={`${intl.getMessage('port')}:`}
|
||||
placeholder={intl.getMessage('port')}
|
||||
type="number"
|
||||
name="webPort"
|
||||
value={values.web.port}
|
||||
onChange={(v) => {
|
||||
const port = v === '' ? '' : parseInt(v, 10);
|
||||
setFieldValue('web.port', port);
|
||||
}}
|
||||
/>
|
||||
<StepButtons
|
||||
setFieldValue={setFieldValue}
|
||||
currentStep={1}
|
||||
values={values}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default AdminInterface;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './AdminInterface';
|
||||
55
client2/src/components/Install/components/Auth/Auth.tsx
Normal file
55
client2/src/components/Install/components/Auth/Auth.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FormikHelpers } from 'formik';
|
||||
|
||||
import { Input } from 'Common/controls';
|
||||
import theme from 'Lib/theme';
|
||||
import Store from 'Store/installStore';
|
||||
|
||||
import StepButtons from '../StepButtons';
|
||||
import { FormValues } from '../../Install';
|
||||
|
||||
interface AuthProps {
|
||||
values: FormValues;
|
||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
||||
}
|
||||
|
||||
const Auth: FC<AuthProps> = observer(({
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => {
|
||||
const { ui: { intl } } = useContext(Store);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={theme.install.title}>
|
||||
{intl.getMessage('install_auth_title')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
||||
{intl.getMessage('install_auth_description')}
|
||||
</div>
|
||||
<Input
|
||||
placeholder={intl.getMessage('login')}
|
||||
type="username"
|
||||
name="username"
|
||||
value={values.username}
|
||||
onChange={(v) => setFieldValue('username', v)}
|
||||
/>
|
||||
<Input
|
||||
placeholder={intl.getMessage('password')}
|
||||
type="password"
|
||||
name="password"
|
||||
value={values.password}
|
||||
onChange={(v) => setFieldValue('password', v)}
|
||||
/>
|
||||
<StepButtons
|
||||
setFieldValue={setFieldValue}
|
||||
currentStep={2}
|
||||
values={values}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Auth;
|
||||
1
client2/src/components/Install/components/Auth/index.ts
Normal file
1
client2/src/components/Install/components/Auth/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Auth';
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Tabs, Grid } from 'antd';
|
||||
import cn from 'classnames';
|
||||
import { FormikHelpers } from 'formik';
|
||||
|
||||
import Store from 'Store/installStore';
|
||||
import theme from 'Lib/theme';
|
||||
import { danger, p } from 'Common/formating';
|
||||
import { DEFAULT_DNS_PORT, DEFAULT_IP_ADDRESS, DEFAULT_IP_PORT } from 'Consts/install';
|
||||
|
||||
import { FormValues } from '../../Install';
|
||||
import StepButtons from '../StepButtons';
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface ConfigureDevicesProps {
|
||||
values: FormValues;
|
||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
||||
}
|
||||
|
||||
const ConfigureDevices: FC<ConfigureDevicesProps> = ({
|
||||
values, setFieldValue,
|
||||
}) => {
|
||||
const { ui: { intl }, install: { addresses } } = useContext(Store);
|
||||
const screens = useBreakpoint();
|
||||
const tabsPosition = screens.md ? 'left' : 'top';
|
||||
|
||||
const dhcp = (e: string) => (
|
||||
<a
|
||||
href="https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={theme.link.link}
|
||||
>
|
||||
{e}
|
||||
</a>
|
||||
);
|
||||
|
||||
const allIps = addresses?.interfaces.reduce<string[]>((all, data) => {
|
||||
const { ipAddresses } = data;
|
||||
if (ipAddresses) {
|
||||
all.push(...ipAddresses);
|
||||
}
|
||||
return all;
|
||||
}, [] as string[]);
|
||||
|
||||
const { web: { ip: webIp }, dns: { ip: dnsIp } } = values;
|
||||
const selectedWebIps = webIp.length === 1 && webIp[0] === DEFAULT_IP_ADDRESS
|
||||
? allIps : webIp;
|
||||
const selectedDnsIps = dnsIp.length === 1 && dnsIp[0] === DEFAULT_IP_ADDRESS
|
||||
? allIps : dnsIp;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={theme.install.title}>
|
||||
{intl.getMessage('install_configure_title')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
||||
{intl.getMessage('install_configure_danger_notice', { danger })}
|
||||
</div>
|
||||
|
||||
<Tabs defaultActiveKey="1" tabPosition={tabsPosition} className={theme.install.tabs}>
|
||||
<TabPane tab={intl.getMessage('router')} key="1">
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_configure_how_to_title', { value: intl.getMessage('router') })}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_router', { p })}
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="Windows" key="2">
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_configure_how_to_title', { value: 'Windows' })}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_windows', { p })}
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="macOS" key="3">
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_configure_how_to_title', { value: 'macOS' })}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_macos', { p })}
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="Linux" key="4">
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_configure_how_to_title', { value: 'Linux' })}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{/* TODO: add linux setup */}
|
||||
{intl.getMessage('install_configure_router', { p })}
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="Android" key="5">
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_configure_how_to_title', { value: 'Android' })}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_android', { p })}
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="iOS" key="6">
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_configure_how_to_title', { value: 'iOS' })}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_ios', { p })}
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_configure_adresses')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_admin_interface_title')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{selectedWebIps?.map((ip) => (
|
||||
<div key={ip} className={theme.install.ip}>
|
||||
{ip}{values.web.port !== DEFAULT_IP_PORT && `:${values.web.port}`}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_dns_server_title')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{selectedDnsIps?.map((ip) => (
|
||||
<div key={ip} className={theme.install.ip}>
|
||||
{ip}{values.dns.port !== DEFAULT_DNS_PORT && `:${values.dns.port}`}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_configure_dhcp', { dhcp })}
|
||||
</div>
|
||||
<StepButtons
|
||||
setFieldValue={setFieldValue}
|
||||
currentStep={4}
|
||||
values={values}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigureDevices;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ConfigureDevices';
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FormikHelpers } from 'formik';
|
||||
|
||||
import { Input, Radio, Switch } from 'Common/controls';
|
||||
import { DEFAULT_IP_ADDRESS } from 'Consts/install';
|
||||
import { chechNetworkType, NETWORK_TYPE } from 'Helpers/installHelpers';
|
||||
import theme from 'Lib/theme';
|
||||
import Store from 'Store/installStore';
|
||||
|
||||
import { FormValues } from '../../Install';
|
||||
import StepButtons from '../StepButtons';
|
||||
|
||||
enum NETWORK_OPTIONS {
|
||||
ALL = 'all',
|
||||
CUSTOM = 'custom',
|
||||
}
|
||||
|
||||
interface DnsServerProps {
|
||||
values: FormValues;
|
||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
||||
}
|
||||
|
||||
const DnsServer: FC<DnsServerProps> = observer(({
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => {
|
||||
const { ui: { intl }, install: { addresses } } = useContext(Store);
|
||||
const { dns: { ip } } = values;
|
||||
const radioValue = ip.length === 1 && ip[0] === DEFAULT_IP_ADDRESS
|
||||
? NETWORK_OPTIONS.ALL : NETWORK_OPTIONS.CUSTOM;
|
||||
|
||||
const onSelectRadio = (v: string | number) => {
|
||||
const value = v === NETWORK_OPTIONS.ALL
|
||||
? [DEFAULT_IP_ADDRESS] : [];
|
||||
setFieldValue('dns.ip', value);
|
||||
};
|
||||
|
||||
const getManualBlock = () => (
|
||||
<div className={theme.install.options}>
|
||||
{addresses?.interfaces.map((a) => {
|
||||
let name = '';
|
||||
const type = chechNetworkType(a.name);
|
||||
switch (type) {
|
||||
case NETWORK_TYPE.ETHERNET:
|
||||
name = `${intl.getMessage('ethernet')} (${a.name}) `;
|
||||
break;
|
||||
case NETWORK_TYPE.LOCAL:
|
||||
name = `${intl.getMessage('localhost')} (${a.name}) `;
|
||||
break;
|
||||
default:
|
||||
name = a.name || '';
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div key={a.name}>
|
||||
<div className={theme.install.name}>
|
||||
{name}
|
||||
</div>
|
||||
{a.ipAddresses?.map((addrIp) => (
|
||||
<div key={addrIp} className={theme.install.option}>
|
||||
<div className={theme.install.address}>
|
||||
{addrIp}
|
||||
</div>
|
||||
<Switch
|
||||
checked={values.dns.ip.includes(addrIp)}
|
||||
onChange={() => {
|
||||
const temp = new Set(ip);
|
||||
if (temp.has(addrIp)) {
|
||||
temp.delete(addrIp);
|
||||
} else {
|
||||
temp.add(addrIp);
|
||||
}
|
||||
setFieldValue('dns.ip', Array.from(temp.values()));
|
||||
}}/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={theme.install.title}>
|
||||
{intl.getMessage('install_dns_server_title')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_block)}>
|
||||
{intl.getMessage('install_dns_server_desc')}
|
||||
</div>
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_dns_server_network_interfaces')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_dns_server_network_interfaces_desc')}
|
||||
</div>
|
||||
<Radio
|
||||
value={radioValue}
|
||||
onSelect={onSelectRadio}
|
||||
options={[
|
||||
{
|
||||
value: NETWORK_OPTIONS.ALL,
|
||||
label: intl.getMessage('install_all_networks'),
|
||||
desc: intl.getMessage('install_all_networks_description'),
|
||||
},
|
||||
{
|
||||
value: NETWORK_OPTIONS.CUSTOM,
|
||||
label: intl.getMessage('install_choose_networks'),
|
||||
desc: intl.getMessage('install_choose_networks_desc'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{ radioValue !== NETWORK_OPTIONS.ALL && getManualBlock()}
|
||||
<div className={theme.install.subtitle}>
|
||||
{intl.getMessage('install_dns_server_port')}
|
||||
</div>
|
||||
<div className={cn(theme.install.text, theme.install.text_base)}>
|
||||
{intl.getMessage('install_dns_server_port_desc')}
|
||||
</div>
|
||||
<Input
|
||||
label={`${intl.getMessage('port')}:`}
|
||||
placeholder={intl.getMessage('port')}
|
||||
type="number"
|
||||
name="dnsPort"
|
||||
value={values.dns.port}
|
||||
onChange={(v) => {
|
||||
const port = v === '' ? '' : parseInt(v, 10);
|
||||
setFieldValue('dns.port', port);
|
||||
}}
|
||||
/>
|
||||
<StepButtons
|
||||
setFieldValue={setFieldValue}
|
||||
currentStep={3}
|
||||
values={values}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default DnsServer;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DnsServer';
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FormikHelpers } from 'formik';
|
||||
|
||||
import Store from 'Store/installStore';
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
import { FormValues } from '../../Install';
|
||||
|
||||
interface StepButtonsProps {
|
||||
setFieldValue: FormikHelpers<FormValues>['setFieldValue'];
|
||||
currentStep: number;
|
||||
values: FormValues;
|
||||
}
|
||||
|
||||
const StepButtons: FC<StepButtonsProps> = observer(({
|
||||
setFieldValue,
|
||||
currentStep,
|
||||
}) => {
|
||||
const { ui: { intl } } = useContext(Store);
|
||||
return (
|
||||
<div className={theme.install.actions}>
|
||||
<Button
|
||||
size="large"
|
||||
type="ghost"
|
||||
className={theme.install.button}
|
||||
onClick={() => setFieldValue('step', currentStep - 1)}
|
||||
>
|
||||
{intl.getMessage('back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className={theme.install.button}
|
||||
>
|
||||
{intl.getMessage('next')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default StepButtons;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './StepButtons';
|
||||
@@ -0,0 +1,66 @@
|
||||
.stepper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 16px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
@media (--m-viewport) {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 16px;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 7px;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--gray400);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
flex: 0;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.current .circle {
|
||||
transform: scale(2);
|
||||
background-color: var(--green400);
|
||||
border-color: var(--green400);
|
||||
}
|
||||
|
||||
&.active .circle {
|
||||
background-color: var(--green400);
|
||||
border-color: var(--green400);
|
||||
}
|
||||
|
||||
&.current:before,
|
||||
&.active:before {
|
||||
background-color: var(--green400);
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--white);
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--gray400);
|
||||
transition: var(--transition) transform, var(--transition) background, var(--transition) border;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
import s from './Stepper.module.pcss';
|
||||
|
||||
interface StepProps {
|
||||
active: boolean;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
const Step: FC<StepProps> = ({ active, current }) => {
|
||||
return (
|
||||
<div className={cn(s.wrap, { [s.active]: active, [s.current]: current })}>
|
||||
<div className={s.circle} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface StepperProps {
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
const Stepper: FC<StepperProps> = ({ currentStep }) => {
|
||||
return (
|
||||
<div className={s.stepper}>
|
||||
<Step current={currentStep === 0} active={currentStep >= 0} />
|
||||
<Step current={currentStep === 1} active={currentStep >= 1} />
|
||||
<Step current={currentStep === 2} active={currentStep >= 2} />
|
||||
<Step current={currentStep === 3} active={currentStep >= 3} />
|
||||
<Step current={currentStep === 4} active={currentStep >= 4} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stepper;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Stepper';
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import Store from 'Store/installStore';
|
||||
import Icon from 'Common/ui/Icon';
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
interface WelcomeProps {
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const Welcome: FC<WelcomeProps> = observer(({ onNext }) => {
|
||||
const { ui: { intl } } = useContext(Store);
|
||||
return (
|
||||
<>
|
||||
<Icon icon="logo" className={theme.install.logo} />
|
||||
<div className={theme.install.title}>
|
||||
{intl.getMessage('install_wellcome_title')}
|
||||
</div>
|
||||
<div className={theme.install.text}>
|
||||
{intl.getMessage('install_wellcome_desc')}
|
||||
</div>
|
||||
<div className={theme.install.actions}>
|
||||
<Button
|
||||
size="large"
|
||||
type="primary"
|
||||
className={theme.install.button}
|
||||
onClick={onNext}
|
||||
>
|
||||
{intl.getMessage('install_wellcome_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Welcome;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Welcome';
|
||||
1
client2/src/components/Install/index.ts
Normal file
1
client2/src/components/Install/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Install';
|
||||
146
client2/src/components/common/controls/Input/Input.tsx
Normal file
146
client2/src/components/common/controls/Input/Input.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { FC, FocusEvent, KeyboardEvent, ClipboardEvent, ChangeEvent, useState } from 'react';
|
||||
import { Input as InputControl } from 'antd';
|
||||
import { InputProps as InputControlProps } from 'antd/lib/input';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { Icon } from 'Common/ui';
|
||||
import theme from 'Lib/theme';
|
||||
|
||||
interface AdminInterfaceProps {
|
||||
autoComplete?: InputControlProps['autoComplete'];
|
||||
autoFocus?: InputControlProps['autoFocus'];
|
||||
className?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
id?: string;
|
||||
inputMode?: InputControlProps['inputMode'];
|
||||
label?: string;
|
||||
wrapperClassName?: string;
|
||||
name: string;
|
||||
onBlur?: (e: FocusEvent<HTMLInputElement>) => void;
|
||||
onChange?: (data: string, e?: ChangeEvent<HTMLInputElement>) => void;
|
||||
onFocus?: (e: FocusEvent<HTMLInputElement>) => void;
|
||||
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onPaste?: (e: ClipboardEvent<HTMLInputElement>) => void;
|
||||
pattern?: InputControlProps['pattern'];
|
||||
placeholder: string;
|
||||
prefix?: InputControlProps['prefix'];
|
||||
size?: InputControlProps['size'];
|
||||
suffix?: InputControlProps['suffix'];
|
||||
type: InputControlProps['type'];
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
const InputComponent: FC<AdminInterfaceProps> = ({
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
className,
|
||||
description,
|
||||
disabled,
|
||||
error,
|
||||
id,
|
||||
inputMode,
|
||||
label,
|
||||
wrapperClassName,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
onKeyDown,
|
||||
onPaste,
|
||||
pattern,
|
||||
placeholder,
|
||||
prefix,
|
||||
size = 'middle',
|
||||
suffix,
|
||||
type,
|
||||
value,
|
||||
}) => {
|
||||
const [inputType, setInputType] = useState(type);
|
||||
|
||||
const inputClass = cn(
|
||||
'input',
|
||||
{ input_big: size === 'large' },
|
||||
{ input_medium: size === 'middle' },
|
||||
{ input_small: size === 'small' },
|
||||
className,
|
||||
);
|
||||
|
||||
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
|
||||
if (onBlur) {
|
||||
onBlur(e);
|
||||
}
|
||||
};
|
||||
|
||||
const showPassword = () => {
|
||||
if (inputType === 'password') {
|
||||
setInputType('text');
|
||||
} else {
|
||||
setInputType('password');
|
||||
}
|
||||
};
|
||||
|
||||
const showPasswordIcon = () => {
|
||||
const icon = inputType === 'password' ? 'visibility_disable' : 'visibility_enable';
|
||||
return (
|
||||
<Icon
|
||||
icon={icon}
|
||||
className={theme.form.reveal}
|
||||
onClick={showPassword}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const validSuffix = (
|
||||
<>
|
||||
{!!suffix && suffix}
|
||||
{(type === 'password') && showPasswordIcon()}
|
||||
</>
|
||||
);
|
||||
|
||||
let descriptionView = null;
|
||||
if (description) {
|
||||
descriptionView = (
|
||||
<div className={theme.form.label}>
|
||||
{description}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label htmlFor={id || name} className={cn(theme.form.group, wrapperClassName)}>
|
||||
{label && (
|
||||
<div className={theme.form.label}>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
<InputControl
|
||||
autoComplete={autoComplete}
|
||||
autoFocus={autoFocus}
|
||||
className={inputClass}
|
||||
disabled={disabled}
|
||||
formNoValidate
|
||||
id={id || name}
|
||||
inputMode={inputMode}
|
||||
name={name}
|
||||
onBlur={handleBlur}
|
||||
onChange={(e) => onChange && onChange(e.target.value ? e.target.value : '', e)}
|
||||
onFocus={onFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
onPaste={onPaste}
|
||||
pattern={pattern}
|
||||
placeholder={placeholder}
|
||||
prefix={prefix}
|
||||
size="large"
|
||||
suffix={validSuffix}
|
||||
type={inputType}
|
||||
value={value}
|
||||
data-error={error}
|
||||
/>
|
||||
{descriptionView}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputComponent;
|
||||
1
client2/src/components/common/controls/Input/index.ts
Normal file
1
client2/src/components/common/controls/Input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Input } from './Input';
|
||||
@@ -0,0 +1,20 @@
|
||||
.group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--gray400);
|
||||
}
|
||||
55
client2/src/components/common/controls/Radio/Radio.tsx
Normal file
55
client2/src/components/common/controls/Radio/Radio.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Radio } from 'antd';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import s from './Radio.module.pcss';
|
||||
|
||||
const { Group } = Radio;
|
||||
|
||||
interface AdminInterfaceProps {
|
||||
options: {
|
||||
label: string;
|
||||
desc?: string;
|
||||
value: string | number;
|
||||
}[];
|
||||
onSelect: (value: string | number) => void;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
const RadioComponent: FC<AdminInterfaceProps> = observer(({
|
||||
options, onSelect, value,
|
||||
}) => {
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onSelect(e.target.value);
|
||||
}}
|
||||
className={s.group}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<Radio
|
||||
key={o.value}
|
||||
value={o.value}
|
||||
className={s.radio}
|
||||
>
|
||||
<div>
|
||||
{o.label}
|
||||
</div>
|
||||
{o.desc && (
|
||||
<div className={s.desc}>
|
||||
{o.desc}
|
||||
</div>
|
||||
)}
|
||||
</Radio>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
export default RadioComponent;
|
||||
1
client2/src/components/common/controls/Radio/index.ts
Normal file
1
client2/src/components/common/controls/Radio/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Radio';
|
||||
3
client2/src/components/common/controls/Switch/Switch.tsx
Normal file
3
client2/src/components/common/controls/Switch/Switch.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Switch as SwitchE } from 'antd';
|
||||
|
||||
export default SwitchE;
|
||||
1
client2/src/components/common/controls/Switch/index.ts
Normal file
1
client2/src/components/common/controls/Switch/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Switch } from './Switch';
|
||||
3
client2/src/components/common/controls/index.ts
Normal file
3
client2/src/components/common/controls/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as Radio } from './Radio';
|
||||
export { Input } from './Input';
|
||||
export { Switch } from './Switch';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user